23ec41c1d6
Adds test/multiPlayerTest.tscn plus a driver that runs the kitchen flow across two game instances: grab/drop, dirt station, sink washing, hob cooking, counter combining and plating. Runs headless (run_mp_test.ps1), in two visible windows (run_mp_test_windowed.ps1), or by hand with keyboard controls (play_mp_test.ps1). 108 checks, exits non-zero on failure. After every step both peers snapshot every item's position and rendered state and the server diffs them. Targeted assertions only look at the thing a step touched, which misses desyncs elsewhere - that audit is what caught the last bug below. Bugs found and fixed: - net_pickable: apply_held_state() only wrote `enabled` in its non-authority branch, so once a client grabbed an item every other peer set enabled=false and regaining authority never restored it. The server could then never pick that item up again, and a station would "snap" it (emitting has_picked_up, so a plate still got marked dirty) while pick_up() bailed out on the disabled item - leaving the zone holding an item with no grab driver. - network_manager: station gating only happened in the spawn path, so stations baked into a scene file kept running their snap zones on clients and grabbed items straight out of the local hand. Added gate_existing_stations(). - network_manager: despawn_item() only freed the server's copy. Items baked into a scene aren't tracked by the MultiplayerSpawner, so consuming one left a ghost on every client, which then blocked the station it sat in and got grabbed instead of its replacement. - network_manager: the snap-into-station decision read the server's own copy of the item position, but the reliable release RPC routinely overtakes the synchronizer's unordered position updates - so it acted on a stale position and teleported items back into the station they had just been carried away from. The releasing peer now sends its final transform and the server adopts it first. - container: contained_ids.append()/erase() mutate the array in place, which never fires the setter that rebuilds the plate's visuals. The peer that put food on a plate was the only peer that never redrew it; remote peers looked right because the synchronizer assigns there. Also null-guards XRServer.get_tracker() in the vendored xr-tools hand grab point, which threw on every successful grab without an XR runtime, and adds multiplayer_world.populate_from_layout so debug scenes can bake their own content instead of spawning the whole kitchen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
61 lines
2.4 KiB
PowerShell
61 lines
2.4 KiB
PowerShell
# Runs the headless two-instance multiplayer test (test/multiPlayerTest.tscn).
|
|
#
|
|
# powershell -File test\run_mp_test.ps1
|
|
#
|
|
# Starts a server instance and a client instance of the game with --xr-mode off
|
|
# (SteamVR's OpenXR runtime crashes a headless process), lets test/mp_test_driver.gd
|
|
# drive the scripted plate/dirt-station sequence, then prints both logs.
|
|
# Exits non-zero if any check failed.
|
|
|
|
param(
|
|
[string]$Godot = "C:\Users\Aqua Aurora\Desktop\Godot_v4.7-stable_win64.exe",
|
|
[int]$TimeoutSec = 120
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$proj = Split-Path -Parent $PSScriptRoot
|
|
$scene = "res://test/multiPlayerTest.tscn"
|
|
$serverLog = Join-Path $proj "logs/mptest_server.log"
|
|
$clientLog = Join-Path $proj "logs/mptest_client.log"
|
|
|
|
foreach ($f in @($serverLog, $clientLog)) { if (Test-Path $f) { Remove-Item $f -Force } }
|
|
|
|
function Start-Instance($extraArgs) {
|
|
# Single argument string with the project path quoted: Start-Process does
|
|
# not quote array elements, so the space in the path would split it.
|
|
$a = "--headless --xr-mode off --path `"$proj`" $scene -- $($extraArgs -join ' ')"
|
|
$p = Start-Process -FilePath $Godot -ArgumentList $a -PassThru -NoNewWindow
|
|
# Touching .Handle caches it, which is what makes .ExitCode readable later;
|
|
# without this it comes back empty even after the process has exited.
|
|
$null = $p.Handle
|
|
return $p
|
|
}
|
|
|
|
Write-Host "Starting server..."
|
|
$server = Start-Instance @("--server", "--mptest")
|
|
Start-Sleep -Seconds 4
|
|
Write-Host "Starting client..."
|
|
$client = Start-Instance @("--join", "127.0.0.1", "--mptest")
|
|
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
|
while ((Get-Date) -lt $deadline -and -not $server.HasExited) { Start-Sleep -Milliseconds 500 }
|
|
|
|
foreach ($p in @($server, $client)) {
|
|
if (-not $p.HasExited) { Write-Host "Killing pid $($p.Id) (still running)"; $p.Kill() }
|
|
}
|
|
Start-Sleep -Milliseconds 500
|
|
|
|
foreach ($pair in @(@("SERVER", $serverLog), @("CLIENT", $clientLog))) {
|
|
Write-Host ""
|
|
Write-Host "======================== $($pair[0]) ========================"
|
|
if (Test-Path $pair[1]) { Get-Content $pair[1] -Encoding UTF8 } else { Write-Host "(no log written)" }
|
|
}
|
|
|
|
# ExitCode is only populated on the process object after a WaitForExit() call,
|
|
# even when HasExited is already true - without this it reads back empty.
|
|
$server.WaitForExit(2000) | Out-Null
|
|
$code = if ($server.HasExited) { $server.ExitCode } else { 1 }
|
|
Write-Host ""
|
|
Write-Host "server exit code: $code"
|
|
exit $code
|