From 91bd28669e5a204413a3b349c9b9dfe2bf72faab Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Wed, 5 Aug 2026 18:36:58 -0500 Subject: [PATCH] #45/#134 authentic score/death report tail -- replaces the scoring stand-ins Reconstructs the dark-gap tail of Mech::TakeDamageMessageHandler (@0x4a02f4-0x4a0890, raw disasm): the three id-0x16 score reports (kill to the shooter's player / type-0 wire-fidelity / received to the victim's player) and the BT 0x38-byte VehicleDeadMessage extension {killed-by player, kill zone} dispatched from the death tail. Retires BTPostDamageScore / BTPostKillScore and the per-hit inflicted credit (never existed in 1995: @0x4c0200 is bound in no handler-table entry -- byte-scan receipt in decomp-reference). Suicides now dispatch and the handler negates the award (the #134 panic penalty). Collision divert falls through to the death tail per @0x4a0375 (wall deaths respawn + blast; no score). ScoreMessage fields renamed to decoded truth (vitalHit/zoneIndex/subsysID) + wire asserts; console VTVDamaged points_transfered corrected (Round(award), not Now()). Benches: scorekill.sh cross-node kill (kills=1 award=4.88, killedBy=2:1 zone=3, single death cycle), scoreself.sh suicide (type=2 award=-39.00 kills=0), deathblast2.sh re-verified (72 bursts at ~9u). Co-Authored-By: Claude Fable 5 --- context/combat-damage.md | 64 ++++- context/decomp-reference.md | 29 ++- context/multiplayer.md | 6 +- context/open-questions.md | 22 ++ docs/KD_SCOREBOARD_PLAN.md | 18 ++ game/reconstructed/btplayer.cpp | 421 +++++++++++++++++++++----------- game/reconstructed/btplayer.hpp | 108 ++++++-- game/reconstructed/mech.cpp | 111 +++++---- game/reconstructed/mech4.cpp | 107 +++----- game/reconstructed/mechweap.cpp | 18 +- scratchpad/night12/scorekill.sh | 57 +++++ scratchpad/night12/scoreself.sh | 41 ++++ 12 files changed, 706 insertions(+), 296 deletions(-) create mode 100644 scratchpad/night12/scorekill.sh create mode 100644 scratchpad/night12/scoreself.sh diff --git a/context/combat-damage.md b/context/combat-damage.md index 2e36383..2bf1278 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -391,7 +391,7 @@ zones — mid-fight darkening DOES accumulate when mechs live long enough. **Field logs confirm dominance**: Rajel's night-9 log alone has 12 bay-fire events (LRM10 455-490 pts); ConnMan 10; the "salvo barely dented my armor" reporter was dying to their OWN cook-offs. **The real lever is HEAT** (bay fire arms at the bin heat-watcher's FAILURE level; -the [[heat-and-coolant]]/#96 too-hot economy arms it constantly) plus player training: EJECT +the [[subsystems]] heat/#96 too-hot economy arms it constantly) plus player training: EJECT **hold** purges the burning bay (`DumpAmmo` → "bay fire EXTINGUISHED"); pod players were manual-trained to purge ([[pod-hardware]] §Manual), testers are not. Novice sim gates also suppress the whole path ([[experience-levels]]). @@ -569,7 +569,9 @@ percent tables, MadCat) — see #73 on the tracker. ## layers are now fixed and live-verified The recovery: the un-exported gap held `Mech::TakeDamageMessageHandler @0x4a0230` (found via the Mech **message table @0x50bdf8** — rows `{id, name, handler}`: 0x12 TakeDamage, 0x14 PlayerLink, -0x15 RealMaxSpeed, 0x16 BalanceCoolant, 0x17/0x18 Set/ClearBurningState, 0x19 EjectPilot @0x49f854, +0x15 RealMaxSpeed, 0x16 BalanceCoolant, 0x17/0x18 Set/ClearBurningState (⚠ label/body mismatch: +the 0x17 handler @0x49f674 randomizes position + graphicAlarm(2) + re-enables sim — not burning; +possibly a shifted name row, cf. the factory ClassID mislabels — open-questions), 0x19 EjectPilot @0x49f854, 0x1a DuckRequest @0x49fa00) and the **crit-chance roll `@0x4a0164`**: `p = clamp(0.7·damageLevel² + 0.01, 0..1)`, gated on the player's `simLive` flag (+0x25c — novice never crits), rolled per BURST on the current zone (skip if the zone is already burning). The @@ -582,12 +584,58 @@ entirely — multi-burst under-applied (burst-1)×) is superseded. `MechSubsyste @0x4ac0bc` is real (zone damage → destroyed alarms → **vital-subsystem kill**: owner `graphicAlarm` level 9, the #28 machinery — CLASSMAP's "HandleMessage@4ac0bc" was a mislabel). Live-verified: `[subarmor]` shows parsed scales at spawn; `[critroll] zone=3 -> Myomers subLvl=1` -— a full chain crit destroying a subsystem. Still deferred [T4→documented]: the `damageType==0` -COLLISION divert (@0x4a0368 → `0x49ffcc`, its own distribution) and the **id-0x16 damage/kill -reports** to shooter+victim players (@0x4a04da-0x4a07b2 — the authentic stats plumbing, feeds #45: -`{tally, zone, destroyed-flag, inflicting subsystem, victim name}`, kill-flagged variant on newly -disabled, plus a killed-by block gated on movementMode 9/10). Diags: `BT_CRIT_LOG` (`[subarmor]` + -`[critroll]`), `BT_DMG_LOG`. +— a full chain crit destroying a subsystem. The `damageType==0` COLLISION divert (@0x4a0368 → +`0x49ffcc`) and the id-0x16 report tail are BOTH reconstructed — see the next section. Diags: +`BT_CRIT_LOG` (`[subarmor]` + `[critroll]`), `BT_DMG_LOG`. + +## ✅ THE AUTHENTIC SCORE/DEATH REPORT TAIL (2026-08-05, #45/#134) [T1 raw disasm, T2 both benches] +The rest of `Mech::TakeDamageMessageHandler` (@0x4a02f4-0x4a0890, dark-gap raw disasm) is now +reconstructed: `BTMechPostCombatReports` + `BTMechPostVehicleDead` (btplayer.cpp), called from the +handler at the binary's exact positions (mech.cpp). Retires `BTPostDamageScore`/`BTPostKillScore`. + +**Entry sequence** [T1]: gyro bounce → death-edge latch `[ebp-0x10]` (was-destroyed at ENTRY — +before the divert!) → `lastInflictingID` stamp (mech+0x43c ← msg inflictor) → shooter resolve +(registry find → `shooter+0x190` = shooter's PLAYER; victim's = `mech+0x190`) → **collision divert +JUMPS TO THE DEATH TAIL** (@0x4a0375 `jmp 0x4a07b5`, NOT a return — a wall/fall death still posts +VehicleDead + blasts, but skips the score blocks: collision deaths credit no one). Our old early +`return` here was a latent "wall-death strands the pilot" hazard, fixed with the move. + +**The three id-0x16 reports** (all `Player::ScoreMessageID` 0x16, sizeof 0x3C — layout in +btplayer.hpp, static_assert-locked; `scoreAward`=APPLIED tally in all three): +- **A @0x4a04da, newly destroyed** → type 2 KillScore to the SHOOTER's player. Basis (+0x24) = + the VICTIM role's `killBonus` (role+0x1c). senderMechID = the victim. Suicide (eject charge, + self-damage) IS dispatched — the handler negates the award (@0x4c03ab `fchs`) and skips + `killCount++`: **the #134 panic penalty, live** (bench: `type=2 award=-39.00 kills=0`). +- **B @0x4a05d9, not newly killed && tally≠0** → type 0 to the shooter. The ONLY registered 0x16 + receiver Verify-rejects type 0 → 1995 folded an UNINITIALIZED stack float into the shooter's + score on every non-lethal hit (real 1995 bug — @0x4c0200, the handler that accepts type 0, is + in NO table entry: dead code). Port sends it for wire fidelity, banks award 0. +- **C @0x4a06c0, tally>0 (kills included)** → type 1 DamageReceivedScore to the VICTIM's player. + Basis = INTENDED damage (burstCount×amount). senderMechID = the INFLICTOR. Feeds the received + penalty (`CalcDamageReceivedScore` returns the NEGATIVE) + the operator-console VTVDamaged line + (gated `GetConsoleHost()` && !`suppressConsole` — which is why that field is named that). +Reports carry the LOOP-ENTRY zone (msg+0x24, never rewritten mid-loop), the vital-wreck flag, and +`inflictingSubsystemID` (msg+0x5c, engine T0 name). + +**Score model consequence** [T1]: 1995 pod scoring = **kill awards + received-damage penalties + +death costs. No per-hit inflicted credit** (the port's old per-hit crediting — and the #95 salvo +fix on top of it — were inventions riding the dead @0x4c0200 channel; both retired). +Kill award = `(victimKillBonus + tally) × killerRole.damageInflictedModifier × +(victimAvgZoneDamage@0x354 × damageBias + 1.0) × (victimTonnage/killerTonnage)`; same-team kill in +a non-FFA game = `-friendlyFirePenalty` basis (inline strcmp of `teamName@0x20c`, gate +`freeForAll@0x250==0`); self-kill negates the whole award. + +**The death tail** (@0x4a07b5, shared by the divert): gate = was-alive-at-entry && movementMode +9|10 → **`Player::VehicleDeadMessage` id 0x17 to the mech's OWN player** — the respawn trigger, +in the BT 0x38-byte extension `{deathCount -1, dropZone Null, +0x2c killed-by PLAYER EntityID, ++0x34 kill zone}` (bench: `killedBy=2:1 zone=3` cross-node) — then the death Explosion (id 3, +0x5C, model 0x31; port fires the authored death list from the transition instead, #42) and the +#89 SplashDamage (re-verified after the reorder: 72 bursts at ~9u). The old mech4 +death-transition dispatch site (which was flagged [T3 sender-undecoded]) is retired — this IS +that sender. The #55 NULL-playerLink fallback + DEAD_NOTIFY forensics moved into the bridge. +Benches: `scratchpad/night12/scorekill.sh` (cross-node kill: killer `kills=1 award=4.88`, +victim respawns, death #1 single-cycle) + `scoreself.sh` (#134 negation). Collision-death tail +fallthrough is inspection-tier [T3] — shares the benched tail code; field wall-deaths exercise it. ## (HISTORICAL — the gap as found 2026-07-29, superseded above) [T1] The authored crit machinery exists and is reconstructed — `Mech__DamageZone::CriticalHit @0049ccc4` diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 381be70..e0cedda 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -247,9 +247,36 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a ### Binary message tables decoded 2026-07-20 (glass input audit) [T1] 16-byte HandlerEntry {id, namePtr, fnPtr, 0} rows in section_dump. Per-receiver-class id spaces. - **Mech (entity)** @0x50BDF8..: 0x12 TakeDamage@004a0230, 0x14 PlayerLink@0049f624, 0x15 - RealMaxSpeed@0049f604, **0x16 BalanceCoolant@0049f728 (WIRED 2026-07-21, issue #20**: press-only, NO novice guard; sets every condenser valveState@0x1D0=1 then the shared redistribute @0049f788 == BTRecomputeCondenserValves; verify BT_BALTEST=1 + BT_VALVE_LOG=1**)**, 0x17 SetBurningState@0049f674, 0x18 + RealMaxSpeed@0049f604, **0x16 BalanceCoolant@0049f728 (WIRED 2026-07-21, issue #20**: press-only, NO novice guard; sets every condenser valveState@0x1D0=1 then the shared redistribute @0049f788 == BTRecomputeCondenserValves; verify BT_BALTEST=1 + BT_VALVE_LOG=1**)**, 0x17 SetBurningState@0049f674 (⚠ body mismatch — randomizes position, graphicAlarm 2, sim re-arm; see open-questions), 0x18 ClearBurningState@0049f700, **0x19 EjectPilot@0049f854**, **0x1a DuckRequest@0049fa00** (the manual's CROUCH; streamed button 0x13 sends it — unreconstructed). +- **BTPlayer** (byte-scanned 2026-08-05, file 0x112dxx; 20-byte rows {id, namePtr, fn, 0, 0} — 6 + entries EXACTLY, matching the T0 PLAYER.h enum): **0x15 DropZoneReply@0x4bffd0, 0x16 + Score@0x4c02e4** (the type-1/2 switch — BT shadows the engine id with its override), **0x17 + VehicleDead@0x4c05c4, 0x18 MissionStarting@0x4bfbe8, 0x19 MissionEnding@0x4bfc20, 0x1a + ScoreUpdate@0x4c02a8** (= Player::NextMessageID). **@0x4c0200 ("ScoreInflicted", type-0-only, + Verify line 0x18b) is in NO entry — dead code in 1995.** The 1995 type-0 path therefore lands in + @0x4c02e4's Verify arm and folds an UNINITIALIZED [ebp-0xc] into currentScore (real bug; port + banks 0). @0x4c02e4 internals [T1 raw]: `+0x40==4` MissionEnding gate; registry-find msg+0x34; + case 1 = `(ownTonnage/senderTonnage) × role->CalcDamageReceivedScore(basis)` + the + console VTVDamaged post (ConsoleHost && !suppressConsole@0x258; **arg 6 = Round(AWARD)** — an + ST0-arg __ftol @0x4dcd94 the decompiler rendered argless; the old "Now()" read was wrong); case + 2 = kill: `CalcKillScore@0x4c052c` × (senderTonnage/ownTonnage), self-kill `fchs`, dual + `killCount@0x27c++` (shooter + victim's player — the phantom partner increment, masked by + replication), StatusMessage{type 0 Destroyed, victim player, 6.0s} from pool @0x512f6c with + vtable @0x513344, clear-target @0x4b04d8 via `playerVehicle+0x128` roster head when the dead + mech == `objectiveMech@0x284`. `CalcKillScore@0x4c052c` [T1]: same-team (strcmp + `teamName@0x20c`, gate `freeForAll@0x250==0`) → `-friendlyFirePenalty(role+0x14)`; else + `victimMech->avgZoneDamage@0x354 × damageBias(role+0x18) + 1.0` (const @0x4c05c0); result × + `(basis + tally) × damageInflictedModifier(role+0xC)`. **ScenarioRole offsets** (Node base): + +0xC inflictedMod, +0x10 receivedMod, +0x14 ffPenalty, +0x18 damageBias, +0x1C killBonus, + +0x20 specialCaseDeathPenalty. **mech+0x354** = average zone damageLevel (computed with the + subsystem average @0x358 by the telemetry refresher, part_012.c:9515). **Wire layouts** + (static_assert-locked, btplayer.hpp): ScoreMessage 0x3C {+0x1c scoreAward=tally, +0x20 type, + +0x24 basis, +0x28 vitalHit, +0x2c zone, +0x30 subsysID(=TakeDamageMessage+0x5c, + inflictingSubsystemID [T0]), +0x34 senderMechID}; BT VehicleDeadMessage 0x38 {engine 0x28 + + 0x28=0, +0x2c killed-by PLAYER EntityID, +0x34 kill zone}. Senders: the TakeDamage report tail + — see [[combat-damage]]. - **HeatableSubsystem** @0x50E41C: {3, "ToggleCooling"→@004ad6f8}. **Disassembled 2026-07-20** (`tools/disas2.py 0x4ad6f8`): a per-subsystem coolant on/off TOGGLE (NOT a multi-level "cycle priority" — that's the emergent effect, not the mechanism): diff --git a/context/multiplayer.md b/context/multiplayer.md index 717a219..4d351b9 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -881,8 +881,10 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on REMAINING note is closed. 5. ✅ **2-window driving — RESOLVED (tasks #48/#51).** Keyboard MP turning was restored at the root (the `pilotArray[1]` shrunk-span overrun fix, task #51); `BT_KEY_NOFOCUS` exists for automated - harnesses. DEATHS scoring lights via the existing BTPostKillScore MP branch. Respawn is - reconstructed (task #52 — see item 3b). + harnesses. DEATHS scoring lights via the kill/death report flow (2026-08-05: now the AUTHENTIC + report tail in the victim's TakeDamage handler — BTMechPostCombatReports, replacing + BTPostKillScore; same cross-node reroute, see [[combat-damage]] §report tail). Respawn is + reconstructed (task #52 — see item 3b; the VehicleDead sender moved to the same tail). ## Key Relationships - Base: [[wintesla-port]] (L4NET). Depends on: [[locomotion]] (update writer), [[combat-damage]] diff --git a/context/open-questions.md b/context/open-questions.md index c781591..b6f5dd4 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -931,6 +931,28 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR but is never CALLED, like the salvo mirror was thought to be, or a missing ForceUpdate). A LIVENESS audit (does a live call path reach each reconstructed fn?) would catch this class. +## Score/death report tail residuals (2026-08-05, #45/#134 reconstruction — the tail itself is DONE + benched, [[combat-damage]]) +- **Mech-table 0x17 "SetBurningState" label/body MISMATCH.** The recovered name row says + Set/ClearBurningState for 0x17/0x18, but @0x49f674's body randomizes the mech's position from + two scaled randoms, rebuilds localToWorld, sets graphicAlarm level 2, ForceUpdates and clears + the not-simulated flag — nothing burns. Possibly a shifted name row (cf. the factory ClassID + mislabels). Neither handler is reconstructed (nothing in the port sends mech-0x17/0x18); + identify the real bodies before wiring. The DEATH-tail id-0x17 message that was once labeled + "SetBurningState dispatch" is RESOLVED: it is Player::VehicleDeadMessage (player table, not mech). +- **Does @0x4c05c4 (VehicleDead handler) CONSUME the BT extension fields** (+0x2c killed-by + player, +0x34 kill zone)? The function is not in the export; our #52 reconstruction predates + the fields. If it reads them, the likely consumer is a killed-by console post + (ConsolePlayerMechDeathWithoutHonor / a killed-by line) + maybe an RP-style "DestroyedBy" + status banner on the victim. The port SENDS them faithfully; raw-disasm the handler to close. +- **Collision-death tail fallthrough is inspection-tier [T3]:** the divert now falls to the death + tail (VehicleDead + blast) per @0x4a0375, but no live wall-death bench has run; field + wall-deaths exercise it nightly (watch for DEAD_NOTIFY on collision deaths). +- **The 1995 type-0 uninit-award bug** (documented in [[decomp-reference]] §BTPlayer table): the + shipped binary folds an uninitialized stack float into the shooter's score on every non-lethal + hit's type-0 report. Era scoreboards looked sane — worth understanding WHY (dispatch path may + deterministically leave ~0 at [ebp-0xc]; or replicant-player reroute delivery differs). Port + deviates safely (award=0). Curiosity, not a blocker. + ## Rendering follow-ups (non-blocking) - ~~Per-pilot mech PAINT (color/badge/patch)~~ — **DONE 2026-07-17, verified live** (crimson MadCat + yellow VGL emblems + hip hazard stripes). Mechanics + the vehicletable color/badge/patch name diff --git a/docs/KD_SCOREBOARD_PLAN.md b/docs/KD_SCOREBOARD_PLAN.md index 87503f9..b126322 100644 --- a/docs/KD_SCOREBOARD_PLAN.md +++ b/docs/KD_SCOREBOARD_PLAN.md @@ -404,3 +404,21 @@ design change for its own pass. `killCount`/`deathTally` are still zeroed only i column should read, `+0x280` vs `deathCount`), §Headline-5 (last-hitter-takes-all and ram kills), and §Headline-6's fidelity question (the wrong-column slip is still reproduced; it is merely no longer visible). + +## Addendum 2026-08-05 — the AUTHENTIC report tail lands (#45/#134) +The kill/damage report sender is no longer the BTPostDamageScore/BTPostKillScore stand-in pair: +`Mech::TakeDamageMessageHandler`'s dark-gap tail (@0x4a02f4-0x4a0890) is reconstructed as +`BTMechPostCombatReports` + `BTMechPostVehicleDead` (btplayer.cpp), dispatching the binary's three +id-0x16 reports (kill to shooter / type-0 wire-fidelity / received to victim) and the BT-extended +0x38 VehicleDead {killed-by player, kill zone} from the victim's handler at the binary's exact +positions. Consequences for this plan: +- KILLS credit path unchanged in shape (victim node -> replicant-player Dispatch reroute -> + killer's master ++killCount) — re-benched cross-node (scorekill.sh: kills=1, award=4.88). +- Suicides now dispatch and the handler negates the award (#134 panic penalty live; + scoreself.sh: type=2 award=-39.00 kills=0). +- Per-hit INFLICTED credit is REMOVED — it never existed in 1995 (@0x4c0200 is bound in no + handler-table entry; scoring = kill awards + received penalties + death costs). SCORE column + behavior changes accordingly; KILLS/DEATHS columns unaffected. +- §Headline-6's wrong-column slip (dual +0x27c increment) remains faithfully reproduced and + replication-masked. The type-1 received report also fixes the console VTVDamaged + points_transfered field (was a Now() misread; it is Round(award), raw disasm @0x4c04a7). diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index a642be1..a81e074 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -203,6 +203,35 @@ static_assert(offsetof(BTPlayer::MakeMessage, roleName) == 0x90, static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0, "BTPlayer::MakeMessage wire size must be 0xD0"); +// Wire-format lock (2026-08-05): the combat score reports and the extended +// VehicleDead cross nodes raw (replicant-player Dispatch reroute), and the +// sender/receiver field map was transcribed from the binary's builds +// (@0x4a04da/@0x4a05d9/@0x4a06c0 and @0x4a07d4) -- lock every offset. +static_assert(offsetof(BTPlayer::ScoreMessage, scoreAward) == 0x1c, + "ScoreMessage::scoreAward must be at wire offset 0x1c"); +static_assert(offsetof(BTPlayer::ScoreMessage, scoreType) == 0x20, + "ScoreMessage::scoreType must be at wire offset 0x20"); +static_assert(offsetof(BTPlayer::ScoreMessage, damageAmount) == 0x24, + "ScoreMessage::damageAmount must be at wire offset 0x24"); +static_assert(offsetof(BTPlayer::ScoreMessage, vitalHit) == 0x28, + "ScoreMessage::vitalHit must be at wire offset 0x28"); +static_assert(offsetof(BTPlayer::ScoreMessage, zoneIndex) == 0x2c, + "ScoreMessage::zoneIndex must be at wire offset 0x2c"); +static_assert(offsetof(BTPlayer::ScoreMessage, subsysID) == 0x30, + "ScoreMessage::subsysID must be at wire offset 0x30"); +static_assert(offsetof(BTPlayer::ScoreMessage, senderMechID) == 0x34, + "ScoreMessage::senderMechID must be at wire offset 0x34"); +static_assert(sizeof(BTPlayer::ScoreMessage) == 0x3c, + "ScoreMessage wire size must be 0x3c (binary build @0x4a052b)"); +static_assert(offsetof(BTPlayer::VehicleDeadMessage, reserved28) == 0x28, + "VehicleDeadMessage::reserved28 must be at wire offset 0x28"); +static_assert(offsetof(BTPlayer::VehicleDeadMessage, killedByPlayerID) == 0x2c, + "VehicleDeadMessage::killedByPlayerID must be at wire offset 0x2c"); +static_assert(offsetof(BTPlayer::VehicleDeadMessage, killZone) == 0x34, + "VehicleDeadMessage::killZone must be at wire offset 0x34"); +static_assert(sizeof(BTPlayer::VehicleDeadMessage) == 0x38, + "VehicleDeadMessage wire size must be 0x38 (binary build @0x4a07f6)"); + // // LAYOUT LOCK (Gitea #48/#57). Our compiled BTPlayer is NOT the binary's -- the // Entity base differs -- so the 1995 offsets must never be used as raw byte @@ -851,30 +880,45 @@ void { // // Points lost for damage we took (unless we hit ourselves). + // sender null guard is PORT SAFETY: the binary derefs the registry + // lookup unguarded (@0x4c0453) -- an inflictor that died between + // shot and impact would have crashed the 1995 pod. // - if (sender_mech != our_mech && scenarioRole != 0) // scoring wave: guard the NULL bring-up role + if (sender_mech != 0 && our_mech != 0 // port guards (dead-window straggler) + && sender_mech != our_mech && scenarioRole != 0) // scoring wave: guard the NULL bring-up role { Scalar received = scenarioRole->CalcDamageReceivedScore(message->damageAmount); // FUN_00429b94 award = (MECH_TONNAGE(our_mech) / MECH_TONNAGE(sender_mech)) * received; } + // BINARY NUANCE [T1 raw disasm @0x4c0433]: on SELF-inflicted damage + // (sender == own vehicle) the 1995 arm skips the math and the common + // tail folds its UNINITIALIZED award local into currentScore -- + // stack garbage, every fall/ram self-hit. The port keeps award=0.0, + // a deliberate safe deviation. // // Tell the console (if any, and not suppressed) that our VTV - // was damaged. + // was damaged. points_transfered CORRECTED 2026-08-05: the binary + // rounds THE COMPUTED AWARD here (raw disasm @0x4c04a7: fld + // [ebp-0xc]; call 0x4dcd94 -- an ST0-arg __ftol the decompiler + // rendered argless, which an earlier pass misread as Now()). // Host *console_host = application->GetHostManager()->GetConsoleHost(); // FUN_00429078 - if (console_host && suppressConsole == 0) // this+0x258 + BTPlayer *damager_player = + (sender_mech != 0) ? MECH_OWNING_PLAYER(sender_mech) : 0; // port guard (dummy shooter) + if (console_host && suppressConsole == 0 // this+0x258 + && damager_player != 0) { ConsolePlayerVTVDamagedMessage damaged_message( ownerID, // this+0x18c - MECH_OWNING_PLAYER(sender_mech)->ownerID, // *(sender+0x190)+0x18c + damager_player->ownerID, // *(sender+0x190)+0x18c Round(message->damageAmount), // FUN_004078fc - message->pointSenderLo, // msg+0x2c - message->pointSenderHi, // msg+0x28 - (int)Now().ticks, // FUN_004dcd94 - message->auxID // msg+0x30 + message->zoneIndex, // msg+0x2c + message->vitalHit, // msg+0x28 + Round(award), // FUN_004dcd94(ST0) + message->subsysID // msg+0x30 ); application->SendMessage( // app+0x20, slot+0x18 @@ -888,6 +932,13 @@ void case BTPlayer::ScoreMessage::KillScore: // 2 { + // PORT SAFETY: an unresolvable victim mech (removed wreck racing the + // cross-node dispatch) or a severed own vehicle skips the arm; the + // binary derefs both unguarded (@0x4c037e). + if (sender_mech == 0 || our_mech == 0) + { + break; + } // gauge scoring wave: the attacking mech's owner (NULL for the ownerless // BT_SPAWN_ENEMY dummy). Resolved once, guarded everywhere below. BTPlayer *sender_owner = MECH_OWNING_PLAYER(sender_mech); // *(sender+0x190), NULL for the dummy @@ -2191,149 +2242,247 @@ Logical BTResolveMessageBoard(Entity * /*tracked_mech*/, int *messageId, BitMap } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// gauge scoring wave: PRODUCERS -- posted by the combat path (mech4.cpp) to feed -// the scoreboard. The inflictor is always the viewpoint mech in bring-up, so the -// local (crediting) player is application->GetMissionPlayer(). Bridges (not inline -// in mech4.cpp) so the message construction lives in this complete-BTPlayer TU. +// THE AUTHENTIC COMBAT REPORT SENDER (#45/#134 -- 2026-08-05) // -void BTPostDamageScore(Entity *victim, Scalar damage, Entity *shooter) // Step 6: per-hit SCORE +// Reconstructed from the raw disasm of the Mech::TakeDamageMessageHandler +// report tail @0x4a02f4-0x4a0890 (dark-gap region, not in the export). This +// RETIRES the bring-up producers BTPostDamageScore / BTPostKillScore (see the +// tombstone below). The binary's flow, transcribed: +// +// resolve: shooterEntity = registry find(msg->inflictingEntity) @0x4a033c +// shooterPlayer = shooterEntity+0x190 @0x4a034f +// victimPlayer = mech+0x190 @0x4a0358 +// block A @0x4a04da (was ALIVE at entry, destroyed now): +// ScoreMessage{id 0x16, type 2 KillScore, +// scoreAward = the APPLIED damage tally, +// damageAmount = victim role's killBonus (role+0x1c), +// vitalHit, zone, subsysID, +// senderMechID = the VICTIM} -> the SHOOTER's player +// block B @0x4a05d9 (not newly killed, tally != 0): +// same shape, type 0 -- which the only registered 0x16 receiver +// (@0x4c02e4) VERIFY-rejects; 1995 then folded an UNINITIALIZED +// award. Sent for wire fidelity; our handler banks 0. +// block C @0x4a06c0 (tally > 0; runs after A too -- kills included): +// ScoreMessage{type 1 DamageReceivedScore, +// scoreAward = tally, +// damageAmount = INTENDED (burstCount x amount), +// senderMechID = the INFLICTOR} -> the victim's player +// +// Score-model consequence [T1]: shooters are credited ONLY for kills; victims +// are PENALIZED for damage received (CalcDamageReceivedScore returns the +// negative). Per-hit inflicted credit never existed in the 1995 pod. +// +// Port accommodations, all guarded: registry/player lookups null-checked (the +// binary derefs them raw); killBonus basis 0 for the ownerless dummy victim; +// a skipped kill credit keeps the #45 NOCREDIT matchlog forensics. +// +void BTMechPostCombatReports( + void *mech_v, // the VICTIM mech (the handler's this) + void *msg_v, // the TakeDamageMessage being handled + float damage_tally, // loop local_24: applied damage + crit bonuses + int vital_hit, // loop local_2c: a zone entered BurningState + int report_zone, // msg->damageZone at loop ENTRY (the binary + // reads msg+0x24, never rewritten mid-loop) + int newly_destroyed) // alive at entry && IsMechDestroyed() now { - if (application == 0 || victim == 0) + Mech *victim = (Mech *)mech_v; + Entity::TakeDamageMessage *msg = (Entity::TakeDamageMessage *)msg_v; + if (application == 0 || victim == 0 || msg == 0) { return; } - BTPlayer *local_player = (BTPlayer *)application->GetMissionPlayer(); - if (local_player == 0) - { - return; - } - // The credit belongs to the SHOOTER, not the viewer: the binary reports the - // victim handler's damage tally to the INFLICTING player (the id-0x16 report - // tail @0x4a04da, deferred #45). This stand-in can only bank score on the - // LOCAL player, so refuse any post whose shooter is not the local vehicle -- - // an AI master's fire on the player (SP), or a dying mech's death-blast - // splash, must not credit the viewer. MP is unaffected: only local fire - // carries live damage on this node. - if (shooter != (Entity *)local_player->GetPlayerVehicle()) - { - return; - } - // ScoreInflicted (scoreType 0): senderMechID = the mech that TOOK the damage. - // -> ScoreInflictedMessageHandler: currentScore += tonnageRatio*CalcInflictedScore. - BTPlayer::ScoreMessage message( - BTPlayer::ScoreInflictedMessageID, // 0x16 - sizeof(BTPlayer::ScoreMessage), - BTPlayer::ScoreMessage::DamageInflictedScore, // 0 - 0.0f, // scoreAward (unused for inflicted) - damage, // damageAmount - victim->GetEntityID()); // senderMechID = victim - local_player->Dispatch(&message); - if (getenv("BT_SCORE_LOG")) - DEBUG_STREAM << "[score] +damage " << damage << " -> currentScore=" - << (Scalar)local_player->GetScore() << "\n" << std::flush; -} -void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP death) -{ - if (application == 0 || victim == 0) - { - return; - } - // KILL credit. Runs on the VICTIM's node -- the only node whose mech carries a - // populated `lastInflictingID` (damage is applied master-side only: 0 of 8800 - // corpus DMG rows target a replicant, which is why every `DEATH inst=R` row reads - // `killer=0:0`). The credit is dispatched to the killer's Player, which HERE is a - // REPLICANT, so `Entity::Dispatch` reroutes it to the owning host - // (ENTITY.cpp:244-251) and `++killCount` lands on the killer's OWN machine. - // - // CORRECTED 2026-07-25 (Gitea #45). The previous banner claimed "every node - // maintains LOCAL score copies ... each node witnesses the death transition and - // tallies its own copies self-consistently". That was FALSE and it is what hid - // this bug: the reroute means exactly ONE node increments, no other node can - // observe the killer at all, and nothing carried the value back out -- so every - // remote pilot's KILLS read 0 on every other pod, all mission (corpus invariant: - // 0 of 18818 DMG rows are inst=R, so no other node can even attribute the kill; - // every DEATH inst=R row reads killer=0:0). The counters are - // now replicated owner->replicant instead; see Read/WriteUpdateRecord above. - BTPlayer *killer_player = 0; + BTPlayer *shooter_player = 0; + Entity *shooter = 0; + extern int BTIsRegisteredMech(Entity *e); if (application->GetHostManager() != 0) { - Entity *killer = application->GetHostManager()->GetEntityPointer( - ((Mech *)victim)->lastInflictingID); - extern int BTIsRegisteredMech(Entity *e); - if (killer != 0 && killer != victim && BTIsRegisteredMech(killer)) - killer_player = (BTPlayer *)((Mech *)killer)->GetPlayerLink(); + shooter = application->GetHostManager()->GetEntityPointer( + msg->inflictingEntity); + if (shooter != 0 && BTIsRegisteredMech(shooter)) + shooter_player = (BTPlayer *)((Mech *)shooter)->GetPlayerLink(); } - if (killer_player != 0) + BTPlayer *victim_player = (BTPlayer *)victim->GetPlayerLink(); + + if (newly_destroyed) { - // KillScore (scoreType 2): senderMechID MUST be the VICTIM (!= the - // receiver's mech) so the handler credits `killCount++` -- a suicide - // (killer == victim) never reaches here. - BTPlayer::ScoreMessage kill( - Player::ScoreMessageID, // 0x12 + // + // Block A: the KILL report, to the SHOOTER's player. On the victim's + // node that player is a replicant, so Entity::Dispatch reroutes to the + // owning host (ENTITY.cpp:244) and the credit lands on the killer's own + // machine -- the #45 flow, unchanged. killer == victim (the eject + // charge) IS dispatched: the handler negates the award. That is the + // #134 panic-penalty path the old BTPostKillScore filtered out. + // + Scalar kill_bonus = 0.0f; // basis: victim role+0x1c + if (victim_player != 0 && victim_player->GetScenarioRole() != 0) + kill_bonus = victim_player->GetScenarioRole()->GetKillBonus(); + if (shooter_player != 0) + { + BTPlayer::ScoreMessage kill( + Player::ScoreMessageID, // 0x16 + sizeof(BTPlayer::ScoreMessage), // 0x3c + BTPlayer::ScoreMessage::KillScore, // 2 + damage_tally, // +0x1c + kill_bonus, // +0x24 + victim->GetEntityID(), // +0x34 the VICTIM + vital_hit, // +0x28 + report_zone, // +0x2c + msg->inflictingSubsystemID); // +0x30 + shooter_player->Dispatch(&kill); + if (getenv("BT_SCORE_LOG")) + DEBUG_STREAM << "[score] *** KILL report *** -> shooterPlayer=" + << (void *)shooter_player << " tally=" << damage_tally + << " bonus=" << kill_bonus << "\n" << std::flush; + } + else if (BTMatchLogActive()) + { + // #45 forensics, carried from BTPostKillScore: WHY the credit was + // skipped -- missing killer entity, unregistered, or NULL link. + BTMatchLog("NOCREDIT", + "victim=%d:%d killerID=%d:%d found=%d registered=%d link=%d", + BTMatchHostOf(victim->GetEntityID()), (int)victim->GetEntityID(), + BTMatchHostOf(msg->inflictingEntity), (int)msg->inflictingEntity, + (int)(shooter != 0), + (int)(shooter != 0 && BTIsRegisteredMech(shooter)), + (int)(shooter != 0 && ((Mech *)shooter)->GetPlayerLink() != 0)); + } + } + else if (damage_tally != 0.0f && shooter_player != 0) + { + // + // Block B: the plain inflicted report. Wire fidelity only -- the 0x16 + // handler Verify-rejects type 0 and banks award 0 (1995 banked an + // uninitialized stack float; see the handler's type-0 arm note). + // + BTPlayer::ScoreMessage inflicted( + Player::ScoreMessageID, sizeof(BTPlayer::ScoreMessage), - BTPlayer::ScoreMessage::KillScore, // 2 - 0.0f, // scoreAward (killBonus; 0 for bring-up) - damage, // damageAmount (killing-blow) - victim->GetEntityID()); // senderMechID = victim - killer_player->Dispatch(&kill); - if (getenv("BT_SCORE_LOG")) - DEBUG_STREAM << "[score] *** KILL *** killerPlayer=" << (void *)killer_player - << " killCount=" << killer_player->GetKillCount() - << " deaths=" << killer_player->GetDeaths() - << " score=" << (Scalar)killer_player->GetScore() << std::endl; - } - else if (BTMatchLogActive()) - { - // - // #45 observability: the credit was SKIPPED. This used to be completely - // silent, which is why the bug survived so long -- the counter simply never - // moved and no log said why. Record which link in the chain broke so a - // single matchlog convicts it: a missing killer entity, an unregistered - // mech, a self-kill, or the NULL playerLink hazard. - // - Entity *k = (application->GetHostManager() != 0) - ? application->GetHostManager()->GetEntityPointer( - ((Mech *)victim)->lastInflictingID) - : 0; - extern int BTIsRegisteredMech(Entity *e); - BTMatchLog("NOCREDIT", - "victim=%d:%d killerID=%d:%d found=%d self=%d registered=%d link=%d", - BTMatchHostOf(victim->GetEntityID()), (int)victim->GetEntityID(), - BTMatchHostOf(((Mech *)victim)->lastInflictingID), - (int)((Mech *)victim)->lastInflictingID, - (int)(k != 0), (int)(k == victim), - (int)(k != 0 && BTIsRegisteredMech(k)), - (int)(k != 0 && ((Mech *)k)->GetPlayerLink() != 0)); + BTPlayer::ScoreMessage::DamageInflictedScore, // 0 + damage_tally, // +0x1c + damage_tally, // +0x24 basis = tally + victim->GetEntityID(), + vital_hit, report_zone, msg->inflictingSubsystemID); + shooter_player->Dispatch(&inflicted); } - // MP DEATH: the victim's death tally. - // - // REMOVED 2026-07-30 (#81): this used to dispatch a second - // `Player::VehicleDeadMessage` to the victim's own player "to credit a - // death". That message is the RESPAWN-CYCLE TRIGGER, not a scoreboard - // increment, and the victim already receives one from the mech's death - // transition (mech4.cpp:2110) -- which is the hardened, authentic notify - // (it carries the #55 NULL-playerLink fallback). Both fire inside the SAME - // death transition (BTPostKillScore is called at mech4.cpp:2006, the notify - // at :2110), so they were always paired: every death dispatched the message - // TWICE. The tally itself is credited by the handler's `++deathTally` - // (:538) on the first one, so this dispatch never added anything. - // - // It was invisible because the `deathPending` latch silently deduped it -- - // that is what EVERY "death ... SWALLOWED" warning in the field logs - // actually was (8 of 8 deaths, a 100% base rate, which is exactly why it - // correlated with nothing). Once the latch was released to match the - // binary (see the death handler's tail), the duplicate stopped being - // masked and started a SECOND death cycle: deathCount double-incremented, - // the first cycle's re-post went stale and tripped the drop-zone - // `*** MISMATCH ***`, and one respawn burned two cycles. Caught on the - // first solo bench of the latch fix. - // - // The kill credit above is unaffected -- it dispatches a ScoreMessage, the - // correct message for a scoreboard change. + if (damage_tally > 0.0f && victim_player != 0) + { + // + // Block C: the RECEIVED report, to the victim's own player -- kills + // included (the binary runs this after block A as well). Carries the + // INTENDED damage (burstCount x amount, @0x4a06d2 fild/fmul) as the + // penalty basis and the INFLICTOR as senderMechID; feeds the score + // penalty and the operator-console VTVDamaged line. + // + int bursts = msg->damageData.burstCount; + if (bursts < 1) + bursts = 1; // port guard (mirrors the loop) + BTPlayer::ScoreMessage received( + Player::ScoreMessageID, + sizeof(BTPlayer::ScoreMessage), + BTPlayer::ScoreMessage::DamageReceivedScore, // 1 + damage_tally, // +0x1c + (Scalar)bursts * msg->damageData.damageAmount, // +0x24 intended + msg->inflictingEntity, // +0x34 the INFLICTOR + vital_hit, report_zone, msg->inflictingSubsystemID); + victim_player->Dispatch(&received); + } } +// +// The authentic VehicleDead sender (@0x4a07d4-0x4a0890): the respawn-cycle +// trigger, dispatched from the death tail of the SAME TakeDamage that killed +// the mech, to the mech's own player -- now carrying the killed-by player and +// the killing zone (the BT 0x38-byte message extension). Replaces the mech4 +// death-transition dispatch site, which carried a [T3 -- the binary's exact +// sender is undecoded] flag; this IS that sender, decoded. The #55 +// NULL-playerLink fallback and the DEAD_NOTIFY forensics move here intact. +// +void BTMechPostVehicleDead(void *mech_v, void *msg_v, int report_zone) +{ + Mech *victim = (Mech *)mech_v; + Entity::TakeDamageMessage *msg = (Entity::TakeDamageMessage *)msg_v; + if (application == 0 || victim == 0) + { + return; + } + + Player *owner = victim->GetPlayerLink(); + // #55 (the David fix), moved intact: resolve the SAME object by the + // reverse link the binary's own respawn branch uses when the once-written + // playerLink was lost. + if (owner == 0) + { + Player *mission_player = (Player *)application->GetMissionPlayer(); + if (mission_player != 0 + && mission_player->GetPlayerVehicle() == (Entity *)victim) + { + owner = mission_player; + } + } + BTMatchLog("DEAD_NOTIFY", "mech=%d:%d link=%p", + BTMatchHostOf(victim->GetEntityID()), (int)victim->GetEntityID(), + (void *)owner); + if (owner == 0) + { + return; + } + + // killed-by: the shooter PLAYER's own EntityID (@0x4a07d4 copies + // shooterPlayer+0x184); Null when the inflictor or its player is gone. + EntityID killed_by = EntityID::Null; + if (msg != 0 && application->GetHostManager() != 0) + { + Entity *shooter = application->GetHostManager()->GetEntityPointer( + msg->inflictingEntity); + extern int BTIsRegisteredMech(Entity *e); + if (shooter != 0 && BTIsRegisteredMech(shooter)) + { + Player *shooter_player = ((Mech *)shooter)->GetPlayerLink(); + if (shooter_player != 0) + killed_by = shooter_player->GetEntityID(); + } + } + + BTPlayer::VehicleDeadMessage vehicle_dead( + Player::VehicleDeadMessageID, // 0x17 + sizeof(BTPlayer::VehicleDeadMessage), // 0x38 + killed_by, + report_zone); + owner->Dispatch(&vehicle_dead); + if (getenv("BT_DEATH_LOG")) + DEBUG_STREAM << "[death] VehicleDead(-1) dispatched to the owning player" + << " (killedBy=" << BTMatchHostOf(killed_by) << ":" << (int)killed_by + << " zone=" << report_zone << ")\n" << std::flush; +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// TOMBSTONE (2026-08-05): BTPostDamageScore / BTPostKillScore -- the gauge +// scoring wave's bring-up producers -- are RETIRED, replaced by the authentic +// report tail above (BTMechPostCombatReports / BTMechPostVehicleDead, called +// from Mech::TakeDamageMessageHandler at the binary's exact positions). +// What changed for the scoreboard: +// - per-hit INFLICTED credit is GONE: it was a port invention riding the +// never-registered @0x4c0200 handler (the binary's BTPlayer table, byte- +// scanned at file 0x112dxx, has 6 entries and none binds it). 1995 +// scoring = kill awards + received-damage penalties, nothing per-hit. +// This also retires the #95 salvo-credit fix that rode it. +// - suicides now DISPATCH the kill report and the handler NEGATES the +// award -- the #134 panic-eject penalty path the old filter blocked. +// - kill credit resolves the shooter from msg->inflictingEntity of the +// killing TakeDamage itself; lastInflictingID (stamped from the same +// field at handler entry) stays for the LOD router + effect orientation. +// - the cross-node reroute story is unchanged: the kill report still +// Dispatches to a replicant player and lands on the owner host +// (ENTITY.cpp:244-251); KILLS/DEATHS still replicate owner->replicant +// (Read/WriteUpdateRecord + the scoreboard heartbeat). +// - the #81 single-VehicleDead rule is unchanged: exactly one dispatch per +// death, now from the authentic tail instead of the death transition. +// The ScoreInflictedMessageID channel and its handler remain registered but +// idle (port infra; the binary's analog was dead code in the table). +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + //############################################################################# // BTPlayerRoleLocksAdvanced -- complete-type bridge (task #12) diff --git a/game/reconstructed/btplayer.hpp b/game/reconstructed/btplayer.hpp index c3242eb..1b97a90 100644 --- a/game/reconstructed/btplayer.hpp +++ b/game/reconstructed/btplayer.hpp @@ -57,14 +57,26 @@ class DropZone__ReplyMessage; // generated the points. Parallels RPPlayer__ScoreMessage, but the BT // score types describe mech combat instead of score zones. // -// Observed message layout (param_2 in the handlers): -// +0x1c scoreAward (Scalar, base Player::ScoreMessage) +// Message layout, sizeof 0x3C (field meanings CORRECTED 2026-08-05 from the +// raw disasm of the three sender builds in the Mech::TakeDamageMessageHandler +// report tail @0x4a04da/@0x4a05d9/@0x4a06c0 -- the old "pointSender" names +// were guesses from RP's smaller message, which stops at +0x28): +// +0x1c scoreAward (Scalar, base) -- the sender puts the APPLIED +// damage tally here; the receiving handler +// overwrites it with the computed award before +// delegating to Player::ScoreMessageHandler // +0x20 scoreType (int) -// +0x24 damageAmount (Scalar) raw damage / point quantity -// +0x28 pointSenderHi (EntityID word) -// +0x2c pointSenderLo (EntityID word) -// +0x30 auxID (EntityID / host word) -// +0x34 senderMechID (EntityID -- resolved to the inflicting Mech) +// +0x24 damageAmount (Scalar) the scoring BASIS: type 0 = tally, +// type 1 = intended (burstCount x amount), +// type 2 = the VICTIM role's killBonus (role+0x1c) +// +0x28 vitalHit (int) a zone reached BurningState during +// this message's applications (loop local_2c) +// +0x2c zoneIndex (int) the struck damage zone (msg+0x24, +// the INITIAL resolve -- not the last burst's) +// +0x30 subsysID (int) inflictingSubsystemID passed through +// from the TakeDamageMessage (+0x5c) +// +0x34 senderMechID (EntityID) types 0/2: the VICTIM mech; +// type 1: the INFLICTING mech // class BTPlayer__ScoreMessage: public Player::ScoreMessage @@ -72,11 +84,16 @@ class DropZone__ReplyMessage; public: // // Kind of scoring event. Recovered from the branch selector at - // @004c02e4 (this->scoreType, message+0x20) and the dedicated - // inflicted-damage handler at @004c0200. + // @004c02e4 (this->scoreType, message+0x20). NOTE: type 0 has NO + // scoring arm in the binary -- @004c02e4 Verify-rejects it (line 662) + // and @004c0200, the only function that accepts it, appears in no + // handler-table entry (byte-scan 2026-08-05: the BTPlayer table at + // file 0x112dxx has exactly 6 entries, none binding it). 1995 pod + // scoring = kills + received-damage penalties; per-hit inflicted + // credit never existed. // enum ScoreType { - DamageInflictedScore = 0, // to ScoreInflictedMessageHandler + DamageInflictedScore = 0, // sent, but scores nothing (see above) DamageReceivedScore = 1, // I took damage KillScore = 2 // I destroyed / was destroyed }; @@ -85,21 +102,19 @@ class DropZone__ReplyMessage; scoreType; // +0x20 Scalar - damageAmount; // +0x24 + damageAmount; // +0x24 scoring basis (see layout table) - // - // Point-source / auxiliary handles carried alongside the damage record - // (read by ScoreMessageHandler when building the console feed message). - // int - pointSenderHi; // +0x28 + vitalHit; // +0x28 zone entered BurningState this message + int - pointSenderLo; // +0x2c + zoneIndex; // +0x2c struck damage zone + int - auxID; // +0x30 + subsysID; // +0x30 inflicting weapon subsystem EntityID - senderMechID; // +0x34 inflicting mech (point sender) + senderMechID; // +0x34 victim (types 0/2) / inflictor (type 1) BTPlayer__ScoreMessage( Receiver::MessageID message_ID, @@ -107,15 +122,63 @@ class DropZone__ReplyMessage; int score_type, Scalar score_award, Scalar damage_amount, - const EntityID &sender_mech_ID + const EntityID &sender_mech_ID, + int vital_hit = 0, + int zone_index = 0, + int subsys_ID = 0 ): Player::ScoreMessage(message_ID, length, score_award), scoreType(score_type), damageAmount(damage_amount), + vitalHit(vital_hit), + zoneIndex(zone_index), + subsysID(subsys_ID), senderMechID(sender_mech_ID) {} }; +//########################################################################### +//################# BTPlayer::VehicleDeadMessage ###################### +//########################################################################### +// +// BT extension of the engine Player__VehicleDeadMessage (PLAYER.h: +// deathCount @0x1c, dropZoneID @0x20). Decoded 2026-08-05 from the death +// tail of Mech::TakeDamageMessageHandler (@0x4a07d4-0x4a0890, raw disasm): +// the binary builds a 0x38-byte message {size 0x38, id 0x17 == +// Player::VehicleDeadMessageID, pri 1, deathCount -1 (the engine ctor +// default), dropZoneID Null, +0x28 = 0, +0x2c = the KILLER PLAYER's own +// EntityID (shooterPlayer+0x184), +0x34 = the killing zone (msg+0x24)} and +// dispatches it to the dying mech's own player -- the respawn-cycle +// trigger, now carrying the killed-by attribution. Whether the @004c05c4 +// handler consumes the two extension fields is still undecoded (the +// function is not in the export); the sender carries them faithfully. +// + class BTPlayer__VehicleDeadMessage: + public Player::VehicleDeadMessage + { + public: + int + reserved28; // +0x28 zeroed by the binary (@0x4a0856) + + EntityID + killedByPlayerID; // +0x2c the killer PLAYER's EntityID + + int + killZone; // +0x34 zone the killing damage struck + + BTPlayer__VehicleDeadMessage( + Receiver::MessageID message_ID, + size_t length, + const EntityID &killed_by_player_ID, + int kill_zone + ): + Player::VehicleDeadMessage(message_ID, length), // deathCount -1, dropZone Null + reserved28(0), + killedByPlayerID(killed_by_player_ID), + killZone(kill_zone) + {} + }; + //########################################################################### //##################### BTPlayer::MakeMessage ######################### //########################################################################### @@ -202,8 +265,9 @@ class DropZone__ReplyMessage; // Player::ScoreMessage.) // public: - typedef BTPlayer__ScoreMessage ScoreMessage; - typedef BTPlayer__MakeMessage MakeMessage; + typedef BTPlayer__ScoreMessage ScoreMessage; + typedef BTPlayer__MakeMessage MakeMessage; + typedef BTPlayer__VehicleDeadMessage VehicleDeadMessage; // BT 0x38 ext (killed-by) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Message Support diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 06d3fa3..c2e2519 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -1063,24 +1063,40 @@ void (float)(message->damageData.impactPoint.z - localOrigin.linearPosition.z)); } + // + // Death-edge latch, MOVED 2026-08-05 to the authentic position (binary + // @0x4a0303: captured at handler ENTRY, before the collision divert). It + // arms the whole death tail -- kill report, VehicleDead, death blast. + // Capturing it after the divert (where #89 first placed it) meant a + // COLLISION death could never arm the tail. + // + const int deathBlastArmed = !IsMechDestroyed(); // [ebp-0x10], inverted + // + // The zone the reports + VehicleDead carry: msg+0x24 as of loop entry. + // The binary never rewrites msg+0x24 after the initial cylinder resolve; + // our loop-exit matchlog write (damageZone = LAST burst's zone) must not + // leak into them. + // + int reportZone = message->damageZone; + DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!) + // // @0x4a0368 -- the COLLISION DIVERT (#83, closing the #82 chain). Damage // type 0 == the mover's collision damage (what the crash response policy // forwards). It NEVER reaches the zone/armor loop below: it is priced + - // distributed as INTERNAL RATTLE by DistributeCollisionDamage (@0049ffcc) - // and the handler is done. This was the missing half of the crash - // self-damage reconstruction -- without the divert, the raw kinetic-energy - // amounts (a 60-ton mech prices ~1000+ per wall tap) fell through into the - // WEAPON loop and a single bump killed the mech (run-14 field report). + // distributed as INTERNAL RATTLE by DistributeCollisionDamage (@0049ffcc). + // CORRECTED 2026-08-05: the binary does NOT return here -- it JUMPS TO THE + // DEATH TAIL (raw disasm @0x4a0375: jmp 0x4a07b5), so a mech that dies of + // the rattle still posts VehicleDead (the respawn trigger) and still + // blasts. The early `return` this block shipped with was a latent + // "wall-death strands the pilot" hazard. No score reports on this path -- + // the jump bypasses them; a collision death credits no one. Authentic. // if (message->damageData.damageType == 0) { DistributeCollisionDamage(&message->damageData); - Check_Fpu(); - return; + goto death_tail; } - - DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!) // #92: when this gate fails the zone keeps whatever the message carried -- // which for an unaimed weapon hit is 0, i.e. EVERY shot lands on zone 0. if (getenv("BT_DMGTABLE_LOG")) @@ -1105,6 +1121,7 @@ void << message->damageData.impactPoint.z << ")\n" << std::flush; } } + reportZone = message->damageZone; // post-resolve (@0x4a0396 write) // // #80 -- the faithful application loop (binary @0x4a0423-0x4a04d8), which // REPLACES the engine-base single application. Three things the base @@ -1126,11 +1143,6 @@ void // nothing (matches ENTITY.cpp:878; the binary would deref -1 -- it can't // happen there because every mech ships a lookup table). // - // #89 DEATH-BLAST edge arm: the binary's tail (@0x4a07b8) gates on the - // victim ENTERING dead/eject during THIS message (entry test edx = the - // pre-application disabled state). Capture the pre-state here. - const int deathBlastArmed = !IsMechDestroyed(); - if (damageZones != 0 && message->damageZone >= 0 && message->damageZone < damageZoneCount) { @@ -1181,14 +1193,20 @@ void } message->damageZone = zoneIndex; // matchlog sees the LAST zone - // Binary tail (@0x4a04da-0x4a07b2, decoded + deferred): builds id-0x16 - // damage/kill REPORT messages -- {tally, zone, zoneDestroyed flag, - // inflicting subsystem, victim name} -- to the shooter's player (with a - // kill-flagged variant when this damage NEWLY disabled the mech) and to - // the victim's player. That is the authentic stats plumbing (#45); the - // port's matchlog + BTPostDamageScore cover the bookkeeping today [T3]. - (void)zoneDestroyed; - (void)damageTally; + // + // THE REPORT TAIL (@0x4a04da-0x4a07b2), reconstructed 2026-08-05 [T1]: + // the kill / inflicted / received score reports to the shooter's and + // victim's players. Sender lives in btplayer.cpp (complete-BTPlayer + // TU, per the databinding rule); it receives the APPLIED tally, the + // vital-wreck flag, and the loop-ENTRY zone. This retires + // BTPostDamageScore/BTPostKillScore -- see the btplayer.cpp tombstone. + // + { + extern void BTMechPostCombatReports(void *, void *, float, int, int, int); + BTMechPostCombatReports((void *)this, (void *)message, + (float)damageTally, zoneDestroyed, reportZone, + deathBlastArmed && IsMechDestroyed()); + } } // MP MATCH FORENSICS (matchlog.hpp): the victim-side authoritative damage @@ -1218,30 +1236,41 @@ void (float)message->damageData.impactPoint.z); } - // #89 DEATH BLAST (raw disasm @0x4a07b8-0x4a0bda -- the un-exported tail - // of THIS handler; found by call-scanning Explosion::SplashDamage - // @0042fad0, which has exactly TWO callers: Missile::Perform and here). - // The binary, when the victim ENTERS dead(9)/eject(10) during the - // applications above: - // 1. self-dispatches SetBurningState (id 0x17, Damage payload, zone -1) - // -- the burning wreck. DEFERRED here: our 0x17 handler is not yet - // reconstructed; the port's wreck-fire visuals come from the death - // effect layer meanwhile. - // 2. spawns the death Explosion (Explosion::Make model 0x31 at the mech - // origin) -- the port's kill path already fires the authored death - // list visuals; not double-spawned. - // 3. calls SplashDamage on it, gated on the owning player's - // advancedDamageOn (+0x264) AND NOT suppressConsole (+0x258 -- set - // by the eject bookkeeping: an EJECTED pilot's mech never blasts): + // THE DEATH TAIL (@0x4a07b5-0x4a0bda, raw disasm -- the un-exported tail + // of THIS handler). Gate [T1]: mech was ALIVE at entry ([ebp-0x10]) and + // is dead(9)/eject(10) now. Sequence: + // 1. Player::VehicleDeadMessage (id 0x17; the BT 0x38-byte extension + // carrying killed-by player + kill zone) -> the mech's OWN player: + // the respawn-cycle trigger. CORRECTED 2026-08-05: the #89 banner + // previously read this build as "SetBurningState (mech id 0x17)" -- + // it is a PLAYER-table id: the binary's BTPlayer handler table (file + // 0x112dxx) binds 0x17 -> @0x4c05c4 VehicleDeadMessageHandler; the + // mech-table 0x17 (@0x49f674) is unrelated. Dispatched from HERE -- + // this is the decoded sender the mech4 transition site's + // [T3 sender-undecoded] flag was waiting for. + // 2. the death Explosion (id 3 MakeMessage, 0x5C bytes, model 0x31 at + // the mech origin, @0x4a08bd) -- the port fires the authored per-mech + // death list from the death transition instead (#42 history): same + // once-per-death edge, not double-spawned. + // 3. SplashDamage (#89), gated advancedDamageOn (+0x264) AND NOT + // suppressConsole (+0x258 -- ejected pilots' mechs never blast): // Damage{type=2 Explosive, amount=deathSplashDamage@0x520, // impact=mech origin, // burstCount=round(0.001 * moverMass@0x20c * 15.0)} - // radius=deathSplashRadius@0x524, excluded=the dying mech. - // Splash falloff (bursts / dist^1.25) applies inside per victim. - // 4. ForceUpdate(1). (A score/console post also rides this tail -- - // the deferred id-0x16 family, tracked with #134.) + // radius=deathSplashRadius@0x524, excluded=the dying mech; + // 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. +death_tail: if (deathBlastArmed && IsMechDestroyed()) { + extern void BTMechPostVehicleDead(void *, void *, int); + BTMechPostVehicleDead((void *)this, (void *)message, reportZone); + extern void BTApplyDeathSplash(void *mech_v); BTApplyDeathSplash((void *)this); } diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 5eb34e3..1726211 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -705,10 +705,12 @@ static int gEnemyDestroyed = 0; // BRING-UP: the spawned target/enemy mech (defined in btplayer.cpp). The player // mech locks onto it as its current target each frame (see targeting step below). extern Entity *gEnemyMech; -// gauge scoring wave: producers (btplayer.cpp) -- feed the scoreboard from combat. -extern void BTPostDamageScore(Entity *victim, Scalar damage, Entity *shooter); // per-hit SCORE (ScoreInflicted) +// RETIRED 2026-08-05: the BTPostDamageScore / BTPostKillScore producers. +// Scoring now flows through the authentic report tail in the victim's +// TakeDamageMessageHandler (BTMechPostCombatReports, btplayer.cpp) -- the +// shooter-side posts here double-credited against it. See the tombstone in +// btplayer.cpp. float gBTWalkElev = 0.0f; // #124 zone walker: servo-driven aim elevation (rad) -extern void BTPostKillScore(Entity *victim, Scalar damage); // KILL (+ MP death) // Mech target slots (verified vs the binary's weapon/fire path, part_013.c): // mech+0x37c Point3D current target world position (range/aim source) @@ -1311,12 +1313,11 @@ static void shooter->GetEntityID(), -1 /*unaimed -> cylinder resolves*/, dmg); e->Dispatch(&td); - // SCORE (#95 follow-up): the binary's victim handler tallies EVERY - // TakeDamage -- splash included -- and reports it to the INFLICTING - // player (id-0x16, deferred #45), so a bystander blast is score too. - // The bridge itself refuses a shooter that is not the local vehicle - // (this function's death-blast caller passes the DYING mech). - BTPostDamageScore(e, dmg.damageAmount * (Scalar)bursts, shooter); + // SCORE: handled by the victim's own TakeDamage handler now -- the + // dispatch above lands in the authentic report tail (#45), which + // posts the kill/received reports itself. The shooter-side + // BTPostDamageScore call that lived here was RETIRED 2026-08-05 + // (per-hit inflicted credit never existed in the 1995 pod). // MP MATCH FORENSICS (matchlog.hpp): shooter-side splash delivery // to a bystander (pairs with the victim's DMG burst>1 line). @@ -1864,17 +1865,12 @@ static void (p.shooter != 0) ? p.shooter->GetEntityID() : EntityID::Null, -1 /*unaimed -> cylinder resolves*/, dmg, p.weaponSubsys); tgt->Dispatch(&take_damage); - // gauge scoring wave (Step 6): a projectile hit credits SCORE with - // the DELIVERED amount -- per-missile damage x the rolled cluster - // count, the same figure the dispatch above hands the victim's - // handler (which applies it burstCount times, tallying the amount - // per burst @0x4a04da). Posting the bare per-missile amount here - // was the "LRM10 registered ~3 points to score" defect (#95): the - // armour took the full salvo while the scoreboard banked one - // missile. (Crit bonuses in the tally remain victim-side - // knowledge -- the deferred id-0x16 report, #45.) - BTPostDamageScore((Entity *)tgt, - p.damage * (Scalar)dmg.burstCount, p.shooter); + // SCORE: handled by the victim's own TakeDamage handler now -- + // the dispatch above lands in the authentic report tail (#45), + // which tallies every burst (crit bonuses included) itself. + // The shooter-side BTPostDamageScore call that lived here (and + // its #95 salvo-credit fix) was RETIRED 2026-08-05: per-hit + // inflicted credit never existed in the 1995 pod. // MP MATCH FORENSICS (matchlog.hpp): shooter-side // projectile impact delivery (pairs with the victim's @@ -2425,12 +2421,11 @@ void // boresight pick skips a dead mech, so that block never ran against // it again: internally dead, smoking, standing, invulnerable. { - // KILL score (once; the ownerless dummy yields no death for us). - // task #60: pass the REAL killing-blow magnitude (latched in the damage - // handler), not the flat bring-up kShotDamage -- the score handler - // @0x4c02e4 derives the whole kill award from this damageAmount, so a - // flat 12 made every kill score identically regardless of weapon. - BTPostKillScore((Entity *)this, lastInflictingDamage); + // KILL score: RETIRED here 2026-08-05 -- the authentic kill report now + // posts from the TakeDamage report tail (@0x4a04da, block A) with the + // REAL applied tally, which supersedes the lastInflictingDamage relay + // this site needed (#60). Suicides dispatch too (the handler negates + // the award -- the #134 panic-penalty path this site's filter blocked). if ((Entity *)this == gEnemyMech) gEnemyDestroyed = 1; // latches off damage score @@ -2496,55 +2491,19 @@ void } // - // --- RESPAWN CYCLE (task #52): notify the owning player of the death. --- - // The BTPlayer VehicleDead handler @004c05c4 receives this with - // deathCount == -1 (the ctor default = "immediate death notification"), - // does the death bookkeeping, severs playerVehicle (this wreck entity - // STAYS in the world) and re-posts the message to itself at +5 seconds - // (5.0f @004c0830) -> the engine drop-zone hunt -> DropZoneReply -> - // CreatePlayerVehicle (a NEW mech). Only the owner pod's master has a - // live playerLink, which is exactly where the cycle must run; replicant - // wrecks just mirror the master's death. [T1 the handler @004c05c4; - // T3 this dispatch site -- the binary's exact sender is undecoded, but - // the once-per-death transition is the only death edge and the message - // ctor's deathCount=-1 default exists for precisely this notification.] + // --- RESPAWN CYCLE (task #52): the VehicleDead notify MOVED 2026-08-05. --- + // This site carried [T3 -- the binary's exact sender is undecoded]. The + // sender is now decoded [T1]: the death tail of the TakeDamage handler + // that killed the mech (@0x4a07d4-0x4a0890, raw disasm) builds the BT + // 0x38-byte VehicleDeadMessage extension {killed-by player, kill zone} + // and dispatches it to the owning player. BTMechPostVehicleDead + // (btplayer.cpp) is that sender; the #55 NULL-playerLink fallback and the + // DEAD_NOTIFY forensics moved into it intact. Every path that reaches + // this transition does so inside the killing TakeDamage (weapon zones, + // collision rattle via the divert fallthrough, the eject charge), so the + // tail covers every death this site covered -- with one dispatch, keeping + // the #81 single-VehicleDead rule. // - { - Player *owner = GetPlayerLink(); - // #55 step 1 (the David fix): the death notification used to hinge on - // this single never-retried pointer -- playerLink is written once by - // Entity::PlayerLinkMessageHandler with no null check, so a lost race - // silently swallowed the whole respawn cycle. Resolve the SAME object - // by the reverse link the binary's own respawn branch uses - // (player+0x1FC == playerVehicle): the mission player whose vehicle is - // this mech. Not a stand-in -- the identical entity, second index. - if (owner == 0 && application != 0) - { - Player *mission_player = (Player *)application->GetMissionPlayer(); - if (mission_player != 0 - && mission_player->GetPlayerVehicle() == (Entity *)this) - { - owner = mission_player; - } - } - // #55 step 0: a NULL link was a SILENT swallow -- record the - // resolution unconditionally so the matchlog can prove it either way. - BTMatchLog("DEAD_NOTIFY", "mech=%d:%d link=%p", - BTMatchHostOf(GetEntityID()), (int)GetEntityID(), - (void *)owner); - if (owner != 0) - { - Player::VehicleDeadMessage - vehicle_dead( - Player::VehicleDeadMessageID, - sizeof(Player::VehicleDeadMessage) - ); - owner->Dispatch(&vehicle_dead); - if (getenv("BT_DEATH_LOG")) - DEBUG_STREAM << "[death] VehicleDead(-1) dispatched to the owning player\n" - << std::flush; - } - } } // Peer heading published each sim update, sampled by the render loop (BT_RENDHDG). diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index 64f6f64..422ceac 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -740,18 +740,12 @@ void target->Dispatch(&message); // no manager: direct (bring-up) } - // PORT bookkeeping (score lived in the retired mech4 fire block): credit - // the delivered amount to the shooter-side score exactly as before. - extern void BTPostDamageScore(Entity *victim, Scalar damage, Entity *shooter); - extern Logical BTIsRegisteredMech(Entity *e); - if (BTIsRegisteredMech(target) - && !((Mech *)target)->IsMechDestroyed()) - { - // Direct-fire damageData always authors burstCount = 1 (emitter.cpp:355), - // so the plain amount IS the delivered amount here. The shooter param - // lets the bridge refuse AI-master fire (score is the shooter's, #45). - BTPostDamageScore(target, damageData.damageAmount, (Entity *)mech); - } + // SCORE: handled by the victim's own TakeDamage handler now -- the + // dispatch above lands in the authentic report tail (#45), which posts + // the kill/received reports itself. The shooter-side BTPostDamageScore + // call that lived here was RETIRED 2026-08-05 (per-hit inflicted credit + // never existed in the 1995 pod; kills + received-damage penalties only). + extern Logical BTIsRegisteredMech(Entity *e); // still read by the matchlog below // MP MATCH FORENSICS (matchlog.hpp): the shooter-side per-shot damage // submission -- pairs with the victim's DMG application line. mech=1 diff --git a/scratchpad/night12/scorekill.sh b/scratchpad/night12/scorekill.sh new file mode 100644 index 0000000..5a13910 --- /dev/null +++ b/scratchpad/night12/scorekill.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# #45/#134 AUTHENTIC SCORE SENDER verify, bench 1: CROSS-NODE KILL. +# A (madcat, shooter, node 1501) zone-walk-hammers ONLY dz_ldleg on B (loki, +# spinner target, node 1601) at 90u until the vital leg dies -> B dies. +# PASS: +# B log (victim master): DMG rows; DEAD_NOTIFY link!=0; "[death] +# VehicleDead(-1) ... killedBy="; "[score] *** KILL report ***"; +# death cycle START (respawn un-regressed). +# A log (killer): matchlog SCORE type=2 award>0 kills=1 (rerouted credit); +# per-hit type-0 Verify prints prove the wire-fidelity block B. +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +bt_assert_player_env +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +rm -f sk_a.log sk_b.log sk_relay.log matchlog_*.txt +bt_expert_egg MP.EGG SK.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/" SK.EGG +python - << 'EOF' +lines = open('SK.EGG').read().splitlines(True) +n = 0 +for i, l in enumerate(lines): + if l.startswith('vehicle='): + n += 1 + lines[i] = 'vehicle=madcat\n' if n == 1 else 'vehicle=loki\n' +open('SK.EGG', 'w').writelines(lines) +print('vehicles set:', n) +EOF + +( + export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 + export BT_MATCHLOG=1 BT_SCORE_LOG=1 + bt_launch sk_b.log SK.EGG 0x0C -net 1601 +) +sleep 2 +( + export BT_ZONE_WALK=8 BT_WALK_ZONES=dz_ldleg + export BT_GOTO=enemy BT_GOTO_STOP=90 BT_KEY_NOFOCUS=1 + export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SCORE_LOG=1 + bt_launch sk_a.log SK.EGG 0x03 -net 1501 +) +sleep 5 +python ../tools/btconsole.py SK.EGG 127.0.0.1:1501 127.0.0.1:1601 > sk_relay.log 2>&1 & +RELAY=$! +sleep 340 +kill $RELAY 2>/dev/null +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 +echo "=== B (victim) death + notify + kill report sent:" +grep -aE "DEAD_NOTIFY|VehicleDead|KILL report|death cycle START" sk_b.log | head -8 +echo "=== B matchlog SCORE lines:" +grep -a "SCORE" matchlog_*.txt 2>/dev/null | tail -6 +echo "=== A (killer) score handler receipts:" +grep -aE "\[score\]|ScoreMessageHandler" sk_a.log | head -8 +echo "=== A kill credit (matchlog):" +grep -a "type=2" matchlog_*.txt 2>/dev/null | tail -3 diff --git a/scratchpad/night12/scoreself.sh b/scratchpad/night12/scoreself.sh new file mode 100644 index 0000000..1610766 --- /dev/null +++ b/scratchpad/night12/scoreself.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# #134 SELF-KILL PENALTY verify: A self-destructs (BT_SELF_DAMAGE, inflictor = +# SELF) with B standing off. The kill report now DISPATCHES for killer==victim +# (the old BTPostKillScore filtered it) and the handler NEGATES the award. +# PASS (A log/matchlog): "[score] *** KILL report ***" to A's OWN player; +# SCORE type=2 with award < 0 and kills UNCHANGED (0); VehicleDead +# killedBy=; death cycle START (respawn un-regressed). +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +bt_assert_player_env +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +rm -f ss_a.log ss_b.log ss_relay.log matchlog_*.txt +bt_expert_egg MP.EGG SG.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" SG.EGG + +( + export BT_SELF_DAMAGE=40 + export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SCORE_LOG=1 + bt_launch ss_a.log SG.EGG 0x0C -net 1601 +) +sleep 2 +( + export BT_GOTO=enemy BT_GOTO_STOP=60 BT_KEY_NOFOCUS=1 + export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SCORE_LOG=1 + bt_launch ss_b.log SG.EGG 0x03 -net 1501 +) +sleep 5 +python ../tools/btconsole.py SG.EGG 127.0.0.1:1501 127.0.0.1:1601 > ss_relay.log 2>&1 & +RELAY=$! +sleep 100 +kill $RELAY 2>/dev/null +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 +echo "=== A self-kill report + death:" +grep -aE "KILL report|VehicleDead\(-1\) dispatched|death cycle START" ss_a.log | head -6 +echo "=== A SCORE lines (want type=2 award<0 kills=0):" +grep -a "SCORE" matchlog_*.txt 2>/dev/null | grep "type=2" | head -4 +echo "=== all SCORE traffic:" +grep -a "SCORE" matchlog_*.txt 2>/dev/null | tail -8