Files
VRyHungry1/test/mp_window_layout.ps1
T
algodoogle 23ec41c1d6 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>
2026-07-26 02:00:50 +01:00

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)
}