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:
@@ -0,0 +1,59 @@
|
||||
extends Node
|
||||
## Measures the gap between where the client *draws* the local player and where
|
||||
## the server actually has it -- the number that decides whether bullets appear
|
||||
## to leave the ship's nose or trail out of its back.
|
||||
##
|
||||
## godot --headless --path . res://tools/diag_prediction.tscn
|
||||
##
|
||||
## Runs the real listen-server path (ServerRuntime + ClientRuntime + the actual
|
||||
## Net loopback), not a mock, so node process order is exactly production's.
|
||||
|
||||
var _ticks: int = 0
|
||||
var _worst: float = 0.0
|
||||
var _sum: float = 0.0
|
||||
var _samples: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameOpts.bot_client = true
|
||||
GameOpts.player_name = "diag"
|
||||
if Net.host(27300) != OK:
|
||||
push_error("could not host")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
Net.start_local_client()
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
_ticks += 1
|
||||
var srv := Net.server
|
||||
var cli := Net.client
|
||||
if srv == null or cli == null:
|
||||
return
|
||||
var inst := srv.instance_of(Net.LOCAL_PEER)
|
||||
if inst == null or not inst.world.players.has(Net.LOCAL_PEER):
|
||||
return
|
||||
var sp: SimPlayer = inst.world.players[Net.LOCAL_PEER]
|
||||
var delta_px := sp.pos.distance_to(cli.predicted_pos)
|
||||
|
||||
# Skip the first few ticks while the handshake settles.
|
||||
if _ticks > 20:
|
||||
_worst = maxf(_worst, delta_px)
|
||||
_sum += delta_px
|
||||
_samples += 1
|
||||
|
||||
if _ticks % 20 == 0:
|
||||
print("tick=%4d srv_tick=%4d srv_pos=%-22s pred=%-22s gap=%6.2fpx acked=%4d cli_tick=%4d queue=%d" % [
|
||||
_ticks, inst.world.tick, str(sp.pos.round()), str(cli.predicted_pos.round()),
|
||||
delta_px, sp.last_input_tick, cli.input_tick, sp.input_queue.size()])
|
||||
|
||||
if _ticks >= 300:
|
||||
print("---")
|
||||
print("GAP worst=%.2fpx mean=%.2fpx over %d samples" % [_worst, _sum / maxf(_samples, 1), _samples])
|
||||
print("player speed is %.0f u/s = %.2f px per tick" % [
|
||||
SimConfig.PLAYER_SPEED, SimConfig.PLAYER_SPEED * SimConfig.TICK_DELTA])
|
||||
print("muzzle offset = %.1fpx, drawn ship radius = %.1fpx, hitbox = %.1fpx" % [
|
||||
SimConfig.PLAYER_MUZZLE_OFFSET, SimConfig.PLAYER_VISUAL_RADIUS,
|
||||
SimConfig.PLAYER_RADIUS])
|
||||
Net.shutdown()
|
||||
get_tree().quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bcsb8u10uk0o1
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/diag_prediction.gd" id="1"]
|
||||
|
||||
[node name="DiagPrediction" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -19,6 +19,7 @@ func _init() -> void:
|
||||
_action("fire", [_mouse(MOUSE_BUTTON_LEFT), _key(KEY_SPACE)])
|
||||
_action("emergency_escape", [_key(KEY_F)])
|
||||
_action("interact", [_key(KEY_E)])
|
||||
_action("system_menu", [_key(KEY_ESCAPE)])
|
||||
var err := ProjectSettings.save()
|
||||
print("input map written, err=%d" % err)
|
||||
quit(0 if err == OK else 1)
|
||||
|
||||
+30
-3
@@ -17,8 +17,11 @@ SERVER_TICKS="${SERVER_TICKS:-2000}"
|
||||
CLIENT_TICKS="${CLIENT_TICKS:-1700}"
|
||||
OUT="$(mktemp -d -t transcience-smoke-XXXXXX)"
|
||||
|
||||
PIDS=()
|
||||
cleanup() {
|
||||
kill %1 %2 %3 2>/dev/null
|
||||
for pid in "${PIDS[@]:-}"; do
|
||||
kill "$pid" 2>/dev/null
|
||||
done
|
||||
wait 2>/dev/null
|
||||
if [[ "${KEEP:-0}" == "1" ]]; then
|
||||
echo "logs kept in $OUT"
|
||||
@@ -31,6 +34,7 @@ trap cleanup EXIT
|
||||
echo "smoke: server on port $PORT, logs in $OUT"
|
||||
"$GODOT" --headless --path . -- --server --port "$PORT" --autoquit "$SERVER_TICKS" \
|
||||
> "$OUT/server.log" 2>&1 &
|
||||
PIDS+=($!)
|
||||
|
||||
for _ in $(seq 1 60); do
|
||||
grep -q "SERVER_READY" "$OUT/server.log" 2>/dev/null && break
|
||||
@@ -43,11 +47,31 @@ fi
|
||||
for n in 1 2; do
|
||||
"$GODOT" --headless --path . -- --bot --host 127.0.0.1 --port "$PORT" \
|
||||
--name "bot$n" --autoquit "$CLIENT_TICKS" > "$OUT/bot$n.log" 2>&1 &
|
||||
PIDS+=($!)
|
||||
sleep 0.4
|
||||
done
|
||||
|
||||
wait %2 %3 2>/dev/null
|
||||
wait %1 2>/dev/null
|
||||
# A third bot that gets SIGKILLed the moment it is inside a dungeon. This is the
|
||||
# anti-disconnect-cheese path: the server must keep its body in the world,
|
||||
# channel it out over the same one second the escape button costs, and only then
|
||||
# forget the peer -- never delete it instantly on socket close.
|
||||
"$GODOT" --headless --path . -- --bot --host 127.0.0.1 --port "$PORT" \
|
||||
--name "dropbot" --autoquit "$CLIENT_TICKS" > "$OUT/dropbot.log" 2>&1 &
|
||||
DROP_PID=$!
|
||||
for _ in $(seq 1 80); do
|
||||
grep -q "entered instance .*DUNGEON" "$OUT/dropbot.log" 2>/dev/null && break
|
||||
sleep 0.25
|
||||
done
|
||||
if grep -q "entered instance .*DUNGEON" "$OUT/dropbot.log" 2>/dev/null; then
|
||||
# Reaped here so bash does not print its own "Killed" job-control line; the
|
||||
# kill is the point of the test, not a failure.
|
||||
{ kill -9 "$DROP_PID"; wait "$DROP_PID"; } 2>/dev/null || true
|
||||
else
|
||||
echo " WARN dropbot never reached a dungeon; drop assertions will fail"
|
||||
PIDS+=("$DROP_PID")
|
||||
fi
|
||||
|
||||
wait 2>/dev/null
|
||||
|
||||
fails=0
|
||||
check() { # check <label> <file> <pattern>
|
||||
@@ -77,6 +101,9 @@ check "a dungeon instance opened" "$OUT/server.log" "opened dungeon instance"
|
||||
check "emergency escape completed" "$OUT/server.log" "escaped to lobby"
|
||||
check "bot1 reached a dungeon" "$OUT/bot1.log" "entered instance .*DUNGEON"
|
||||
check "bot1 returned to the lobby" "$OUT/bot1.log" "entered instance .*LOBBY"
|
||||
check "a hard drop is channelled, not instant" \
|
||||
"$OUT/server.log" "dropped in instance [0-9]+, channelling out"
|
||||
check "the dropped body is released" "$OUT/server.log" "released from instance [0-9]+ after drop"
|
||||
refute "no server script errors" "$OUT/server.log" "SCRIPT ERROR|Parse Error|USER ERROR"
|
||||
refute "no client script errors" "$OUT/bot1.log" "SCRIPT ERROR|Parse Error|USER ERROR"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user