PC3 Phase 2.1: server-authoritative sim + prediction/reconciliation with replay

Made the multiplayer core actually work (it had never run a real sim):

- project.godot: physics pinned to 30 Hz -- the shared sim/snapshot/prediction rate.
- server.gd: loads the world's level (real ground + collision), spawns a
  server Player per peer at the level's PlayerSpawn, broadcasts a full
  snapshot every physics frame with each peer's state AND last_seq (the last
  input seq applied for that peer). Accepts `--port/--world/--status-file`
  args (status file = player count for a future idle reaper); defaults keep
  `godot res://scenes/server.tscn` working standalone.
- player_spawner.gd (server Player): rewrote -- drains its whole input queue
  each physics frame in seq order, integrates gravity/walk/jump with the
  shared constants, move_and_slide(), tracks last_applied_seq. No more no-op
  process_server_input().
- network_client.gd: rewrote reconciliation -- drops inputs the server has
  acked (last_seq), re-simulates the still-in-flight ones from the
  authoritative state, snaps only on real drift. Exposes get_remote_states()
  (peer_id -> latest snapshot state) for the game to render other players.
  predict_and_send() now also sends body yaw.
- proxies trimmed to the RPC set the rewrite actually uses.
- scripts/mp_test_client.gd + run_multiplayer_2p_test.sh: two real client
  processes vs one real server -- client A sees client B and its own
  movement round-trips. PASS.
- run_checkpoint_tests.py: also run the 2p shell test.

smoke_test_network_client 4/4, player 15/15, avatar_in_world 10/10, all green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
2026-09-05 22:00:18 +00:00
parent 587a15a52e
commit 682f1f89f7
9 changed files with 437 additions and 525 deletions
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Two real client processes against one real dedicated server: client A must
# see client B and its own movement must round-trip through the server.
#
# Usage: scripts/run_multiplayer_2p_test.sh
# Exit 0 iff A reports moved=true and remotes>=1.
set -uo pipefail
cd "$(dirname "$0")/.."
GODOT_BIN="${GODOT_BIN:-godot}"
command -v "$GODOT_BIN" >/dev/null 2>&1 || GODOT_BIN="$HOME/godot/godot"
PORT=7787
cleanup() { kill "${SRV_PID:-}" "${B_PID:-}" 2>/dev/null; wait 2>/dev/null; }
trap cleanup EXIT
echo "=== starting dedicated server on $PORT ==="
"$GODOT_BIN" --headless --path . res://scenes/server.tscn -- --port "$PORT" >/tmp/mp2p_server.log 2>&1 &
SRV_PID=$!
sleep 3
echo "=== starting client B (idle observer) ==="
"$GODOT_BIN" --headless --path . --script res://scripts/mp_test_client.gd -- --role b --port "$PORT" >/tmp/mp2p_b.log 2>&1 &
B_PID=$!
# Wait until B is actually connected before starting A.
for _ in $(seq 1 30); do grep -q 'RESULT b connected' /tmp/mp2p_b.log && break; sleep 1; done
echo "=== running client A (mover) ==="
"$GODOT_BIN" --headless --path . --script res://scripts/mp_test_client.gd -- --role a --port "$PORT" 2>&1 | tee /tmp/mp2p_a.log
A_LINE=$(grep '^RESULT a' /tmp/mp2p_a.log || true)
wait "$B_PID" 2>/dev/null
B_LINE=$(grep '^RESULT b' /tmp/mp2p_b.log || true)
echo
echo "server: $(grep -c 'joined' /tmp/mp2p_server.log) joins seen"
echo "A: ${A_LINE:-<none>}"
echo "B: ${B_LINE:-<none>}"
if echo "$A_LINE" | grep -q 'moved=true' && echo "$A_LINE" | grep -qE 'remotes=[1-9]'; then
echo "=== SMOKE RESULT: PASS ==="
exit 0
fi
echo "=== SMOKE RESULT: FAIL ==="
exit 1