Skip to content

Commit

Permalink
When multiple windows are open, wait until the user activates one and…
Browse files Browse the repository at this point in the history
… then use that window (instead of simply using the window of the first found process).

Fixes #27
  • Loading branch information
kpreisser committed Aug 11, 2018
1 parent 0b7ee51 commit 722f4de
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 109 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Mouse Click Simulator for TT Rewritten

This is a new implementation of the [**TT Mouse Click Simulator**](https://old.preisser-it.de/tt-mausklick/) that is intended to work with TT Rewritten. It is implemented in C# and runs on Windows on .NET 4.6 or higher.
This is a new implementation of the [**TT Mouse Click Simulator**](https://old.preisser-it.de/tt-mausklick/) that is intended to work with TT Rewritten. It is implemented in C# and runs on Windows on .NET Framework 4.6 or higher.

Among automating tasks like saying SpeedChat phrases to train a Doodle, the TTR Mouse Click Simulator is able to automatically fish in specific locations like Punchling Place, TTC. To accomplish this, it scans the screen to detect fish bubbles, calculates how far the rod must be casted to catch the fish, and moves the Toon to the fisherman to sell the fish.

Expand Down
149 changes: 80 additions & 69 deletions TTMouseclickSimulator/Core/Environment/AbstractWindowsEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Net;
using System.Windows.Forms;

Expand All @@ -15,14 +15,14 @@ namespace TTMouseclickSimulator.Core.Environment
public abstract class AbstractWindowsEnvironment
{
/// <summary>
/// Finds the process with the specified name (without ".exe").
/// Finds processes with the specified name (without ".exe").
/// </summary>
/// <param name="processname"></param>
/// <returns></returns>
protected Process FindProcessByName(string processname)
protected List<Process> FindProcessesByName(string processname)
{
var processes = Process.GetProcessesByName(processname);
var foundProcess = null as Process;
var foundProcesses = new List<Process>();

// Use the first applicable process.
foreach (var p in processes)
Expand All @@ -32,25 +32,24 @@ protected Process FindProcessByName(string processname)
// Check if we actually have access to this process. This can fail with a
// Win32Exception e.g. if the process is from another user.
GC.KeepAlive(p.HasExited);
foundProcess = p;
break;
foundProcesses.Add(p);
}
catch (Win32Exception)
catch
{
// Ignore
// Dispose of the process since we won't use it.
p.Dispose();
}
}

if (foundProcess == null)
throw new ArgumentException($"Could not find Process '{processname}.exe'.");
if (foundProcesses.Count == 0)
throw new ArgumentException($"Could not find process '{processname}.exe'.");

// Need to dispose of the other process instances.
foreach (var otherProcess in processes.Where(p => p != foundProcess))
otherProcess.Dispose();

return foundProcess;
return foundProcesses;
}

public abstract List<Process> FindProcesses();

/// <summary>
/// Finds the main window of the given process and returns its window handle.
/// </summary>
Expand All @@ -76,18 +75,17 @@ public void BringWindowToForeground(IntPtr hWnd)
throw new Exception("Could not bring specified window to foreground.");
}

public abstract Process FindProcess();

/// <summary>
/// Determines the position and location of the client rectangle of the specified
/// window. This method also checks if the specified window is in foreground.
/// </summary>
/// <param name="hWnd"></param>
/// <returns></returns>
public WindowPosition GetWindowPosition(IntPtr hWnd)
public WindowPosition GetWindowPosition(IntPtr hWnd, out bool isInForeground, bool failIfNotInForeground = true)
{
// Check if the specified window is in foreground.
if (NativeMethods.GetForegroundWindow() != hWnd)
isInForeground = NativeMethods.GetForegroundWindow() == hWnd;
if (failIfNotInForeground && !isInForeground)
throw new Exception("The window is not in foreground any more.");

// Get the client size.
Expand All @@ -111,12 +109,12 @@ public WindowPosition GetWindowPosition(IntPtr hWnd)
Coordinates = new Coordinates(relPos.X, relPos.Y),
Size = new Size(clientRect.Right - clientRect.Left, clientRect.Bottom - clientRect.Top)
};

// Validate the position.
ValidateWindowPosition(pos);
return pos;
}


