Players reported scoring and K/D going screwy on 4.11.817. Cause: build 787
(#45/#134) retired the port's per-hit inflicted crediting as an "invention",
on the strength of a KB claim that the type-0 score handler was dead code.
That claim was wrong.
BTPlayer overrides Dispatch -- vtable @00513300 slot 3 = FUN_004bffa0 -- and
splits type 0 off BEFORE base dispatch:
if (msg->id == 0x16 && msg->type == 0) FUN_004c0200(...); // ScoreInflicted
else base dispatch;
@004c0200 names itself in its own Verify string
("BTPlayer::ScoreInflictedMessageHandler") and computes
CalcInflicted(basis) -> negate if target==self -> x (targetTonnage/ownTonnage)
-> accumulate into +0x278. ScoreMessageHandler's type-0 arm Verify-rejects
precisely BECAUSE this interceptor guarantees type 0 never reaches it.
The port had the handler, faithfully reconstructed, and no interceptor -- so
Block B's inflicted reports all landed in the rejecting arm and banked 0.
Per-hit damage credit was silently deleted.
Independently corroborated by the ORIGINAL MANUAL'S SCORING CHART (filed as
reference/manual/scoring_chart.webp, from Lynx): "+1 each damage point scored
on opponent's armor" and "-1 each self-inflicted point of armor damage" -- the
negate-if-target-is-self arm exactly. Without that chart the dead-code note
would probably have stood.
Verified (scratchpad/night13/scoreverify.sh, cross-node kill, 2 nodes):
type-0 Verify rejections 0 (was firing on every non-lethal hit)
inflicted score rows 83 awards 0.98..25.00, all positive, tracking damage
kill path un-regressed type=2 award=4.88 kills=1, victim respawn x1
KB: combat-damage.md report B and the score-model paragraph rewritten, with
the full chart and THREE unreconciled rows flagged [T4] -- flat +500 kill vs
the benched 4.88, +1000 at game start, and -1000 eject / -500 ammo (which
would live in ScenarioRole::specialCaseDeathPenalty @role+0x20, read by the
port but authored nowhere in shipped content).
KNOWN, NOT FIXED HERE: in MP the running total does not persist -- currentScore
is flushed to the operator console and ZEROED (btplayer.cpp ~1219) because the
binary treats it as a console DELTA. Restoring the credit makes that very
visible (bench: totals climb to ~35 then reset). Needs its own decision; the
chart's "+1000 starting the game" implies a persistent total lives somewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
92 lines
4.8 KiB
Python
92 lines
4.8 KiB
Python
"""Night 13: post the ghost/eject findings to #108 and settle #144. ASCII only."""
|
|
import sys
|
|
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
import gitea
|
|
|
|
gitea.comment(108, """Night 13 (2026-08-06, build **4.11.817**) -- eject-ghosts, and why the logs barely saw them.
|
|
|
|
## Field finding
|
|
|
|
Testers reported MANY ghost mechs, all from panic-button self-destructs, none respawning
|
|
properly -- and the operator has seen eject-respawn work every time before this night, so this is a
|
|
REGRESSION, not a gap (night 12 = build 774).
|
|
|
|
The logs show it happening, on three independent witnesses. Dave punch-ejected; on HIS machine the
|
|
whole death path ran (`DeathWithoutHonor`, `PUNCH-OUT`, death transition, wreck, explosion), he
|
|
respawned, took a drop-zone slot, and drove another 193 logged frames **in the same round**, which
|
|
then ended normally -- no crash, no disconnect. On BOTH peers his mech's history ends at the wreck
|
|
and never resumes; host 2 vanishes from santo's world for the remaining ~4300 lines of that round.
|
|
|
|
The discriminator is clean: the SAME mech's NORMAL deaths replicated their respawns 4-6 times in
|
|
that same session. Only the eject failed.
|
|
|
|
## Why the ghost detector said almost nothing
|
|
|
|
One `[ghost]` line all night, for an unrelated live mech. The gate is
|
|
|
|
&& !owner->WreckBuried() // "buried wrecks are expected-silent"
|
|
WreckBuried() { return collisionVolumeCount == 0; }
|
|
|
|
An eject-ghost IS a wreck that stopped receiving records, so the detector files it under
|
|
expected-silent. It can only ever catch a LIVE replicant going quiet -- structurally blind to this
|
|
failure.
|
|
|
|
## Why the census said "one" when testers saw many
|
|
|
|
Counting ghosts meant pairing the un-wreck receipt against
|
|
`[BTrender] wreck: 'thrdbr.bgf' missing -> gendbr.bgf fallback` -- a MISSING-ASSET warning, not a
|
|
death, which only prints for chassis whose wreck model is absent. Fixed in `4642129`: every
|
|
replicant entering the wreck state now emits an ungated
|
|
|
|
[wreck] replicant H:E entered wreck state (mode X->9) at (x,z)
|
|
|
|
symmetric with the existing un-wreck line, so the count is exactly (enters - un-wrecks) per entity.
|
|
Verified 2-node: 5 enters / 5 exits, exactly paired -- **while the old marker printed ZERO times in
|
|
the same run**. Five real deaths, invisible to what the census was reading. Treat the "one ghost on
|
|
night 13" number as a floor, not a count.
|
|
|
|
Census tooling: `scratchpad/night13/ghostcensus.py` (NB: filter the log owner's OWN mech -- an
|
|
owner's own wreck can never pair, since `un-wrecked` only logs for replicants; not doing so
|
|
manufactures false positives in every log).
|
|
|
|
## NOT reproduced by bench -- five rigs failed to trigger a punch-out at all
|
|
|
|
`BT_BTNTEST` never reached the mapper for the panic button (0x3D) or the searchlight (0x14): no
|
|
`[eject]`, no `PUNCH-OUT`, not even a lamp change -- so it is the SEAM, not the address.
|
|
`BT_EJECT_AT` (mech4.cpp:3310, the purpose-built hook) did not fire either. Benches are staged
|
|
(`ejectghost.sh`, `ejectlight.sh`, `ejectat.sh`) with their failure modes in the headers.
|
|
|
|
What the benches DID establish: **normal death replication is healthy on 817** -- 9 force-kill
|
|
deaths, 8 peer un-wrecks, 0 ghost lines. So an eject-only shortfall is the defect, not a broken rig.
|
|
|
|
## Open lead
|
|
|
|
The operator observed that the ghost mechs all had their SEARCHLIGHTS ON. Untested -- there is
|
|
currently no key or env that toggles the searchlight headlessly, so it needs a small bench hook.
|
|
Mechanically plausible: the searchlight is the only thing that attaches extra geometry into a peer's
|
|
mech render tree (btl4vid.cpp builds a `spot.bgf` cone as a `DPLStaticChildRenderable` parented to
|
|
the lamp's mount-segment renderable), and the death path does a wreck swap while respawn does
|
|
"rebuilt intact model (N segs restored, hulk dropped)" -- a child renderable held across those two
|
|
rebuilds is a lifetime hazard, and it would only bite mechs whose lamp was ON.
|
|
|
|
## Regression window
|
|
|
|
Commits 775-817. Two touch the death path: `91bd286` (787) rewrote the VehicleDead dispatch --
|
|
the message that drives the respawn cycle -- including the `killer == victim` eject case and the
|
|
filter that had blocked the panic-eject path; and `297127d` (784, death blast). Unverified.""")
|
|
|
|
gitea.close(144, """**NOT A BUG -- authentic. Closing.**
|
|
|
|
The eject sets `suppressConsole` (+0x258, `BTPlayerEjectBookkeeping`), and the #89 death-blast gate
|
|
reads it:
|
|
|
|
gates : owning player's advancedDamageOn (+0x264) AND
|
|
NOT suppressConsole (+0x258 -- eject sets it: punch-outs never blast)
|
|
[T1 @0x4a0aa8-0x4a0ad6]
|
|
|
|
So a mech that punches out does not splash its neighbourhood, by design, in the 1995 binary. SAURON's
|
|
observation ("splash worked on mechs that died close, but no splash on a panic / eject / suicide
|
|
death") is a correct reading of authentic behaviour.
|
|
|
|
Closing so nobody "fixes" it. Found while investigating the night-13 eject ghosts (#108).""")
|