Fix bullet/ship desync; rework death, escape, arrival and hub awareness
(1) Bullets appeared to trail the ship. Two independent causes, measured with
the new tools/diag_prediction.gd rather than guessed at:
- ServerRuntime ticked before ClientRuntime, so input sampled on frame N was
not consumed until frame N+1, leaving the drawn ship a constant one tick
(4.00px at 240 u/s) ahead of the authoritative one that bullets spawn from.
ClientRuntime now sets process_physics_priority = -10. Gap on a listen
server: 4.00px -> 0.10px mean, 0.30px worst.
- PLAYER_MUZZLE_OFFSET was PLAYER_RADIUS + 6 = 12px against a 13px drawn
ship, so bullets were born inside the sprite. Regression from the previous
commit's hitbox shrink; it now derives from PLAYER_VISUAL_RADIUS.
(2) No more timed respawn. A downed player stays down until they ask for the
hub (E), which is an ordinary input -- the server has no "revive me" message.
(3) Escape channel 3s -> 1s, and damage no longer cancels it. An interruptible
channel makes killing the process strictly better than using the button, so a
dropped connection now runs the same channel: the player stays in the world as
linkdead, still killable, and is only released once it completes. Instances
refuse to close while a linkdead body is resolving, or a solo drop would delete
it on the next tick and hand the exploit straight back.
(4) Escape opens an in-game menu: return to hub (routed through the same held-
escape channel, not a new message), disconnect, quit.
(5) Server pushes a roster so the hub shows who is online and which dungeon
they are in. Entering a dungeon grants 2s arrival protection -- invulnerable
AND weapons-cold, since invulnerability alone would make the spawn a free
firing position -- flagged in the snapshot and drawn on every protected ship.
(6) Cleared dungeons hold the party 30s (was 5s) with a visible countdown.
(7) The hub's grey circle was a 100k-HP target dummy that read as scenery. Now
drawn as a bullseye so its purpose is legible.
Protocol version 1 -> 2. 91 tests (was 78); smoke.sh gains a bot that is
SIGKILLed mid-dungeon to prove the disconnect path end to end. check.sh,
test.sh and smoke.sh all pass.
This commit is contained in:
+59
-3
@@ -8,10 +8,14 @@ extends RefCounted
|
||||
|
||||
# --- Snapshot ---------------------------------------------------------------
|
||||
|
||||
static func encode_snapshot(world: SimWorld) -> PackedByteArray:
|
||||
## [param cleared_countdown] is whole seconds until a cleared dungeon returns
|
||||
## its party, or Protocol.COUNTDOWN_NONE when that does not apply.
|
||||
static func encode_snapshot(world: SimWorld,
|
||||
cleared_countdown: int = Protocol.COUNTDOWN_NONE) -> PackedByteArray:
|
||||
var b := StreamPeerBuffer.new()
|
||||
b.big_endian = false
|
||||
b.put_u32(world.tick)
|
||||
b.put_u8(clampi(cleared_countdown, 0, Protocol.COUNTDOWN_NONE))
|
||||
|
||||
b.put_u8(mini(world.players.size(), 255))
|
||||
for p in world.players.values():
|
||||
@@ -27,6 +31,10 @@ static func encode_snapshot(world: SimWorld) -> PackedByteArray:
|
||||
flags |= Protocol.F_INVULN
|
||||
if p.escape_ticks > 0:
|
||||
flags |= Protocol.F_ESCAPING
|
||||
if p.spawn_grace > 0:
|
||||
flags |= Protocol.F_SPAWN_GRACE
|
||||
if p.linkdead:
|
||||
flags |= Protocol.F_LINKDEAD
|
||||
b.put_u8(flags)
|
||||
b.put_u8(clampi(roundi(p.escape_progress() * 255.0), 0, 255))
|
||||
# Echoed so the owning client knows how far to rewind when reconciling.
|
||||
@@ -62,7 +70,11 @@ static func decode_snapshot(data: PackedByteArray) -> Dictionary:
|
||||
var b := StreamPeerBuffer.new()
|
||||
b.big_endian = false
|
||||
b.data_array = data
|
||||
var snap := {"tick": b.get_u32(), "players": [], "enemies": [], "boss": null}
|
||||
var snap := {
|
||||
"tick": b.get_u32(),
|
||||
"cleared_countdown": b.get_u8(),
|
||||
"players": [], "enemies": [], "boss": null,
|
||||
}
|
||||
|
||||
var pcount := b.get_u8()
|
||||
for _i in pcount:
|
||||
@@ -99,7 +111,13 @@ static func decode_snapshot(data: PackedByteArray) -> Dictionary:
|
||||
# --- Events -----------------------------------------------------------------
|
||||
|
||||
## Events the client never sees; the instance layer consumes them server-side.
|
||||
const SERVER_ONLY := [SimEvent.Type.PORTAL_USED, SimEvent.Type.ESCAPE_COMPLETED]
|
||||
## These describe a decision the server is about to act on, not an outcome --
|
||||
## leaking them would tell a client about a message it might try to forge.
|
||||
const SERVER_ONLY := [
|
||||
SimEvent.Type.PORTAL_USED,
|
||||
SimEvent.Type.ESCAPE_COMPLETED,
|
||||
SimEvent.Type.RESPAWN_REQUESTED,
|
||||
]
|
||||
|
||||
|
||||
## [param server_tick] rides along so the client can fast-forward a bullet by
|
||||
@@ -232,3 +250,41 @@ static func decode_inputs(data: PackedByteArray) -> Array[InputFrame]:
|
||||
for _i in count:
|
||||
out.append(InputFrame.read(b))
|
||||
return out
|
||||
|
||||
|
||||
# --- Roster -----------------------------------------------------------------
|
||||
# Who is online and where. Low frequency (membership changes only), so it is
|
||||
# the one message that carries strings; everything hot stays fixed-width.
|
||||
|
||||
static func encode_roster(entries: Array[Dictionary]) -> PackedByteArray:
|
||||
var b := StreamPeerBuffer.new()
|
||||
b.big_endian = false
|
||||
b.put_u8(mini(entries.size(), 255))
|
||||
for e in entries:
|
||||
b.put_u32(int(e["peer"]))
|
||||
b.put_utf8_string(String(e["name"]))
|
||||
b.put_u8(int(e["kind"]))
|
||||
b.put_u32(int(e["instance"]))
|
||||
b.put_u8(1 if e["alive"] else 0)
|
||||
return b.data_array
|
||||
|
||||
|
||||
static func decode_roster(data: PackedByteArray) -> Array[Dictionary]:
|
||||
var out: Array[Dictionary] = []
|
||||
if data.size() < 1:
|
||||
return out
|
||||
var b := StreamPeerBuffer.new()
|
||||
b.big_endian = false
|
||||
b.data_array = data
|
||||
var count := b.get_u8()
|
||||
for _i in count:
|
||||
# get_utf8_string reads its own length prefix, so a truncated packet
|
||||
# yields empty strings rather than reading off the end.
|
||||
out.append({
|
||||
"peer": b.get_u32(),
|
||||
"name": b.get_utf8_string(),
|
||||
"kind": b.get_u8(),
|
||||
"instance": b.get_u32(),
|
||||
"alive": b.get_u8() == 1,
|
||||
})
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user