/// <summary>
/// When overridden in subclasses, throws an exception if the window position is
/// not valid. This implementation does nothing.
Expand All @@ -127,16 +125,26 @@ protected virtual void ValidateWindowPosition(WindowPosition pos)
// Do nothing.
}

public ScreenshotContent CreateWindowScreenshot(IntPtr hWnd, ScreenshotContent existingScreenshot = null) =>
ScreenshotContent.Create(GetWindowPosition(hWnd), existingScreenshot);


public ScreenshotContent CreateWindowScreenshot(IntPtr hWnd, ScreenshotContent existingScreenshot = null)
{
bool isInForeground;
return ScreenshotContent.Create(GetWindowPosition(hWnd, out isInForeground), existingScreenshot);
}

public void MoveMouse(int x, int y) => DoMouseInput(x, y, true, null);
public void MoveMouse(int x, int y)
{
DoMouseInput(x, y, true, null);
}

public void PressMouseButton() => DoMouseInput(0, 0, false, true);
public void PressMouseButton()
{
DoMouseInput(0, 0, false, true);
}

public void ReleaseMouseButton() => DoMouseInput(0, 0, false, false);
public void ReleaseMouseButton()
{
DoMouseInput(0, 0, false, false);
}

private void DoMouseInput(int x, int y, bool absoluteCoordinates, bool? mouseDown)
{
Expand Down Expand Up @@ -176,17 +184,6 @@ private void DoMouseInput(int x, int y, bool absoluteCoordinates, bool? mouseDow
throw new Win32Exception();
}

public void SendWindowMouseMovement(IntPtr hwnd, ushort x, ushort y, bool? mouseDown)
{
// Post a WM_MOUSEMOVE message.
int mouseParams = 0;
if (mouseDown == true)
mouseParams = NativeMethods.MK_LBUTTON;
else if (mouseDown == false)
mouseParams = NativeMethods.MK_RBUTTON;
NativeMethods.PostMessage(hwnd, NativeMethods.WindowMessage.WM_MOUSEMOVE, (IntPtr)mouseParams, (IntPtr)(y << 16 | x));
}

private Coordinates GetMouseCoordinatesFromScreenCoordinates(Coordinates screenCoords)
{
// Note: The mouse coordinates are relative to the primary monitor size and
Expand Down Expand Up @@ -222,14 +219,22 @@ If we would want to place the mouse cursor at the rightmost pixel of the second
}


public void PressKey(VirtualKeyShort keyCode) => PressOrReleaseKey(keyCode, true);
public void PressKey(VirtualKeyShort keyCode)
{
PressOrReleaseKey(keyCode, true);
}

public void ReleaseKey(VirtualKeyShort keyCode) => PressOrReleaseKey(keyCode, false);
public void ReleaseKey(VirtualKeyShort keyCode)
{
PressOrReleaseKey(keyCode, false);
}

private void PressOrReleaseKey(VirtualKeyShort keyCode, bool down)
{
var ki = new NativeMethods.KEYBDINPUT();
ki.wVk = keyCode;
var ki = new NativeMethods.KEYBDINPUT
{
wVk = keyCode
};
if (!down)
ki.dwFlags = NativeMethods.KEYEVENTF.KEYUP;

Expand All @@ -248,8 +253,10 @@ public void WriteText(string characters)
var inputs = new NativeMethods.INPUT[2 * characters.Length];
for (int i = 0; i < inputs.Length; i++)
{
var ki = new NativeMethods.KEYBDINPUT();
ki.dwFlags = NativeMethods.KEYEVENTF.UNICODE;
var ki = new NativeMethods.KEYBDINPUT
{
dwFlags = NativeMethods.KEYEVENTF.UNICODE
};
if (i % 2 == 1)
ki.dwFlags |= NativeMethods.KEYEVENTF.KEYUP;
ki.wScan = (short)characters[i / 2];
Expand All @@ -271,10 +278,32 @@ public unsafe class ScreenshotContent : IScreenshotContent
{
private bool disposed;
private WindowPosition windowPosition;
private Rectangle rect;

private readonly Bitmap bmp;
private BitmapData bmpData;
private int* scan0;


private ScreenshotContent(WindowPosition pos)
{
// Ensure we use Little Endian as byte order.
// TODO: Is there a better way than using IPAddress to check this?
if (IPAddress.HostToNetworkOrder((short)1) == 1)
throw new InvalidOperationException("This class currently only works " +
"on systems using little endian as byte order.");

// Set the window position which will create a new rectangle.
this.WindowPosition = pos;

this.bmp = new Bitmap(this.rect.Width, this.rect.Height,
PixelFormat.Format32bppRgb);
}

~ScreenshotContent()
{
Dispose(false);
}


public Size Size => new Size(this.bmp.Width, this.bmp.Height);
Expand All @@ -297,10 +326,11 @@ private set
this.windowPosition.Size.Width, this.windowPosition.Size.Height);
}
}
private Rectangle rect;

public static ScreenshotContent Create(WindowPosition pos,
ScreenshotContent existingScreenshot = null)

public static ScreenshotContent Create(
WindowPosition pos,
ScreenshotContent existingScreenshot = null)
{
// Try to reuse the existing screenshot's bitmap, if it has the same size.
if (existingScreenshot != null && !(existingScreenshot.Size.Width == pos.Size.Width
Expand All @@ -322,21 +352,6 @@ public static ScreenshotContent Create(WindowPosition pos,
}


private ScreenshotContent(WindowPosition pos)
{
// Set the window position which will create a new rectangle.
this.WindowPosition = pos;

// Ensure we use Little Endian as byte order.
// TODO: Is there a better way than using IPAddress to check this?
if (IPAddress.HostToNetworkOrder((short)1) == 1)
throw new InvalidOperationException("This class currently only works "
+ "on systems using little endian as byte order.");

this.bmp = new Bitmap(this.rect.Width, this.rect.Height,
PixelFormat.Format32bppRgb);
}

private void OpenBitmapData()
{
if (this.bmpData == null)
Expand Down Expand Up @@ -374,7 +389,10 @@ private void FillScreenshot()
OpenBitmapData();
}

public ScreenshotColor GetPixel(Coordinates coords) => GetPixel(coords.X, coords.Y);
public ScreenshotColor GetPixel(Coordinates coords)
{
return GetPixel(coords.X, coords.Y);
}

public ScreenshotColor GetPixel(int x, int y)
{
Expand Down Expand Up @@ -408,12 +426,6 @@ public ScreenshotColor GetPixel(int x, int y)
};
}


~ScreenshotContent()
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
Expand All @@ -430,7 +442,6 @@ protected void Dispose(bool disposing)
this.disposed = true;
}
}



public enum VirtualKeyShort : short
Expand Down
27 changes: 24 additions & 3 deletions TTMouseclickSimulator/Core/Environment/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ internal static extern uint SendInput(uint nInputs,
[MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs,
int cbSize);

[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "PostMessageW", ExactSpelling = true, SetLastError = true)]
internal static extern IntPtr PostMessage(
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "SendMessageW", ExactSpelling = true, SetLastError = true)]
internal static extern IntPtr SendMessage(
IntPtr windowHandle,
WindowMessage message,
IntPtr wparam,
Expand Down Expand Up @@ -108,7 +108,13 @@ internal enum KEYEVENTF : uint

internal enum WindowMessage : int
{
WM_MOUSEMOVE = 0x0200
WM_MOUSEMOVE = 0x0200,
WM_LBUTTONDOWN = 0x0201,
WM_LBUTTONUP = 0x0202,

WM_ACTIVATE = 0x0006,
WM_MOUSEACTIVATE = 0x0021

}

internal const int MK_LBUTTON = 0x0001;
Expand Down Expand Up @@ -143,5 +149,20 @@ internal struct POINT

[DllImport("user32.dll")]
internal static extern bool SetForegroundWindow(IntPtr hWnd);


[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong);

[DllImport("user32.dll", EntryPoint= "SetWindowLongPtr")]
private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

public static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
{
if (IntPtr.Size == 8)
return SetWindowLongPtr64(hWnd, nIndex, dwNewLong);
else
return new IntPtr(SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32()));
}
}
}
Loading

0 comments on commit 722f4de

Please sign in to comment.