699 lines
30 KiB
PowerShell
699 lines
30 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]$AceLogPath = 'C:\ACE\Server\ACE_Log.txt',
|
|
[switch]$SkipBuild,
|
|
[int]$SessionTimeoutSeconds = 420,
|
|
[int]$CollisionShadowEvery = 0,
|
|
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
|
|
[string]$RenderPackPreset = 'retail',
|
|
[hashtable]$RenderPackSettingOverrides = @{}
|
|
)
|
|
|
|
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' }
|
|
|
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$root = Join-Path $Repository "logs\connected-world-gate-$stamp"
|
|
$null = New-Item -ItemType Directory -Force -Path $root
|
|
$reportPath = Join-Path $root 'report.json'
|
|
$exe = Join-Path $Repository 'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe'
|
|
$failures = [System.Collections.Generic.List[string]]::new()
|
|
$warnings = [System.Collections.Generic.List[string]]::new()
|
|
$sessions = [System.Collections.Generic.List[object]]::new()
|
|
$startedUtc = [DateTime]::UtcNow
|
|
|
|
function Get-PatternCount([string]$Path, [string]$Pattern) {
|
|
if (-not (Test-Path -LiteralPath $Path)) { return 0 }
|
|
return @(Get-Content -LiteralPath $Path -ErrorAction SilentlyContinue |
|
|
Select-String -SimpleMatch $Pattern).Count
|
|
}
|
|
|
|
function Wait-ForPattern(
|
|
[Diagnostics.Process]$Client,
|
|
[string]$Path,
|
|
[string]$Pattern,
|
|
[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 ((Get-PatternCount $Path $Pattern) -gt 0) { return }
|
|
Start-Sleep -Milliseconds 250
|
|
}
|
|
throw "timed out after $TimeoutSeconds seconds waiting for '$Pattern'"
|
|
}
|
|
|
|
function Wait-ForFileAppendPattern(
|
|
[string]$Path,
|
|
[long]$StartOffset,
|
|
[string]$Pattern,
|
|
[int]$TimeoutSeconds)
|
|
{
|
|
$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
|
|
while ([DateTime]::UtcNow -lt $deadline) {
|
|
if (Test-Path -LiteralPath $Path) {
|
|
$stream = [System.IO.File]::Open(
|
|
$Path,
|
|
[System.IO.FileMode]::Open,
|
|
[System.IO.FileAccess]::Read,
|
|
[System.IO.FileShare]::ReadWrite)
|
|
try {
|
|
if ($stream.Length -gt $StartOffset) {
|
|
$null = $stream.Seek($StartOffset, [System.IO.SeekOrigin]::Begin)
|
|
$reader = [System.IO.StreamReader]::new($stream)
|
|
try { $appended = $reader.ReadToEnd() }
|
|
finally { $reader.Dispose() }
|
|
if ([Text.RegularExpressions.Regex]::IsMatch(
|
|
$appended,
|
|
$Pattern,
|
|
[Text.RegularExpressions.RegexOptions]::CultureInvariant)) { return }
|
|
}
|
|
}
|
|
finally { $stream.Dispose() }
|
|
}
|
|
Start-Sleep -Milliseconds 100
|
|
}
|
|
throw "timed out after $TimeoutSeconds seconds waiting for ACE log '$Pattern'"
|
|
}
|
|
|
|
function Close-ClientGracefully([Diagnostics.Process]$Client) {
|
|
$Client.Refresh()
|
|
if ($Client.HasExited) { return $true }
|
|
if (-not $Client.CloseMainWindow()) { return $false }
|
|
if (-not $Client.WaitForExit(45000)) { return $false }
|
|
$Client.WaitForExit()
|
|
return $true
|
|
}
|
|
|
|
function Test-Png([string]$Path) {
|
|
if (-not (Test-Path -LiteralPath $Path)) { return $false }
|
|
$info = Get-Item -LiteralPath $Path
|
|
if ($info.Length -lt 1024) { return $false }
|
|
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
|
if ($bytes.Length -lt 8) { return $false }
|
|
$signature = @(137, 80, 78, 71, 13, 10, 26, 10)
|
|
for ($i = 0; $i -lt $signature.Count; $i++) {
|
|
if ($bytes[$i] -ne $signature[$i]) { return $false }
|
|
}
|
|
return $true
|
|
}
|
|
|
|
function Add-LogFailures([string]$Label, [string]$Stdout, [string]$Stderr) {
|
|
$fatalPatterns = @(
|
|
'event=invariant-failure',
|
|
'Unhandled exception',
|
|
'AccessViolation',
|
|
'OutOfMemoryException',
|
|
'WeenieError',
|
|
'device removed',
|
|
'GPU reset',
|
|
'live: disconnected',
|
|
'[shutdown]',
|
|
'ObjectDisposedException',
|
|
'screenshot-failed',
|
|
'graceful logout confirmation timed out',
|
|
'graceful logout failed',
|
|
'transport disconnect failed'
|
|
)
|
|
foreach ($pattern in $fatalPatterns) {
|
|
$count = (Get-PatternCount $Stdout $pattern) + (Get-PatternCount $Stderr $pattern)
|
|
if ($count -gt 0) { $failures.Add("${Label}: '$pattern' appeared $count time(s)") }
|
|
}
|
|
|
|
$missingLandblocks = Get-PatternCount $Stdout 'LandblockLoader.Load returned null'
|
|
if ($missingLandblocks -gt 0) {
|
|
$warnings.Add("${Label}: $missingLandblocks expected world-edge landblock miss(es)")
|
|
}
|
|
}
|
|
|
|
function Read-Checkpoints([string]$Path) {
|
|
if (-not (Test-Path -LiteralPath $Path)) { return @() }
|
|
return @(Get-Content -LiteralPath $Path | ForEach-Object { $_ | ConvertFrom-Json })
|
|
}
|
|
|
|
function Validate-Checkpoint([string]$SessionLabel, [object]$Checkpoint) {
|
|
$name = $Checkpoint.name
|
|
$reveal = $Checkpoint.reveal
|
|
$environmentOwnership = $Checkpoint.environmentOwnership
|
|
$transitOwnership = $Checkpoint.transitOwnership
|
|
$resources = $Checkpoint.resources
|
|
if (-not $reveal.readiness.isReady) {
|
|
$failures.Add("${SessionLabel}/${name}: reveal was not ready")
|
|
}
|
|
if (-not $reveal.worldViewportObserved) {
|
|
$failures.Add("${SessionLabel}/${name}: normal world viewport was never observed")
|
|
}
|
|
if ($reveal.invariantFailureCount -ne 0) {
|
|
$failures.Add("${SessionLabel}/${name}: reveal has $($reveal.invariantFailureCount) invariant failure(s)")
|
|
}
|
|
if (-not $reveal.readiness.isUnhydratable) {
|
|
if (-not $reveal.readiness.isRenderNeighborhoodReady) {
|
|
$failures.Add("${SessionLabel}/${name}: render neighborhood was not ready")
|
|
}
|
|
if (-not $reveal.readiness.areCompositeTexturesReady) {
|
|
$failures.Add("${SessionLabel}/${name}: composite textures were not ready")
|
|
}
|
|
if (-not $reveal.readiness.isCollisionReady) {
|
|
$failures.Add("${SessionLabel}/${name}: collision was not ready")
|
|
}
|
|
}
|
|
if (-not $environmentOwnership.isInitialized) {
|
|
$failures.Add("${SessionLabel}/${name}: Runtime world environment is not initialized")
|
|
}
|
|
if (($environmentOwnership.dayGroupDefinitionCount -le 0) -or
|
|
($environmentOwnership.activeDayGroupCount -ne 1)) {
|
|
$failures.Add((
|
|
"${SessionLabel}/${name}: Runtime environment ownership is {0}/{1}, expected definitions with one active group" -f
|
|
$environmentOwnership.dayGroupDefinitionCount,
|
|
$environmentOwnership.activeDayGroupCount))
|
|
}
|
|
foreach ($field in @(
|
|
'bufferedTeleportDestinationCount',
|
|
'pendingTeleportStartCount',
|
|
'activeTeleportCount',
|
|
'acceptedTeleportDestinationCount',
|
|
'activeRevealCount',
|
|
'pendingDestinationReadinessCount',
|
|
'hostProjectionCount',
|
|
'pendingHostAcknowledgementCount')) {
|
|
if ([int]$transitOwnership.$field -ne 0) {
|
|
$failures.Add((
|
|
"${SessionLabel}/${name}: transitOwnership.$field={0}, expected zero at a stable checkpoint" -f
|
|
$transitOwnership.$field))
|
|
}
|
|
}
|
|
if ($resources.pendingLiveTeardowns -ne 0) {
|
|
$failures.Add("${SessionLabel}/${name}: $($resources.pendingLiveTeardowns) live teardown(s) pending")
|
|
}
|
|
if ($resources.pendingLandblockRetirements -ne 0) {
|
|
$failures.Add("${SessionLabel}/${name}: $($resources.pendingLandblockRetirements) landblock retirement(s) pending")
|
|
}
|
|
if ($resources.stagedMeshUploads -ne 0) {
|
|
$failures.Add("${SessionLabel}/${name}: $($resources.stagedMeshUploads) staged mesh upload(s) remain at stable checkpoint")
|
|
}
|
|
if ($resources.compositeWarmupPending -ne 0) {
|
|
$failures.Add("${SessionLabel}/${name}: $($resources.compositeWarmupPending) composite warmup item(s) remain")
|
|
}
|
|
if ($resources.loadedLandblocks -le 0 -or $resources.worldEntities -le 0) {
|
|
$failures.Add("${SessionLabel}/${name}: world ownership is empty at a visible checkpoint")
|
|
}
|
|
if ($null -eq $resources.lastFrameProfile) {
|
|
$failures.Add("${SessionLabel}/${name}: no frame-profiler sample was available")
|
|
}
|
|
if ($CollisionShadowEvery -gt 0) {
|
|
$shadow = $resources.PSObject.Properties['collisionShadow']
|
|
if ($null -eq $shadow) {
|
|
$failures.Add("${SessionLabel}/${name}: collision shadow counters are missing")
|
|
}
|
|
else {
|
|
foreach ($field in @('mismatches', 'faults')) {
|
|
$property = $shadow.Value.PSObject.Properties[$field]
|
|
if ($null -eq $property) {
|
|
$failures.Add("${SessionLabel}/${name}: collisionShadow.$field is missing")
|
|
}
|
|
elseif ([long]$property.Value -ne 0) {
|
|
$failures.Add(
|
|
"${SessionLabel}/${name}: collisionShadow.$field=$($property.Value), expected zero")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Invoke-Session(
|
|
[string]$Label,
|
|
[string]$RoutePath,
|
|
[bool]$Uncapped,
|
|
[string[]]$ExpectedCheckpoints,
|
|
[string[]]$ExpectedScreenshots,
|
|
[hashtable]$ScreenshotStateOverrides = @{})
|
|
{
|
|
$sessionDir = Join-Path $root $Label
|
|
$artifactDir = Join-Path $sessionDir 'artifacts'
|
|
$null = New-Item -ItemType Directory -Force -Path $artifactDir
|
|
$stdout = Join-Path $sessionDir 'stdout.log'
|
|
$stderr = Join-Path $sessionDir 'stderr.log'
|
|
$timeline = Join-Path $artifactDir 'world-lifecycle.checkpoints.jsonl'
|
|
$collisionShadowDir = Join-Path $artifactDir 'collision-shadow'
|
|
$client = $null
|
|
$clientPort = $null
|
|
$graceful = $false
|
|
$exitCode = $null
|
|
$elapsed = [Diagnostics.Stopwatch]::StartNew()
|
|
$aceLogOffset = (Get-Item -LiteralPath $AceLogPath).Length
|
|
|
|
$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 = $Account
|
|
$env:ACDREAM_TEST_PASS = $Password
|
|
$env:ACDREAM_RETAIL_UI = '1'
|
|
$env:ACDREAM_FRAME_PROF = '1'
|
|
$env:ACDREAM_UNCAPPED_RENDER = if ($Uncapped) { '1' } else { $null }
|
|
$env:ACDREAM_DEVTOOLS = '0'
|
|
$env:ACDREAM_UI_PROBE_DUMP = '0'
|
|
$env:ACDREAM_UI_PROBE_SCRIPT = $RoutePath
|
|
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $artifactDir
|
|
$env:ACDREAM_DUMP_MOVE_TRUTH = $null
|
|
$env:ACDREAM_WB_DIAG = $null
|
|
# Campaign V slice V10 cleared this rather than setting it, because the route
|
|
# is renderer-agnostic and its job after the cutover was to exercise whatever
|
|
# backend the process defaults to -- which was Vulkan even before Campaign V
|
|
# slice V11 deleted the GL arm this variable used to select. The clear stays
|
|
# harmless now that nothing reads it: cleared the same way every other
|
|
# unwanted knob here is, for a variable that is never set by this gate again.
|
|
$env:ACDREAM_RENDER_BACKEND = $null
|
|
# N5: this gate is the DECORATOR-ABSENT baseline -- a leaked
|
|
# ACDREAM_NET_DROP_PCT from a loss-gate run in the same shell must never
|
|
# inject loss here. Cleared like every other unwanted knob above.
|
|
$env:ACDREAM_NET_DROP_PCT = $null
|
|
$env:ACDREAM_NET_DROP_SEED = $null
|
|
$env:ACDREAM_NET_DROP_DIR = $null
|
|
$env:ACDREAM_COLLISION_SHADOW_EVERY =
|
|
if ($CollisionShadowEvery -gt 0) { "$CollisionShadowEvery" } else { $null }
|
|
$env:ACDREAM_COLLISION_SHADOW_DIR =
|
|
if ($CollisionShadowEvery -gt 0) { $collisionShadowDir } else { $null }
|
|
|
|
try {
|
|
$client = Start-Process -FilePath $exe -WorkingDirectory $Repository `
|
|
-ArgumentList @('--session-config', ('"' + $sessionConfigPath + '"')) `
|
|
-RedirectStandardOutput $stdout -RedirectStandardError $stderr -PassThru
|
|
Wait-ForPattern $client $stdout '[UI-PROBE] UI probe script complete' $SessionTimeoutSeconds
|
|
|
|
$clientPorts = @(Get-NetUDPEndpoint -OwningProcess $client.Id -ErrorAction SilentlyContinue |
|
|
Select-Object -First 1 -ExpandProperty LocalPort)
|
|
if ($clientPorts.Count -ne 1) {
|
|
throw "could not resolve the client's UDP endpoint for ACE disconnect verification"
|
|
}
|
|
$clientPort = [int]$clientPorts[0]
|
|
|
|
$client.Refresh()
|
|
$processSample = [pscustomobject][ordered]@{
|
|
ProcessId = $client.Id
|
|
StartTimeUtc = $client.StartTime.ToUniversalTime().ToString('O')
|
|
WorkingSetMiB = [Math]::Round($client.WorkingSet64 / 1MB, 1)
|
|
PrivateMiB = [Math]::Round($client.PrivateMemorySize64 / 1MB, 1)
|
|
HandleCount = $client.HandleCount
|
|
ThreadCount = $client.Threads.Count
|
|
WindowTitle = $client.MainWindowTitle
|
|
}
|
|
|
|
$checkpoints = @(Read-Checkpoints $timeline)
|
|
if ($checkpoints.Count -ne $ExpectedCheckpoints.Count) {
|
|
$failures.Add("${Label}: expected $($ExpectedCheckpoints.Count) checkpoints, found $($checkpoints.Count)")
|
|
}
|
|
foreach ($name in $ExpectedCheckpoints) {
|
|
$matches = @($checkpoints | Where-Object { $_.name -eq $name })
|
|
if ($matches.Count -ne 1) {
|
|
$failures.Add("${Label}: expected one checkpoint '$name', found $($matches.Count)")
|
|
}
|
|
}
|
|
foreach ($checkpoint in $checkpoints) { Validate-Checkpoint $Label $checkpoint }
|
|
if ($CollisionShadowEvery -gt 0 -and $checkpoints.Count -gt 0) {
|
|
$lastShadow = $checkpoints[-1].resources.PSObject.Properties['collisionShadow']
|
|
$samples = if ($null -ne $lastShadow) {
|
|
$lastShadow.Value.PSObject.Properties['samples']
|
|
}
|
|
if ($null -eq $samples -or [long]$samples.Value -le 0) {
|
|
$failures.Add("${Label}: collision shadow did not sample any query")
|
|
}
|
|
$mismatchArtifacts = @(
|
|
Get-ChildItem -LiteralPath $collisionShadowDir `
|
|
-Filter 'collision-shadow-*.json' `
|
|
-File `
|
|
-ErrorAction SilentlyContinue)
|
|
if ($mismatchArtifacts.Count -ne 0) {
|
|
$failures.Add(
|
|
"${Label}: collision shadow emitted $($mismatchArtifacts.Count) mismatch artifact(s)")
|
|
}
|
|
}
|
|
|
|
foreach ($name in $ExpectedScreenshots) {
|
|
$png = Join-Path $artifactDir "screenshots\$name.png"
|
|
if (-not (Test-Png $png)) { $failures.Add("${Label}: missing or invalid screenshot '$png'") }
|
|
}
|
|
foreach ($name in $ExpectedScreenshots) {
|
|
$expectedState = if ($ScreenshotStateOverrides.ContainsKey($name)) {
|
|
$ScreenshotStateOverrides[$name]
|
|
}
|
|
else { $renderPackGate }
|
|
Add-ConnectedRenderPackMetadataFailures `
|
|
-ArtifactDirectory $artifactDir `
|
|
-ScreenshotNames @($name) `
|
|
-State $expectedState `
|
|
-Failures $failures `
|
|
-Label $Label
|
|
}
|
|
|
|
$graceful = Close-ClientGracefully $client
|
|
$client.Refresh()
|
|
if ($client.HasExited) { $exitCode = [int]$client.ExitCode }
|
|
if (-not $graceful) { $failures.Add("${Label}: client did not close through WM_CLOSE") }
|
|
if ($null -ne $exitCode -and $exitCode -ne 0) {
|
|
$failures.Add("${Label}: client exited with code $exitCode")
|
|
}
|
|
Add-LogFailures $Label $stdout $stderr
|
|
if ((Get-PatternCount $stdout '[session] graceful logout confirmed') -ne 1) {
|
|
$failures.Add("${Label}: server did not authoritatively confirm graceful character logout")
|
|
}
|
|
Wait-ForFileAppendPattern `
|
|
$AceLogPath `
|
|
$aceLogOffset `
|
|
"Session .*\\127\.0\.0\.1:$clientPort dropped\..*Reason: PacketHeader Disconnect" `
|
|
15
|
|
|
|
$session = [pscustomobject][ordered]@{
|
|
Label = $Label
|
|
Uncapped = $Uncapped
|
|
ElapsedSeconds = [Math]::Round($elapsed.Elapsed.TotalSeconds, 3)
|
|
GracefulExit = $graceful
|
|
ExitCode = $exitCode
|
|
Process = $processSample
|
|
Checkpoints = @($checkpoints)
|
|
Stdout = $stdout
|
|
Stderr = $stderr
|
|
ArtifactDirectory = $artifactDir
|
|
}
|
|
$sessions.Add($session)
|
|
return $session
|
|
}
|
|
catch {
|
|
$failures.Add("${Label}: $($_.Exception.Message)")
|
|
return $null
|
|
}
|
|
finally {
|
|
if ($null -ne $client) {
|
|
$client.Refresh()
|
|
if (-not $client.HasExited) {
|
|
$graceful = Close-ClientGracefully $client
|
|
if (-not $graceful -and -not $client.HasExited) {
|
|
$failures.Add("${Label}: required forced termination after WM_CLOSE timeout")
|
|
Stop-Process -Id $client.Id -Force
|
|
$client.WaitForExit(10000)
|
|
}
|
|
}
|
|
$client.Dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
function Add-AtmosphericTransitionSemanticGates([object]$Session) {
|
|
if ($null -eq $Session) { return }
|
|
$screenshots = Join-Path $Session.ArtifactDirectory 'screenshots'
|
|
$rows = @{}
|
|
foreach ($name in @(
|
|
'transition_selected_high',
|
|
'transition_disabled_retail',
|
|
'transition_reenabled_high',
|
|
'transition_resized_high',
|
|
'transition_dusk_high',
|
|
'transition_overcast_high',
|
|
'transition_rain_high')) {
|
|
$path = Join-Path $screenshots "$name.metadata.json"
|
|
if (Test-Path -LiteralPath $path) {
|
|
$rows[$name] = Get-Content -Raw -LiteralPath $path | ConvertFrom-Json
|
|
}
|
|
}
|
|
if ($rows.Count -ne 7) { return }
|
|
|
|
$selected = $rows.transition_selected_high.RenderPack
|
|
$disabled = $rows.transition_disabled_retail.RenderPack
|
|
$reenabled = $rows.transition_reenabled_high.RenderPack
|
|
if ([long]$disabled.ActivationGeneration -le [long]$selected.ActivationGeneration -or
|
|
[long]$reenabled.ActivationGeneration -le [long]$disabled.ActivationGeneration) {
|
|
$failures.Add('atmospheric-transitions: select/disable/re-enable activation generations were not strictly monotonic')
|
|
}
|
|
if ([int]$selected.ShadowCasterCount -le 0) {
|
|
$failures.Add('atmospheric-transitions: dense outdoor High row published no directional-shadow casters')
|
|
}
|
|
if ([int]$selected.ShadowTransformChurn.LiveDynamicRootChanges -le 0) {
|
|
$failures.Add('atmospheric-transitions: moving High row published no live-dynamic caster transform')
|
|
}
|
|
if ([int]$selected.ShadowTransformChurn.EquippedChildChanges -le 0) {
|
|
$failures.Add('atmospheric-transitions: moving High row published no equipped-child caster transform')
|
|
}
|
|
$casterClasses = $selected.ShadowTransformChurn.CasterClasses
|
|
if ($null -eq $casterClasses) {
|
|
$failures.Add('atmospheric-transitions: High row published no authoritative caster-class diagnostics')
|
|
}
|
|
else {
|
|
foreach ($property in @(
|
|
'TerrainCommands',
|
|
'OutdoorStatics',
|
|
'Buildings',
|
|
'AnimatedStatics',
|
|
'LocalPlayers',
|
|
'RemotePlayers',
|
|
'NonPlayerCreatures',
|
|
'OtherLiveDynamics',
|
|
'EquippedChildren')) {
|
|
if ($property -notin @($casterClasses.PSObject.Properties.Name)) {
|
|
$failures.Add(
|
|
"atmospheric-transitions: caster diagnostics omitted required '$property' metadata")
|
|
}
|
|
elseif ([int]$casterClasses.$property -lt 0) {
|
|
$failures.Add(
|
|
"atmospheric-transitions: caster diagnostics published negative '$property' metadata")
|
|
}
|
|
}
|
|
foreach ($required in @(
|
|
@('TerrainCommands', 'terrain command'),
|
|
@('OutdoorStatics', 'outdoor-static scenery'),
|
|
@('Buildings', 'building'),
|
|
@('LocalPlayers', 'local-player'),
|
|
@('EquippedChildren', 'equipped-child'))) {
|
|
$property = [string]$required[0]
|
|
if ([int]$casterClasses.$property -le 0) {
|
|
$failures.Add(
|
|
"atmospheric-transitions: dense outdoor High row published no $($required[1]) caster evidence")
|
|
}
|
|
}
|
|
}
|
|
|
|
$resized = $rows.transition_resized_high
|
|
if ([int]$resized.Width -ne 1024 -or [int]$resized.Height -ne 768) {
|
|
$failures.Add(
|
|
"atmospheric-transitions: resized screenshot was $($resized.Width)x$($resized.Height), expected exact 1024x768 framebuffer")
|
|
}
|
|
$dusk = $rows.transition_dusk_high.RenderPack
|
|
if ([Math]::Abs(
|
|
[double]$dusk.SunElevationDegrees -
|
|
[double]$reenabled.SunElevationDegrees) -lt 0.01) {
|
|
$failures.Add('atmospheric-transitions: authored time change did not alter published sun elevation')
|
|
}
|
|
if ([string]$rows.transition_overcast_high.RenderPack.Weather -cnotmatch '(?i)^overcast$') {
|
|
$failures.Add('atmospheric-transitions: first weather edge did not publish Overcast')
|
|
}
|
|
if ([string]$rows.transition_rain_high.RenderPack.Weather -cnotmatch '(?i)^rain$') {
|
|
$failures.Add('atmospheric-transitions: second weather edge did not publish Rain')
|
|
}
|
|
}
|
|
|
|
function Get-FreshContextRecreationGate(
|
|
[object]$FirstSession,
|
|
[object]$SecondSession)
|
|
{
|
|
$definition = 'graceful full graphical-process teardown followed by a fresh process; the fresh process constructs a new Vulkan device/context/swapchain ownership graph'
|
|
if ($null -eq $FirstSession -or $null -eq $SecondSession) {
|
|
$failures.Add('new-context recreation could not be proven because a required session is missing')
|
|
return [pscustomobject][ordered]@{
|
|
Definition = $definition
|
|
Passed = $false
|
|
FirstProcess = $null
|
|
SecondProcess = $null
|
|
}
|
|
}
|
|
$firstIdentity = "$($FirstSession.Process.ProcessId)@$($FirstSession.Process.StartTimeUtc)"
|
|
$secondIdentity = "$($SecondSession.Process.ProcessId)@$($SecondSession.Process.StartTimeUtc)"
|
|
$passed = $FirstSession.GracefulExit -and
|
|
$SecondSession.GracefulExit -and
|
|
$firstIdentity -cne $secondIdentity
|
|
if (-not $passed) {
|
|
$failures.Add('new-context recreation did not prove graceful teardown and a distinct fresh process')
|
|
}
|
|
return [pscustomobject][ordered]@{
|
|
Definition = $definition
|
|
Passed = $passed
|
|
FirstProcess = $firstIdentity
|
|
SecondProcess = $secondIdentity
|
|
}
|
|
}
|
|
|
|
function Add-SameLocationGates([object]$CappedSession) {
|
|
if ($null -eq $CappedSession) { return }
|
|
$first = @($CappedSession.Checkpoints | Where-Object { $_.name -eq 'aerlinthe_first' }) | Select-Object -First 1
|
|
$revisit = @($CappedSession.Checkpoints | Where-Object { $_.name -eq 'aerlinthe_revisit' }) | Select-Object -First 1
|
|
if ($null -eq $first -or $null -eq $revisit) { return }
|
|
|
|
$managedLimit = [Math]::Max(256MB, [double]$first.resources.managedBytes * 0.40)
|
|
$gpuLimit = [Math]::Max(512MB, [double]$first.resources.trackedGpuBytes * 0.50)
|
|
if (($revisit.resources.managedBytes - $first.resources.managedBytes) -gt $managedLimit) {
|
|
$failures.Add("Aerlinthe revisit: managed memory grew beyond the connected gate tolerance")
|
|
}
|
|
if (($revisit.resources.trackedGpuBytes - $first.resources.trackedGpuBytes) -gt $gpuLimit) {
|
|
$failures.Add("Aerlinthe revisit: tracked GPU memory grew beyond the connected gate tolerance")
|
|
}
|
|
|
|
foreach ($property in @('particleOwners', 'effectOwners', 'lightOwners', 'scriptOwners', 'compositeTextureOwners', 'particleTextureOwners')) {
|
|
$before = [double]$first.resources.$property
|
|
$after = [double]$revisit.resources.$property
|
|
$limit = [Math]::Max(64.0, $before * 0.50)
|
|
if (($after - $before) -gt $limit) {
|
|
$failures.Add("Aerlinthe revisit: owner '$property' grew $before -> $after")
|
|
}
|
|
}
|
|
}
|
|
|
|
$renderPackGate = New-ConnectedRenderPackGateState `
|
|
-Root $root `
|
|
-Preset $RenderPackPreset `
|
|
-SettingOverrides $RenderPackSettingOverrides
|
|
$sessionConfigPath = New-ConnectedGraphicalSessionConfig `
|
|
-State $renderPackGate `
|
|
-Account $Account
|
|
try {
|
|
if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0) {
|
|
throw 'an AcDream.App client is already running; close it gracefully before the gate'
|
|
}
|
|
if (@(Get-NetUDPEndpoint -LocalPort 9000 -ErrorAction SilentlyContinue).Count -eq 0) {
|
|
throw 'local ACE is not listening on UDP port 9000'
|
|
}
|
|
if (-not (Test-Path -LiteralPath $AceLogPath)) {
|
|
throw "ACE log was not found: $AceLogPath"
|
|
}
|
|
|
|
if (-not $SkipBuild) {
|
|
& 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)) { throw "client executable not found: $exe" }
|
|
|
|
$binaryIdentity = Get-ConnectedGateBinaryIdentity `
|
|
-Repository $Repository -Executable $exe -SkipBuild:$SkipBuild
|
|
|
|
$capped = Invoke-Session `
|
|
'capped' `
|
|
(Join-Path $Repository 'tools\connected-world-lifecycle.route.txt') `
|
|
$false `
|
|
@('capped_login', 'aerlinthe_first', 'rynthid', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit') `
|
|
@('capped_login', 'aerlinthe_first', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit')
|
|
|
|
Add-SameLocationGates $capped
|
|
|
|
# The second process starts as soon as ACE records accepting the first
|
|
# process's transport Disconnect. No elapsed-time settle delay hides a
|
|
# shutdown race.
|
|
$uncapped = Invoke-Session `
|
|
'uncapped-reconnect' `
|
|
(Join-Path $Repository 'tools\connected-world-reconnect.route.txt') `
|
|
$true `
|
|
@('uncapped_reconnect') `
|
|
@('uncapped_reconnect')
|
|
|
|
$contextRecreation = Get-FreshContextRecreationGate $capped $uncapped
|
|
|
|
# The medium matrix row is the one canonical transition row. It starts
|
|
# from a known enhanced selection, then proves an in-process High select,
|
|
# retail disable, exact High re-enable, live resize, and authored
|
|
# sun/weather changes without multiplying this long route across all five
|
|
# preset rows.
|
|
$transitionSession = $null
|
|
if ($RenderPackPreset -eq 'medium') {
|
|
$highExpectation = Get-ConnectedRenderPackExpectation -Preset high
|
|
$retailExpectation = Get-ConnectedRenderPackExpectation -Preset retail
|
|
$transitionSession = Invoke-Session `
|
|
'atmospheric-transitions' `
|
|
(Join-Path $Repository 'tools\connected-render-pack-transitions.route.txt') `
|
|
$false `
|
|
@('atmospheric_transitions') `
|
|
@(
|
|
'transition_selected_high',
|
|
'transition_disabled_retail',
|
|
'transition_reenabled_high',
|
|
'transition_resized_high',
|
|
'transition_dusk_high',
|
|
'transition_overcast_high',
|
|
'transition_rain_high') `
|
|
@{
|
|
transition_selected_high = $highExpectation
|
|
transition_disabled_retail = $retailExpectation
|
|
transition_reenabled_high = $highExpectation
|
|
transition_resized_high = $highExpectation
|
|
transition_dusk_high = $highExpectation
|
|
transition_overcast_high = $highExpectation
|
|
transition_rain_high = $highExpectation
|
|
}
|
|
Add-AtmosphericTransitionSemanticGates $transitionSession
|
|
}
|
|
|
|
$report = [pscustomobject][ordered]@{
|
|
Passed = $failures.Count -eq 0
|
|
StartedUtc = $startedUtc.ToString('O')
|
|
FinishedUtc = [DateTime]::UtcNow.ToString('O')
|
|
Commit = $binaryIdentity.BinaryCommit
|
|
SourceCommit = $binaryIdentity.SourceCommit
|
|
BinaryProductVersion = $binaryIdentity.BinaryProductVersion
|
|
BinaryCommit = $binaryIdentity.BinaryCommit
|
|
BinaryMatchesSource = $binaryIdentity.BinaryMatchesSource
|
|
SkipBuild = $binaryIdentity.SkipBuild
|
|
SourceStatus = @(& git -C $Repository status --short)
|
|
SessionName = $env:SESSIONNAME
|
|
CollisionShadowEvery = $CollisionShadowEvery
|
|
RenderPackSelection = (Get-ConnectedRenderPackGateReport $renderPackGate)
|
|
ContextRecreation = $contextRecreation
|
|
TransitionAutomation = [pscustomobject][ordered]@{
|
|
Executed = $null -ne $transitionSession
|
|
CanonicalRow = 'medium'
|
|
Session = $transitionSession
|
|
ProvenCasterRoutes = @(
|
|
'terrain shadow-command publication',
|
|
'outdoor-static scenery publication (including trees, without a tree discriminator)',
|
|
'building caster publication',
|
|
'local-player caster publication',
|
|
'moving live-dynamic root transform publication',
|
|
'equipped-child caster and moving-transform publication')
|
|
RemainingCasterClassEvidence = @(
|
|
'a second live client is still required to prove a nonzero remote-player caster count',
|
|
'a deterministic populated connected row is still required to prove nonzero active animated-static and non-player creature counts',
|
|
'create-object render metadata proves non-player creature, not hostile monster versus non-hostile NPC',
|
|
'outdoor DAT scenery has no authoritative tree discriminator, so trees remain grouped with other outdoor statics')
|
|
}
|
|
VideoControllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
|
|
ForEach-Object { [pscustomobject]@{
|
|
Name = $_.Name
|
|
DriverVersion = $_.DriverVersion
|
|
AdapterRam = $_.AdapterRAM
|
|
} })
|
|
Failures = @($failures)
|
|
Warnings = @($warnings)
|
|
Sessions = @($sessions)
|
|
}
|
|
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $reportPath -Encoding utf8
|
|
|
|
Write-Output "REPORT=$reportPath"
|
|
Write-Output "RESULT=$(if ($report.Passed) { 'PASS' } else { 'FAIL' })"
|
|
foreach ($failure in $failures) { Write-Output "FAILURE=$failure" }
|
|
foreach ($warning in $warnings) { Write-Output "WARNING=$warning" }
|
|
}
|
|
finally {
|
|
try {
|
|
Remove-ConnectedGraphicalSessionConfig `
|
|
-State $renderPackGate -Path $sessionConfigPath
|
|
}
|
|
finally {
|
|
Restore-ConnectedRenderPackGateEnvironment $renderPackGate
|
|
}
|
|
}
|
|
|
|
if ($failures.Count -gt 0) { exit 1 }
|