From 1b6d247e51e775d43d2bb823451da9d2c542e17e Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Tue, 11 Aug 2026 17:52:51 -0500 Subject: [PATCH] #162 4-node retest CAUGHT + FIXED a same-frame double-credit: two lethal deliveries of one salvo (direct + splash, 2ms apart, no sim tick between) each read destroyedAtEntry=0 -- the entry latch samples movementMode 2||9 but our death transition (mode 9 write) ran NEXT TICK in UpdateDeathState, while the binary flips mode SYNCHRONOUSLY inside the same damage pass (its tail gate part_012.c:14761 reads 9||10 already set). Result: duplicate kill credit to the shooter (bench receipts: kills 1->2 in 2ms, awards 525.00/516.67) and a duplicate VehicleDead whose death cycle the receiver dedup swallowed -- but the kill report had already escaped. Invisible to every 2-node bench (direct kills deliver one lethal hit; splash+direct overlap needs crowds) -- the 8-player field 'kills way off' shape. Fix: the death tail now runs the once-per-death transition inline (mode 9 + type-6 + effects) before returning, exactly the binary's sequence; the next lethal message latches on both destroyedAtEntry and deathBlastArmed. The wrong 'synchronous' claim in the old comment corrected. Benches: kd4_bench (receipt-derived per-player invariants + bystander mirror convergence + no-mirror-exceeds-authority + inst=R audit) PASS 9/9/9/9 SWALLOWED=0; kd8_stress staged (8-node all-vs-all melee). Co-Authored-By: Claude Fable 5 --- game/reconstructed/mech.cpp | 25 +++++- scratchpad/night15/kd4_bench.sh | 131 +++++++++++++++++++++++++++++++ scratchpad/night15/kd8_stress.sh | 122 ++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 scratchpad/night15/kd4_bench.sh create mode 100644 scratchpad/night15/kd8_stress.sh diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 96adb72..1f0b31f 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -1357,10 +1357,18 @@ void // falloff bursts/dist^1.25 per victim inside the shared core. // 4. ForceUpdate(1). // The gate predicate here is IsMechDestroyed() (graphicAlarm >= 9, the - // structural flag @0x49fb54) -- the binary tests movementMode 9|10, but - // the death transition sets mode 9 synchronously with the structural flag - // on every path through here, so the edges coincide; this exact predicate - // is the one the #89 blast benches verified both ways. + // structural flag @0x49fb54) -- the binary tests movementMode 9|10, and + // sets mode 9 SYNCHRONOUSLY inside this same damage pass (its own tail + // gate part_012.c:14761 reads mode 9||10 already flipped). An earlier + // comment here claimed our transition was synchronous too -- it was NOT + // (UpdateDeathState ran per-frame), and the 4-node retest caught the + // window: TWO lethal deliveries in one event drain (direct + splash of + // the same salvo, 2ms apart, no sim tick between) each read + // destroyedAtEntry=0 and re-ran the whole death block -- duplicate kill + // credit to the shooter (kills 1->2 in 2ms) and a duplicate VehicleDead + // (the receiver dedup swallowed the death cycle but the kill report had + // already been posted). The transition is now invoked synchronously + // below, closing the window exactly the way the binary does. death_tail: // #162 (2026-08-11) -- MASTER ONLY: the discipline #150 restored for the // score reports, now applied to the death tail it forgot. In 1995 this @@ -1388,6 +1396,15 @@ death_tail: extern void BTApplyDeathSplash(void *mech_v); BTApplyDeathSplash((void *)this); + + // Run the once-per-death transition NOW (mode 9 + type-6 ForceUpdate + + // death effects), not next tick -- the binary flips mode inside this + // same pass, which is the entire same-frame double-delivery latch: the + // NEXT lethal message of this volley reads destroyedAtEntry=1 and + // deathBlastArmed=0, so it can neither re-credit the kill nor re-post + // VehicleDead. dt=0: the transition branch has no time dependence; the + // per-frame caller takes the wreck-maintenance branch from here on. + UpdateDeathState(0.0f); } } diff --git a/scratchpad/night15/kd4_bench.sh b/scratchpad/night15/kd4_bench.sh new file mode 100644 index 0000000..01b73ca --- /dev/null +++ b/scratchpad/night15/kd4_bench.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# #162 4-NODE RETEST -- the K/D invariants under bystanders, which the 2-node +# bench structurally cannot see (every kill involved both nodes there). +# A (shooter, autofire missiles) kills B (walker, respawns); C and D are +# BYSTANDERS parked at far dropzones. All 2-node invariants hold, plus: +# I6 BYSTANDER CONVERGENCE: C and D's replicant mirrors (SBMIRROR) reach +# A.kills == kill count and B.deaths == death count -- the #45 record +# is the only way bystanders learn tallies. +# I7 ALL-NODE AGREEMENT: no node ever mirrors a tally HIGHER than the +# authoritative counts (no phantom kills surviving on any pod). +# I8 DELIVERY AUDIT: DMG receipts on the victim are inst=M only -- any +# inst=R delivery is the audit's unmapped replicant path going live. +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3 +rm -f kd4_a.log kd4_b.log kd4_c.log kd4_d.log kd4_r.log +mkdir -p /c/git/bt411/scratchpad/night15/mlbak && mv matchlog_*.txt /c/git/bt411/scratchpad/night15/mlbak/ 2>/dev/null +bt_expert_egg MP.EGG KD4.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" KD4.EGG +python - <<'EOF' +import io, re +txt = io.open('KD4.EGG', encoding='latin-1').read() +# clone the 1602 pilot page for 1702/1802 (names C/D, far dropzones) +m = re.search(r"\[127\.0\.0\.1:1602\](?:\n[^\[\n][^\n]*)*", txt) +page = m.group(0) +for addr, name, dz in (("127.0.0.1:1702", "Bystc", "one"), ("127.0.0.1:1802", "Bystd", "one")): + p = page.replace("[127.0.0.1:1602]", "[%s]" % addr) + p = re.sub(r"name=\S+", "name=%s" % name, p) + p = re.sub(r"dropzone=\S+", "dropzone=%s" % dz, p) + txt += "\n" + p + "\n" +txt = txt.replace("pilot=127.0.0.1:1602", + "pilot=127.0.0.1:1602\npilot=127.0.0.1:1702\npilot=127.0.0.1:1802") +io.open('KD4.EGG', 'w', encoding='latin-1').write(txt) +import sys +pages = txt.count("advancedDamage=1") +print("pilot pages with advancedDamage=1:", pages) +sys.exit(0 if pages >= 4 else 1) +EOF +[ $? -eq 0 ] || { echo "FAIL: egg extension broke advancedDamage stamping"; exit 1; } + +# D + C: bystanders -- present, parked, watching (mirrors only) +( export BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 + bt_launch kd4_d.log KD4.EGG 0xC0 -net 1801 ) +sleep 2 +( export BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 + bt_launch kd4_c.log KD4.EGG 0x30 -net 1701 ) +sleep 2 +# B: the victim -- walks at A, no weapons held +( export BT_GOTO=enemy BT_GOTO_STOP=80 BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 + bt_launch kd4_b.log KD4.EGG 0x0C -net 1601 ) +sleep 2 +# A: the shooter +( export BT_GOTO=enemy BT_GOTO_STOP=100 BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4 + export BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 + bt_launch kd4_a.log KD4.EGG 0x03 -net 1501 ) +sleep 5 +python ../tools/btconsole.py KD4.EGG 127.0.0.1:1501 127.0.0.1:1601 127.0.0.1:1701 127.0.0.1:1801 > kd4_r.log 2>&1 & +R=$!; sleep 360; kill $R 2>/dev/null; sleep 2 +bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1 + +python - <<'PY' +import glob, io, re, sys + +deaths = pdead = type2 = swall = applying = skipped = dmg_r = 0 +mirrors = {} # (log, player) -> max kills/deaths mirrored +for path in glob.glob("matchlog_*.txt"): + for line in io.open(path, encoding="latin-1", errors="replace"): + if line.startswith("DEATH ") and "inst=M" in line: + deaths += 1 + elif line.startswith("PLAYER_DEAD"): + pdead += 1 + elif line.startswith("SCORE") and "type=2" in line: + type2 += 1 + elif line.startswith("DMG") and "inst=R" in line: + dmg_r += 1 + elif line.startswith("SBMIRROR"): + m = re.search(r"player=(\d+:\d+) kills=(\d+) deaths=(\d+)", line) + if m: + key = (path, m.group(1)) + k, d = int(m.group(2)), int(m.group(3)) + pk, pd = mirrors.get(key, (0, 0)) + mirrors[key] = (max(pk, k), max(pd, d)) +for path in ("kd4_a.log", "kd4_b.log", "kd4_c.log", "kd4_d.log"): + txt = io.open(path, encoding="latin-1", errors="replace").read() + swall += txt.count("SWALLOWED") + applying += txt.count("-> APPLYING") + skipped += txt.count("-> SKIPPED") + +print("=== #162 4-NODE K/D INVARIANTS ===") +print("real deaths / PLAYER_DEAD / type2 / APPLYING:", deaths, pdead, type2, applying) +print("SWALLOWED:", swall, " DMG inst=R:", dmg_r) +maxk = {}; maxd = {} +for (path, player), (k, d) in sorted(mirrors.items()): + print(" mirror %-28s %-6s kills<=%d deaths<=%d" % (path[-16:], player, k, d)) + maxk[player] = max(maxk.get(player, 0), k) + maxd[player] = max(maxd.get(player, 0), d) + +fails = [] +if deaths == 0: fails.append("NO DEATHS -- rerun") +if pdead != deaths: fails.append("I1 FAIL: PLAYER_DEAD %d != deaths %d" % (pdead, deaths)) +if applying != deaths: fails.append("I2 FAIL: APPLYING %d != deaths %d" % (applying, deaths)) +if type2 != deaths: fails.append("I3 FAIL: type2 %d != deaths %d" % (type2, deaths)) +if swall: fails.append("I4 FAIL: %d SWALLOWED" % swall) +# authoritative per-player tallies from the receipts themselves: +# deaths: max PLAYER_DEAD tally per player; kills: max kills= on its own SCORE rows +auth_d = {}; auth_k = {} +for path in glob.glob("matchlog_*.txt"): + for line in io.open(path, encoding="latin-1", errors="replace"): + m = re.search(r"PLAYER_DEAD .*player=(\d+:\d+) .*tally=(\d+)", line) + if m: auth_d[m.group(1)] = max(auth_d.get(m.group(1), 0), int(m.group(2))) + m = re.search(r"SCORE .*player=(\d+:\d+) type=2 .*kills=(\d+)", line) + if m: auth_k[m.group(1)] = max(auth_k.get(m.group(1), 0), int(m.group(2))) +print("authoritative kills:", auth_k, " deaths:", auth_d) +# I6: for every player with authority, SOME bystander mirror reaches it +for pl, want in auth_k.items(): + got = max((k for (p, q), (k, d) in mirrors.items() if q == pl), default=0) + if want and got != want: + fails.append("I6 FAIL: %s kills mirrored %d != %d" % (pl, got, want)) +for pl, want in auth_d.items(): + got = max((d for (p, q), (k, d) in mirrors.items() if q == pl), default=0) + if want and got != want: + fails.append("I6 FAIL: %s deaths mirrored %d != %d" % (pl, got, want)) +# I7: no mirror exceeds authority +for (p, pl), (k, d) in mirrors.items(): + if k > auth_k.get(pl, 0): fails.append("I7 FAIL: %s kills mirror %d > auth %d" % (pl, k, auth_k.get(pl, 0))) + if d > auth_d.get(pl, 0): fails.append("I7 FAIL: %s deaths mirror %d > auth %d" % (pl, d, auth_d.get(pl, 0))) +if dmg_r: fails.append("I8 FLAG: %d DMG inst=R deliveries" % dmg_r) +print("VERDICT:", "PASS -- all 4-node invariants hold" if not fails else "; ".join(sorted(set(fails)))) +sys.exit(0 if not fails else 1) +PY diff --git a/scratchpad/night15/kd8_stress.sh b/scratchpad/night15/kd8_stress.sh new file mode 100644 index 0000000..882485f --- /dev/null +++ b/scratchpad/night15/kd8_stress.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# #162 8-NODE STRESS -- maximum-entropy K/D chaos before the Friday field test. +# Eight nodes, one drop zone, EVERYONE autofires missile salvos at everyone +# (staggered periods -> continuous overlapping splash), plus GOTO=enemy so the +# pack stays in knife-fight range. Simultaneous kills, mutual kills, splash +# suicides, respawn pileups -- all of it. +# Invariants are RECEIPT-DERIVED (scenario-agnostic): +# I1 PLAYER_DEAD == real deaths I2 deathcost APPLYING == deaths +# I3 SCORE type=2 == deaths I4 SWALLOWED == 0 (the synchronous +# latch means a duplicate can no longer even be GENERATED) +# I6 every player's kills/deaths mirror converges to authority on some node +# I7 no mirror anywhere exceeds authority (no phantom tallies) +# I8 zero DMG inst=R deliveries +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3 +rm -f kd8_*.log +mkdir -p /c/git/bt411/scratchpad/night15/mlbak && mv matchlog_*.txt /c/git/bt411/scratchpad/night15/mlbak/ 2>/dev/null +bt_expert_egg MP.EGG KD8.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" KD8.EGG +python - <<'EOF' +import io, re, sys +txt = io.open('KD8.EGG', encoding='latin-1').read() +m = re.search(r"\[127\.0\.0\.1:1602\](?:\n[^\[\n][^\n]*)*", txt) +page = m.group(0) +adds = [] +for i in range(6): + addr = "127.0.0.1:%d" % (1702 + i * 100) + p = page.replace("[127.0.0.1:1602]", "[%s]" % addr) + p = re.sub(r"name=\S+", "name=Strs%d" % (i + 3), p) + p = re.sub(r"dropzone=\S+", "dropzone=one", p) + txt += "\n" + p + "\n" + adds.append("pilot=%s" % addr) +txt = txt.replace("pilot=127.0.0.1:1602", + "pilot=127.0.0.1:1602\n" + "\n".join(adds)) +io.open('KD8.EGG', 'w', encoding='latin-1').write(txt) +pages = txt.count("advancedDamage=1") +print("pilot pages with advancedDamage=1:", pages) +sys.exit(0 if pages >= 8 else 1) +EOF +[ $? -eq 0 ] || { echo "FAIL: egg extension broke advancedDamage stamping"; exit 1; } + +# Eight combatants, one core each, staggered salvo periods. +PORTS=(1501 1601 1701 1801 1901 2001 2101 2201) +AFFS=(0x01 0x02 0x04 0x08 0x10 0x20 0x40 0x80) +PERIODS=(3 4 5 7 3 4 5 7) +for i in 0 1 2 3 4 5 6 7; do + ( export BT_GOTO=enemy BT_GOTO_STOP=90 + export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=${PERIODS[$i]} + export BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 + bt_launch kd8_$i.log KD8.EGG ${AFFS[$i]} -net ${PORTS[$i]} ) + sleep 2 +done +sleep 5 +python ../tools/btconsole.py KD8.EGG \ + 127.0.0.1:1501 127.0.0.1:1601 127.0.0.1:1701 127.0.0.1:1801 \ + 127.0.0.1:1901 127.0.0.1:2001 127.0.0.1:2101 127.0.0.1:2201 > kd8_r.log 2>&1 & +R=$!; sleep 480; kill $R 2>/dev/null; sleep 2 +bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1 + +python - <<'PY' +import glob, io, re, sys + +deaths = pdead = type2 = swall = applying = dmg_r = 0 +mirrors = {} +for path in glob.glob("matchlog_*.txt"): + for line in io.open(path, encoding="latin-1", errors="replace"): + if line.startswith("DEATH ") and "inst=M" in line: + deaths += 1 + elif line.startswith("PLAYER_DEAD"): + pdead += 1 + elif line.startswith("SCORE") and "type=2" in line: + type2 += 1 + elif line.startswith("DMG") and "inst=R" in line: + dmg_r += 1 + elif line.startswith("SBMIRROR"): + m = re.search(r"player=(\d+:\d+) kills=(\d+) deaths=(\d+)", line) + if m: + key = (path, m.group(1)) + k, d = int(m.group(2)), int(m.group(3)) + pk, pd = mirrors.get(key, (0, 0)) + mirrors[key] = (max(pk, k), max(pd, d)) +for path in glob.glob("kd8_*.log"): + txt = io.open(path, encoding="latin-1", errors="replace").read() + swall += txt.count("SWALLOWED") + applying += txt.count("-> APPLYING") + +print("=== #162 8-NODE STRESS INVARIANTS ===") +print("deaths / PLAYER_DEAD / type2 / APPLYING:", deaths, pdead, type2, applying) +print("SWALLOWED:", swall, " DMG inst=R:", dmg_r) +auth_d = {}; auth_k = {} +for path in glob.glob("matchlog_*.txt"): + for line in io.open(path, encoding="latin-1", errors="replace"): + m = re.search(r"PLAYER_DEAD .*player=(\d+:\d+) .*tally=(\d+)", line) + if m: auth_d[m.group(1)] = max(auth_d.get(m.group(1), 0), int(m.group(2))) + m = re.search(r"SCORE .*player=(\d+:\d+) type=2 .*kills=(\d+)", line) + if m: auth_k[m.group(1)] = max(auth_k.get(m.group(1), 0), int(m.group(2))) +print("authoritative kills:", auth_k) +print("authoritative deaths:", auth_d) + +fails = [] +if deaths < 10: fails.append("LOW STRESS: only %d deaths -- lengthen/retune" % deaths) +if pdead != deaths: fails.append("I1 FAIL: PLAYER_DEAD %d != deaths %d" % (pdead, deaths)) +if applying != deaths: fails.append("I2 FAIL: APPLYING %d != deaths %d" % (applying, deaths)) +if type2 != deaths: fails.append("I3 FAIL: type2 %d != deaths %d" % (type2, deaths)) +if swall: fails.append("I4 FAIL: %d SWALLOWED (duplicate GENERATED despite the synchronous latch)" % swall) +for pl, want in auth_k.items(): + got = max((k for (p, q), (k, d) in mirrors.items() if q == pl), default=0) + if want and got != want: + fails.append("I6 FAIL: %s kills mirrored %d != %d" % (pl, got, want)) +for pl, want in auth_d.items(): + got = max((d for (p, q), (k, d) in mirrors.items() if q == pl), default=0) + if want and got != want: + fails.append("I6 FAIL: %s deaths mirrored %d != %d" % (pl, got, want)) +for (p, pl), (k, d) in mirrors.items(): + if k > auth_k.get(pl, 0): fails.append("I7 FAIL: %s kills mirror %d > auth" % (pl, k)) + if d > auth_d.get(pl, 0): fails.append("I7 FAIL: %s deaths mirror %d > auth" % (pl, d)) +if dmg_r: fails.append("I8 FLAG: %d DMG inst=R" % dmg_r) +print("VERDICT:", "PASS -- all 8-node stress invariants hold" if not fails else "; ".join(sorted(set(fails)))) +sys.exit(0 if not fails else 1) +PY