XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEWarframe

【WinUI】基于Warframe游戏本体的公开服务器以及安装器

公开
关注 0 Fork 0 Star 0
UTF-8
#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"