#requires -Version 7.0
param(
[string]$GameDirectory = 'D:\Warframe',
[string]$SessionFile = (Join-Path $PSScriptRoot '../artifacts/multi-ability-game-test/session.json'),
[string]$CryptoAssembly = (Join-Path $PSScriptRoot '../Server/XFEWarframe.Server/bin/Release/net10.0/BouncyCastle.Cryptography.dll'),
[string]$ReportDirectory = (Join-Path $PSScriptRoot '../artifacts/multi-ability-game-test/engine-validation'),
[switch]$LoginOnly
)
$ErrorActionPreference = 'Stop'
$taskTest = Get-Content -LiteralPath $SessionFile -Raw | ConvertFrom-Json
if ($taskTest.origin -ne 'http://127.0.0.1:18444' -or $taskTest.accountId -notmatch '^[0-9a-f]{24}$' -or $taskTest.suitId -notmatch '^[0-9a-f]{24}$') {
throw 'Expected the dedicated local multi-ability test session.'
}
$taskGamePath = [IO.Path]::GetFullPath((Join-Path $GameDirectory 'Warframe.x64.exe'))
if ($taskGamePath -ne 'D:\Warframe\Warframe.x64.exe') { throw 'This experiment is restricted to the D:\Warframe copy.' }
$taskGame = @(Get-Process -Name Warframe.x64 -ErrorAction SilentlyContinue | Where-Object Path -EQ $taskGamePath)
if ($taskGame.Count -ne 1) { throw 'Expected one running test game.' }
$taskDeadline = [DateTimeOffset]::UtcNow.AddSeconds(45)
$taskSession = $null
do {
$taskSession = @(Get-ChildItem -LiteralPath (Join-Path $env:LOCALAPPDATA 'XFEWarframe/Runtime') -Filter '*.json' -Recurse -ErrorAction SilentlyContinue |
ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json } | Where-Object processId -EQ $taskGame[0].Id)
if ($taskSession.Count -eq 1) { break }
Start-Sleep -Milliseconds 500
} while ([DateTimeOffset]::UtcNow -lt $taskDeadline)
if ($taskSession.Count -ne 1) { throw 'The C# runtime session was not published.' }
$taskSession = $taskSession[0]
if (([uri]$taskSession.endpoint).Scheme -ne 'http' -or ([uri]$taskSession.endpoint).Host -ne '127.0.0.1') { throw 'The runtime must be local.' }
$taskHttp = [Net.Http.HttpClient]::new([Net.Http.HttpClientHandler]@{UseProxy=$false})
$taskHttp.Timeout = [TimeSpan]::FromSeconds(8)
$taskHttp.DefaultRequestHeaders.Authorization = [Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer', $taskSession.token)
$taskEvidence = [ordered]@{started=[DateTimeOffset]::UtcNow.ToString('o');gamePid=$taskGame[0].Id;
executableSha256=(Get-FileHash -LiteralPath $taskGamePath -Algorithm SHA256).Hash;
bridgeSha256=(Get-FileHash -LiteralPath (Join-Path $GameDirectory 'XFEWarframe/XFEWarframe.Bridge.dll') -Algorithm SHA256).Hash;
passed=$false;checks=[Collections.Generic.List[object]]::new()}
New-Item -ItemType Directory -Path $ReportDirectory -Force | Out-Null
function Invoke-Runtime([string]$Path, $Body = $null) {
$request = [Net.Http.HttpRequestMessage]::new($(if ($null -eq $Body) {'GET'} else {'POST'}), $taskSession.endpoint + $Path)
$bytes = $null
try {
if ($null -ne $Body) {
$bytes = [Text.Encoding]::UTF8.GetBytes(($Body | ConvertTo-Json -Depth 40 -Compress))
$request.Content = [Net.Http.ByteArrayContent]::new($bytes)
$request.Content.Headers.ContentType = [Net.Http.Headers.MediaTypeHeaderValue]::new('application/json')
}
$response = $taskHttp.SendAsync($request).GetAwaiter().GetResult()
try {
$json = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
if (-not $response.IsSuccessStatusCode) { throw "Game engine request failed ($([int]$response.StatusCode)): $json" }
return $json | ConvertFrom-Json
} finally { $response.Dispose() }
} finally { $request.Dispose(); if ($bytes) { [Security.Cryptography.CryptographicOperations]::ZeroMemory($bytes) } }
}
function Invoke-Engine($Steps) { return (Invoke-Runtime '/runtime/v1/game/lua' @{steps=$Steps}).results }
function Wait-Engine([scriptblock]$Condition, [string]$Description, [int]$Seconds=40) {
$deadline = [DateTimeOffset]::UtcNow.AddSeconds($Seconds)
do {
if ($taskGame[0].HasExited) { throw 'The game exited during validation.' }
try { if (& $Condition) { return } } catch { $lastError = $_.Exception.Message }
Start-Sleep -Milliseconds 500
} while ([DateTimeOffset]::UtcNow -lt $deadline)
throw "Timed out: $Description. $lastError"
}
$taskSuitSteps = @(@{global='gRegion'},@{from=0;field='GetLocalPlayerAvatar'},@{call=1;args=@(@{ref=0})},
@{from=2;field='InventoryControl'},@{call=3;args=@(@{ref=2})},@{from=4;field='GetActivePowerSuit'},@{call=5;args=@(@{ref=4})})
$taskPresetSteps = @(@{global='gGameData'},@{from=0;field='GetInventory'},@{call=1;args=@(@{ref=0})},
@{from=2;field='GetPresets'},@{call=3;args=@(@{ref=2},0)},@{global='rawget'},@{call=5;args=@(@{ref=4},1)},
@{from=6;field='GetEntry'},@{call=7;args=@(@{ref=6},0)})
function Get-Abilities {
foreach ($slot in 1..4) {
$reply = Invoke-Engine ($taskSuitSteps + @(@{from=6;field='GetAbilities'},@{call=7;args=@(@{ref=6})},
@{global='rawget'},@{call=9;args=@(@{ref=8},$slot)},@{from=10;field='GetType'},@{call=11;args=@(@{ref=10})},
@{from=12;field='GetName'},@{call=13;args=@(@{ref=12})}))
if (-not $reply[14].text) { throw 'Ability instance is missing.' }
$reply[14].text
}
}
function Select-Config([int]$Index, [string[]]$Expected) {
# Follow the game preset -> effective loadout -> InventoryController equipment path.
# 0xDF459F8E is the audited Lua binding at RVA 0x1B8D640 (loadout, type, slot, force).
$null = Invoke-Engine ($taskPresetSteps + @(@{meta=8},@{from=9;field='__newindex';literal=$true},
@{call=10;args=@(@{ref=8},'mModSlot',$Index)},@{from=6;field='SetEntry'},@{call=12;args=@(@{ref=6},0,@{ref=8})},
@{from=2;field='SetLoadoutPreset'},@{call=14;args=@(@{ref=2},0,@{ref=6},$true)},
@{from=0;field='ApplyLoadOutPreset'},@{call=16;args=@(@{ref=0},0,@{ref=6},$true,$false,$true)}))
$null = Invoke-Engine @(@{global='gGameRules'},@{from=0;field='SendLobbyLoadout'},@{call=1;args=@(@{ref=0})})
$null = Invoke-Engine ($taskSuitSteps[0..4] + @(@{global='gGameData'},@{from=5;field='GetLoadOut'},@{call=6;args=@(@{ref=5})},
@{from=4;field='0xDF459F8E'},@{call=8;args=@(@{ref=4},@{ref=7},0,0,$true)}))
Wait-Engine { (@(Get-Abilities) -join ',') -eq ($Expected -join ',') } "live config $Index"
$taskEvidence.checks.Add(@{kind='configuration';config=$Index;abilities=@(Get-Abilities);passed=$true})
Write-Host "Config ${Index}: actual ability instances verified."
}
function Test-Cast([int]$Config, [int]$Slot, [string]$Expected, [float]$Cost) {
$before = Invoke-Engine ($taskSuitSteps + @(@{from=6;field='GetMaxEnergy'},@{call=7;args=@(@{ref=6})},
@{from=6;field='SetEnergy'},@{call=9;args=@(@{ref=6},@{ref=8})},
@{from=6;field='0xEF680AA6'},@{call=11;args=@(@{ref=6})}, # Energy getter, RVA 0x1BA34E0.
@{from=6;field='TryToActivateAbility'},@{call=13;args=@(@{ref=6},$Slot)}))
Start-Sleep -Milliseconds 350
$after = Invoke-Engine ($taskSuitSteps + @(@{from=6;field='0xEF680AA6'},@{call=7;args=@(@{ref=6})},
@{from=6;field='GetAbilities'},@{call=9;args=@(@{ref=6})},@{global='rawget'},@{call=11;args=@(@{ref=10},($Slot+1))},
@{from=12;field='IsActive'},@{call=13;args=@(@{ref=12})},@{from=12;field='GetType'},@{call=15;args=@(@{ref=12})},
@{from=16;field='GetName'},@{call=17;args=@(@{ref=16})}))
$passed = $after[18].text -eq $Expected -and $after[14].hashOrBoolean -eq '0x00000001' -and
[Math]::Abs(($before[12].number - $after[8].number) - $Cost) -lt 0.01
$taskEvidence.checks.Add(@{kind='cast';config=$Config;slot=$Slot;ability=$after[18].text;active=$after[14].hashOrBoolean;
energyBefore=$before[12].number;energyAfter=$after[8].number;passed=$passed})
if (-not $passed) { throw "Ability did not activate correctly: config $Config, slot $Slot." }
Write-Host "Config $Config, slot ${Slot}: $Expected activated, energy cost $Cost."
Start-Sleep -Seconds 2 # Allow the normal cast animation to finish before another cast.
}
try {
$status = Invoke-Runtime '/runtime/v1/status'
$taskEvidence.build = $status.gameVersion
$taskEvidence.bridgeVersion = $status.version
if ($status.serverUrl -ne $taskTest.origin -or -not ($status.capabilities | Where-Object { $_.key -eq 'game_test_control' -and $_.available })) {
throw 'Start the isolated test copy with --game-test-control.'
}
Wait-Engine { (Invoke-Runtime '/runtime/v1/game/status').profileCount -gt 0 } 'profile initialization'
$profile = Invoke-Runtime '/runtime/v1/game/status'
$taskRequestedLogin = $profile.profileState -ne 4
if ($profile.profileState -ne 4) {
Add-Type -Path ([IO.Path]::GetFullPath($CryptoAssembly))
$digest = [Org.BouncyCastle.Crypto.Digests.WhirlpoolDigest]::new()
$password = [Text.Encoding]::UTF8.GetBytes('LocalAbilityCheck!2026')
$hash = [byte[]]::new(64)
try {
$digest.BlockUpdate($password,0,$password.Length); $null = $digest.DoFinal($hash,0)
$null = Invoke-Runtime '/runtime/v1/game/login' @{email='multi-ability@example.invalid';passwordHash=[Convert]::ToHexString($hash).ToLowerInvariant()}
} finally { [Security.Cryptography.CryptographicOperations]::ZeroMemory($password); [Security.Cryptography.CryptographicOperations]::ZeroMemory($hash) }
Wait-Engine { (Invoke-Runtime '/runtime/v1/game/status').loginResult -eq 'succeeded' } 'native account login'
}
# Verify the test suit before modifying anything in the live profile.
Wait-Engine {
$identity = Invoke-Engine ($taskPresetSteps + @(@{from=8;field='mItemId';literal=$true},@{from=9;field='mId';literal=$true}))
$identity[10].text -eq $taskTest.suitId
} 'isolated test inventory'
$flag = Invoke-Engine @(@{global='_T'},@{from=0;field='gShowingLoginScreen';literal=$true})
if ($flag[1].hashOrBoolean -eq '0x00000001') {
$null = Invoke-Engine @(@{global='_T'},@{from=0;field='BackgroundMovie';literal=$true},@{from=1;field='Execute'},
@{call=2;args=@(@{ref=1},'OnLoginDone','')},@{global='rawset'},@{call=4;args=@(@{ref=0},'gShowingLoginScreen',$false)})
}
Wait-Engine { @(Get-Abilities).Count -eq 4 } 'local player suit'
$taskEvidence.checks.Add(@{kind='native-login';passed=$true;accountId=$taskTest.accountId;requestedByTest=$taskRequestedLogin})
Write-Host 'Native login and test inventory verified; no desktop input used.'
if ($LoginOnly) { $taskEvidence.passed = $true; return }
$a = @('ShockAbility','RhinoRoarAbility','RadialJavelinAbility','SwordOfDoomAbility')
$b = @('SlashDashNewAbility','RadialBlindAbility','RadialJavelinAbility','SwordOfDoomAbility')
$c = @('ShockAbility','PullAbility','IronSkinAbility','RhinoRoarAbility')
Select-Config 0 $a; Select-Config 1 $b; Select-Config 2 $c; Select-Config 0 $a
$null = Invoke-Engine @(@{global='require'},@{call=0;args=@('Lotus.Scripts.SimulacrumUtilities')},
@{from=1;field='LaunchDangerRoom';literal=$true},@{call=2;args=@()})
Wait-Engine {
$scene = Invoke-Engine @(@{global='gGameRules'},@{from=0;field='GetType'},@{call=1;args=@(@{ref=0})},
@{from=2;field='GetFullName'},@{call=3;args=@(@{ref=2})})
$scene[4].text -eq '/Lotus/Types/GameRules/LotusDangerRoomGameRules' -and @(Get-Abilities).Count -eq 4
} 'Simulacrum scene and avatar' 55
Start-Sleep -Seconds 3
$taskEvidence.checks.Add(@{kind='scene';gameRules='/Lotus/Types/GameRules/LotusDangerRoomGameRules';passed=$true})
Select-Config 2 $c
Test-Cast 2 3 'RhinoRoarAbility' 75; Test-Cast 2 2 'IronSkinAbility' 50
Test-Cast 2 0 'ShockAbility' 15; Test-Cast 2 1 'PullAbility' 25
Select-Config 0 $a
Test-Cast 0 1 'RhinoRoarAbility' 75; Test-Cast 0 0 'ShockAbility' 15
Select-Config 1 $b; Select-Config 0 $a
$taskEvidence.passed = $true
} catch { $taskEvidence.error = $_.Exception.Message; throw }
finally {
$taskEvidence.finished = [DateTimeOffset]::UtcNow.ToString('o')
$taskEvidence | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $ReportDirectory 'result.json') -Encoding utf8
$taskHttp.Dispose()
}
Write-Host "Game validation passed: $ReportDirectory/result.json"
#requires -Version 7.0
param(
[string]$GameDirectory = 'D:\Warframe',
[string]$SessionFile = (Join-Path $PSScriptRoot '../artifacts/multi-ability-game-test/session.json'),
[string]$CryptoAssembly = (Join-Path $PSScriptRoot '../Server/XFEWarframe.Server/bin/Release/net10.0/BouncyCastle.Cryptography.dll'),
[string]$ReportDirectory = (Join-Path $PSScriptRoot '../artifacts/multi-ability-game-test/engine-validation'),
[switch]$LoginOnly
)
$ErrorActionPreference = 'Stop'
$taskTest = Get-Content -LiteralPath $SessionFile -Raw | ConvertFrom-Json
if ($taskTest.origin -ne 'http://127.0.0.1:18444' -or $taskTest.accountId -notmatch '^[0-9a-f]{24}$' -or $taskTest.suitId -notmatch '^[0-9a-f]{24}$') {
throw 'Expected the dedicated local multi-ability test session.'
}
$taskGamePath = [IO.Path]::GetFullPath((Join-Path $GameDirectory 'Warframe.x64.exe'))
if ($taskGamePath -ne 'D:\Warframe\Warframe.x64.exe') { throw 'This experiment is restricted to the D:\Warframe copy.' }
$taskGame = @(Get-Process -Name Warframe.x64 -ErrorAction SilentlyContinue | Where-Object Path -EQ $taskGamePath)
if ($taskGame.Count -ne 1) { throw 'Expected one running test game.' }
$taskDeadline = [DateTimeOffset]::UtcNow.AddSeconds(45)
$taskSession = $null
do {
$taskSession = @(Get-ChildItem -LiteralPath (Join-Path $env:LOCALAPPDATA 'XFEWarframe/Runtime') -Filter '*.json' -Recurse -ErrorAction SilentlyContinue |
ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json } | Where-Object processId -EQ $taskGame[0].Id)
if ($taskSession.Count -eq 1) { break }
Start-Sleep -Milliseconds 500
} while ([DateTimeOffset]::UtcNow -lt $taskDeadline)
if ($taskSession.Count -ne 1) { throw 'The C# runtime session was not published.' }
$taskSession = $taskSession[0]
if (([uri]$taskSession.endpoint).Scheme -ne 'http' -or ([uri]$taskSession.endpoint).Host -ne '127.0.0.1') { throw 'The runtime must be local.' }
$taskHttp = [Net.Http.HttpClient]::new([Net.Http.HttpClientHandler]@{UseProxy=$false})
$taskHttp.Timeout = [TimeSpan]::FromSeconds(8)
$taskHttp.DefaultRequestHeaders.Authorization = [Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer', $taskSession.token)
$taskEvidence = [ordered]@{started=[DateTimeOffset]::UtcNow.ToString('o');gamePid=$taskGame[0].Id;
executableSha256=(Get-FileHash -LiteralPath $taskGamePath -Algorithm SHA256).Hash;
bridgeSha256=(Get-FileHash -LiteralPath (Join-Path $GameDirectory 'XFEWarframe/XFEWarframe.Bridge.dll') -Algorithm SHA256).Hash;
passed=$false;checks=[Collections.Generic.List[object]]::new()}
New-Item -ItemType Directory -Path $ReportDirectory -Force | Out-Null
function Invoke-Runtime([string]$Path, $Body = $null) {
$request = [Net.Http.HttpRequestMessage]::new($(if ($null -eq $Body) {'GET'} else {'POST'}), $taskSession.endpoint + $Path)
$bytes = $null
try {
if ($null -ne $Body) {
$bytes = [Text.Encoding]::UTF8.GetBytes(($Body | ConvertTo-Json -Depth 40 -Compress))
$request.Content = [Net.Http.ByteArrayContent]::new($bytes)
$request.Content.Headers.ContentType = [Net.Http.Headers.MediaTypeHeaderValue]::new('application/json')
}
$response = $taskHttp.SendAsync($request).GetAwaiter().GetResult()
try {
$json = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
if (-not $response.IsSuccessStatusCode) { throw "Game engine request failed ($([int]$response.StatusCode)): $json" }
return $json | ConvertFrom-Json
} finally { $response.Dispose() }
} finally { $request.Dispose(); if ($bytes) { [Security.Cryptography.CryptographicOperations]::ZeroMemory($bytes) } }
}
function Invoke-Engine($Steps) { return (Invoke-Runtime '/runtime/v1/game/lua' @{steps=$Steps}).results }
function Wait-Engine([scriptblock]$Condition, [string]$Description, [int]$Seconds=40) {
$deadline = [DateTimeOffset]::UtcNow.AddSeconds($Seconds)
do {
if ($taskGame[0].HasExited) { throw 'The game exited during validation.' }
try { if (& $Condition) { return } } catch { $lastError = $_.Exception.Message }
Start-Sleep -Milliseconds 500
} while ([DateTimeOffset]::UtcNow -lt $deadline)
throw "Timed out: $Description. $lastError"
}
$taskSuitSteps = @(@{global='gRegion'},@{from=0;field='GetLocalPlayerAvatar'},@{call=1;args=@(@{ref=0})},
@{from=2;field='InventoryControl'},@{call=3;args=@(@{ref=2})},@{from=4;field='GetActivePowerSuit'},@{call=5;args=@(@{ref=4})})
$taskPresetSteps = @(@{global='gGameData'},@{from=0;field='GetInventory'},@{call=1;args=@(@{ref=0})},
@{from=2;field='GetPresets'},@{call=3;args=@(@{ref=2},0)},@{global='rawget'},@{call=5;args=@(@{ref=4},1)},
@{from=6;field='GetEntry'},@{call=7;args=@(@{ref=6},0)})
function Get-Abilities {
foreach ($slot in 1..4) {
$reply = Invoke-Engine ($taskSuitSteps + @(@{from=6;field='GetAbilities'},@{call=7;args=@(@{ref=6})},
@{global='rawget'},@{call=9;args=@(@{ref=8},$slot)},@{from=10;field='GetType'},@{call=11;args=@(@{ref=10})},
@{from=12;field='GetName'},@{call=13;args=@(@{ref=12})}))
if (-not $reply[14].text) { throw 'Ability instance is missing.' }
$reply[14].text
}
}
function Select-Config([int]$Index, [string[]]$Expected) {
# Follow the game preset -> effective loadout -> InventoryController equipment path.
# 0xDF459F8E is the audited Lua binding at RVA 0x1B8D640 (loadout, type, slot, force).
$null = Invoke-Engine ($taskPresetSteps + @(@{meta=8},@{from=9;field='__newindex';literal=$true},
@{call=10;args=@(@{ref=8},'mModSlot',$Index)},@{from=6;field='SetEntry'},@{call=12;args=@(@{ref=6},0,@{ref=8})},
@{from=2;field='SetLoadoutPreset'},@{call=14;args=@(@{ref=2},0,@{ref=6},$true)},
@{from=0;field='ApplyLoadOutPreset'},@{call=16;args=@(@{ref=0},0,@{ref=6},$true,$false,$true)}))
$null = Invoke-Engine @(@{global='gGameRules'},@{from=0;field='SendLobbyLoadout'},@{call=1;args=@(@{ref=0})})
$null = Invoke-Engine ($taskSuitSteps[0..4] + @(@{global='gGameData'},@{from=5;field='GetLoadOut'},@{call=6;args=@(@{ref=5})},
@{from=4;field='0xDF459F8E'},@{call=8;args=@(@{ref=4},@{ref=7},0,0,$true)}))
Wait-Engine { (@(Get-Abilities) -join ',') -eq ($Expected -join ',') } "live config $Index"
$taskEvidence.checks.Add(@{kind='configuration';config=$Index;abilities=@(Get-Abilities);passed=$true})
Write-Host "Config ${Index}: actual ability instances verified."
}
function Test-Cast([int]$Config, [int]$Slot, [string]$Expected, [float]$Cost) {
$before = Invoke-Engine ($taskSuitSteps + @(@{from=6;field='GetMaxEnergy'},@{call=7;args=@(@{ref=6})},
@{from=6;field='SetEnergy'},@{call=9;args=@(@{ref=6},@{ref=8})},
@{from=6;field='0xEF680AA6'},@{call=11;args=@(@{ref=6})}, # Energy getter, RVA 0x1BA34E0.
@{from=6;field='TryToActivateAbility'},@{call=13;args=@(@{ref=6},$Slot)}))
Start-Sleep -Milliseconds 350
$after = Invoke-Engine ($taskSuitSteps + @(@{from=6;field='0xEF680AA6'},@{call=7;args=@(@{ref=6})},
@{from=6;field='GetAbilities'},@{call=9;args=@(@{ref=6})},@{global='rawget'},@{call=11;args=@(@{ref=10},($Slot+1))},
@{from=12;field='IsActive'},@{call=13;args=@(@{ref=12})},@{from=12;field='GetType'},@{call=15;args=@(@{ref=12})},
@{from=16;field='GetName'},@{call=17;args=@(@{ref=16})}))
$passed = $after[18].text -eq $Expected -and $after[14].hashOrBoolean -eq '0x00000001' -and
[Math]::Abs(($before[12].number - $after[8].number) - $Cost) -lt 0.01
$taskEvidence.checks.Add(@{kind='cast';config=$Config;slot=$Slot;ability=$after[18].text;active=$after[14].hashOrBoolean;
energyBefore=$before[12].number;energyAfter=$after[8].number;passed=$passed})
if (-not $passed) { throw "Ability did not activate correctly: config $Config, slot $Slot." }
Write-Host "Config $Config, slot ${Slot}: $Expected activated, energy cost $Cost."
Start-Sleep -Seconds 2 # Allow the normal cast animation to finish before another cast.
}
try {
$status = Invoke-Runtime '/runtime/v1/status'
$taskEvidence.build = $status.gameVersion
$taskEvidence.bridgeVersion = $status.version
if ($status.serverUrl -ne $taskTest.origin -or -not ($status.capabilities | Where-Object { $_.key -eq 'game_test_control' -and $_.available })) {
throw 'Start the isolated test copy with --game-test-control.'
}
Wait-Engine { (Invoke-Runtime '/runtime/v1/game/status').profileCount -gt 0 } 'profile initialization'
$profile = Invoke-Runtime '/runtime/v1/game/status'
$taskRequestedLogin = $profile.profileState -ne 4
if ($profile.profileState -ne 4) {
Add-Type -Path ([IO.Path]::GetFullPath($CryptoAssembly))
$digest = [Org.BouncyCastle.Crypto.Digests.WhirlpoolDigest]::new()
$password = [Text.Encoding]::UTF8.GetBytes('LocalAbilityCheck!2026')
$hash = [byte[]]::new(64)
try {
$digest.BlockUpdate($password,0,$password.Length); $null = $digest.DoFinal($hash,0)
$null = Invoke-Runtime '/runtime/v1/game/login' @{email='multi-ability@example.invalid';passwordHash=[Convert]::ToHexString($hash).ToLowerInvariant()}
} finally { [Security.Cryptography.CryptographicOperations]::ZeroMemory($password); [Security.Cryptography.CryptographicOperations]::ZeroMemory($hash) }
Wait-Engine { (Invoke-Runtime '/runtime/v1/game/status').loginResult -eq 'succeeded' } 'native account login'
}
# Verify the test suit before modifying anything in the live profile.
Wait-Engine {
$identity = Invoke-Engine ($taskPresetSteps + @(@{from=8;field='mItemId';literal=$true},@{from=9;field='mId';literal=$true}))
$identity[10].text -eq $taskTest.suitId
} 'isolated test inventory'
$flag = Invoke-Engine @(@{global='_T'},@{from=0;field='gShowingLoginScreen';literal=$true})
if ($flag[1].hashOrBoolean -eq '0x00000001') {
$null = Invoke-Engine @(@{global='_T'},@{from=0;field='BackgroundMovie';literal=$true},@{from=1;field='Execute'},
@{call=2;args=@(@{ref=1},'OnLoginDone','')},@{global='rawset'},@{call=4;args=@(@{ref=0},'gShowingLoginScreen',$false)})
}
Wait-Engine { @(Get-Abilities).Count -eq 4 } 'local player suit'
$taskEvidence.checks.Add(@{kind='native-login';passed=$true;accountId=$taskTest.accountId;requestedByTest=$taskRequestedLogin})
Write-Host 'Native login and test inventory verified; no desktop input used.'
if ($LoginOnly) { $taskEvidence.passed = $true; return }
$a = @('ShockAbility','RhinoRoarAbility','RadialJavelinAbility','SwordOfDoomAbility')
$b = @('SlashDashNewAbility','RadialBlindAbility','RadialJavelinAbility','SwordOfDoomAbility')
$c = @('ShockAbility','PullAbility','IronSkinAbility','RhinoRoarAbility')
Select-Config 0 $a; Select-Config 1 $b; Select-Config 2 $c; Select-Config 0 $a
$null = Invoke-Engine @(@{global='require'},@{call=0;args=@('Lotus.Scripts.SimulacrumUtilities')},
@{from=1;field='LaunchDangerRoom';literal=$true},@{call=2;args=@()})
Wait-Engine {
$scene = Invoke-Engine @(@{global='gGameRules'},@{from=0;field='GetType'},@{call=1;args=@(@{ref=0})},
@{from=2;field='GetFullName'},@{call=3;args=@(@{ref=2})})
$scene[4].text -eq '/Lotus/Types/GameRules/LotusDangerRoomGameRules' -and @(Get-Abilities).Count -eq 4
} 'Simulacrum scene and avatar' 55
Start-Sleep -Seconds 3
$taskEvidence.checks.Add(@{kind='scene';gameRules='/Lotus/Types/GameRules/LotusDangerRoomGameRules';passed=$true})
Select-Config 2 $c
Test-Cast 2 3 'RhinoRoarAbility' 75; Test-Cast 2 2 'IronSkinAbility' 50
Test-Cast 2 0 'ShockAbility' 15; Test-Cast 2 1 'PullAbility' 25
Select-Config 0 $a
Test-Cast 0 1 'RhinoRoarAbility' 75; Test-Cast 0 0 'ShockAbility' 15
Select-Config 1 $b; Select-Config 0 $a
$taskEvidence.passed = $true
} catch { $taskEvidence.error = $_.Exception.Message; throw }
finally {
$taskEvidence.finished = [DateTimeOffset]::UtcNow.ToString('o')
$taskEvidence | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $ReportDirectory 'result.json') -Encoding utf8
$taskHttp.Dispose()
}
Write-Host "Game validation passed: $ReportDirectory/result.json"