Files
transcience/tools/smoke.sh
T
claude 005679f1b5 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.
2026-09-03 18:43:19 +02:00

119 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# End-to-end integration test: boot a dedicated server, connect two scripted
# bot clients over a real ENet socket, and assert that the whole loop happened --
# handshake, lobby, portal into a dungeon, and the emergency escape back.
#
# This is the test that catches everything unit tests structurally cannot: RPC
# wiring, codec round-trips over the wire, instance transfers, and the client's
# reconciliation loop. Run it before calling any networking change done.
#
# tools/smoke.sh # ~35s
# KEEP=1 tools/smoke.sh # keep the logs and print where they are
set -uo pipefail
cd "$(dirname "$0")/.."
GODOT="${GODOT:-godot}"
PORT="${PORT:-27099}"
SERVER_TICKS="${SERVER_TICKS:-2000}"
CLIENT_TICKS="${CLIENT_TICKS:-1700}"
OUT="$(mktemp -d -t transcience-smoke-XXXXXX)"
PIDS=()
cleanup() {
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"
else
rm -rf "$OUT"
fi
}
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
sleep 0.25
done
if ! grep -q "SERVER_READY" "$OUT/server.log" 2>/dev/null; then
echo "FAIL: server never became ready"; cat "$OUT/server.log"; exit 1
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
# 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>
if grep -qE "$3" "$2"; then
echo " ok $1"
else
echo " FAIL $1 (no match for /$3/ in $(basename "$2"))"
fails=$((fails + 1))
fi
}
refute() { # refute <label> <file> <pattern>
local hits
hits=$(grep -cE "$3" "$2" || true)
if [[ "$hits" == "0" ]]; then
echo " ok $1"
else
echo " FAIL $1 ($hits line(s) matched /$3/ in $(basename "$2"))"
grep -E "$3" "$2" | head -5 | sed 's/^/ /'
fails=$((fails + 1))
fi
}
echo "assertions:"
check "server accepted bot1" "$OUT/server.log" "joined as 'bot1'"
check "server accepted bot2" "$OUT/server.log" "joined as 'bot2'"
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"
echo
if [[ $fails -eq 0 ]]; then
echo "SMOKE PASS"
else
echo "SMOKE FAIL ($fails)"
echo "--- server.log (tail) ---"; tail -40 "$OUT/server.log"
echo "--- bot1.log (tail) ---"; tail -40 "$OUT/bot1.log"
fi
exit $fails