Fix five multiplayer sync bugs, add automated two-instance test
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>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# 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)
|
||||
}
|
||||
Reference in New Issue
Block a user