# Drive the game for automated tests: start it, click, press keys, take # screenshots. Dot-source it: . .\tools\game-input.ps1 # # $game = Start-Sacred2 # launcher, returns the sacred2.exe process # Wait-Log "Orc initialized" -Game $game # Click-At 1720 779 # screen pixels (left click) # Press-Key 0x1B # virtual key (ESC) # Save-Screen screenshots\x.png # primary monitor # Get-GameWindows $game # visible top-level windows of the process # Stop-Sacred2 $game # the game and its local server s2gs.exe # # Input goes to the foreground window: don't touch mouse/keyboard meanwhile. Add-Type -AssemblyName System.Drawing Add-Type -Namespace Sacred2Re -Name Input -MemberDefinition @' [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); [DllImport("user32.dll")] public static extern void mouse_event(uint flags, uint dx, uint dy, uint data, System.UIntPtr extra); [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, System.UIntPtr extra); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(System.IntPtr window); public delegate bool EnumProc(System.IntPtr window, System.IntPtr param); [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc proc, System.IntPtr param); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(System.IntPtr window, out uint process); [DllImport("user32.dll")] public static extern bool IsWindowVisible(System.IntPtr window); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(System.IntPtr window, System.Text.StringBuilder text, int size); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetClassName(System.IntPtr window, System.Text.StringBuilder text, int size); [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left, Top, Right, Bottom; } [DllImport("user32.dll")] public static extern bool GetWindowRect(System.IntPtr window, out Rect rect); [DllImport("user32.dll")] public static extern int GetWindowLong(System.IntPtr window, int index); '@ $script:Root = Split-Path $PSScriptRoot $script:LogPath = Join-Path $script:Root "build\x86-debug\sacred2K.log" # Visible top-level windows of a process: handle, class, title, rect, style. function Get-GameWindows($Game) { $found = New-Object System.Collections.ArrayList $callback = [Sacred2Re.Input+EnumProc] { param($window, $param) [uint32]$process = 0 [Sacred2Re.Input]::GetWindowThreadProcessId($window, [ref]$process) | Out-Null if ($process -eq $Game.Id -and [Sacred2Re.Input]::IsWindowVisible($window)) { $title = New-Object System.Text.StringBuilder 256 $class = New-Object System.Text.StringBuilder 256 [Sacred2Re.Input]::GetWindowText($window, $title, 256) | Out-Null [Sacred2Re.Input]::GetClassName($window, $class, 256) | Out-Null $rect = New-Object Sacred2Re.Input+Rect [Sacred2Re.Input]::GetWindowRect($window, [ref]$rect) | Out-Null $style = [Sacred2Re.Input]::GetWindowLong($window, -16) [void]$found.Add([pscustomobject]@{ Handle = $window; Class = $class.ToString(); Title = $title.ToString() Rect = "$($rect.Left),$($rect.Top)-$($rect.Right),$($rect.Bottom)"; Style = ('0x{0:x8}' -f $style) }) } $true } [Sacred2Re.Input]::EnumWindows($callback, [IntPtr]::Zero) | Out-Null $found } # Windows does not hand the foreground to a window started from a background # process; a synthetic Alt press lifts that restriction for SetForegroundWindow. function Focus-Sacred2($Game) { $Game.Refresh() if (-not $Game.MainWindowHandle -or $Game.MainWindowHandle -eq [IntPtr]::Zero) { return $false } [Sacred2Re.Input]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero) [Sacred2Re.Input]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero) [Sacred2Re.Input]::SetForegroundWindow($Game.MainWindowHandle) } # Waits until sacred2K.log (written anew at every start) has at least $Count # lines containing $Pattern; then waits $AfterMs. function Wait-Log([string]$Pattern, [int]$Count = 1, [int]$TimeoutSeconds = 60, [int]$AfterMs = 500, $Game = $null) { $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { if ($Game) { if ($Game.HasExited) { throw "sacred2.exe exited (code $($Game.ExitCode)) waiting for '$Pattern'" } Focus-Sacred2 $Game | Out-Null } $found = 0 if (Test-Path $script:LogPath) { $found = @(Select-String -Path $script:LogPath -Pattern $Pattern -SimpleMatch).Count } if ($found -ge $Count) { Start-Sleep -Milliseconds $AfterMs; return } Start-Sleep -Milliseconds 200 } throw "timeout waiting for '$Pattern' in sacred2K.log" } # Starts the game through the launcher; returns the sacred2.exe process. function Start-Sacred2([string]$Arguments = "") { if (Get-Process sacred2 -ErrorAction SilentlyContinue) { throw "sacred2.exe is already running" } if (Test-Path $script:LogPath) { Remove-Item $script:LogPath -ErrorAction SilentlyContinue } $launcher = Join-Path $script:Root "build\x86-debug\sacred2K.exe" if ($Arguments) { $null = Start-Process $launcher -ArgumentList $Arguments } else { $null = Start-Process $launcher } $game = $null for ($i = 0; $i -lt 80 -and -not $game; $i++) { Start-Sleep -Milliseconds 250 $game = Get-Process sacred2 -ErrorAction SilentlyContinue | Select-Object -First 1 } if (-not $game) { throw "sacred2.exe did not start" } $game } function Stop-Sacred2($Game) { if ($Game -and -not $Game.HasExited) { Stop-Process -Id $Game.Id -Force $Game.WaitForExit(10000) | Out-Null } Get-Process s2gs -ErrorAction SilentlyContinue | Stop-Process -Force } function Click-At([int]$X, [int]$Y, [int]$AfterMs = 800) { [Sacred2Re.Input]::SetCursorPos($X, $Y) | Out-Null Start-Sleep -Milliseconds 150 [Sacred2Re.Input]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero) # left down Start-Sleep -Milliseconds 80 [Sacred2Re.Input]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero) # left up Start-Sleep -Milliseconds $AfterMs } function Press-Key([byte]$VirtualKey, [int]$AfterMs = 800) { [Sacred2Re.Input]::keybd_event($VirtualKey, 0, 0, [UIntPtr]::Zero) Start-Sleep -Milliseconds 80 [Sacred2Re.Input]::keybd_event($VirtualKey, 0, 2, [UIntPtr]::Zero) # key up Start-Sleep -Milliseconds $AfterMs } function Save-Screen([string]$Path, [int]$Width = 3440, [int]$Height = 1440) { $bitmap = New-Object System.Drawing.Bitmap $Width, $Height $graphics = [System.Drawing.Graphics]::FromImage($bitmap) $graphics.CopyFromScreen(0, 0, 0, 0, $bitmap.Size) $full = if ([IO.Path]::IsPathRooted($Path)) { $Path } else { Join-Path $script:Root $Path } New-Item -ItemType Directory -Force (Split-Path $full) | Out-Null $bitmap.Save($full) $graphics.Dispose(); $bitmap.Dispose() $full }