<# .SYNOPSIS Campaign V repeat-run connected gate: N full connect-render-screenshot cycles against the local ACE server, with a pass/fail verdict per run. .DESCRIPTION The V4c blank-world regression was INTERMITTENT: roughly 1 launch in 3 at the worst location, 0 in 7 at the parent commit. A single connected capture is therefore worthless as a gate for this defect class - the broken binary passes it most of the time. This script exists so no future slice can call itself connected-clean on one lucky run. Each run: launch connected -> teleloc to the pinned worst-case cell -> wait for world-visible -> settle -> screenshot -> desktop grab -> graceful close -> cooldown. The teleloc is pinned because the failure rate is location-sensitive. THE VERDICT IS THE DESKTOP GRAB, not the client's own screenshot. Campaign plan section 5.5.2 established that on a blank run the client's read of framebuffer 0 returns RGBA(0,0,0,0) in every pixel INCLUDING pixels where the UI is demonstrably on screen - so a verdict derived from screenshot bytes is reporting the readback apparatus, not the renderer. This script therefore launches the window visible, brings it to the foreground, and grabs the composited client rectangle with CopyFromScreen: a witness that shares nothing with the renderer below the compositor. The client's own capture is still recorded, as a second column, because a disagreement between the two is itself a finding. Observed magnitudes at 1280x720: a rendered desktop grab is ~1.6 MB, a blank one (atmosphere clear plus the complete retained UI, no 3-D) is ~0.23 MB, and a blank client capture is ~5 KB. One threshold separates all three. Run this with nothing else on the primary monitor: CopyFromScreen captures whatever is composited at those coordinates, so an overlapping window would be captured instead of the client. A visible foregrounded window can also take stray input, which moves the character off the pinned cell - do not type while the gate is running. Graceful close matters: a hard kill leaves ACE holding the session ~3 minutes and every subsequent run fails with 'CharacterList not received'. .PARAMETER Runs Cycles to run. Default 10. The defect's observed rate was ~1/3, so ten clean runs bound it below ~4% at 90% confidence - adequate for a re-land gate. .PARAMETER ExePath Client executable. Defaults to this repo's Release output. .PARAMETER Teleloc The pinned location, as the full /teleloc argument string. Default is the cell with the worst observed failure rate (5/5 blank at the V4c HEAD). .PARAMETER MinRenderedBytes PNG size below which an image is called blank. Applied to both the desktop grab (the verdict) and the client capture (recorded only). Default 500000. #> [CmdletBinding()] param( [int]$Runs = 10, [string]$ExePath, [string]$Teleloc = "0x2E430012 57.895 42.116 16.802 1 0 0 0", [int]$MinRenderedBytes = 500000 ) $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot if (-not $ExePath) { $ExePath = Join-Path $repo 'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe' } if (-not (Test-Path $ExePath)) { throw "Client not found at $ExePath." } # Never race a session someone is already playing: same account, same server. if (Get-Process -Name AcDream.App -ErrorAction SilentlyContinue) { throw 'AcDream.App is already running. This gate uses the shared test account and must not steal its session.' } Add-Type -AssemblyName System.Drawing Add-Type @' using System; using System.Runtime.InteropServices; public static class RepeatGateSurface { [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr hWnd, out RECT r); [DllImport("user32.dll")] public static extern bool ClientToScreen(IntPtr hWnd, ref POINT p); [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X, Y; } } '@ $root = Join-Path $env:TEMP "claude\repeat-connected-$([DateTime]::Now.ToString('HHmmss'))" New-Item -ItemType Directory -Force -Path $root | Out-Null $results = @() for ($i = 1; $i -le $Runs; $i++) { $dir = Join-Path $root "run-$i" New-Item -ItemType Directory -Force -Path $dir | Out-Null Set-Content -Encoding utf8 (Join-Path $dir 'probe.txt') -Value @" wait world-ready 90000 command /teleloc $Teleloc wait materialized 1 90000 wait world-visible 30000 sleep 15000 screenshot repeat-run 30000 sleep 20000 "@ $env:ACDREAM_DAT_DIR = Join-Path $env:USERPROFILE "Documents\Asheron's Call" $env:ACDREAM_LIVE = '1' $env:ACDREAM_TEST_HOST = '127.0.0.1' $env:ACDREAM_TEST_PORT = '9000' $env:ACDREAM_TEST_USER = 'testaccount' $env:ACDREAM_TEST_PASS = 'testpassword' $env:ACDREAM_RETAIL_UI = '1' $env:ACDREAM_UI_PROBE_SCRIPT = Join-Path $dir 'probe.txt' $env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $dir # Campaign V slice V10: deliberately cleared rather than set. The blank-world # defect this gate exists for is a property of whichever backend the client # actually starts, so the gate must launch the process default -- Vulkan # after the cutover -- and an ambient override from the caller's shell would # silently make it a GL run wearing the default's report. Remove-Item Env:\ACDREAM_RENDER_BACKEND -ErrorAction SilentlyContinue $log = Join-Path $dir 'client.log' # Normal window style: the frame must be composited on screen to be grabbed. $proc = Start-Process -FilePath $ExePath -RedirectStandardOutput $log ` -RedirectStandardError "$log.err" -PassThru $shot = Join-Path $dir 'screenshots\repeat-run.png' $grab = Join-Path $dir 'desktop-grab.png' $deadline = (Get-Date).AddSeconds(180) while ((Get-Date) -lt $deadline) { if (Test-Path $shot) { break } if ($proc.HasExited) { break } Start-Sleep -Milliseconds 500 } if (Test-Path $shot) { # The client is now in its trailing sleep, still rendering this scene, # so both images observe one steady frame. Start-Sleep -Milliseconds 800 try { $proc.Refresh() $h = $proc.MainWindowHandle if ($h -ne [IntPtr]::Zero) { [RepeatGateSurface]::ShowWindow($h, 9) | Out-Null # SW_RESTORE [RepeatGateSurface]::SetForegroundWindow($h) | Out-Null Start-Sleep -Milliseconds 1200 $r = New-Object RepeatGateSurface+RECT [RepeatGateSurface]::GetClientRect($h, [ref]$r) | Out-Null $p = New-Object RepeatGateSurface+POINT [RepeatGateSurface]::ClientToScreen($h, [ref]$p) | Out-Null $w = $r.R - $r.L; $ht = $r.B - $r.T if ($w -gt 0 -and $ht -gt 0) { $bmp = New-Object System.Drawing.Bitmap $w, $ht $g = [System.Drawing.Graphics]::FromImage($bmp) $g.CopyFromScreen($p.X, $p.Y, 0, 0, (New-Object System.Drawing.Size $w, $ht)) $bmp.Save($grab, [System.Drawing.Imaging.ImageFormat]::Png) $g.Dispose(); $bmp.Dispose() } } } catch { Write-Host "[repeat-gate] desktop grab failed: $_" } } $app = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue if ($app) { $app.CloseMainWindow() | Out-Null if (-not $app.WaitForExit(12000)) { $app | Stop-Process -Force } } $grabSize = if (Test-Path $grab) { (Get-Item $grab).Length } else { 0 } $shotSize = if (Test-Path $shot) { (Get-Item $shot).Length } else { 0 } $verdict = if ($grabSize -ge $MinRenderedBytes) { 'RENDERED' } elseif ($grabSize -gt 0) { 'BLANK' } else { 'NO-CAPTURE' } $clientView = if ($shotSize -ge $MinRenderedBytes) { 'RENDERED' } elseif ($shotSize -gt 0) { 'BLANK' } else { 'NO-CAPTURE' } $results += [pscustomobject]@{ Run = $i; Verdict = $verdict; GrabBytes = $grabSize ClientCapture = $clientView; ClientBytes = $shotSize } Write-Host ("[repeat-gate] run {0}/{1}: screen={2} ({3} B) client={4} ({5} B)" -f ` $i, $Runs, $verdict, $grabSize, $clientView, $shotSize) Start-Sleep -Seconds 10 # let ACE clear the graceful logout before the next login } Write-Host '' $blank = @($results | Where-Object Verdict -ne 'RENDERED') $results | Format-Table -AutoSize | Out-String | Write-Host $split = @($results | Where-Object { $_.Verdict -ne $_.ClientCapture }) if ($split.Count -gt 0) { Write-Host ("[repeat-gate] note: {0}/{1} runs had the two instruments disagree." -f ` $split.Count, $Runs) } if ($blank.Count -gt 0) { Write-Host ("[repeat-gate] FAILED: {0}/{1} runs did not render on the desktop witness. Artifacts: {2}" -f $blank.Count, $Runs, $root) -ForegroundColor Red exit 1 } Write-Host ("[repeat-gate] PASS: {0}/{0} runs rendered on the desktop witness. Artifacts: {1}" -f $Runs, $root) exit 0