Files
sacred2_k/tools/game-input.ps1
T
khallaandClaude Opus 5.5 8a7bdfd99f Framework for Sacred 2 Gold, game log mirror, borderless fullscreen
Ported from sacredK with module-relative addresses, hooks by exported
name and a SHA-256 check of the six game modules (Community Patch
2.65.1). Borderless windowed at monitor size replaces exclusive
fullscreen, whose focus loss during loading made the game quit and
crash on exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:39:44 +01:00

138 lines
7.0 KiB
PowerShell

# 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
}