Fix permanent input-timing desync; no i-frames; UI respawn; guard dead joins
ci / verify (push) Successful in 45s
ci / verify (push) Successful in 45s
The real cause of the ship/bullet separation, which the previous commit only half-addressed. The server dropped inputs past a lead of 12 while the client only re-synced past 16, so a client whose lead drifted into 13-16 had every input silently rejected while believing its timing was fine. The server coasted on held_input and then stopped; the client kept predicting. The two separated permanently and the reconciler fought it every snapshot -- "shoved around". It needed two independent clocks to drift, hence "only after some time", and nothing in the loop could notice, hence "then persists". The listen-server diagnostic could never reproduce it: one process, one physics tick, lead constant by construction. Two defences: INPUT_MAX_LEAD (40) is now far wider than the client's correction band (3..20), asserted by tests/unit/test_input_lead.gd so narrowing it fails a test; and an ack-stall detector re-syncs when last_input_tick stops advancing, which catches the whole class regardless of cause -- lead alone cannot, because a wrong lead looks normal from the client. diag_prediction.gd now injects a +14 tick drift and exits non-zero unless the gap recovers. Also: - No invulnerability frames. Every bullet that touches a player lands; i-frames made dense patterns safer than sparse ones, which inverts the genre. Measured: a stationary player survives ~13.6s of the Warden's opening phase, ~17.5s drifting. spawn_grace remains the only invulnerable state. - Death is exited with a HUD button, disabled for the first 3s. The lockout is enforced in SimWorld, not just by graying the button -- a client that ignores its own UI still waits. The interact key no longer respawns. - Joining a server that is not there no longer drops the player into an empty lobby they cannot act in. Net.join() only creates an ENet object; the game scene now waits for the server to actually place us in an instance, with an 8s timeout, and headless runs exit non-zero instead of idling. Protocol 2 -> 3. 98 tests; check.sh, test.sh and smoke.sh all pass.
This commit is contained in:
+11
-4
@@ -9,10 +9,11 @@ var pos := Vector2.ZERO
|
||||
var aim: float = 0.0
|
||||
var hp: int = SimConfig.PLAYER_MAX_HP
|
||||
var alive: bool = true
|
||||
var iframes: int = 0
|
||||
var fire_cooldown: int = 0
|
||||
## Ticks of arrival protection left: invulnerable, and unable to shoot.
|
||||
var spawn_grace: int = 0
|
||||
## Ticks before a downed player may ask to return to the hub.
|
||||
var respawn_lockout: int = 0
|
||||
|
||||
## The peer's connection dropped, but the player is deliberately still in the
|
||||
## world. Held here rather than deleted so a disconnect cannot be used to dodge
|
||||
@@ -37,9 +38,10 @@ func escape_progress() -> float:
|
||||
return clampf(float(escape_ticks) / float(SimConfig.ESCAPE_CHANNEL_TICKS), 0.0, 1.0)
|
||||
|
||||
|
||||
## Invulnerable while arriving, or in the usual post-hit window.
|
||||
## Arrival protection only -- there is no post-hit invulnerability. See the
|
||||
## note on SimConfig.SPAWN_GRACE_TICKS.
|
||||
func invulnerable() -> bool:
|
||||
return spawn_grace > 0 or iframes > 0
|
||||
return spawn_grace > 0
|
||||
|
||||
|
||||
## Arrival protection also locks the gun, so the spawn point is not a free
|
||||
@@ -54,8 +56,13 @@ func reset_for_instance(spawn: Vector2, grace: int = 0) -> void:
|
||||
pos = spawn
|
||||
hp = SimConfig.PLAYER_MAX_HP
|
||||
alive = true
|
||||
iframes = 0
|
||||
spawn_grace = grace
|
||||
respawn_lockout = 0
|
||||
fire_cooldown = 0
|
||||
escape_ticks = 0
|
||||
input_queue.clear()
|
||||
|
||||
|
||||
## True once the lockout has run out and the hub is available again.
|
||||
func can_request_respawn() -> bool:
|
||||
return not alive and respawn_lockout <= 0
|
||||
|
||||
@@ -149,8 +149,6 @@ func step() -> void:
|
||||
|
||||
func _step_players() -> void:
|
||||
for p in players.values():
|
||||
if p.iframes > 0:
|
||||
p.iframes -= 1
|
||||
if p.spawn_grace > 0:
|
||||
p.spawn_grace -= 1
|
||||
if p.fire_cooldown > 0:
|
||||
@@ -159,10 +157,13 @@ func _step_players() -> void:
|
||||
var frame := _take_input(p)
|
||||
|
||||
if not p.alive:
|
||||
if p.respawn_lockout > 0:
|
||||
p.respawn_lockout -= 1
|
||||
# No timed respawn: a downed player waits for the hub. Asking to go
|
||||
# is an input like any other, so a dead client cannot be revived by
|
||||
# anything except its own request reaching the server.
|
||||
if frame.pressed(InputFrame.BTN_INTERACT) or p.linkdead:
|
||||
# anything except its own request reaching the server -- and not
|
||||
# before the lockout expires, however early its UI lets it ask.
|
||||
if p.can_request_respawn() and (frame.pressed(InputFrame.BTN_INTERACT) or p.linkdead):
|
||||
events.append({"t": SimEvent.Type.RESPAWN_REQUESTED, "peer": p.peer_id})
|
||||
continue
|
||||
|
||||
@@ -372,11 +373,11 @@ func _kill_bullet(slot: int) -> void:
|
||||
## SimConfig.ESCAPE_CHANNEL_TICKS for why that would reward pulling the plug.
|
||||
func _damage_player(p: SimPlayer, amount: int) -> void:
|
||||
p.hp = maxi(p.hp - amount, 0)
|
||||
p.iframes = SimConfig.PLAYER_IFRAMES
|
||||
events.append({"t": SimEvent.Type.PLAYER_HIT, "peer": p.peer_id, "dmg": amount, "hp": p.hp})
|
||||
if p.hp <= 0:
|
||||
p.alive = false
|
||||
p.escape_ticks = 0
|
||||
p.respawn_lockout = SimConfig.RESPAWN_LOCKOUT_TICKS
|
||||
events.append({"t": SimEvent.Type.PLAYER_DIED, "peer": p.peer_id})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user