fix(net): honor retail graceful logout handshake
Send the active character id, drain until the authoritative server confirmation, then emit retail's zero-sequence connection disconnect with the negotiated receiver iteration. The connected gate now waits for ACE to remove the exact UDP session before reconnecting, eliminating fixed-delay races.
This commit is contained in:
parent
b03371c03d
commit
68578fa5fa
10 changed files with 372 additions and 41 deletions
|
|
@ -3,6 +3,7 @@ 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
|
||||
)
|
||||
|
|
@ -47,11 +48,44 @@ function Wait-ForPattern(
|
|||
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(30000)) { return $false }
|
||||
if (-not $Client.WaitForExit(45000)) { return $false }
|
||||
$Client.WaitForExit()
|
||||
return $true
|
||||
}
|
||||
|
|
@ -79,7 +113,10 @@ function Add-LogFailures([string]$Label, [string]$Stdout, [string]$Stderr) {
|
|||
'device removed',
|
||||
'GPU reset',
|
||||
'live: disconnected',
|
||||
'screenshot-failed'
|
||||
'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)
|
||||
|
|
@ -155,9 +192,11 @@ function Invoke-Session(
|
|||
$stderr = Join-Path $sessionDir 'stderr.log'
|
||||
$timeline = Join-Path $artifactDir 'world-lifecycle.checkpoints.jsonl'
|
||||
$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'
|
||||
|
|
@ -180,6 +219,12 @@ function Invoke-Session(
|
|||
-RedirectStandardOutput $stdout -RedirectStandardError $stderr -PassThru
|
||||
Wait-ForPattern $client $stdout '[UI-PROBE] UI probe script complete' $SessionTimeoutSeconds
|
||||
|
||||
$clientPort = @(Get-NetUDPEndpoint -OwningProcess $client.Id -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1 -ExpandProperty LocalPort)
|
||||
if ($clientPort.Count -ne 1) {
|
||||
throw "could not resolve the client's UDP endpoint for ACE disconnect verification"
|
||||
}
|
||||
|
||||
$client.Refresh()
|
||||
$processSample = [pscustomobject][ordered]@{
|
||||
WorkingSetMiB = [Math]::Round($client.WorkingSet64 / 1MB, 1)
|
||||
|
|
@ -206,7 +251,6 @@ function Invoke-Session(
|
|||
if (-not (Test-Png $png)) { $failures.Add("${Label}: missing or invalid screenshot '$png'") }
|
||||
}
|
||||
|
||||
Add-LogFailures $Label $stdout $stderr
|
||||
$graceful = Close-ClientGracefully $client
|
||||
$client.Refresh()
|
||||
if ($client.HasExited) { $exitCode = [int]$client.ExitCode }
|
||||
|
|
@ -214,6 +258,15 @@ function Invoke-Session(
|
|||
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
|
||||
|
|
@ -281,6 +334,9 @@ if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0)
|
|||
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
|
||||
|
|
@ -297,10 +353,9 @@ $capped = Invoke-Session `
|
|||
|
||||
Add-SameLocationGates $capped
|
||||
|
||||
# The second process is both the session-teardown/reconnect gate and the
|
||||
# uncapped renderer sample. A short pause lets ACE finish releasing the first
|
||||
# session before the same account logs in again.
|
||||
Start-Sleep -Seconds 3
|
||||
# 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.
|
||||
$null = Invoke-Session `
|
||||
'uncapped-reconnect' `
|
||||
(Join-Path $Repository 'tools\connected-world-reconnect.route.txt') `
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue