From 46ff8dd262b20a08334d44338e69fc686a6c7958 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Mon, 10 Aug 2026 10:30:12 -0500 Subject: [PATCH] #150 double kill FIXED (two binary-cited gates) + #151 measured AUTHENTIC to the point #150 -- one kill credited twice (Rajel on Oracle, Oracle on Conn Man). CAUGHT: one death -> two type-2 arms 1ms apart (awards 501.96/505.88 -- the tally GREW between them), victim inst=R. Mechanism: this port deliberately applies damage on replicants for local visual response; the report tail ran there too, and a replicant's movement mode cannot flip to wreck until the victim's death record round-trips -- so each round of a killing VOLLEY inside that window re-posted the kill. Why missiles, why intermittent. The binary forbids the whole situation: its handler OPENS with the assertion if ((flags & 0xc) == 4) Fail("Replicant Mech recieving takedamage", MECH.CPP, 0x3da) [part_012.c:14613] -- TakeDamage on a replicant was a BUG in 1995; the tail only ever ran on the victim's master. Fix: (1) gate the REPORT TAIL master-only (local damage application stays); (2) adopt the binary's once-latch -- the 0->1 edge of IsMechDestroyed() across the handler call (local_14), replacing the deathBlastArmed form for the report gate. VERIFIED (scratchpad/night14/scorepair.sh): 3 victim deaths, 5 shooter respawns -> exactly one type-2 per credited kill, kills 0->1->2, zero doubles (was: kills 0->2 on one death). #151 -- panic eject "-499 instead of 0" is AUTHENTIC 1995 SCORING, decoded from EjectPilot @0049f854: the punch-out dispatches a SELF TakeDamage of damageAmount = role->killBonus (+0x1c, 500 in dfltrole), type Explosive, senderMechID = SELF, player->suppressConsole = 1, graphicAlarm -> 10 which flows through the report tail as a SUICIDE kill: award = -CalcKillScore (your own death), plus the type-1 death cost. 1000 - ~999 - 500 = -499: the binary predicts Oracle's exact number. Our port already used role->killBonus for the blast (mech.cpp:652) -- faithful end to end; bench shows each eject charged exactly once (-900/-1050 across three ejects). Panic eject = scored as suicide. Whether that is fun is a design question; it is not a porting defect. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC --- game/reconstructed/mech.cpp | 28 +++++++++++++++++++++++++++- scratchpad/night14/scorepair.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 scratchpad/night14/scorepair.sh diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index d96c376..88b42a4 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -1032,6 +1032,12 @@ void { Check(message); + // #150 -- the binary's ONCE-PER-DEATH latch (local_14 @0x4a0230 body, + // part_012.c:14625-14629): IsMechDestroyed() sampled AT ENTRY. The kill + // report posts only on the 0->1 edge (!destroyedAtEntry && destroyed-now); + // a later salvo of the killing volley takes the inflicted-only path. + const int destroyedAtEntry = IsMechDestroyed(); + // MP DIAGNOSTIC (task #47): confirm the handler runs on the OWNING MASTER // for a cross-pod (network-delivered) TakeDamage + what state it carries. if (getenv("BT_MP_NET")) @@ -1242,11 +1248,31 @@ void // vital-wreck flag, and the loop-ENTRY zone. This retires // BTPostDamageScore/BTPostKillScore -- see the btplayer.cpp tombstone. // + // #150 DOUBLE-KILL FIX, two binary-grounded gates (2026-08-10): + // + // (1) MASTER ONLY. The binary OPENS this handler with the assertion + // `if ((flags & 0xc) == 4) Fail("Replicant Mech recieving takedam...", + // MECH.CPP, 0x3da)` (part_012.c:14613-14616) -- in 1995 a replicant + // executing TakeDamage was a BUG, so the report tail could only ever + // run on the victim's master, once. This port deliberately applies + // damage on replicants too (local visual responsiveness), so the tail + // ALSO ran there -- and the replicant's movement mode can't flip to + // wreck until the death record round-trips, so a multi-round killing + // volley posted a kill report PER ROUND inside that window. Measured: + // one death -> two type-2 arms 1ms apart, kills 0->2 (the field's + // "one kill counted twice", #150). Gate the REPORTS to the master; + // the local damage application stays. + // + // (2) ENTRY LATCH. The binary's newly-destroyed test is the 0->1 edge of + // IsMechDestroyed() across THIS handler call (local_14), not the + // movement-mode-derived deathBlastArmed -- on the master both agree, + // but the binary's form is the spec. + if (GetInstance() != ReplicantInstance) { extern void BTMechPostCombatReports(void *, void *, float, int, int, int); BTMechPostCombatReports((void *)this, (void *)message, (float)damageTally, zoneDestroyed, reportZone, - deathBlastArmed && IsMechDestroyed()); + !destroyedAtEntry && IsMechDestroyed()); } } diff --git a/scratchpad/night14/scorepair.sh b/scratchpad/night14/scorepair.sh new file mode 100644 index 0000000..8209f57 --- /dev/null +++ b/scratchpad/night14/scorepair.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# #150/#151: count type-2 arm executions per kill + measure the eject score arithmetic +. /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 sc_a.log sc_b.log sc_r.log +mkdir -p /c/git/bt411/scratchpad/night14/mlbak && mv matchlog_*.txt /c/git/bt411/scratchpad/night14/mlbak/ 2>/dev/null +bt_expert_egg MP.EGG SC.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" SC.EGG +# B: unarmed slow target, respawns on death +( export BT_GOTO=enemy BT_GOTO_STOP=80 BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 BT_MP_LOG=1 + bt_launch sc_b.log SC.EGG 0x0C -net 1601 ) +sleep 2 +# A: kills B with autofire, then panic-ejects late +( export BT_GOTO=enemy BT_GOTO_STOP=100 BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4 + export BT_EJECT_AT=9000 + export BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_MATCHLOG=1 BT_MP_LOG=1 + bt_launch sc_a.log SC.EGG 0x03 -net 1501 ) +sleep 5 +python ../tools/btconsole.py SC.EGG 127.0.0.1:1501 127.0.0.1:1601 > sc_r.log 2>&1 & +R=$!; sleep 300; kill $R 2>/dev/null; sleep 2 +bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1 +echo RESULT +echo "--- B deaths (kills A should have earned) ---" +grep -ac "Mech::Reset" sc_b.log +echo "--- A: kill-arm executions + score sequence (matchlog) ---" +grep -ah "SCORE\|KILL\|EJECT\|DEATH" matchlog_*.txt 2>/dev/null | head -25 +echo "--- A: scorepost receipts (producer side) ---" +grep -a "scorepost" sc_a.log | head -10 +grep -a "scorepost" sc_b.log | head -10