extends GutTest ## The client numbers its input frames a fixed "lead" ahead of the server's ## tick, and the server accepts a window around its own tick. These two numbers ## have to be chosen together. ## ## They were not, and the result was the worst kind of bug: the server silently ## dropped every input from a client whose lead had drifted into the gap between ## "server stops accepting" and "client notices", the client never corrected ## because by its own reckoning nothing was wrong, and the server coasted on a ## stale input forever. It needed two independent clocks to drift apart, so it ## only appeared after a few minutes -- and then never went away. var world: SimWorld const PEER := 5 func before_each() -> void: world = SimWorld.new(1) world.add_player(PEER, "tester") world.tick = 1000 func _queue_at_lead(lead: int) -> void: var frames: Array[InputFrame] = [ InputFrame.make(world.tick + lead, Vector2.RIGHT, 0.0, 0)] world.queue_input(PEER, frames) ## The invariant that was violated. The client corrects itself at ## INPUT_LEAD_MAX; the server must keep accepting well past that, or there is a ## band where the server refuses input the client still thinks is fine. func test_the_server_accepts_every_lead_the_client_tolerates() -> void: assert_gt(SimConfig.INPUT_MAX_LEAD, SimConfig.INPUT_LEAD_MAX, "the server's acceptance window must be wider than the client's " + "correction band, or drift lands in a silent dead zone") func test_inputs_across_the_whole_client_band_are_accepted() -> void: for lead in range(SimConfig.INPUT_LEAD_MIN, SimConfig.INPUT_LEAD_MAX + 1): world.players[PEER].input_queue.clear() world.players[PEER].last_input_tick = 0 _queue_at_lead(lead) assert_eq(world.players[PEER].input_queue.size(), 1, "lead %d is inside the band the client will not correct, so the " % lead + "server has to accept it") func test_absurd_lead_is_still_rejected() -> void: _queue_at_lead(SimConfig.INPUT_MAX_LEAD + 1) assert_eq(world.players[PEER].input_queue.size(), 0, "the window is wider, not gone -- a client claiming a far-future tick " + "still buys nothing") ## What the drift actually looked like from the server's side: nothing arrives, ## so it coasts on the last input it saw. That is correct behaviour for a brief ## hiccup and catastrophic as a permanent state, which is why the client now ## has a stall detector rather than relying on its lead estimate alone. func test_a_starved_server_coasts_then_stops() -> void: var p: SimPlayer = world.players[PEER] _queue_at_lead(SimConfig.INPUT_TARGET_LEAD) world.step() var moved_once: Vector2 = p.pos assert_ne(moved_once, Vector2.ZERO, "setup: the input should have moved us") for _i in SimConfig.INPUT_MAX_AGE + 5: world.step() var coasted: Vector2 = p.pos for _i in 60: world.step() assert_eq(p.pos, coasted, "coasting has to end, or a silent client drifts forever")