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>
This commit is contained in:
commit
8a7bdfd99f
44 files changed
+2505
No files matched your search
@@ -0,0 +1,137 @@
|
||||
# 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
|
||||
}
|
||||
Reference in new issue
Block a user