329 lines
14 KiB
PowerShell
329 lines
14 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$Repository = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path,
|
|
[string]$Account = $env:ACDREAM_TEST_USER,
|
|
[string]$Password = $env:ACDREAM_TEST_PASS,
|
|
[string]$ObserverAccount,
|
|
[string]$ObserverPassword,
|
|
[ValidateRange(0, [int]::MaxValue)][int]$PrimaryCharacterIndex = 0,
|
|
[ValidateRange(0, [int]::MaxValue)][int]$ObserverCharacterIndex = 1,
|
|
[switch]$SkipBuild,
|
|
[int]$LoginTimeoutSeconds = 90
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
. (Join-Path $PSScriptRoot 'connected-render-pack-gate-common.ps1')
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Account)) { $Account = 'testaccount' }
|
|
if ([string]::IsNullOrWhiteSpace($Password)) { $Password = 'testpassword' }
|
|
if ([string]::IsNullOrWhiteSpace($ObserverAccount)) { $ObserverAccount = $Account }
|
|
if ([string]::IsNullOrWhiteSpace($ObserverPassword)) { $ObserverPassword = $Password }
|
|
if ($PrimaryCharacterIndex -eq $ObserverCharacterIndex) {
|
|
throw 'Primary and observer character indexes must differ.'
|
|
}
|
|
|
|
$startedUtc = [DateTime]::UtcNow.ToString('O')
|
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$root = Join-Path $Repository `
|
|
"artifacts\atmospheric-rendering\connected-remote-player-$stamp"
|
|
$observerRoot = Join-Path $root 'observer'
|
|
$primaryRoot = Join-Path $root 'primary'
|
|
$observerRoute = Join-Path $Repository `
|
|
'tools\connected-render-pack-remote-observer.route.txt'
|
|
$primaryRoute = Join-Path $Repository `
|
|
'tools\connected-render-pack-remote-primary.route.txt'
|
|
$exe = Join-Path $Repository `
|
|
'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe'
|
|
$observerState = $null
|
|
$primaryState = $null
|
|
$observerConfig = $null
|
|
$primaryConfig = $null
|
|
$observer = $null
|
|
$primary = $null
|
|
$binaryIdentity = $null
|
|
$result = $null
|
|
$failure = $null
|
|
|
|
function Set-ClientEnvironment {
|
|
param(
|
|
[Parameter(Mandatory = $true)][object]$State,
|
|
[Parameter(Mandatory = $true)][string]$ArtifactDirectory,
|
|
[Parameter(Mandatory = $true)][string]$Route,
|
|
[Parameter(Mandatory = $true)][string]$SessionAccount,
|
|
[Parameter(Mandatory = $true)][string]$SessionPassword
|
|
)
|
|
$env:ACDREAM_CONFIG_DIR = $State.ConfigDirectory
|
|
$env:ACDREAM_DATA_DIR = $State.DataDirectory
|
|
$env:ACDREAM_CACHE_DIR = $State.CacheDirectory
|
|
$env:ACDREAM_DAT_DIR = "$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 = $SessionAccount
|
|
$env:ACDREAM_TEST_PASS = $SessionPassword
|
|
$env:ACDREAM_RETAIL_UI = '1'
|
|
$env:ACDREAM_DEVTOOLS = '0'
|
|
$env:ACDREAM_UI_PROBE_DUMP = '0'
|
|
$env:ACDREAM_UI_PROBE_SCRIPT = $Route
|
|
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $ArtifactDirectory
|
|
$env:ACDREAM_NO_AUDIO = '1'
|
|
}
|
|
|
|
function Wait-ForLogPattern {
|
|
param(
|
|
[Parameter(Mandatory = $true)][Diagnostics.Process]$Client,
|
|
[Parameter(Mandatory = $true)][string]$Path,
|
|
[Parameter(Mandatory = $true)][string]$Pattern,
|
|
[Parameter(Mandatory = $true)][int]$TimeoutSeconds
|
|
)
|
|
$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
|
|
while ([DateTime]::UtcNow -lt $deadline) {
|
|
$Client.Refresh()
|
|
if ($Client.HasExited) {
|
|
throw "client exited with code $($Client.ExitCode) while waiting for '$Pattern'"
|
|
}
|
|
if (Test-Path -LiteralPath $Path) {
|
|
$sessionFailure = Select-String -LiteralPath $Path `
|
|
-SimpleMatch 'live: session failed:' | Select-Object -Last 1
|
|
if ($null -ne $sessionFailure) {
|
|
throw "client session failed while waiting for '$Pattern': $($sessionFailure.Line)"
|
|
}
|
|
if (Select-String -LiteralPath $Path -SimpleMatch $Pattern -Quiet) {
|
|
return
|
|
}
|
|
}
|
|
Start-Sleep -Milliseconds 250
|
|
}
|
|
throw "timed out after $TimeoutSeconds seconds waiting for '$Pattern'"
|
|
}
|
|
|
|
function Close-ClientGracefully {
|
|
param([Parameter(Mandatory = $true)][Diagnostics.Process]$Client)
|
|
$Client.Refresh()
|
|
if ($Client.HasExited) { return }
|
|
if (-not $Client.CloseMainWindow() -or -not $Client.WaitForExit(30000)) {
|
|
throw "client $($Client.Id) did not accept graceful close within 30 seconds"
|
|
}
|
|
$Client.WaitForExit()
|
|
}
|
|
|
|
function Stop-ExactClient {
|
|
param([Diagnostics.Process]$Client)
|
|
if ($null -eq $Client) { return }
|
|
$Client.Refresh()
|
|
if (-not $Client.HasExited) {
|
|
$closed = $Client.CloseMainWindow()
|
|
if (-not $closed -or -not $Client.WaitForExit(10000)) {
|
|
Stop-Process -Id $Client.Id -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
function Read-StatusEvents {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
throw "session status stream is missing: '$Path'"
|
|
}
|
|
return @(Get-Content -LiteralPath $Path | ForEach-Object {
|
|
$_ | ConvertFrom-Json
|
|
})
|
|
}
|
|
|
|
function Assert-TerminalStatus {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
$events = @(Read-StatusEvents $Path)
|
|
foreach ($name in @('started', 'connected', 'characterList', 'enteredWorld',
|
|
'disconnected', 'exited')) {
|
|
$matching = @($events | Where-Object { $_.e -ceq $name })
|
|
if ($matching.Count -ne 1) {
|
|
throw "'$Path' expected one '$name' event, observed $($matching.Count)"
|
|
}
|
|
}
|
|
$entered = @($events | Where-Object { $_.e -ceq 'enteredWorld' })[0]
|
|
$exited = @($events | Where-Object { $_.e -ceq 'exited' })[0]
|
|
if ([int]$exited.code -ne 0) {
|
|
throw "'$Path' terminal status code was $($exited.code)"
|
|
}
|
|
return $entered
|
|
}
|
|
|
|
function Read-RemoteMetadata {
|
|
param([Parameter(Mandatory = $true)][string]$Name)
|
|
Assert-ConnectedGateSafeLeafName $Name
|
|
$path = Join-Path $primaryRoot "screenshots\$Name.metadata.json"
|
|
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
|
throw "remote-player metadata is missing: '$path'"
|
|
}
|
|
$metadata = Get-Content -Raw -LiteralPath $path | ConvertFrom-Json
|
|
$pack = $metadata.RenderPack
|
|
$classes = $pack.ShadowTransformChurn.CasterClasses
|
|
if ([int]$metadata.SchemaVersion -ne 1 -or [int]$pack.State -ne 2 -or
|
|
[string]$pack.PackId -cne 'acdream.atmospheric' -or
|
|
[string]$pack.PresetId -cne 'medium') {
|
|
throw "'$Name' did not capture active Atmospheric Medium metadata"
|
|
}
|
|
if (-not [bool]$pack.Outdoor -or [double]$pack.DirectionalShadowStrength -le 0) {
|
|
throw "'$Name' did not capture an active outdoor directional-shadow frame"
|
|
}
|
|
if ([int]$classes.LocalPlayers -ne 1 -or [int]$classes.RemotePlayers -lt 1) {
|
|
throw "'$Name' caster classes were local=$($classes.LocalPlayers), remote=$($classes.RemotePlayers)"
|
|
}
|
|
if ([int]$pack.ShadowCasterCount -le 0 -or [int]$pack.CascadeDrawCount -ne 3) {
|
|
throw "'$Name' did not record complete Medium shadow work"
|
|
}
|
|
return [pscustomobject][ordered]@{
|
|
Name = $Name
|
|
Path = $path
|
|
PackId = $pack.PackId
|
|
PresetId = $pack.PresetId
|
|
ShadowCasterCount = $pack.ShadowCasterCount
|
|
CascadeDrawCount = $pack.CascadeDrawCount
|
|
LocalPlayers = $classes.LocalPlayers
|
|
RemotePlayers = $classes.RemotePlayers
|
|
NonPlayerCreatures = $classes.NonPlayerCreatures
|
|
EquippedChildren = $classes.EquippedChildren
|
|
DirectionalShadowSourceKind = $pack.DirectionalShadowSourceKind
|
|
DirectionalShadowStrength = $pack.DirectionalShadowStrength
|
|
RemoteRootChanges = $pack.ShadowTransformChurn.LiveDynamicRootChanges
|
|
SharedWorldTransformUsedInstances = $pack.SharedWorldTransformUsedInstances
|
|
}
|
|
}
|
|
|
|
if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0) {
|
|
throw 'an AcDream.App client is already running; close it gracefully before the two-client gate'
|
|
}
|
|
if (@(Get-NetUDPEndpoint -LocalPort 9000 -ErrorAction SilentlyContinue).Count -eq 0) {
|
|
throw 'local ACE is not listening on UDP port 9000'
|
|
}
|
|
foreach ($required in @($observerRoute, $primaryRoute)) {
|
|
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
|
|
throw "route file not found: '$required'"
|
|
}
|
|
}
|
|
if (-not $SkipBuild) {
|
|
Write-Output 'Building Release solution...'
|
|
& dotnet build (Join-Path $Repository 'AcDream.slnx') -c Release --no-restore
|
|
if ($LASTEXITCODE -ne 0) { throw "Release build failed with exit code $LASTEXITCODE" }
|
|
}
|
|
if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) {
|
|
throw "client executable not found: '$exe'"
|
|
}
|
|
|
|
$null = New-Item -ItemType Directory -Path $observerRoot, $primaryRoot
|
|
foreach ($path in @($root, $observerRoot, $primaryRoot)) {
|
|
Assert-ConnectedGateNoReparsePoint $path
|
|
}
|
|
$observerState = New-ConnectedRenderPackGateState -Root $observerRoot -Preset medium
|
|
$primaryState = New-ConnectedRenderPackGateState -Root $primaryRoot -Preset medium
|
|
try {
|
|
$binaryIdentity = Get-ConnectedGateBinaryIdentity `
|
|
-Repository $Repository -Executable $exe -SkipBuild:$SkipBuild
|
|
$observerStatus = Join-Path $observerState.StateDirectory 'status\observer.jsonl'
|
|
$primaryStatus = Join-Path $primaryState.StateDirectory 'status\primary.jsonl'
|
|
$observerConfig = New-ConnectedGraphicalSessionConfig `
|
|
-State $observerState -Account $ObserverAccount `
|
|
-CharacterIndex $ObserverCharacterIndex -Plugins @() `
|
|
-StatusFile $observerStatus
|
|
$primaryConfig = New-ConnectedGraphicalSessionConfig `
|
|
-State $primaryState -Account $Account `
|
|
-CharacterIndex $PrimaryCharacterIndex -Plugins @() `
|
|
-StatusFile $primaryStatus
|
|
|
|
$observerStdout = Join-Path $observerRoot 'client.stdout.log'
|
|
$observerStderr = Join-Path $observerRoot 'client.stderr.log'
|
|
Set-ClientEnvironment $observerState $observerRoot $observerRoute `
|
|
$ObserverAccount $ObserverPassword
|
|
$observer = Start-Process -FilePath $exe -WorkingDirectory $Repository `
|
|
-ArgumentList @('--session-config', ('"' + $observerConfig + '"')) `
|
|
-RedirectStandardOutput $observerStdout `
|
|
-RedirectStandardError $observerStderr -PassThru
|
|
Wait-ForLogPattern $observer $observerStdout 'live: in world' $LoginTimeoutSeconds
|
|
Wait-ForLogPattern $observer $observerStdout 'live: teleport materialized' 90
|
|
|
|
$primaryStdout = Join-Path $primaryRoot 'client.stdout.log'
|
|
$primaryStderr = Join-Path $primaryRoot 'client.stderr.log'
|
|
Set-ClientEnvironment $primaryState $primaryRoot $primaryRoute `
|
|
$Account $Password
|
|
$primary = Start-Process -FilePath $exe -WorkingDirectory $Repository `
|
|
-ArgumentList @('--session-config', ('"' + $primaryConfig + '"')) `
|
|
-RedirectStandardOutput $primaryStdout `
|
|
-RedirectStandardError $primaryStderr -PassThru
|
|
Wait-ForLogPattern $primary $primaryStdout 'live: in world' $LoginTimeoutSeconds
|
|
Wait-ForLogPattern $primary $primaryStdout `
|
|
'[world-gate] screenshot-complete name=remote-player-after' 120
|
|
Wait-ForLogPattern $primary $primaryStdout `
|
|
'[world-gate] checkpoint name=remote-primary' 30
|
|
|
|
Close-ClientGracefully $primary
|
|
Close-ClientGracefully $observer
|
|
if ($primary.ExitCode -ne 0 -or $observer.ExitCode -ne 0) {
|
|
throw "client exit codes were primary=$($primary.ExitCode), observer=$($observer.ExitCode)"
|
|
}
|
|
$primaryEntered = Assert-TerminalStatus $primaryStatus
|
|
$observerEntered = Assert-TerminalStatus $observerStatus
|
|
if ([uint32]$primaryEntered.characterId -eq [uint32]$observerEntered.characterId) {
|
|
throw 'both clients entered world with the same character identity'
|
|
}
|
|
$before = Read-RemoteMetadata 'remote-player-before'
|
|
$after = Read-RemoteMetadata 'remote-player-after'
|
|
$result = [pscustomobject][ordered]@{
|
|
Passed = $true
|
|
PrimaryProcessId = $primary.Id
|
|
ObserverProcessId = $observer.Id
|
|
PrimaryCharacterId = $primaryEntered.characterId
|
|
ObserverCharacterId = $observerEntered.characterId
|
|
PrimaryExitCode = $primary.ExitCode
|
|
ObserverExitCode = $observer.ExitCode
|
|
Before = $before
|
|
After = $after
|
|
}
|
|
Write-Output (
|
|
"PASS remote-player casters before=$($before.RemotePlayers) " +
|
|
"after=$($after.RemotePlayers) primary=0x$('{0:X8}' -f [uint32]$primaryEntered.characterId) " +
|
|
"observer=0x$('{0:X8}' -f [uint32]$observerEntered.characterId)")
|
|
}
|
|
catch {
|
|
$failure = $_.Exception.ToString()
|
|
}
|
|
finally {
|
|
Stop-ExactClient $primary
|
|
Stop-ExactClient $observer
|
|
if ($null -ne $primary) { $primary.Dispose() }
|
|
if ($null -ne $observer) { $observer.Dispose() }
|
|
if ($null -ne $primaryConfig -and $null -ne $primaryState) {
|
|
Remove-ConnectedGraphicalSessionConfig -State $primaryState -Path $primaryConfig
|
|
}
|
|
if ($null -ne $observerConfig -and $null -ne $observerState) {
|
|
Remove-ConnectedGraphicalSessionConfig -State $observerState -Path $observerConfig
|
|
}
|
|
if ($null -ne $primaryState) {
|
|
Restore-ConnectedRenderPackGateEnvironment $primaryState
|
|
}
|
|
if ($null -ne $observerState) {
|
|
Restore-ConnectedRenderPackGateEnvironment $observerState
|
|
}
|
|
$report = [pscustomobject][ordered]@{
|
|
Passed = $null -ne $result -and [string]::IsNullOrWhiteSpace($failure)
|
|
StartedUtc = $startedUtc
|
|
FinishedUtc = [DateTime]::UtcNow.ToString('O')
|
|
SourceCommit = if ($null -eq $binaryIdentity) { $null } else { $binaryIdentity.SourceCommit }
|
|
BinaryCommit = if ($null -eq $binaryIdentity) { $null } else { $binaryIdentity.BinaryCommit }
|
|
BinaryMatchesSource = if ($null -eq $binaryIdentity) { $false } else { $binaryIdentity.BinaryMatchesSource }
|
|
TrackedSourceStatus = if ($null -eq $binaryIdentity) { @() } else { @($binaryIdentity.SourceTrackedStatus) }
|
|
PrimaryCharacterIndex = $PrimaryCharacterIndex
|
|
ObserverCharacterIndex = $ObserverCharacterIndex
|
|
DistinctAccountsConfigured = -not [string]::Equals(
|
|
$Account, $ObserverAccount, [StringComparison]::OrdinalIgnoreCase)
|
|
Result = $result
|
|
Failure = $failure
|
|
}
|
|
$reportPath = Join-Path $root 'report.json'
|
|
$report | ConvertTo-Json -Depth 10 |
|
|
Set-Content -Encoding utf8 -LiteralPath $reportPath
|
|
Write-Output "REPORT=$reportPath"
|
|
Write-Output "RESULT=$(if ($report.Passed) { 'PASS' } else { 'FAIL' })"
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($failure)) { throw $failure }
|
|
if ($null -eq $result) { throw 'remote-player gate produced no result' }
|