extends SceneTree ## Real integration test for NetworkClient (client-side prediction + ## reconciliation) against an actual dedicated server process. ## ## This does NOT boot its own server -- run scenes/server.tscn as a separate ## process first (see docs/architecture/networking.md's ENet/port setup): ## godot --headless --path . scenes/server.tscn & ## then, from the same project directory: ## godot --headless --path . --script res://scripts/smoke_test_network_client.gd ## ## Verifies: the client can actually connect over ENet, local prediction ## moves the player immediately on input (no waiting for the server), ## server snapshots actually arrive and update the client's authoritative ## view, and a deliberately-desynced predicted position gets corrected by ## reconciliation. var passed := 0 var failed := 0 func _initialize() -> void: _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", 7777) if not _check("connected to the real dedicated server", ok): printerr(" Is `godot --headless --path . scenes/server.tscn` running on port 7777?") _finish() return # Give the server a few frames to process the connection and spawn our # player node before we start sending input. for i in 10: await process_frame # 1. Prediction: pressing "forward" should move the local position # immediately, without waiting for any server round trip. var pos_before: Vector3 = net.position for i in 10: net.predict_and_send(1.0 / 30.0, 0.0, -1.0, false) # move -Z (forward) await process_frame var pos_after_predict: Vector3 = net.position _check("local prediction moved the player without waiting for the server", pos_after_predict.distance_to(pos_before) > 0.1) # 2. Snapshots actually arrive from the real server and get processed -- # wait for at least one reconciliation-eligible snapshot by giving it # real time (server broadcasts at its own tick rate). var got_snapshot := false net.reconciled.connect(func(_err): got_snapshot = true) for i in 60: net.predict_and_send(1.0 / 30.0, 0.0, -1.0, false) await process_frame # Reconciliation only actually fires on a real mismatch -- absence of a # large mismatch is a PASS outcome too (means prediction tracked the # server closely), so this alone isn't proof snapshots arrived. Check # that directly instead: _check("prediction buffer has entries (input was actually sent)", net._prediction_buffer.size() > 0) # 3. Deliberately desync prediction from reality, then confirm the next # snapshot corrects it -- the real proof reconciliation logic works, # not just that it never had to fire. net.position = Vector3(9999, 9999, 9999) var desynced_pos := net.position var reconciled_this_round := false var correction_seen: float = -1.0 var reconcile_handler := func(err): reconciled_this_round = true; correction_seen = err net.reconciled.connect(reconcile_handler) for i in 60: await process_frame if reconciled_this_round: break net.reconciled.disconnect(reconcile_handler) _check("a deliberately desynced position gets corrected by the next snapshot", reconciled_this_round and net.position.distance_to(desynced_pos) > 100.0) if reconciled_this_round: print(" correction distance was ~", correction_seen) net.disconnect_from_server() _finish() func _check(description: String, condition: bool) -> bool: if condition: print(" PASS: ", description) passed += 1 else: printerr(" FAIL: ", description) failed += 1 return condition func _finish() -> void: print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed]) quit(0 if failed == 0 else 1)