Files
sacred_k/tools/game-input.ps1
T
2026-10-25 18:24:29 +01:00

126 lines
5.8 KiB
PowerShell

# Drive the game for automated tests: start it, click, press keys, take
# screenshots. Dot-source it: . .\tools\game-input.ps1
#
# Start-Sacred # launcher, returns the sacred.exe process
# Click-At 1720 779 # screen pixels (left click)
# Press-Key 0x1B # virtual key (ESC)
# Hold-Key 0x27 -Ms 1500 # hold a key (right arrow)
# Scroll-Wheel -3 # mouse wheel, 3 notches down
# Save-Screen screenshots\x.png # primary monitor area of the game
# Stop-Sacred $game # only the given process
#
# Input goes to the foreground window: don't touch mouse/keyboard meanwhile.
Add-Type -AssemblyName System.Drawing
Add-Type -Namespace SacredRe -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);
'@
$script:Root = Split-Path $PSScriptRoot
# The game pauses its start-up until its window is active, and 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-Sacred($Game) {
$Game.Refresh()
if (-not $Game.MainWindowHandle -or $Game.MainWindowHandle -eq [IntPtr]::Zero) { return $false }
[SacredRe.Input]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero)
[SacredRe.Input]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
[SacredRe.Input]::SetForegroundWindow($Game.MainWindowHandle)
}
$script:LogPath = Join-Path $script:Root "build\x86-debug\sacredK.log"
# Waits until sacredK.log (written anew at every start) has at least $Count
# lines containing $Pattern, focusing the game meanwhile; 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) { Focus-Sacred $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 sacredK.log"
}
# Starts the game and returns once its main menu is up (the UI manager
# starts its menu thread a second time when the menu appears).
function Start-Sacred([string]$Arguments = "log") {
if (Get-Process sacred -ErrorAction SilentlyContinue) { throw "sacred.exe is already running" }
if (Test-Path $script:LogPath) { Remove-Item $script:LogPath -ErrorAction SilentlyContinue }
$null = Start-Process (Join-Path $script:Root "build\x86-debug\sacredK.exe") -ArgumentList $Arguments
$game = $null
for ($i = 0; $i -lt 80 -and -not $game; $i++) {
Start-Sleep -Milliseconds 250
$game = Get-Process sacred -ErrorAction SilentlyContinue | Select-Object -First 1
}
if (-not $game) { throw "sacred.exe did not start" }
Wait-Log "starting menu thread!" -Count 2 -AfterMs 1500 -Game $game
if ($game.HasExited) { throw "sacred.exe crashed" }
$game
}
function Stop-Sacred($Game) {
if ($Game -and -not $Game.HasExited) {
Stop-Process -Id $Game.Id -Force
$Game.WaitForExit(10000) | Out-Null
}
}
function Click-At([int]$X, [int]$Y, [int]$AfterMs = 800) {
[SacredRe.Input]::SetCursorPos($X, $Y) | Out-Null
Start-Sleep -Milliseconds 150
[SacredRe.Input]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero) # left down
Start-Sleep -Milliseconds 80
[SacredRe.Input]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero) # left up
Start-Sleep -Milliseconds $AfterMs
}
function Press-Key([byte]$VirtualKey, [int]$AfterMs = 800) {
[SacredRe.Input]::keybd_event($VirtualKey, 0, 0, [UIntPtr]::Zero)
Start-Sleep -Milliseconds 80
[SacredRe.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 }
$bitmap.Save($full)
$graphics.Dispose(); $bitmap.Dispose()
$full
}
# Moves the cursor in small steps (the game only sees real mouse movement).
function Move-To([int]$X, [int]$Y, [int]$AfterMs = 800) {
[SacredRe.Input]::SetCursorPos($X - 20, $Y - 20) | Out-Null
for ($i = 1; $i -le 10; $i++) {
Start-Sleep -Milliseconds 20
[SacredRe.Input]::SetCursorPos($X - 20 + 2 * $i, $Y - 20 + 2 * $i) | Out-Null
}
Start-Sleep -Milliseconds $AfterMs
}
# Holds a key down for a while (polled keys such as movement need this).
function Hold-Key([byte]$VirtualKey, [int]$Ms = 1500, [int]$AfterMs = 500) {
[SacredRe.Input]::keybd_event($VirtualKey, 0, 0, [UIntPtr]::Zero)
Start-Sleep -Milliseconds $Ms
[SacredRe.Input]::keybd_event($VirtualKey, 0, 2, [UIntPtr]::Zero)
Start-Sleep -Milliseconds $AfterMs
}
# Turns the mouse wheel by $Notches (negative: towards the user, "down").
function Scroll-Wheel([int]$Notches, [int]$AfterMs = 500) {
[SacredRe.Input]::mouse_event(0x0800, 0, 0, [BitConverter]::ToUInt32([BitConverter]::GetBytes([int]($Notches * 120)), 0), [UIntPtr]::Zero)
Start-Sleep -Milliseconds $AfterMs
}