diff --git a/TTMouseclickSimulator/Core/Actions/AbstractAction.cs b/TTMouseclickSimulator/Core/Actions/AbstractAction.cs index ed7d0a5..bde619b 100644 --- a/TTMouseclickSimulator/Core/Actions/AbstractAction.cs +++ b/TTMouseclickSimulator/Core/Actions/AbstractAction.cs @@ -9,6 +9,11 @@ public abstract class AbstractAction : IAction { public event Action? ActionInformationUpdated; + public abstract SimulatorCapabilities RequiredCapabilities + { + get; + } + public abstract ValueTask RunAsync(IInteractionProvider provider); protected void OnActionInformationUpdated(string text) diff --git a/TTMouseclickSimulator/Core/Actions/AbstractActionContainer.cs b/TTMouseclickSimulator/Core/Actions/AbstractActionContainer.cs index 798110f..87b4aa6 100644 --- a/TTMouseclickSimulator/Core/Actions/AbstractActionContainer.cs +++ b/TTMouseclickSimulator/Core/Actions/AbstractActionContainer.cs @@ -7,7 +7,20 @@ public abstract class AbstractActionContainer : AbstractAction, IActionContainer { public event Action? SubActionStartedOrStopped; - public abstract IList SubActions { get; } + public abstract IReadOnlyList SubActions { get; } + + public override SimulatorCapabilities RequiredCapabilities + { + get + { + var capabilities = default(SimulatorCapabilities); + + foreach (var subAction in this.SubActions ?? Array.Empty()) + capabilities |= subAction.RequiredCapabilities; + + return capabilities; + } + } protected void OnSubActionStartedOrStopped(int? index) { diff --git a/TTMouseclickSimulator/Core/Actions/CompoundAction.cs b/TTMouseclickSimulator/Core/Actions/CompoundAction.cs index 3b773ce..3528d61 100644 --- a/TTMouseclickSimulator/Core/Actions/CompoundAction.cs +++ b/TTMouseclickSimulator/Core/Actions/CompoundAction.cs @@ -14,14 +14,14 @@ public class CompoundAction : AbstractActionContainer public const int PauseIntervalMinimum = 0; public const int PauseIntervalMaximum = 600000; - private readonly IList actionList; + private readonly IReadOnlyList actionList; private readonly CompoundActionType type; private readonly int minimumPauseDuration; private readonly int maximumPauseDuration; private readonly bool loop; - private readonly Random rng = new Random(); + private readonly Random rng = new(); /// /// @@ -36,14 +36,15 @@ public class CompoundAction : AbstractActionContainer /// it will loop endlessly. Note that using false is not possible when specifying /// CompoundActionType.RandomIndex as type. public CompoundAction( - IList actionList, + IReadOnlyList actionList, CompoundActionType type = CompoundActionType.Sequential, int minimumPause = 0, int maximumPause = 0, bool loop = true) { if (actionList is null || actionList.Count is 0) - throw new ArgumentException("There must be at least one IAction to start the simulator."); + throw new ArgumentException( + "There must be at least one IAction to start the simulator."); if (minimumPause < PauseIntervalMinimum || minimumPause > PauseIntervalMaximum @@ -61,7 +62,7 @@ public CompoundAction( if (type is CompoundActionType.RandomIndex && !loop) throw new ArgumentException( "When using CompoundActionType.RandomIndex, it is not possible " + - " to disable the loop."); + "to disable the loop."); this.actionList = actionList; this.type = type; @@ -70,7 +71,7 @@ public CompoundAction( this.loop = loop; } - public override sealed IList SubActions + public override sealed IReadOnlyList SubActions { get => this.actionList; } @@ -79,8 +80,8 @@ public override sealed async ValueTask RunAsync(IInteractionProvider provider) { // Run the actions. int currentIdx = -1; - Func getNextActionIndex; + if (this.type is CompoundActionType.Sequential) { getNextActionIndex = () => @@ -150,7 +151,7 @@ public override sealed async ValueTask RunAsync(IInteractionProvider provider) await provider.WaitAsync(waitInterval); } - catch (Exception ex) when (!(ex is OperationCanceledException)) + catch (Exception ex) when (ex is not OperationCanceledException) { await provider.CheckRetryForExceptionAsync(ex); continue; @@ -175,6 +176,7 @@ public enum CompoundActionType : int /// (if loop is true) or the compound action returns. /// Sequential = 0, + /// /// Specifies that the inner actions should be executed in random order. /// This means if n actions are specified and the n-th action has been executed, @@ -183,6 +185,7 @@ public enum CompoundActionType : int /// (if loop is true) or the compound action returns. /// RandomOrder = 1, + /// /// Specifies that the inner actions should be executed randomly. That means /// that some actions might be executed more often than others and some actions diff --git a/TTMouseclickSimulator/Core/Actions/IAction.cs b/TTMouseclickSimulator/Core/Actions/IAction.cs index 791cae5..e20d23f 100644 --- a/TTMouseclickSimulator/Core/Actions/IAction.cs +++ b/TTMouseclickSimulator/Core/Actions/IAction.cs @@ -19,6 +19,15 @@ public interface IAction /// event Action? ActionInformationUpdated; + /// + /// Gets the simulator capabilities that are required by this action. + /// + /// + SimulatorCapabilities RequiredCapabilities + { + get; + } + /// /// Asynchonously runs the action using the specified IInteractionProvider. /// diff --git a/TTMouseclickSimulator/Core/Actions/IActionContainer.cs b/TTMouseclickSimulator/Core/Actions/IActionContainer.cs index ad513de..9e8afbe 100644 --- a/TTMouseclickSimulator/Core/Actions/IActionContainer.cs +++ b/TTMouseclickSimulator/Core/Actions/IActionContainer.cs @@ -11,5 +11,5 @@ public interface IActionContainer : IAction /// event Action? SubActionStartedOrStopped; - IList SubActions { get; } + IReadOnlyList SubActions { get; } } diff --git a/TTMouseclickSimulator/Core/Actions/LoopAction.cs b/TTMouseclickSimulator/Core/Actions/LoopAction.cs index f4f601a..c75d47a 100644 --- a/TTMouseclickSimulator/Core/Actions/LoopAction.cs +++ b/TTMouseclickSimulator/Core/Actions/LoopAction.cs @@ -29,14 +29,15 @@ public LoopAction(IAction action, int? count = null) this.count = count; } - public override IList SubActions + public override IReadOnlyList SubActions { - get => new List() { this.action }; + get => new IAction[] { this.action }; } public override sealed async ValueTask RunAsync(IInteractionProvider provider) { this.OnSubActionStartedOrStopped(0); + try { for (int i = 0; !this.count.HasValue || i < this.count.Value; i++) diff --git a/TTMouseclickSimulator/Core/Environment/AbstractWindowsEnvironment.cs b/TTMouseclickSimulator/Core/Environment/AbstractWindowsEnvironment.cs index bee1f65..a529bd2 100644 --- a/TTMouseclickSimulator/Core/Environment/AbstractWindowsEnvironment.cs +++ b/TTMouseclickSimulator/Core/Environment/AbstractWindowsEnvironment.cs @@ -107,7 +107,7 @@ public unsafe WindowPosition GetWindowPosition( var pos = new WindowPosition() { - Coordinates = new Coordinates(relPos.x, relPos.y), + Coordinates = (relPos.x, relPos.y), Size = new Size(clientRect.right, clientRect.bottom) }; @@ -115,30 +115,18 @@ public unsafe WindowPosition GetWindowPosition( if (failIfMinimized && pos.IsMinimized) throw new Exception("The window has been minimized."); - // Validate the position. - this.ValidateWindowPosition(pos); return pos; } - /// - /// When overridden in subclasses, throws an exception if the window position is - /// not valid. This implementation does nothing. - /// - /// The WindowPosition to validate. - protected virtual void ValidateWindowPosition(WindowPosition pos) - { - // Do nothing. - } - public void CreateWindowScreenshot( IntPtr hWnd, + WindowPosition windowPosition, [NotNull] ref ScreenshotContent? existingScreenshot, - bool failIfNotInForeground = true, bool fromScreen = false) { ScreenshotContent.Create( fromScreen ? IntPtr.Zero : hWnd, - this.GetWindowPosition(hWnd, out _, failIfNotInForeground), + windowPosition, ref existingScreenshot); } @@ -170,6 +158,11 @@ public bool TrySetWindowTopmost(IntPtr hWnd, bool topmost, bool throwIfNotSucces return result; } + public void SetWindowEnabled(IntPtr hWnd, bool enabled) + { + _ = NativeMethods.EnableWindow(hWnd, enabled); + } + public void MoveMouse(int x, int y) { this.DoMouseInput(x, y, true, null); @@ -188,13 +181,12 @@ public void ReleaseMouseButton() private void DoMouseInput(int x, int y, bool absoluteCoordinates, bool? mouseDown) { // Convert the screen coordinates into mouse coordinates. - var cs = new Coordinates(x, y); - cs = this.GetMouseCoordinatesFromScreenCoordinates(cs); + var (mouseX, mouseY) = this.GetMouseCoordinatesFromScreenCoordinates(x, y); var mi = new NativeMethods.MOUSEINPUT() { - dx = cs.X, - dy = cs.Y + dx = mouseX, + dy = mouseY }; if (absoluteCoordinates) @@ -213,7 +205,8 @@ private void DoMouseInput(int x, int y, bool absoluteCoordinates, bool? mouseDow NativeMethods.MOUSEEVENTF.LEFTUP; } - var input = new NativeMethods.INPUT + Span inputs = stackalloc NativeMethods.INPUT[1]; + inputs[0] = new NativeMethods.INPUT { type = NativeMethods.InputType.INPUT_MOUSE, InputUnion = { @@ -221,18 +214,18 @@ private void DoMouseInput(int x, int y, bool absoluteCoordinates, bool? mouseDow } }; - NativeMethods.SendInput(input); + NativeMethods.SendInput(inputs); } - private Coordinates GetMouseCoordinatesFromScreenCoordinates(Coordinates screenCoords) + private (int mouseX, int mouseY) GetMouseCoordinatesFromScreenCoordinates(int screenX, int screenY) { // Note: The mouse coordinates are relative to the primary monitor size and // location, not to the virtual screen size, so we use // SystemInformation.PrimaryMonitorSize. var primaryScreenSize = SystemInformation.PrimaryMonitorSize; - double x = (double)0x10000 * screenCoords.X / primaryScreenSize.Width; - double y = (double)0x10000 * screenCoords.Y / primaryScreenSize.Height; + double x = (double)0x10000 * screenX / primaryScreenSize.Width; + double y = (double)0x10000 * screenY / primaryScreenSize.Height; /* For correct conversion when converting the flointing point numbers * to integers, we need round away from 0, e.g. @@ -255,7 +248,7 @@ private Coordinates GetMouseCoordinatesFromScreenCoordinates(Coordinates screenC int resX = checked((int)(x >= 0 ? Math.Ceiling(x) : Math.Floor(x))); int resY = checked((int)(y >= 0 ? Math.Ceiling(y) : Math.Floor(y))); - return new Coordinates(resX, resY); + return (resX, resY); } public Coordinates GetCurrentMousePosition() @@ -284,7 +277,8 @@ private void PressOrReleaseKey(VirtualKey keyCode, bool down) if (!down) ki.dwFlags = NativeMethods.KEYEVENTF.KEYUP; - var input = new NativeMethods.INPUT + Span inputs = stackalloc NativeMethods.INPUT[1]; + inputs[0] = new NativeMethods.INPUT { type = NativeMethods.InputType.INPUT_KEYBOARD, InputUnion = @@ -293,12 +287,15 @@ private void PressOrReleaseKey(VirtualKey keyCode, bool down) } }; - NativeMethods.SendInput(input); + NativeMethods.SendInput(inputs); } public void WriteText(string characters) { - var inputs = new NativeMethods.INPUT[2 * characters.Length]; + int inputsLength = 2 * characters.Length; + var inputs = inputsLength <= 128 ? + stackalloc NativeMethods.INPUT[inputsLength] : + new NativeMethods.INPUT[inputsLength]; for (int i = 0; i < inputs.Length; i++) { @@ -323,7 +320,7 @@ public void WriteText(string characters) inputs[i] = input; } - NativeMethods.SendInputs(inputs); + NativeMethods.SendInput(inputs); } public void MoveWindowMouse(IntPtr hWnd, int x, int y, bool isButtonDown) @@ -468,7 +465,7 @@ private ScreenshotContent(WindowPosition pos) public Size Size { - get => new Size(this.bmp.Width, this.bmp.Height); + get => new(this.bmp.Width, this.bmp.Height); } public WindowPosition WindowPosition @@ -614,7 +611,9 @@ private void FillScreenshot(IntPtr windowHandle) public ScreenshotColor GetPixel(Coordinates coords) { - return this.GetPixel(coords.X, coords.Y); + return this.GetPixel( + checked((int)MathF.Round(coords.X)), + checked((int)MathF.Round(coords.Y))); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/TTMouseclickSimulator/Core/Environment/StandardInteractionProvider.cs b/TTMouseclickSimulator/Core/Environment/InteractionProvider.cs similarity index 69% rename from TTMouseclickSimulator/Core/Environment/StandardInteractionProvider.cs rename to TTMouseclickSimulator/Core/Environment/InteractionProvider.cs index 2e2e832..f4d37e8 100644 --- a/TTMouseclickSimulator/Core/Environment/StandardInteractionProvider.cs +++ b/TTMouseclickSimulator/Core/Environment/InteractionProvider.cs @@ -7,7 +7,7 @@ namespace TTMouseclickSimulator.Core.Environment; -internal class StandardInteractionProvider : IInteractionProvider, IDisposable +internal class InteractionProvider : IInteractionProvider, IDisposable { private readonly bool backgroundMode; @@ -28,12 +28,12 @@ internal class StandardInteractionProvider : IInteractionProvider, IDisposable // Window-relative (when using backgroundMode) or absolute mouse coordinates private Coordinates? lastSetMouseCoordinates; - // Specifies we could already take the first sceenshot successfully. - private bool firstWindowScreenshotSuccessful; + private bool windowIsDisabled; + private bool windowIsTopmost; private bool canRetryOnException = true; - public StandardInteractionProvider( + public InteractionProvider( Simulator simulator, AbstractWindowsEnvironment environmentInterface, bool backgroundMode) @@ -66,7 +66,6 @@ public void Dispose() public async Task InitializeAsync() { this.lastSetMouseCoordinates = null; - this.firstWindowScreenshotSuccessful = false; while (true) { @@ -84,7 +83,7 @@ public async Task InitializeAsync() { if (processes.Count is 1) { - if (lastInitializingEventParameter != false) + if (lastInitializingEventParameter is not false) { lastInitializingEventParameter = false; this.simulator.OnSimulatorInitializing(lastInitializingEventParameter); @@ -124,7 +123,7 @@ public async Task InitializeAsync() } else { - if (lastInitializingEventParameter != true) + if (lastInitializingEventParameter is not true) { lastInitializingEventParameter = true; this.simulator.OnSimulatorInitializing(lastInitializingEventParameter); @@ -178,26 +177,58 @@ public async Task InitializeAsync() if (this.backgroundMode) { - // When starting in background mode, wait a second before doing anything, to - // handle the case when the user clicks into the window to activate it - // (when more than one processes were detected). - await this.WaitAsync(900); - - // Then, simulate a click to (0, 0). This seems currently to be necessary so - // that the first WM_LBUTTONDOWN that we send to the window works correctly - // if the window is currently inactive (otherwise, the first message would - // have the effect that the mouse button is pressed but is then immediately - // released; probably due to a WM_MOUSELEAVE message being sent by Windows). - this.MoveMouse(0, 0); - this.PressMouseButton(); - await this.WaitAsync(50); - this.ReleaseMouseButton(); - await this.WaitAsync(50); + if (this.simulator.RequiredCapabilities.IsSet(SimulatorCapabilities.CaptureScreenshot)) + { + // Verify that we actually can create a screenshot directly from the + // window instead of from the screen. + this.GetCurrentWindowScreenshot(isInitialization: true); + } + + if (this.simulator.RequiredCapabilities.IsSet(SimulatorCapabilities.MouseInput)) + { + // When we require mouse input, disable the window, so that it is harder to + // interrupt our actions by user input. Note that it seems wven though the + // window can't be activated by clicking into it, moving the mouse over it + // while we send the LBUTTONDOWN message can still interfere with our + // actions (can move the mouse pointer to the current cursor's position, + // and can release the mouse button). + // Additionally, it's possible to activate the window by clicking on it's + // task bar button, and then keyboard input will be possible. + this.environmentInterface.SetWindowEnabled( + this.windowHandle, + enabled: false); + + this.windowIsDisabled = true; + + if (lastInitializingEventParameter.Value) + { + // Wait a bit after there were multiple windows, so that the user can + // deactivate the window before the first mouse click. + // TODO: Waybe we should wait until the window has been deactivated + // again in this case before continuing, so the mouse input isn't + // interrupted afterwards when the user then deactivates the window. + await this.WaitAsync(800); + } + } + + // Wait a bit before starting. + await this.WaitAsync(200); } else { + if (this.simulator.RequiredCapabilities.IsSet(SimulatorCapabilities.MouseInput) || + this.simulator.RequiredCapabilities.IsSet(SimulatorCapabilities.CaptureScreenshot)) + { + // When we require mouse input or screenshots in non-background mode, set + // the window to topmost, to ensure other topmost windows don't hide the + // area that we want to click or scan. + this.windowIsTopmost = this.environmentInterface.TrySetWindowTopmost( + this.windowHandle, + topmost: true); + } + // Also wait a short time when not using background mode. - await this.WaitAsync(500); + await this.WaitAsync(200); } } catch (Exception ex) @@ -264,52 +295,12 @@ public WindowPosition GetCurrentWindowPosition() { this.CancellationToken.ThrowIfCancellationRequested(); - return this.GetMainWindowPosition(); + return this.GetWindowPositionCore(); } public IScreenshotContent GetCurrentWindowScreenshot() { - this.CancellationToken.ThrowIfCancellationRequested(); - - // When using background mode, create the screenshot directly from the window's - // DC instead of from the whole screen. However, this seems not always to work - // (apparently on older Windows versions), e.g. on a Windows 8.1 machine I just - // got a black screen using this method. Also, e.g. with DirectX games on - // Windows 10 and 11, this might not work. - // Therefore, when not using background mode, we still create a screenshot from - // the whole screen, which should work in every case (and the window also needs - // to be in the foreground in this mode). - // Currently, there doesn't seem another easy way to get a screenshot froom a - // window client area if using the DC doesn't work (e.g. creating a DWM thumbnail - // won't allow us to access the pixel data). If this will generally no longer work - // with a future verison of the game, we may need to revert using the screen copy - // also for background mode, but set the game window as topmost window. - bool fromScreen = !this.backgroundMode; - this.environmentInterface.CreateWindowScreenshot( - this.windowHandle, - ref this.currentScreenshot, - failIfNotInForeground: !this.backgroundMode, - fromScreen: fromScreen); - - if (!fromScreen && !this.firstWindowScreenshotSuccessful) - { - // If we took the first screenshot from the window rather than the screen, - // check whether it only contains black pixels. In that case, throw an - // exception to inform the user that background mode won't work. - if (this.currentScreenshot.ContainsOnlyBlackPixels()) - { - // Don't allow to retry in this case since it would lead to the same - // exception. - this.canRetryOnException = false; - throw new InvalidOperationException( - "Couldn't capture screenshot from window. " + - "Please disable background mode and try again."); - } - - this.firstWindowScreenshotSuccessful = true; - } - - return this.currentScreenshot; + return this.GetCurrentWindowScreenshot(false); } public void PressKey(AbstractWindowsEnvironment.VirtualKey key) @@ -317,7 +308,7 @@ public void PressKey(AbstractWindowsEnvironment.VirtualKey key) this.CancellationToken.ThrowIfCancellationRequested(); // Check if the window is still active and in foreground. - this.GetMainWindowPosition(failIfMinimized: !this.backgroundMode); + this.GetWindowPositionCore(failIfMinimized: !this.backgroundMode); if (!this.keysCurrentlyPressed.Contains(key)) { @@ -351,7 +342,7 @@ public void WriteText(string text) this.CancellationToken.ThrowIfCancellationRequested(); // Check if the window is still active and in foreground. - this.GetMainWindowPosition(failIfMinimized: !this.backgroundMode); + this.GetWindowPositionCore(failIfMinimized: !this.backgroundMode); if (!this.backgroundMode) this.environmentInterface.WriteText(text); @@ -371,19 +362,22 @@ public void MoveMouse(Coordinates c) if (!this.backgroundMode) { // Check if the window is still active and in foreground. - var pos = this.GetMainWindowPosition(); + var pos = this.GetWindowPositionCore(); // Convert the relative coordinates to absolute ones, then simulate the click. var absoluteCoords = pos.RelativeToAbsoluteCoordinates(c); - this.environmentInterface.MoveMouse(absoluteCoords.X, absoluteCoords.Y); + this.environmentInterface.MoveMouse( + checked((int)MathF.Round(absoluteCoords.X)), + checked((int)MathF.Round(absoluteCoords.Y))); + this.lastSetMouseCoordinates = absoluteCoords; } else { this.environmentInterface.MoveWindowMouse( this.windowHandle, - c.X, - c.Y, + checked((int)MathF.Round(c.X)), + checked((int)MathF.Round(c.Y)), this.isMouseButtonPressed); this.lastSetMouseCoordinates = c; @@ -397,7 +391,7 @@ public void PressMouseButton() if (!this.backgroundMode) { // Check if the window is still active and in foreground. - this.GetMainWindowPosition(); + this.GetWindowPositionCore(); if (!this.isMouseButtonPressed) { @@ -417,8 +411,8 @@ public void PressMouseButton() this.environmentInterface.PressWindowMouseButton( this.windowHandle, - this.lastSetMouseCoordinates.Value.X, - this.lastSetMouseCoordinates.Value.Y); + checked((int)MathF.Round(this.lastSetMouseCoordinates.Value.X)), + checked((int)MathF.Round(this.lastSetMouseCoordinates.Value.Y))); this.isMouseButtonPressed = true; } @@ -444,9 +438,9 @@ public void ReleaseMouseButton() } this.environmentInterface.ReleaseWindowMouseButton( - this.windowHandle, - this.lastSetMouseCoordinates.Value.X, - this.lastSetMouseCoordinates.Value.Y); + this.windowHandle, + checked((int)MathF.Round(this.lastSetMouseCoordinates.Value.X)), + checked((int)MathF.Round(this.lastSetMouseCoordinates.Value.Y))); } this.isMouseButtonPressed = false; @@ -455,19 +449,27 @@ public void ReleaseMouseButton() public void CancelActiveInteractions() { - // Release mouse buttons and keys that are currently pressed. + // Release mouse buttons and keys that are currently pressed, and revert changes to the + // window. We need to ignore exceptions here as this method should never fail. if (this.isMouseButtonPressed) { - if (!this.backgroundMode) + try { - this.environmentInterface.ReleaseMouseButton(); + if (!this.backgroundMode) + { + this.environmentInterface.ReleaseMouseButton(); + } + else + { + this.environmentInterface.ReleaseWindowMouseButton( + this.windowHandle, + checked((int)MathF.Round(this.lastSetMouseCoordinates!.Value.X)), + checked((int)MathF.Round(this.lastSetMouseCoordinates.Value.Y))); + } } - else + catch { - this.environmentInterface.ReleaseWindowMouseButton( - this.windowHandle, - this.lastSetMouseCoordinates!.Value.X, - this.lastSetMouseCoordinates.Value.Y); + // Ignore. } this.isMouseButtonPressed = false; @@ -475,13 +477,50 @@ public void CancelActiveInteractions() foreach (var key in this.keysCurrentlyPressed) { - if (!this.backgroundMode) - this.environmentInterface.ReleaseKey(key); - else - this.environmentInterface.ReleaseWindowKey(this.windowHandle, key); + try + { + if (!this.backgroundMode) + this.environmentInterface.ReleaseKey(key); + else + this.environmentInterface.ReleaseWindowKey(this.windowHandle, key); + } + catch + { + // Ignore. + } } this.keysCurrentlyPressed.Clear(); + + if (this.windowIsDisabled) + { + try + { + this.environmentInterface.SetWindowEnabled(this.windowHandle, enabled: true); + } + catch + { + // Ignore. + } + + this.windowIsDisabled = false; + } + + if (this.windowIsTopmost) + { + try + { + this.windowIsTopmost = this.environmentInterface.TrySetWindowTopmost( + this.windowHandle, + topmost: false); + } + catch + { + // Ignore. + } + + this.windowIsTopmost = false; + } } private async ValueTask WaitCoreAsync(int milliseconds, bool checkWindow = true) @@ -503,7 +542,7 @@ await Task.Delay( { // Check if the window is still active (and, if not using background mode, // in foreground). - this.GetMainWindowPosition(failIfMinimized: !this.backgroundMode); + this.GetWindowPositionCore(failIfMinimized: !this.backgroundMode); long remaining = milliseconds - sw.ElapsedMilliseconds; if (remaining <= 0) @@ -516,6 +555,81 @@ await Task.Delay( } } + private WindowPosition GetWindowPositionCore(bool failIfMinimized = true) + { + // Fail if the window is no longer in foreground (active) and we are not + // using background mode. + var windowPosition = this.environmentInterface.GetWindowPosition( + this.windowHandle, + out _, + failIfNotInForeground: !this.backgroundMode, + failIfMinimized: failIfMinimized); + + // When we use mouse input or capture screenshots, check that the aspect + // ratio of the window is 4:3 or higher if the window currently isn't minimized. + if ((this.simulator.RequiredCapabilities.IsSet(SimulatorCapabilities.MouseInput) || + this.simulator.RequiredCapabilities.IsSet(SimulatorCapabilities.CaptureScreenshot)) && + !windowPosition.IsMinimized) + { + if (((double)windowPosition.Size.Width / windowPosition.Size.Height) < 4d / 3d) + { + throw new ArgumentException( + "The Toontown window must have an aspect ratio " + + "of 4:3 or higher (e.g. 16:9)."); + } + + // TODO: Check if the window is beyond the virtual screen size (if in non-background + // mode and we require mouse or screenshot capabilities, or if in background mode and + // we require screenshot capabilities). + } + + return windowPosition; + } + + private IScreenshotContent GetCurrentWindowScreenshot(bool isInitialization) + { + this.CancellationToken.ThrowIfCancellationRequested(); + + // When using background mode, create the screenshot directly from the window's + // DC instead of from the whole screen. However, this seems not always to work + // (apparently on older Windows versions), e.g. on a Windows 8.1 machine I just + // got a black screen using this method. Also, e.g. with DirectX games on + // Windows 10 and 11, this might not work. + // Therefore, when not using background mode, we still create a screenshot from + // the whole screen, which should work in every case (and the window also needs + // to be in the foreground in this mode). + // Currently, there doesn't seem another easy way to get a screenshot froom a + // window client area if using the DC doesn't work (e.g. creating a DWM thumbnail + // won't allow us to access the pixel data). If this will generally no longer work + // with a future verison of the game, we may need to revert using the screen copy + // also for background mode, but set the game window as topmost window. + bool fromScreen = !this.backgroundMode; + var windowPosition = this.GetWindowPositionCore(); + this.environmentInterface.CreateWindowScreenshot( + this.windowHandle, + windowPosition, + ref this.currentScreenshot, + fromScreen: fromScreen); + + if (!fromScreen && isInitialization) + { + // If we took the first screenshot from the window rather than the screen, + // check whether it only contains black pixels. In that case, throw an + // exception to inform the user that background mode won't work. + if (this.currentScreenshot.ContainsOnlyBlackPixels()) + { + // Don't allow to retry in this case since it would lead to the same + // exception. + this.canRetryOnException = false; + throw new InvalidOperationException( + "Couldn't capture screenshot directly from window. " + + "Please disable background mode and try again."); + } + } + + return this.currentScreenshot; + } + private async ValueTask CheckRetryForExceptionAsync(Exception ex, bool reinitialize) { if (!this.canRetryOnException || this.simulator.AsyncRetryHandler is null) @@ -541,17 +655,6 @@ private async ValueTask CheckRetryForExceptionAsync(Exception ex, bool reinitial } } - private WindowPosition GetMainWindowPosition(bool failIfMinimized = true) - { - // Fail if the window is no longer in foreground (active) and we are not - // using background mode. - return this.environmentInterface.GetWindowPosition( - this.windowHandle, - out _, - !this.backgroundMode, - failIfMinimized); - } - /// /// Disposes of this StandardInteractionProvider. /// diff --git a/TTMouseclickSimulator/Core/Environment/NativeMethods.cs b/TTMouseclickSimulator/Core/Environment/NativeMethods.cs index 63cbdb8..8777269 100644 --- a/TTMouseclickSimulator/Core/Environment/NativeMethods.cs +++ b/TTMouseclickSimulator/Core/Environment/NativeMethods.cs @@ -67,6 +67,11 @@ public static extern BOOL SetWindowPos( int xy, SWP flags); + [DllImport("user32.dll", EntryPoint = "EnableWindow", ExactSpelling = true)] + public static extern BOOL EnableWindow( + IntPtr hWnd, + BOOL bEnable); + [DllImport("user32.dll", EntryPoint = "GetDC", ExactSpelling = true)] public static extern IntPtr GetDC(IntPtr hWnd); @@ -93,13 +98,7 @@ public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong) return (IntPtr)SetWindowLong32(hWnd, nIndex, (int)dwNewLong); } - public static unsafe void SendInput(INPUT input) - { - if (SendInputNative(1, &input, sizeof(INPUT)) is 0) - throw new Win32Exception(); - } - - public static unsafe void SendInputs(INPUT[] inputs) + public static unsafe void SendInput(ReadOnlySpan inputs) { fixed (INPUT* inputsPtr = inputs) { diff --git a/TTMouseclickSimulator/Core/Environment/WindowParameters.cs b/TTMouseclickSimulator/Core/Environment/WindowParameters.cs index 173eee5..833d049 100644 --- a/TTMouseclickSimulator/Core/Environment/WindowParameters.cs +++ b/TTMouseclickSimulator/Core/Environment/WindowParameters.cs @@ -9,7 +9,7 @@ public struct WindowPosition /// /// The coordinates to the upper left point of the window contents. /// - public Coordinates Coordinates + public (int X, int Y) Coordinates { get; set; @@ -32,25 +32,25 @@ public bool IsMinimized public Coordinates RelativeToAbsoluteCoordinates(Coordinates c) { - return this.Coordinates.Add(c); + return c.Add(this.Coordinates); } } public struct Coordinates { - public Coordinates(int x, int y) + public Coordinates(float x, float y) { this.X = x; this.Y = y; } - public int X + public float X { get; set; } - public int Y + public float Y { get; set; @@ -60,6 +60,11 @@ public Coordinates Add(Coordinates c) { return new Coordinates(this.X + c.X, this.Y + c.Y); } + + public static implicit operator Coordinates((int X, int Y) intCoordinates) + { + return new Coordinates(intCoordinates.X, intCoordinates.Y); + } } public struct Size diff --git a/TTMouseclickSimulator/Core/Simulator.cs b/TTMouseclickSimulator/Core/Simulator.cs index c30fbe3..0a87c0d 100644 --- a/TTMouseclickSimulator/Core/Simulator.cs +++ b/TTMouseclickSimulator/Core/Simulator.cs @@ -10,7 +10,7 @@ public class Simulator : IDisposable { private readonly IAction mainAction; - private readonly StandardInteractionProvider provider; + private readonly InteractionProvider provider; public event Action? SimulatorStarted; public event Action? SimulatorStopped; @@ -27,13 +27,19 @@ public Simulator( throw new ArgumentNullException(nameof(environmentInterface)); this.mainAction = mainAction; + this.RequiredCapabilities = mainAction.RequiredCapabilities; - this.provider = new StandardInteractionProvider( + this.provider = new InteractionProvider( this, environmentInterface, backgroundMode); } + public SimulatorCapabilities RequiredCapabilities + { + get; + } + /// /// When an exception (which is not a ) occurs while an action runs, /// this allows the action to check if it should retry or cancel the simulator (in that case, it should diff --git a/TTMouseclickSimulator/Core/SimulatorCapabilities.cs b/TTMouseclickSimulator/Core/SimulatorCapabilities.cs new file mode 100644 index 0000000..81e27e9 --- /dev/null +++ b/TTMouseclickSimulator/Core/SimulatorCapabilities.cs @@ -0,0 +1,16 @@ +using System; + +namespace TTMouseclickSimulator.Core +{ + [Flags] + public enum SimulatorCapabilities + { + None = 0, + + KeyboardInput = 1 << 0, + + MouseInput = 1 << 1, + + CaptureScreenshot = 1 << 2 + } +} diff --git a/TTMouseclickSimulator/Core/SimulatorCapabilitiesExtensions.cs b/TTMouseclickSimulator/Core/SimulatorCapabilitiesExtensions.cs new file mode 100644 index 0000000..b6770ba --- /dev/null +++ b/TTMouseclickSimulator/Core/SimulatorCapabilitiesExtensions.cs @@ -0,0 +1,12 @@ +namespace TTMouseclickSimulator.Core +{ + public static class SimulatorCapabilitiesExtensions + { + public static bool IsSet( + this SimulatorCapabilities capabilities, + SimulatorCapabilities value) + { + return (capabilities & value) == value; + } + } +} diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/DoodleInteraction/DoodlePanelAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/DoodleInteraction/DoodlePanelAction.cs index 6deadfa..db7274f 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/DoodleInteraction/DoodlePanelAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/DoodleInteraction/DoodlePanelAction.cs @@ -17,6 +17,11 @@ public DoodlePanelAction(DoodlePanelButton button) this.button = button; } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.MouseInput; + } + public override async ValueTask RunAsync(IInteractionProvider provider) { var c = new Coordinates(1397, 206 + (int)this.button * 49); diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/AbstractFishingRodAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/AbstractFishingRodAction.cs index 8208c76..067304e 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/AbstractFishingRodAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/AbstractFishingRodAction.cs @@ -38,6 +38,11 @@ public AbstractFishingRodAction() { } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.MouseInput | SimulatorCapabilities.CaptureScreenshot; + } + // This is determined by the class type, not by the instance so implement it // as abstract property instead of a field. This avoids it being serialized. /// diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/AutomaticFishingAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/AutomaticFishingAction.cs index 5e72a62..bebb455 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/AutomaticFishingAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/AutomaticFishingAction.cs @@ -10,7 +10,11 @@ public class AutomaticFishingAction : AbstractFishingRodAction { private readonly FishingSpotData spotData; - public AutomaticFishingAction(int[] scan1, int[] scan2, byte[] bubbleColorRgb, byte[] toleranceRgb) + public AutomaticFishingAction( + float[] scan1, + float[] scan2, + byte[] bubbleColorRgb, + byte[] toleranceRgb) { this.spotData = new FishingSpotData( new Coordinates(scan1[0], scan1[1]), @@ -47,9 +51,9 @@ protected override sealed async ValueTask FinishCastFishingRodAsync(IInteraction // TODO: The fish bubble detection should be changed so that it does not scan // for a specific color, but instead checks that for a point if the color is // darker than the neighbor pixels (in some distance). - for (int y = this.spotData.Scan1.Y; y <= this.spotData.Scan2.Y && !newCoords.HasValue; y += scanStep) + for (float y = this.spotData.Scan1.Y; y <= this.spotData.Scan2.Y && !newCoords.HasValue; y += scanStep) { - for (int x = this.spotData.Scan1.X; x <= this.spotData.Scan2.X; x += scanStep) + for (float x = this.spotData.Scan1.X; x <= this.spotData.Scan2.X; x += scanStep) { var c = new Coordinates(x, y); c = screenshot.WindowPosition.ScaleCoordinates( @@ -76,8 +80,8 @@ protected override sealed async ValueTask FinishCastFishingRodAsync(IInteraction this.OnActionInformationUpdated(actionInformationScanning); if (newCoords.HasValue && oldCoords.HasValue - && Math.Abs(oldCoords.Value.X - newCoords.Value.X) <= scanStep - && Math.Abs(oldCoords.Value.Y - newCoords.Value.Y) <= scanStep) + && MathF.Abs(oldCoords.Value.X - newCoords.Value.X) <= scanStep + && MathF.Abs(oldCoords.Value.Y - newCoords.Value.Y) <= scanStep) { // The new coordinates are (nearly) the same as the previous ones. coordsMatchCounter++; @@ -101,9 +105,9 @@ protected override sealed async ValueTask FinishCastFishingRodAsync(IInteraction { // Calculate the destination coordinates. newCoords = new Coordinates( - (int)Math.Round(800d + 120d / 429d * (800d - newCoords.Value.X) * + (float)(800d + 120d / 429d * (800d - newCoords.Value.X) * (0.75 + (820d - newCoords.Value.Y) / 820 * 0.38)), - (int)Math.Round(846d + 169d / 428d * (820d - newCoords.Value.Y)) + (float)(846d + 169d / 428d * (820d - newCoords.Value.Y)) ); } diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/QuitFishingAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/QuitFishingAction.cs index deaebee..b093c62 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/QuitFishingAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/QuitFishingAction.cs @@ -11,6 +11,11 @@ public QuitFishingAction() { } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.MouseInput; + } + public override sealed async ValueTask RunAsync(IInteractionProvider provider) { var c = new Coordinates(1503, 1086); diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/SellFishAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/SellFishAction.cs index ef982f6..7ab8f6e 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/SellFishAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Fishing/SellFishAction.cs @@ -11,6 +11,11 @@ public SellFishAction() { } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.MouseInput; + } + public override sealed async ValueTask RunAsync(IInteractionProvider provider) { var c = new Coordinates(1159, 911); diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/ConfirmFlowerPlantedDialogAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/ConfirmFlowerPlantedDialogAction.cs index a7e5889..ba12c2f 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/ConfirmFlowerPlantedDialogAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/ConfirmFlowerPlantedDialogAction.cs @@ -14,6 +14,11 @@ public ConfirmFlowerPlantedDialogAction() { } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.MouseInput; + } + public override sealed async ValueTask RunAsync(IInteractionProvider provider) { // Click on the "Ok" button. diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/PlantFlowerAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/PlantFlowerAction.cs index 8a6498d..ea56c2e 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/PlantFlowerAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/PlantFlowerAction.cs @@ -32,6 +32,11 @@ public PlantFlowerAction(int[] jellybeanCombination) this.jellybeanCombination = jellybeanCombination; } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.MouseInput; + } + public override sealed async ValueTask RunAsync(IInteractionProvider provider) { // Click on the "Plant Flower" button. diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/WaterAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/WaterAction.cs index cbf6708..7c7d8ec 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/WaterAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Gardening/WaterAction.cs @@ -14,6 +14,11 @@ public WaterAction() { } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.MouseInput; + } + public override sealed async ValueTask RunAsync(IInteractionProvider provider) { // Click on the "Water" button. diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Keyboard/PressKeyAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Keyboard/PressKeyAction.cs index 6768154..2a2b435 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Keyboard/PressKeyAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Keyboard/PressKeyAction.cs @@ -19,6 +19,11 @@ public PressKeyAction(AbstractWindowsEnvironment.VirtualKey key, int duration) this.duration = duration; } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.KeyboardInput; + } + public override sealed async ValueTask RunAsync(IInteractionProvider provider) { provider.PressKey(this.key); diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Keyboard/WriteTextAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Keyboard/WriteTextAction.cs index 0e2381e..6d572f8 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Keyboard/WriteTextAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Keyboard/WriteTextAction.cs @@ -30,6 +30,11 @@ public WriteTextAction(string text, int? pauseDuration = null) this.pauseDuration = pauseDuration; } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.KeyboardInput; + } + public override sealed async ValueTask RunAsync(IInteractionProvider provider) { // write the text and presses enter. diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/PauseAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/PauseAction.cs index a439d62..8518efc 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/PauseAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/PauseAction.cs @@ -17,6 +17,11 @@ public PauseAction(int duration) this.duration = duration; } + public override SimulatorCapabilities RequiredCapabilities + { + get => default; + } + public override sealed ValueTask RunAsync(IInteractionProvider provider) { return provider.WaitAsync(this.duration); diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Speedchat/SpeedchatAction.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Speedchat/SpeedchatAction.cs index 0a2aeb4..e059ba9 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Speedchat/SpeedchatAction.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Actions/Speedchat/SpeedchatAction.cs @@ -12,8 +12,9 @@ public class SpeedchatAction : AbstractAction private static readonly int[] xWidths = { 215, - 215 + 250, - 215 + 250 + 180 + 215 + 230, + 215 + 230 + 175, + 215 + 230 + 175 + 160 }; private readonly int[] menuItems; @@ -21,12 +22,17 @@ public class SpeedchatAction : AbstractAction public SpeedchatAction(params int[] menuItems) { this.menuItems = menuItems; - if (menuItems.Length > 3) - throw new ArgumentException("Only 3 levels are supported."); + if (menuItems.Length > xWidths.Length) + throw new ArgumentException($"Only {xWidths.Length} levels are supported."); if (menuItems.Length is 0) throw new ArgumentException("The menuItems array must not be empty."); } + public override SimulatorCapabilities RequiredCapabilities + { + get => SimulatorCapabilities.MouseInput; + } + public override sealed async ValueTask RunAsync(IInteractionProvider provider) { // Click on the Speedchat Icon. @@ -40,7 +46,7 @@ public override sealed async ValueTask RunAsync(IInteractionProvider provider) currentYNumber += this.menuItems[i]; - c = new Coordinates(xWidths[i], 40 + currentYNumber * 38); + c = new Coordinates(xWidths[i], 40 + currentYNumber * 37.55f); await MouseHelpers.DoSimpleMouseClickAsync(provider, c, HorizontalScaleAlignment.Left, 100); } } diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/Environment/TTRWindowsEnvironment.cs b/TTMouseclickSimulator/Core/ToontownRewritten/Environment/TTRWindowsEnvironment.cs index c05ae3e..2d03565 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/Environment/TTRWindowsEnvironment.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/Environment/TTRWindowsEnvironment.cs @@ -40,14 +40,4 @@ public override sealed List FindProcesses() return processes; } - - protected override sealed void ValidateWindowPosition(WindowPosition pos) - { - // Check if the aspect ratio of the window is 4:3 or higher. - if (!pos.IsMinimized && - ((double)pos.Size.Width / pos.Size.Height) < 4d / 3d) - throw new ArgumentException( - "The TT Rewritten window must have an aspect ratio " + - "of 4:3 or higher (e.g. 16:9)."); - } } diff --git a/TTMouseclickSimulator/Core/ToontownRewritten/TTRScaleExtensions.cs b/TTMouseclickSimulator/Core/ToontownRewritten/TTRScaleExtensions.cs index 5acc675..a9007dc 100644 --- a/TTMouseclickSimulator/Core/ToontownRewritten/TTRScaleExtensions.cs +++ b/TTMouseclickSimulator/Core/ToontownRewritten/TTRScaleExtensions.cs @@ -30,14 +30,14 @@ public static Coordinates ScaleCoordinates( double aspectWidth = pos.Size.Height / 3d * 4d; double widthDifference = pos.Size.Width - aspectWidth; - int newX; + float newX; if (align is HorizontalScaleAlignment.NoAspectRatio) { - newX = (int)Math.Round((double)coords.X / referenceSize.Width * pos.Size.Width); + newX = (float)((double)coords.X / referenceSize.Width * pos.Size.Width); } else { - newX = (int)Math.Round((double)coords.X / referenceSize.Width * aspectWidth + + newX = (float)((double)coords.X / referenceSize.Width * aspectWidth + (align is HorizontalScaleAlignment.Left ? 0 : align is HorizontalScaleAlignment.Center ? widthDifference / 2 : widthDifference)); } @@ -45,7 +45,7 @@ public static Coordinates ScaleCoordinates( return new Coordinates() { X = newX, - Y = (int)Math.Round((double)coords.Y / referenceSize.Height * pos.Size.Height) + Y = (float)((double)coords.Y / referenceSize.Height * pos.Size.Height) }; } } diff --git a/TTMouseclickSimulator/MainWindow.xaml b/TTMouseclickSimulator/MainWindow.xaml index f8540eb..d69881f 100644 --- a/TTMouseclickSimulator/MainWindow.xaml +++ b/TTMouseclickSimulator/MainWindow.xaml @@ -45,10 +45,6 @@ The Toontown window can even be hidden behind other apps, but it shouldn't protrude beyond the edge of the screen. Also, you shouldn't move the mouse pointer into the Toontown window while the simulator is running. - - - However, this mode might not always work some configurations. In that case, you can disable it, - so that gobal mouse and keyboard input is synthesized. diff --git a/TTMouseclickSimulator/Project/XmlProjectDeserializer.cs b/TTMouseclickSimulator/Project/XmlProjectDeserializer.cs index ce1bec7..5d1362c 100644 --- a/TTMouseclickSimulator/Project/XmlProjectDeserializer.cs +++ b/TTMouseclickSimulator/Project/XmlProjectDeserializer.cs @@ -136,7 +136,7 @@ private static SimulatorConfiguration ParseConfiguration(XElement configEl) return config; } - private static IList ParseActionList(XElement parent) + private static IReadOnlyList ParseActionList(XElement parent) { var actionList = new List(); foreach (var child in parent.Elements()) @@ -174,7 +174,7 @@ private static IList ParseActionList(XElement parent) // Check if the element specifies the parameter. If not, use the default // value if available. var attr = child.Attribute(param.Name!); - if (typeof(IList).IsAssignableFrom(param.ParameterType)) + if (typeof(IReadOnlyList).IsAssignableFrom(param.ParameterType)) { paramSubactionListIdx = i; } @@ -220,7 +220,8 @@ private static IList ParseActionList(XElement parent) parameterValues[i] = number; } else if (param.ParameterType.IsAssignableFrom(typeof(int[])) || - param.ParameterType.IsAssignableFrom(typeof(byte[]))) + param.ParameterType.IsAssignableFrom(typeof(byte[])) || + param.ParameterType.IsAssignableFrom(typeof(float[]))) { var valueElements = attrval.Split( new string[] { "," }, @@ -234,8 +235,10 @@ private static IList ParseActionList(XElement parent) object v; if (param.ParameterType.IsAssignableFrom(typeof(byte[]))) v = byte.Parse(valueElements[j].Trim(), CultureInfo.InvariantCulture); - else + else if (param.ParameterType.IsAssignableFrom(typeof(int[]))) v = int.Parse(valueElements[j].Trim(), CultureInfo.InvariantCulture); + else + v = float.Parse(valueElements[j].Trim(), CultureInfo.InvariantCulture); values.SetValue(v, j); }