Files
modbison/scripts/mp_test_client.gd
T
daddy 682f1f89f7 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
2026-09-05 22:00:18 +00:00

59 lines
1.8 KiB
GDScript

extends SceneTree
## One client process for the 2-player multiplayer test
## (scripts/run_multiplayer_2p_test.sh). Two real OS processes = real network.
##
## godot --headless --path . --script res://scripts/mp_test_client.gd -- --role a --port 7777
##
## role "a": moves forward for ~2 s, then prints
## RESULT a remotes=<n> moved=<bool> and exits 0 iff it both moved
## and saw >=1 remote player.
## role "b": connects, holds still for ~4 s, prints RESULT b connected exits 0.
var role := "b"
var port := 7777
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
for i in args.size():
if args[i] == "--role" and i + 1 < args.size():
role = args[i + 1]
elif args[i] == "--port" and i + 1 < args.size():
port = int(args[i + 1])
_run()
func _run() -> void:
await process_frame
var net := NetworkClient.new()
root.add_child(net)
var ok: bool = await net.connect_to_server("127.0.0.1", port)
if not ok:
print("RESULT %s connect_failed" % role)
quit(1)
return
for i in 15:
await process_frame
if role == "b":
# Hold still and stay connected long enough for A to boot, connect,
# and observe us (A's Godot startup alone is a couple of seconds).
print("RESULT b connected")
for i in 1200:
await process_frame
net.disconnect_from_server()
quit(0)
return
# role a: move forward, watch for a remote and our own displacement.
var start: Vector3 = net.position
var max_remotes := 0
for i in 90:
net.predict_and_send(1.0 / 30.0, 0.0, -1.0, false, 0.0)
await process_frame
max_remotes = maxi(max_remotes, net.get_remote_states().size())
var moved: bool = net.position.distance_to(start) > 1.0
print("RESULT a remotes=%d moved=%s" % [max_remotes, str(moved)])
net.disconnect_from_server()
quit(0 if (moved and max_remotes >= 1) else 1)