102 lines
3.6 KiB
GDScript
102 lines
3.6 KiB
GDScript
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)
|