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>
48 lines
2.0 KiB
PowerShell
48 lines
2.0 KiB
PowerShell
# Shared helper for placing the two game windows side by side.
|
|
#
|
|
# Godot's --position flag is ignored on this setup (the window lands at x=-7
|
|
# whatever you pass), so the windows are moved with the Win32 API after they
|
|
# come up. Dot-source this file to get Move-GameWindow.
|
|
|
|
if (-not ('MpWin' -as [type])) {
|
|
Add-Type @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
public class MpWin {
|
|
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
|
|
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
}
|
|
"@
|
|
}
|
|
|
|
# Godot's windows are DPI aware; PowerShell's process is not by default, so
|
|
# GetWindowRect/SetWindowPos would otherwise be talking in virtualised
|
|
# coordinates and the windows land nowhere near where we asked.
|
|
[void][MpWin]::SetProcessDPIAware()
|
|
|
|
# Moves a just-launched game window to (x, y) and returns its width, so the
|
|
# caller can place the next window immediately to its right. Never resizes:
|
|
# changing the window size independently of Godot's --resolution distorts the
|
|
# rendered aspect ratio.
|
|
function Move-GameWindow($proc, [int]$x, [int]$y) {
|
|
for ($i = 0; $i -lt 60 -and $proc.MainWindowHandle -eq 0; $i++) {
|
|
Start-Sleep -Milliseconds 250
|
|
$proc.Refresh()
|
|
}
|
|
if ($proc.MainWindowHandle -eq 0) {
|
|
Write-Host " (window never appeared; leaving it where it is)"
|
|
return 620
|
|
}
|
|
# The handle shows up before Godot has finished sizing/positioning the
|
|
# window - moving it too early gets overwritten by Godot's own setup.
|
|
Start-Sleep -Milliseconds 2500
|
|
$h = $proc.MainWindowHandle
|
|
# SWP_NOSIZE (0x1) | SWP_NOZORDER (0x4)
|
|
[void][MpWin]::SetWindowPos($h, [IntPtr]::Zero, $x, $y, 0, 0, 0x0005)
|
|
$r = New-Object MpWin+RECT
|
|
[void][MpWin]::GetWindowRect($h, [ref]$r)
|
|
return [int]($r.Right - $r.Left)
|
|
}
|