Commit Graph
345 Commits
Author SHA1 Message Date
Joe DiPrimaandClaude Fable 5 b6656e35c5 KB: #124 twist-sign verified [T2] -- adapter flip correct, bench + diags recorded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:51:19 -05:00
Joe DiPrimaandClaude Fable 5 375fa28fef KB: #91 inside-view roster claim upgraded to T1 (FUN_004cef28 decomp anchor)
The binary's inside renderable build sets type A (=4, part_014.c:5077) and
its per-segment loop looks up EVERY segment's type-A mesh (FUN_00424084,
:5570) with no torso/cop filter -- the arcade drew all authored own-body
meshes (thor pod, owens legs). No opt-out exists in the binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 14:17:21 -05:00
Joe DiPrimaandClaude Fable 5 6180a44c64 #91 thor black rectangle: it's the OWN missile pod -- blakskn now material-keyed
Three testers reported a black rectangle swaying with the footsteps in the
thor cockpit (Summoner = the thr1 label, same THX canopy -- no isolation).
Decode: the inside view is a PER-MECH AUTHORED type-A set, not the cop
alone (no fallback in EntitySegment::GetVideoObjectName -- authored data):
madcat/vulture/bhk1 = cop; sunder/loki/avatar = +tor; thor = +tor +MSL
(the shoulder pod, the reported rectangle); owens = +both legs +tshd.
The pod/leg pilot-facing surfaces use the SAME "<pfx>skin:blakskn_dz_*"
interior-structure material as the canopy frame, but the unlit frame
constant was keyed on the _cop FILENAME -- identical material rendered
(0.13,0.12,0.15) on the canopy and pure (0,0,0) on the pod/legs
([matlog]: owx_cop blakskn vcol=FF211F26 vs owx_lule vcol=FF000000).

Fix (bgfload.cpp): the frame-constant treatment keys on meshIsCop OR
material contains "skin:blakskn_dz_". mechfx:blakskn_mtl (tshd shadow
quads) deliberately excluded. Verified: zero pure-black px in the lower
view band across walk captures, the pod plate renders frame-toned and
blends with the bar at rest (the reported anomaly dissolves), owens legs
read as coherent structure, canopy/terrain un-regressed.

Diags added: BT_MAT_LOG=<stem> per-batch material routing dump (bgfload),
BT_HIDE_INSIDE_SEG=<substr> inside-mesh hide (btl4vid), [view] per-segment
inside-roster names. KB: cockpit-view.md exactly-one claim corrected +
the #91 section; bench scratchpad/night11/thorrect.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 12:43:36 -05:00
Joe DiPrimaandClaude Fable 5 a728fa6de9 #118 alarm dig complete: the TechStatus bit model -- MarkGeneratorOut bridge removed
The gauge-alarm "condition" is a STATUS-FLAG BIT INDEX edge-scanned by
MechTech (bit 0/1 structure, 2 leak, 3 heat, 4 AmmoBurning, 5 Jammed,
6 !HasVoltage). Conditions 4/5 -> the engEject flash = the AMMO purge/unjam
invite (flashing the very key whose streamed function is EjectAmmo) --
never pilot eject; cond 6 -> the bus-switch invite; the PANIC lamp is the
sole pilot-eject indicator.

The destruction->stateAlarm(4) bridge is REMOVED as unfounded: alarms never
read stateAlarm; state 4 is the THERMAL BREAKER state produced by
GeneratorSimulation itself (byte-matched vs FUN_004b1f7c). And the binary's
crit distributor (@0049c9a8, read raw) touches nothing electrical -- a
generator destroyed in place keeps stale Ready voltage until any transition
recomputes output via (1 - damage) x rated. The port now matches that
subtlety exactly (verified: single-gen force-kill -> no bus invite, no arm,
silent unarmed keypad -- all authentic).

KB: the full bit table + invite semantics + electrical subtlety recorded in
decomp-reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 03:18:50 -05:00
Joe DiPrimaandClaude Fable 5 0535a7ab74 KB: gotcha #27 -- drawing against compositor invariants (the #118 MFD corruption class)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:06:55 -05:00
Joe DiPrimaandClaude Fable 5 8165cee9da #118: the eject option comes alive -- per-frame arm chain closed (FUN_004a9b5c+0x10)
The evaluator's "unexported caller" found by E8-scan: the Mech MASTER
PERFORMANCE (FUN_004a9b5c, in the 004a977x..004ab188 export hole) calls
EvaluateEjectPermission as its FIRST act every frame. Without that call the
port's ejectPermitted stayed at ctor-0 forever -- the panic-armed mode never
raised, so the PANIC lamp never lit and testers "never saw the option for
eject come alive". Restored at the top of Mech::PerformAndWatch (masters
only, like the binary).

Mislabel swept: mech+0x414 is ejectPermitted, NOT "missionReviewMode" -- the
mapper's @004d196c edge-watch arms PANIC mode 0x200000 from it (the real
review mode is the GLOBAL DAT_004fd550; btl4pb now reads that global, the
GetMissionReviewMode stub is retired, mapper member renamed
previousEjectPermitted).

Verified live: kill 4 generators -> "[eject] panic-arm mode ON" -> the
miniconsole Panic button lights dark->bright (pixel captures; the pad panel
shades it from the same PadRIO lamp state the pod's physical button uses) ->
BT_EJECT_AT press -> PUNCH-OUT -> respawn heals -> mode disarms -> healthy
presses REFUSED. BT_EJECT_LOG=1 logs the arm edges.

Note: no shipped gauge rides mode 0x200000 -- the 1995 eject indication is
the button lamp; the weapon-eng MFD "UNJAM/EJECT" is the ammo-jam indicator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 13:53:09 -05:00
Joe DiPrimaandClaude Fable 5 440cee1e4f #87 dig closure: host response is linear (raw decomp re-read); the pixel curve was div-card firmware
Re-read the material damage watcher RAW (@004573e4 ctor / @00457784
change-push): 16 floats snapshotted, damaged = pristine x 0.1 (literal at
the MakeMechRenderables call site, watching zone+0x158), linear lerp, 13
floats written back, then FUN_0048d4d4 -- which is "flush_material": a dpl
command-stream marshal (opcode 100) to the i860 division card. The
material->ramp->texel response curve was CARD firmware and is not in
BTL4OPT.EXE; logged as an open question (ask: firmware/DIV docs/period
screenshots).

Field reconciliation, measured live: one 25-pt hit (ERLG class) snaps a
77-pt arm 0 -> 0.3247 in a frame -- "a single missile leaves visible
armor damage" holds under the linear curve for heavy hits; 2-pt ERS creep
(the leg-audit weapon) is the invisible case.

Bench additions: BT_WALK_ZONES=<name-list> walker filter (concentrated
per-region audits, scratchpad/night10/legwalk.sh) + BindArmourDamage
ORPHAN inventory under BT_ARMOR_LOG (proved every drawn op is
zone-claimed on the full tree).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 13:10:00 -05:00
Joe DiPrimaandClaude Fable 5 fe48accb6b #87 root cause: the level-crossing zone-record send was missing -- observers never saw damage
The binary's effect watcher (FUN_0042aa2c) raises
ForceUpdate(DamageZoneUpdateModelFlag) when a zone's damageLevel CROSSES a
band-descriptor threshold (FUN_0042a5f4, master-gated on entity+0x28 & 0xc)
-- that send is how every other pod's replicant learns zone levels
mid-fight. The port's band hub kept only the graphic-state branch, so an
observer's copy sat at 0.0 until destruction: no enemy hull darkening, no
doll movement, ever (the night-10 "no armour discoloration" report).

Restored the level branch with the already-reconstructed DescriptorCrossed
(@0042a5f4) + the master gate on both branches (mesh swap still runs on
every instance). 2-node verified: A-side armour watcher pushed 114 level
changes; every replicant peak matched the master's finals to 4 decimals
(dtorso 0.9321 == 0.9321).

Render path cleared separately (gotcha #23 discharged): BT_ARMOR_FORCE 0/1
A/B captures prove the tint renders (hull -> charcoal at 1.0). Solo
perception is the authentic economy: 2-25 pt lasers vs 68-185 pt pools.

Bench: zonewalk.sh node A now carries BT_ARMOR_LOG + BT_SHOT_EVERY (the
standing zone-replication receipt) + digest_walk.py; armorpx.sh is the
force-pair pixel rig.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 12:24:36 -05:00
Joe DiPrimaandClaude Fable 5 cb5426cc7f KB: #124 close-out -- triangle pick + the authored one-hull-segment zone rule
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 11:32:26 -05:00
Joe DiPrimaandClaude Fable 5 16e75d6e1a #124 bench: the ZONE-WALK MATRIX -- systematic per-panel targeting, operator-watchable
The precision rig the operator specified: two nodes, target visible,
every zone aimed at deliberately through the REAL reticle/pick path.

* BT_SPIN_SELF=<deg/s> (target node): rotates the node's own viewpoint
  mech in place (the BT_SPAWN_AT write pattern per frame) so every
  aspect passes the shooter's boresight; the operator watches this
  node's paper doll take the hits.
* BT_ZONE_WALK=<secs/zone> (shooter node): walks the target's damage
  zones in order, resolves each zone's carrier segment's live world
  position (BTResolveSegmentWorld + the new BTMechZoneSegAndName
  bridge), SERVOS the torso twist + aim elevation until the centered
  reticle ray (BTGetAimRay) points at the segment, fires 3 laser
  pulses, advances. [walk] ZONE/FIRE/HOLD on A pairs with [dmghit]
  zone/level lines on B.
* scratchpad/night10/zonewalk.sh: launch both + relay, map sed'd to
  grass/day, NO kill timer -- the session stays up for observation.

Hard-won servo constraints (documented in test-harness.md so they are
never relearned): the ENGAGE GATE (ray live + range<150 + bearing<1.1;
outside it RELAX the twist -- servoing at the twist limit while the
goto marches is a limit-clamp fight that visibly shakes the mech, and
a 0.55 gate deadlocks against the goto's ~0.55 resting bearing); YAW
POLARITY -1 (the twist cell's angular sense is opposite atan2(x,-z)
world yaw -- operator-observed live, the SECOND witness for the #124
SelectSlice twist-sign flip) with a divergence watchdog that
self-flips; damped correction (gain .40, cap .025/frame -- the aim ray
lags the twist write a frame); and a one-shot approach PORT to 100u
off the target instead of a cross-map march.

First live run: walker cycles zones, settles, fires; the target's
luleg climbed 0.458->0.523 under its own aimed pulses -- aimed shot,
correct panel, damage consumed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 10:04:03 -05:00
Joe DiPrimaandClaude Fable 5 b0b7adc4ea #124 CORRECTION: the frame adapter is a Z-REFLECTION, not a pi rotation --
proven by the mech's own pods

The first fix (c5dfb00) negated x AND z. The operator challenged it:
'they couldn't hit rear panels AT ALL -- a 180 rotation couldn't explain
that' -- and a rotation would also have relocated rear damage, not
erased it, while the bench premise (the dummy faces its attacker) was
assumed, never measured.

The zero-premise probe (BT_ASPECT_TEST): four self-impacts at known
WORLD cardinals + one at each weapon's PHYSICAL MUZZLE position -- the
mech's own asymmetric geometry as the left/right anchor. Measured:

* WorldToLocal is a clean R(yaw), no hidden terms (yaws -30/150/40 deg
  all consistent);
* our frame: RIGHT = +X -- SAME as the 1995 ring (W0/W7 = Right*) --
  and FORWARD = -Z, flipped vs the ring's Front*-in-+Z-arc;
* under the pi rotation the pods CROSSED (left muzzle -> rtorso);
  under z-negation every anchor lands its own side.

So: negate local z only, and -- since a reflection reverses angular
direction -- the SelectSlice torso-twist term flips sign with it
(twist was 0 in all probes; twisted-torso verify tracked on #124).

Anchor acceptance (fresh exe, third yaw): LRM15_1 left pod -> ltorso,
LRM15_2 -> right wedges, left/right arm muzzles -> larm/rarm, nose ->
front cells, tail -> rear cells, flank cardinals -> rgun / left wedges.

The BT_ASPECT_TEST probe + BTWeaponMuzzleWorld bridge stay as bench
scalpels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 09:05:35 -05:00
Joe DiPrimaandClaude Fable 5 c5dfb002f3 #124: hit-location ASPECT was rotated 180 degrees -- the resolver now speaks the 1995 frame
Night-10's systematic targeting audit (rear panels unhittable from
direct fire, Owens left torso hitting the RIGHT weapon pod and vice
versa, missiles favoring the far side) measured down to one geometric
fact: the authored wedge ring puts the Front* cells in the +Z arc
(slice-name audit, bit-verified tables), i.e. the 1995 resolver frame
has mech-forward = +Z -- while OUR engine's entity frame carries
forward along -Z. Bench proof: a victim facing its attacker took
frontal beams at local z = -4..-10, theta ~270 deg, resolving the
REAR-named cells (zones 12/14/15) every time.

The two frames differ by a pi ROTATION about Y -- rotation, not
reflection, because the field flank observations CROSS (left->right)
rather than persist. ResolveHit now rotates the local impact into the
cylinder's frame (negate x and z; y untouched; handedness preserved so
the SelectSlice torso-twist add keeps its sign).

A/B (172 resolves, live missiles+beams on a facing victim): before
theta~4.7 -> rear zones only; after theta~1.42-1.59 -> utorso/dtorso/
ltorso/rtorso/arm -- the front-authored cells, with the per-burst
re-roll scatter intact.

NOT this bug: the "one band LOW" Y-axis half of the night-10 table --
that is #16 (pick-ray boresight parallax, code-confirmed, still open).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 01:12:32 -05:00
Joe DiPrimaandClaude Fable 5 1efe8efc68 #118 tails: death score cost + the DeathWithoutHonor console notice
* SpecialCaseDeathPenalty restored to the engine ScenarioRole (SCNROLE.h/
  .cpp) -- the 4.10-only role field the 2007 WinTesla source dropped
  (record slot [1] after KillBonus, fail-hard read; role+0x20; the
  DefaultRendererRate pattern). Its consumer decoded from the @004c05c4
  export gap (@004c07cd, missing from the #52 reconstruction): every
  death with advancedDamageOn hands the ENGINE base ScoreMessageHandler
  (@0042da20 == PLAYER.cpp:138, a DIRECT call bypassing the BT type
  Verifies) a type-1 award of -penalty. Wired verbatim. Shipped content
  authors the key nowhere -> cost 0 in the field. NUANCE [T4]: the 1995
  engine adds at Player+0x1c8, which no BT scoreboard reads (+0x278) --
  the pod's cost may never have displayed; our single-cell port shows it.

* The eject console notice: FUN_004c198c IS the
  ConsolePlayerMechDeathWithoutHonorMessage ctor (T0 BTCNSL.CPP survives
  and is compiled) -- "death without honor" is the punch-out record.
  BTPlayerEjectBookkeeping now sends it to the console host through the
  same ConsoleClientID path as the VTVDamaged notify, then latches
  suppressConsole: the eject notice REPLACES the death notify. Verified
  two-node: "notice -> console host 1" fires on the ejecting node; the
  relay-side delivery of client-5 traffic is a pre-existing question
  shared with the never-observed VTVDamaged sibling (operator-console
  workstream).

* Eject alarm-10 audio: resolved NEGATIVE -- the five Eject*.wav ship in
  AUDIO/ but BTL4.RES references them nowhere (orphaned like the SQUAT
  clips); wiring them would be a stand-in. Operator-optional deviation.

* RIO 0x38 panic control: deferred with the plumbing documented (the
  glass Flight-Controls window already renders the 0x38 bank; consumer =
  read buttonGroup[ButtonPanic] alongside the binding-engine edge) --
  pod-hardware-only, untestable on this bench.

Two-node verify (fresh-exe rerun after a stale-exe trap): REFUSED
healthy, PUNCH-OUT after generator kill, death + wreck swap + smoke +
un-wreck warp all replicated on the observer, respawned mech refuses
again, console notice fired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:12:48 -05:00
Joe DiPrimaandClaude Fable 5 e996be249d EJECT/PANIC wired: Mech::EjectPilot (id 0x19 @0049f854) + the crippled-mech gate
The button died twice before reaching game logic: no handler (0x19 was
unregistered) and no sender (the pod's panic was a control bit, not a
mappable). Both halves reconstructed from raw disasm -- the handler AND
its permission evaluator sat in export gaps.

@0049f854 EjectPilot: press-only; gated on ejectPermitted (@0x414) and
!IsDisabled; console eject notice (relay wire = tracked tail; the
suppressConsole@0x258 latch that prevents the death double-notify IS
wired); graphicAlarm -> 10, which kills via the >=9 predicate; then a
self TakeDamage {inflicting=SELF, zone -1, Explosive, amount =
ScenarioRole::killBonus}. Role layout byte-settled via the role reader
@00429bec dest offsets + the ctor record copy: +0x1c IS killBonus, +0x20
is the 4.10-only SpecialCaseDeathPenalty, +0x28 returnFromDeath (all
prior citations reconciled). KillBonus authors NOWHERE in shipped
content -> the charge is 0 and the ALARM does the killing: our bench
outcome is the pod outcome.

@0049fa1c EvaluateEjectPermission: eject only from a CRIPPLED mech --
bank coolant fraction < 0.05 | zero live generators | live weapons
below mech+0x448 (no exported writer: zero, clause inert) | leg-gimped
novice. A healthy mech REFUSES the button; no free resets.

Input: binding-engine "Eject" action -- Backspace / pad LeftThumb
(default profile + shipped CONTROLS.MAP). Bridges per the databinding
rule: weapon/generator/bank/player reads land in their complete TUs;
MechSubsystem gains the both-cells destroyed accessors (gotcha #22).
Bench scalpels: BT_EJECT_AT=<frame> (path-identical synthetic press),
BT_KILL_SUBSYS now takes a comma list.

Verified single-node: healthy press REFUSED (x2), four generators
killed, next press PUNCH-OUT -> death, wreck smoke, respawn; the
respawned mech refuses again (permission re-evaluates after Reset).
Death rides the normal damage/death chain, so MP replication is the
proven path. Tails tracked: console relay notice, RIO 0x38 panic
control, alarm-10 eject audio/canopy, SpecialCaseDeathPenalty consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 16:45:54 -05:00
Joe DiPrimaandClaude Fable 5 e1ae92264a #96: the sim TIME MODEL closed from bytes -- myomer kinetic term restored to the faithful 28 Hz
The open question under every heat bracket was what the 1995 sim does with
time. Answered at the byte level:

* FUN_0041c018 IS the arcade Simulation::PerformAndWatch: slice =
  (till.ticks - last.ticks) / DAT_0052140c, stored, Perform(slice) --
  VARIABLE-STEP in real SECONDS, line-for-line the WinTesla SIMULATE.cpp
  body. Every dt-carrying reconstruction is unit-faithful; no tick/second
  gap exists anywhere.
* The myomer heat term's ONE caller is the unexported MyomersSimulation
  body @0x4b8b9a (E8-scan + raw disasm; export gap): once per Perform, raw
  slice, gated measuredVoltage>0 only. The dt-less kinetic pulse therefore
  fires at the FRAME cadence -- 28 Hz nominal, sagging under load
  (emergent hardware, not a code constant).
* Mover velocity is u/s (dead-reckoner @0x421f7c: pos += vel x ticksD/28),
  so the v operand is unit-faithful too.

The 0.5 "loaded-rate" witness bracket is RETIRED -- it modeled the sag as
a constant, wrong in exactly the reported scenario (open-field cruising is
a light scene; the pod held ~28 there). kSeekHeatCalib = 1.0.

Measured at the faithful rate (madcat cruise benches): recommended gear
equilibrates ~1450 -- inside the authored degradation band as a SOFT 20%
output governor (heatFactor = 1 - d^2/range^2), weapons untouched at
~110-150, ZERO jams, zero bay fires from movement alone; supercharge
~1770 (deep governor -- the manual's own expert-mode warning). That is
the authored system: degradeT=1000 is governor onset, failT=2000 the
cliff, and the coolant-loop controls are the player's lever.

New leads filed in open-questions: the climb term's gravity operand
samples 0 (bridge points at a dead cell); the engine Mover's own gravity
subtraction is per-frame dt-less (port impact unread); gait-noise phantom
acceleration feeds the accel term.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 16:01:04 -05:00
Joe DiPrimaandClaude Fable 5 d3de5f94ee #87/#96: 'mechs die with lightly damaged armor' AUDITED -- no port bug; it's ammo cook-off
Byte-level audit of the entire damage chain vs the binary, all links exact:
zone stream reads (@0041df5c == engine DAMAGE.cpp), ctor normalization
(@0049ce50; constants 1.0/1e-4/0.5; shipped cells uniform 1/armorPts),
TakeDamage @0041e4e0, the BT override @0049c690 (vital/leg 1.0/0.5/0.0),
CriticalHit @0049ccc4 (0.5 fraction, 1.0 cap, authored-percentage cap),
cook-off chain (@004bc3fc per-salvo stamp / @004bd300 rounds x amount /
@004ac274 divide across the bin's plugged zones).

Measured kill route: 6 of 7 bench deaths were bay-fire detonations --
LRM15 bin 10 rounds x 50 = 500 into its ONE authored carrier zone
(content: madcat bins single-zone; 15 of 22 zones vital) -> one
application 0->1.0 -> instant death, armor bright everywhere else.
Field logs confirm dominance (Rajel 12 bay-fire events, ConnMan 10).
The mech that died conventionally ended 0.97/0.93/0.92/0.85 charred --
darkening accumulates fine when mechs live long enough.

Levers: the #96 heat economy (arms the bay fires) and purge training
(EJECT hold extinguishes -- wired, logged, manual-trained in the pod).

Also corrected a wrong KB address: FUN_0041e4e0 was labeled '.DZM res
compiler' in decomp-reference.md/rendering.md -- it is DamageZone::
TakeDamage (byte-matches DAMAGE.cpp:379); swept both files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 14:42:04 -05:00
Joe DiPrimaandClaude Fable 5 152249cb76 #95: the SCOREBOARD banked one missile per salvo -- credit the DELIVERED amount
Two consumers of one impact, only one was ever verified: the victim gets
TakeDamage{amount=per-missile, burstCount=cluster roll} and applies it
burstCount times (armor was always right); the score post on the next line
sent the bare per-missile amount. An LRM10 salvo dealing 7-35 armor banked
3.5 points -- Rajel's "~3 points to score", to the digit.

Ground truth: the binary's score is the victim handler's tally (amount once
per applied burst @0x4a04da + crit bonuses) reported to the INFLICTING
player (the id-0x16 tail, deferred #45). The shooter-side stand-in now
posts amount x burstCount -- the identical figure handed to the victim, at
the identical one-post-per-TakeDamage granularity. Same pass:

* splash never credited score at all -- the binary tallies every
  TakeDamage; now posted per splash victim (amount x falloff bursts);
* the bridge credited the LOCAL player for ANY registered hit -- AI-master
  fire on the player, a dying mech's death-blast splash; now refused
  unless the shooter IS the local vehicle (MP unaffected: only local fire
  carries live damage on a node);
* direct-fire unchanged -- beams author burstCount=1 (emitter.cpp:355).

Verified per the harness doctrine: single-node field composition (madcat,
real fire, real enemy) 22/22 impact credits paired at damage x burst, zero
bare 3.33 posts; two-node replicant-victim run BOTH directions 31/31
paired across three missile authorings (3.33/2.0/5.0 per-missile) + 25-pt
ballistics, leftovers all burst-1 beam amounts. test-harness.md gains the
map=grass note (MP.EGG authors cavern/night; GOTO mechs shoot rock).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 14:11:28 -05:00
Joe DiPrimaandClaude Opus 5 47d9a61402 hit-location cylinder: raw-bytes audit of the shipped tables + chassis->table map + wedge orientation
Audit of Cyd's hit-model artifact against THE BINARY (user mandate: not vs the
port).  res29_scan.py parses the type-29 streams straight from BTL4.RES bytes:
18 streams / 8 distinct confirmed; the 7-wedge cells (BLH band 6, VUL band 4)
and the all-fixed BLH/OWN twist patterns are authored fact; every artifact
spot-check cell matched the raw bytes verbatim.  Chassis->table resolved by
live-dump matching (MadCat and Avatar ride DIFFERENT same-zone-set tables).
Authored slice names fix the wedge orientation: FRONT = the +Z arc, W1|W2 seam
= dead ahead -- corrects the #92 comment's W6-frontal claim.  Collision type-0
divert re-grounded at 0x4a0361/0x4a036d: rams never touch the cylinder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 12:52:20 -05:00
Joe DiPrimaandClaude Opus 5 6dbeb3d266 wreck smoke dies WITH the wreck; the audio/effect clock is calibrated to the byte-proven 28
USER REPORT: "when you destroy a mech, it continues to smoke for a very long
time after the mech disappears... up to 30secs to a minute."

MEASURED (new [pfx] timed census with per-def particle attribution): the
death plume (psfx 1 DDTHSMK) was re-armed on a 10s cadence as a WORLD effect
(ownerTag=0) -- so neither the burial transition nor the respawn cleanup
(StopAllEntityEffects) could touch it.  The last-armed window kept emitting
over the empty spot for up to 10s after the hulk vanished, plus 6+-2s authored
particle lives, and back-to-back windows stretched the visible tail to the
reported range.  The re-arm itself is a marked PORT ADDITION [T3]; the binary
fired DDTHSMK once -- its 10s window + particle lives ended right around the
~17s hulk sink.  The authored design: the smoke dies WITH the burial.

FIX: the plume is spawned ATTACHED (BTStartPfxAttached, tagged to the dying
entity -- it also rides the sinking hulk now), and the burial transition calls
BTStopEntityPfx alongside the respawn cleanup that already did.  Verified over
a 175s run containing FOUR wreck events (the enemy kill + three player bay-
fire deaths): every plume's emission ends at its wreck's burial or respawn,
particles fade within ~8-11s, no orphan emitters remain.

THE COUPLING (user relay of Oracle: "prolonged smoke and explosion INCLUDING
SOUND... same lever?") -- CONFIRMED as a family: the binary's audio layer
times in FRAME COUNTS converted by the engine frame-rate global:
    AudioTime::Seconds_To_Frames == fmul [0x52140c]=28.0; fadd 0.5; round
    (@0x42c611, @0x42dd86 -- the 0x42xxxx cluster of the ~90 global consumers)
Our engine's DefaultRendererRate said 30: every audio duration, sequence
delay and compression window ran ~7% fast.  Calibrated to 28 [T1].  NOT yet
explained by this: a truly SUSTAINED/looping explosion sound (7% is not
"prolonged") -- the effect stop-path trace stays open on #114/#51.

Tooling: BTPfxParticle carries defIndex; [pfx] census (BT_PFX_LOG, 2s) prints
emitters + particles PER EFFECT SLOT with a timestamp; wrecksmoke.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:25:49 -05:00
Joe DiPrimaandClaude Opus 5 3109cfc49a #116: steam JOINERS were silently locked to VETERAN -- restore their experience selector
Oracle isolated it: "standard mode still having heat and leaks only occurred in
steam not in solo.  Sauron and Lynx did not have the issue but Conn Man, Rajel
and myself did."  The night-9 logs close the case:

    affected  (Oracle/ConnMan/Rajel):  [exp] experience=2 heatModelOn=1
    clean     (Sauron's std sessions): [exp] experience=1 heatModelOn=0

CAUSE: the glass front end's JOIN layout trimmed the menu to "the mech list +
the JOIN button; everything else is the operator's call" -- and the experience
selector's hidden default is 2 (veteran).  So HOSTS got whatever they picked
(Sauron hosted the lobbies all night -> his standard landed), while every
JOINER launched as veteran regardless of anything they did -- which is why
Oracle's cycling-the-settings experiment changed nothing, and why solo (full
menu) never showed the problem.  heat >= veteran, hence "standard but heating".

DESIGN CONFIRMED BY THE USER before fixing: experience is PER-PLAYER by the
original design -- the sysop set each user's tier and mixed-experience matches
were legal.  Architecture already supports it end to end: each node's master
player reads its OWN egg's experience (btMission+0xE4 -> BTPlayer @004c0bc8),
so per-node choice IS per-player choice.

FIX: the join layout gains the GroupExperience selector (the selection->egg
write path is the pre-existing host path, field-proven).  Also: the front end
now prints "[fe] pilot experience=<x> (join|host/solo)" at launch, so every
future field log answers this class of report without asking anyone.

VERIFICATION BOUNDARY, stated plainly: builds both configs; the write path is
shared with the host flow which the field already exercises; the visible
selector + the [fe] line need one join-mode launch to eyeball, and the field
re-test is Oracle running a standard steam match as a JOINER and seeing no
heat.

Tooling note recorded in test-harness.md: bash-heredoc python collapses one
backslash level even single-quoted -- a "\n" arrives as a real newline and
replaces silently no-op.  Build backslashes from bytes([92]); verify replaces
by length delta, not by the script saying "fixed".  (This burned four edit
rounds tonight and several earlier C2001 hunts.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 10:40:51 -05:00
Joe DiPrimaandClaude Opus 5 1bcee1f9eb #96: the effective-rate theory is corroborated -- upgrade the calibration provenance T3 -> T2
Frame-rate testimony from the morning follow-up, now on record beside the bytes:
  * Oracle, asked if the pod frame rate was rock solid: "I think I could do as
    well with a flipbook"; heavy fights "would turn to a slide show".
  * Lynx (original era): "RP had a target frame rate of 30FPS.  It wasn't hard
    coded or locked.  BT never had a locked frame rate in 1st release
    testing... coming from 3.0, where 20FPS was a miracle."
  * The engine pacer's own design agrees: fixed budget + background fill + NO
    overrun catch-up (APPMGR.cpp, T0) = "target 28 but allowed to slip", with
    the 18.2065 BIOS fallback anticipating degraded timing.

Also settles epilectrik's open confirmation ("frame rate is advancing at least
some of the subsystems -- i have to fully confirm that"): ALL of them --
UpdateManager::Execute runs every interesting master every frame,
Entity::PerformAndWatch runs every executable subsystem per entity tick, no
throttle at any layer.  The crisp rule: CADENCE is frame-linked for
everything; only terms missing a dt multiply are rate-SENSITIVE.

Corollary worth keeping: a choking 486 generated LESS heat/s exactly during
the biggest fights -- the slideshow was mercy.  Veteran memory of seek-4
endurance encodes the LOADED rate, which is why bracketing by their consensus
is the correct instrument rather than a concession.

Follow-up filed in the KB: sweep the reconstruction for OTHER dt-less per-tick
accumulators (known: particle trail density, rendering.md; the 15-sample
heatFilter window is rate-dependent smoothing, benign).  Each found one should
normalize against the same 28-nominal/bracketed-effective pair.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 10:20:52 -05:00
Joe DiPrimaandClaude Opus 5 c28555bdc9 #96: TRACE THE CLOCK ORIGIN -- the pod ran at 28 fps (byte-proven); myomer heat re-bracketed to 14 Hz effective
epilectrik: "the issue seems to be related to a tick rate that advances the
heat accumulation... i need to trace out what the clock origin was."  Traced.

THE CLOCK ORIGIN [T1]: the DOS binary keeps its engine frame rate in the
global DAT_0052140c, set once at startup:
    0x401ace:  mov [0x52140c], 0x41E00000   = 28.0f     (nominal)
    0x401ada:  mov [0x52140c], 0x4191A6E0   = 18.2065f  (BIOS-tick fallback)
The DOS main pushes it straight into the ApplicationManager ctor (0x401189:
push [0x52140c]), and ~90 sites across the image fmul/fdiv by it -- it is THE
per-frame<->per-second conversion scalar of the whole 1995 engine.  The engine
executes every interesting master every frame and every executable subsystem
every entity tick (UPDATE.cpp / ENTITY.cpp, T0 -- no throttle at any layer),
so subsystem Performance cadence == this rate: the pod's myomer tick was
NOMINALLY 28 Hz.  My previous 30 Hz reference (the i860 BOARD frame) was
wrong by 7%, not the 2x the veterans hear.

THE RESIDUAL IS EFFECTIVE RATE, NOT NOMINAL: Oracle, on the halved build,
still asks "If you could please halve the myomer heat rate again.  We will
bracket to something reasonable."  A 486 host missing beats halves the real
Performance cadence without changing any constant -- and the 18.2 fallback
existing at all says slow paths were expected.  Statics cannot settle the
in-pod effective rate, so his bracket is the right instrument:

    effective default = 28 [T1 nominal] x 0.5 [T3 testimony calib] = 14 Hz
    BT_MYO_HZ=<hz> still overrides absolutely for bracketing
    the resolved rate now logs under BT_MYO_LOG, tier tags inline

Measured (Owens, seek 4, flat out): peak T 1007 -> ~640, degradation never
reached -- the halving Oracle requested, delivered as one auditable knob
instead of a silent constant edit.

KB: decomp-reference.md gains the DAT_0052140c section (~90 consumer sites --
whenever the decomp shows an unexplained mul/div by _DAT_0052140c it is
per-second<->per-frame at 28); btl4main.cpp's "30 = pod authentic" pacer note
corrected (30 kept deliberately for display smoothness, the divergence now
documented).  Candidate follow-up recorded: the frame-rate-dependent particle
trail density (rendering.md) should normalize against the same 28, which may
bear on Ronin's smoke-density report (#114).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:39:00 -05:00
Joe DiPrimaandClaude Opus 5 c791fae0f8 KB: new topic test-harness.md -- the bench machinery + the verification doctrine
User mandate (2026-08-02): "do tests like this from now on" -- after the #110
grind bench, where the field composition (peer fire destroying an arm over the
wire) replaced the constructed proxy that had let #86 be called fixed while
players kept hitting it.

The topic carries two halves on purpose:

DOCTRINE -- what counts as VERIFIED:
  1. reproduce the REPORTED scenario, not a convenient adjacent one
  2. scalpel hooks (BT_SELF_DAMAGE_ZONE / BT_KILL_SUBSYS / BT_FORCE_*) locate
     defects; they support a "fixed" claim only with proven path-identity to
     the field composition -- and the field composition still gets run
  3. MP symptom -> two-node proof (master-side correctness says nothing about
     what a peer sees)
  4. visual symptom -> pixel proof (gotcha 23)
  5. coverage claims need the axis enumerated and measured (all gates, all
     chassis), because per-chassis behaviour lives in authored data
  6. an unexplained extra effect in a passing run means the run has not passed

MACHINERY -- the bench_common.sh contract (summarized, file = source of truth),
single-node and two-node skeletons (relay, ports, affinity, fire cadence,
GOTO_STOP standoff), process hygiene (stale-node taskkill first, never
double-background, teardown kill order, stale-exe tells), and log-reading rules
(capped diagnostics are not evidence of absence; alarm lines are not trends;
name the actor at every refusal; field logs have no gates set -- spawn-time
summaries ungated, per-frame traces gated).

Routed: Quick Lookup row, CLAUDE.md reasoning step 4, build-and-run parity
section, reconstruction-method Key Relationships.  checkctx CLEAN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:17:01 -05:00
Joe DiPrimaandClaude Opus 5 06adaee523 #110: the zone destruction cascade is LIVE -- an arm now takes its gun pod with it
Lynx: "You can destroy an upper arm, and the gun pod is intact and can still
fire."  Conn Man's audit: three chassis, both arms, reproducible.  And the user
is right that this family was claimed fixed before: #86 gated fire on the
WEAPON's own destruction and was verified by destroying the weapon directly
(BT_KILL_SUBSYS) -- the field scenario was the ARM ZONE dying with the weapon
subsystem healthy, which that bench never reproduced.  Oracle's 693-night "I can
still fire destroyed energy weapons" was almost certainly this mechanism.

ROOT CAUSE -- a silent stub.  Mech__DamageZone::RecurseSegmentTable @0049cad4
was transcribed faithfully... against three local shim types whose iterators
unconditionally return NULL:
    struct SegmentIterator { DZRef *Next() { return 0; } };
    struct SegTableX       { SegmentRecord *operator[](int) { return 0; } };
The cascade fired on zone death (bench: 72 [cascade] lines), "descended", and
touched nothing.  Every line read correct; none of it did anything.

THE REAL WALK (decomp re-read, FUN_0049cad4): iterate the mech's segment table
(mech+0x300, vtbl+0x34 GetNth) to this zone's segment, then
    destroySiblingsOnDestruction (Wword 0x68): recurse every other zone on the
        SAME segment (seg+0xD0), setting graphic state 2 (Gone)
    descendOnDestruction (Wword 0x67): recurse every zone on every CHILD
        segment (seg+0xE8 -> child+0xD0) -- the arm takes the gun
The engine ALREADY HAS both structures: EntitySegment::damageZoneTable @0xD0 /
childIndexTable @0xE8 (TableOf is 0x18 -- byte-identical to the decomp offsets),
populated at stream time by JMOVER.cpp:373 [T0].  The stub was never necessary.
Implemented with the engine iterators via two new read accessors on
EntitySegment (SEGMENT.h); the binary's asymmetry is reproduced as-is (the
sibling loop checks graphic state != 1, the child loop does not; re-hits on a
1.0 zone re-run the cascade -- idempotent in effect).

VERIFIED (ava1, self-damage to one zone, autofire everything):
  dz_rarm -> [cascade] zone 9 -> zone 17; AFC100 + AmmoBinAFC100 + Condenser6
      ForceCriticalFailure'd; "[ammo] AFC100 -> NoAmmo (gate1): destroyed=1";
      torso LRM5/SRM2 keep firing (correct)
  dz_larm -> [cascade] zone 2 -> zone 6; PPC + Condenser4 force-failed;
      "[emitter] 'PPC' fire REFUSED (destroyed=1)" x4998 -- the emitter gate now
      logs refusals BY NAME (the FIRED line never named its weapon, which is how
      #86's verification gap survived)
  regression smoke: frame time 7.13/7.20ms, 0 asserts, 0 cascades in ordinary
      combat (they fire only on genuine zone deaths)

Authored data (now dumped under BT_DMG_LOG): descend=1 on exactly the four arm
zones (ava1: 2/6/9/17), destroySibs=0 everywhere -- so legs/torso behavior is
untouched by this change.

KB: combat-damage.md cascade section; reconstruction-gotchas #26 (the
silent-stub trap: a shim that returns EMPTY converts a reconstruction into
fiction that reads correct -- shims must Fail() loudly or log their emptiness).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 08:26:53 -05:00
Joe DiPrimaandClaude Opus 5 da70bd58b2 #84: the replicant salvo now HOMES AT THE LIVE TARGET -- the stale-point double detonation is gone
Oracle (674): "missile appear to register hit explosions twice, once where
target was and again where the target is".  Rajel (693): "once where the mech
was when I fired and on the expected impact".

MECHANISM, measured on two nodes (BT_PROJ_LOG "DET at" now logs EVERY contact
detonation, including damage-0 rounds, which were invisible before): the
replicant salvo mirror pushed rounds with NO entity handle at the FROZEN
fire-time aim point, and the pool's contact test is proximity to p.targetPos --
so mirror rounds "contacted" the empty air where the target USED to be and
detonated there, on every peer, every salvo (45 salvos -> 234 frozen-point
air bursts in a 200s bench).  The true impact appears at the live position
(the shooter's own homing rounds on his node; the victim's damage reaction on
peers) -- two sites per salvo, exactly as reported.

THE BINARY'S MODEL (CLASSMAP, Missile flight-entity cluster): missiles are
ENTITIES with authoritative/GHOST Performance variants (Projectile
PTR_LAB_005129e8 / 005129f4) and their own WriteUpdateRecord (@4bef4c, slot 7)
-- peers ran ghost missiles updated from the wire and saw the TRUE trajectory.
One explosion, correct place, every node.  The frozen-point mirror is the port
infidelity (the entity Missile itself stays blocked by the documented 2007
Entity-base mismatch; mislanch.cpp:301).

FIX: the MissileLauncher update record now carries the locked target's EntityID
(salvoTargetID; EntityID::Null = point fire).  The mirror resolves it on the
receiving node via HostManager::GetEntityPointer (engine T0, an index-socket
Find -- NULL-safe on any ID), refuses non-mechs via BTIsRegisteredMech, and
pushes the mirror rounds WITH the handle -- they re-lead on the live local
replicant exactly as the master's own rounds do, and detonate at the true
position.  Guards: record-length gate (a short record from an older build has
no ID field -- its tail would be garbage and must not resolve) + Null check +
registered-mech check.

VERIFIED (two nodes, 200s, missile autofire both ways):
  * every record arrives len=52/52 with sane aims ([mlrec] diag)
  * 22/46 salvos resolve (tgtID=3:22 -> the victim's local master) and their
    mirror rounds home: the frozen-air DET population fell 234 -> 102, the
    live-homing population rose to 155/207 per node
  * the unresolved remainder is BENIGN and correct: EntityID::Null point fire
    plus hostID=-1 LOCAL entities -- i.e. the victim's WRECK, which autofire
    keeps shooting after the kill; a wreck does not move, so the frozen aim IS
    its true position
Downstream expectations: Ronin's smoke-screen (#114) should drop by ~the mirror
half, and the audio census (#32) loses the duplicate Static3DPatchSource
explosion sounds on peers -- both to be read from the next field logs.

Also in this commit: [projectile] DET diagnostic (every contact detonation with
position/damage/handle/aim), [mlrec] record-arrival diagnostic (len/recID/
subsys/counter/aim, capped 200), scratchpad/night9/mp_double.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 02:29:58 -05:00
Joe DiPrimaandClaude Opus 5 3354db4bd1 #32: RETRACT the retention diagnosis; count what matters (steals + true drops); name the saturating class
The reopen said "the pool fills and never returns a source".  The 30s census in
the SAME field logs disproves it: free returns to ~227-230 between bursts and
reuses climbs ~20/s all session.  free=0 on the failure line is true by
DEFINITION at the instant of a failed acquire -- the third instance of the
counter-sampling trap (live=256-vs-6, then hsparm greps, now this), read off the
alarm line instead of the trend line.

WHAT THE LOGS ACTUALLY SHOW
  * The pool cycles; release-on-stop exists and works (the engine steal loop).
  * During firefights CONCURRENT demand exceeds 240 and the priority steal loop
    services each new sound by killing an old one -- continuously through
    combat (failures spread across every decile of every combat session).
  * Idle standing demand is ~13 sources.  My "each component holds its SourceSet
    to entity teardown, ~20-25 per mech" narrative was wrong.
  * The raw ACQUIRE FAILED line count (6.8k-19k per log) is NOISE: the steal
    loop retries after every failed attempt, so lines accumulate per EVENT and
    most events still play via a steal.  True drops were never counted.

CHANGES
  1. Census now carries steals= and drops= (drops = the steal loop ran dry and
     the sound NEVER played) plus a per-class drop histogram
     ("[audio] dropped by class: {class 1005 x4v: N} ...") -- all ungated, so
     the next field logs are decisive instead of suggestive.
  2. The ACQUIRE FAILED print is rate-limited to 1/30s and now names the
     requesting class + voice count.  19k-line log spam distorted this triage.
  3. BT_AUDIO_SOURCES=<n> now raises the POOL cap too (it previously raised the
     AL context budget while the pool stayed at 240, making the field
     experiment impossible to run).

MEASURED (9 mechs, missile autofire, 150s)
  * cap 240: peak 130 sources, 0 fails -- demand tracks SHOOTER count, not mech
    count; one shooter cannot saturate.  A 6-shooter lobby pins 240.
  * cap 48 (BT_AUDIO_SOURCES=64): saturation reproduced -- census
    steals=223 drops=455, histogram names the classes.
  * Dominant field requester (requested=4) = class 1005 Static3DPatchSource:
    world-placed effect sounds, i.e. EXPLOSIONS.  1001 DirectPatchSource x1v
    dominates drops at low cap; 1002 Dynamic3DPatchSource x3v present.

CONSEQUENCE FOR THE FIX ORDER: #84's stale-aim double detonation duplicates
exactly the saturating class on observer nodes.  Fix #84 FIRST, then re-read the
field census; only if it still saturates does the budget experiment
(BT_AUDIO_SOURCES with frame time measured) become the play.

KB: the wrong night-9 entry in open-questions.md replaced with the corrected
diagnosis; gotcha candidate noted -- an alarm-line counter is not a trend.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 01:42:10 -05:00
Joe DiPrimaandClaude Opus 5 202ebbf84a KB + tracker: night-9 playtest (build 4.11.693)
CONFIRMED FIXED and closed: #95 (missile salvo damage), #97 (leak gauge 3 levels),
#98 (loop-6 lamp), #99 (leak alarm audible).

#95 deserves a note: a tester reported "LRMs still doing 3.3 damage", but that is
the PER-MISSILE figure.  382 impacts across three 693 logs show the cluster being
delivered correctly -- LRM20 3.25 x burst 5..20 (total up to 65), LRM15 3.33 x
3..15, LRM10 3.50 x 2..10 -- and those burst ranges match the binary's
Random(n)+n/4 roll exactly.

#32 REOPENED.  The pooling change removed the alloc/free churn (the ~10% frame
time is real) but every 693 field log still shows
    ACQUIRE FAILED (requested=4 live=240 pooled=240 free=0 ...)
19,182 fails (Sauron) / 15,333 (Rajel) / 6,818 (Ronin), high-water 225 of 240,
starting ~1% into every session.  `free=0` is the diagnosis: sources are never
returned.  This is precisely the risk flagged when the fix landed -- retention,
not pool size.  Players called audio "fine" the same night; the log disagrees.

#84 still present, but the surviving explosion is a DIFFERENT mechanism from the
bundled one that was fixed: "where the mech was when I fired" is the stale
fire-time aim point already recorded as an open in rendering.md, i.e. an MP
visual, not the message-manager path.  Needs a two-node bench logging the
OBSERVER -- the earlier two-node run verified damage delivery but never the visual.

New: #108 ghost mech (peer never sees the death; shots still register, so it is a
visual/replication desync, the inverse of #94), #109 panic/eject, #110 arm
parent/child linkage (gun pod survives its mount -- three chassis, both arms;
parentArtifactZone exists but only drives LOD), #111 death warp bubble shown to
peers, #112 AFC shotgun spread (may be authentic Ultra-AFC burst), #113 LRM burn
duration, #114 missile smoke as a smoke screen, #115 hot box, #116 Standard-vs-
Expert heat gating, #117 per-chassis cockpit bounce.

⚠ A MEMBER OF THE ORIGINAL TEAM IS NOW TESTING -- "Ronin" (log user torak).  He
worked on the 1995 game and has played the pods recently.  Treat his pod
comparisons as primary evidence above reconstruction inference.  First two
contributions recorded in the KB: per-chassis cockpit bounce was deliberate, and
NARC likely shipped as data without an implementation.

⚠ Conn Man ran the OLD build (.674) for all seven of his sessions, so his reports
are not evidence about 693.  His arm-linkage audit is still valuable -- nothing in
693 touched that path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 01:15:45 -05:00
Joe DiPrimaandClaude Opus 5 8daac37e40 #99: VERIFY IN THE IMAGE -- the shared-alarm Stop is authentic; do not diverge
Answering "did you check the decomp or disassembly?" -- I had not.  I had checked
the authored data (BTL4.RES) and the MUNGA engine source and called it authentic
on source lineage alone.  Now verified in BTL4OPT.EXE:

  same sources compiled in : d:\tesla_bt\munga\AUDSEQ.CPP / AUDWTHR.CPP strings
  object factory           : @00466184  case 0x4b -> alloc 0x5C -> ctor @0043ccec
  vtable                   : 0x4ead48
  ReceiveControl           : @0043d3a4 -- switch on control IDs 1 / 2 / 9 / 12,
                             which is exactly our enum Start / Stop / Idle / Tempo
  Stop case                : @0043d3ca -> unconditional call StopSequence @0043d2d4
  StopSequence             : tests only its OWN isRunning (+0x28) then the Chase
                             loop -- NO refcount, NO awareness of other leak sources

Structurally identical to engine/MUNGA/AUDSEQ.cpp.  So the arcade behaved the same
way: with 19 subsystems sharing one alarm sequence, any one of them clearing its
leak silences the alarm while the others still leak.  AUTHENTIC -- do not "fix".

(The tempo bound check could not be used as an anchor: Verify() compiles out of the
retail build, so the AUDSEQ.CPP assert strings are present in .data but unreferenced
by code and invisible to the decomp export -- gotcha 21 again.  The object-factory
route via the numeric ClassID is what actually located the class.)

Recorded in context/gauges-hud.md with the full chain and the address table, so the
next person does not re-chase it or "fix" it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 18:32:33 -05:00
Joe DiPrimaandClaude Opus 5 c23d06cbb5 KB: correct the burstCount claim -- it is honoured by the CALLER, not cosmetic
The KB asserted as [T1] that "burstCount is cosmetic for zone damage".  Half of
that is byte-verified (DamageZone::TakeDamage @0041e4e0 really does ignore it);
the other half was an inference that inherited the [T1] tag and then justified
task #62's salvo-lead design -- which silently divided every missile salvo by its
missile count (gitea #95).

Corrected in both places it appeared (combat-damage.md:334 and :884): the CALLER,
Mech::TakeDamageMessageHandler @0x4a0423-0x4a04d8, applies TakeDamage burstCount
times and re-rolls the struck zone per burst.  burstCount = number of
applications; load-bearing for missile cluster count, splash falloff and the gyro
bounce.  Also records that the arcade Missile dispatches DIRECTLY at the victim
rather than through the message manager (whose consolidation drops burstCount).

New gotcha 24: a verified fact, over-generalised, becomes a wrong design premise.
Detection smell -- a "cosmetic/unused" claim about a field other code still
computes carefully.  Nobody spends instructions randomising a decorative value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 04:43:34 -05:00
Joe DiPrimaandClaude Opus 5 839b040619 KB: night-8 playtest intelligence (build 4.11.674)
Player testimony worth keeping out of the issue tracker alone:

* SENSOR PANEL model, from VGL Lynx -- the last major system without one.
  Sensor damage flickers the HUD (corroborated by period footage), degrades
  radar/map, affects the searchlight, and possibly the hot box.  Gitea #105.
* COOLANT LEAK severity is a THREE-level display (1/2/3 triangles + alarm);
  we only ever produce the lowest.  Our leak rate is a continuous scalar, so
  either the banding is missing or the magnitude never leaves the bottom band.
* MYOMER heat calibration is CHASSIS-RELATIVE: a Thor sustains seek 4
  indefinitely, a chicken-walker cooks in under a minute.  That spread is the
  acceptance test -- a uniform "too hot" indicts the drive-heat coefficient
  rather than the sink capacities.
* RAM damage: the binary's internal-only collision divert MATCHES what players
  observe (gyro/myomer damage, coolant leak, NO armor damage, grinding inert).
  May be working as intended; awaiting a player ruling.  Gitea #103.
* MP stability: 5-player match, no stalls or lag spikes -- bounds how often the
  carried cross-fire-stall / silent-node-crash opens occur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:09:56 -05:00
Joe DiPrima 137c151951 #87: mech armour PANELS now darken with damage -- the .DZM material system was loaded and unused
Players: "the actual enemy mech in external view is not showing darkened armor
panels".  We swapped destroyed LIMB meshes but never darkened a panel.

The 1995 game darkens armour through the MATERIALS.  MakeMechRenderables
(FUN_004cef28) built, per (damage zone, material), a watcher
FUN_004573e4(material, &zone->damageLevel, 0.1f) that snapshotted the materials
2026-07-31 19:52:55 -05:00
Joe DiPrimaandClaude Fable 5 5410371b0c #86: destroyed weapons can no longer fire -- the fire gates read a
never-written cell (the split-cell gotcha)

Both weapon fire gates (ProjectileWeapon gate 1 @4bbd36, Emitter
hard-failure @4baab9) test the binary's subsystem+0x40 for Destroyed(1).
In the 1995 layout that offset sits INSIDE the embedded status alarm
(statusAlarm@0x2C + level@+0x14 = 0x40) -- ONE cell, written by
ForceCriticalFailure when a zone's crit cascade kills the subsystem.  The
port models the same address as TWO members: the AlarmIndicator AND a
plain int simulationState@0x40.  Every destruction path writes the ALARM
(so the MFD draws its X correctly) while the gates read the plain int,
which nothing ever writes -- so a weapon on a blown-off arm showed
destroyed on every panel and kept firing and scoring, locally and on
peers (night-7: all three testers, screenshots of a missile leaving a
destroyed pod).

The tell had been sitting in our own logs for weeks:
  [ammo] SRM6_1 -> NoAmmo (gate1): destroyed=0 ...
on a mech whose launcher was X'd out.

Fix: both gates now read statusAlarm.GetLevel()==1 as well as the int.
Also added: the crit-cascade log names the destroyed subsystem, a
BT_SELF_DAMAGE_ZONE=dz_* named-zone bench mode, and BT_KILL_SUBSYS=<name>
(force ForceCriticalFailure on one named subsystem -- the exact call the
zone cascade makes, so a bench can ask 'the panel says dead, does it
still shoot?' without hunting for the zone that carries a given weapon).

Verified A/B in ONE run: before the kill both SRM6 launchers fired 2
salvos each; after, the destroyed launcher fired ZERO (gate log
destroyed=1) while its twin kept firing normally.

KB: new gotcha #22 (the SPLIT CELL -- one binary offset, two port
members, only one written; sibling of #1) + combat-damage entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 12:11:09 -05:00
Joe DiPrimaandClaude Fable 5 905a4a86c0 RETRACTION + FIX: the myomer seek->speed coupling IS in the 4.10 binary --
restored byte-grounded; the morning's removal was export-gap blindness

Player testimony (Oracle, pod veteran: seek-4 humanoid = 182 kph -- the
manual's printed Super Charged figure; overheat = 'freezing up') forced a
re-audit of the morning's 'no dynamic myomer->speed feed' verdict.  The
verdict was WRONG: a raw-image capstone sweep for FPU reads of [reg+0x31C]
found the consumer inside the UN-EXPORTED master-perf region -- the same
decomp gap that hid the #93 crash block.  Decoded @0x4a9cf2-0x4a9da4 [T1]:

  per frame: MAX speedEffect@0x31C over the myomer chain (+0x7AC)
             -> mech+0x79C
             mapper->speedDemand@0x128 *= it        (the SPEED coupling)
             if |drive| <= 1e-4 (@0x4ab16c):
                 mapper->turnDemand@0x12C = 0       (the FREEZE)

Everything the manual + the veterans describe emerges: gear 4 rides
speedEffect ~1.43 into the RegisterMaxOutput gait cap (base x 1.4284 --
the two mechanisms interlock: 61.5 x 1.43 = 87.9 vs cap 87.8 measured) =
SUPERCHARGE; myomer heatFactor degradation slows you; overheat freezes
speed AND turn; damage slows (speedEffect carries 1-damage -- #75's
original premise was RIGHT and its 'falsified' close was wrong).

Restored in mechmppr.cpp, byte-grounded and better than the pre-removal
draft: MAX over the chain (was first-found), NO 1.0 clamp (the old clamp
ate the overdrive), turn-freeze included.  BTMyomersSpeedEffectOf
(unclamped) replaces the deleted BTMyomersDriveOf.

Verified live: healthy drive=1.0 (no speed change), collision-rattled
myomers drive 0.999 -> 0.892 -> 0.742 with demand scaling in lockstep
(61.5 -> 54.9 -> 45.7).

KB corrected in place (subsystems WAVE 6, pod-hardware manual audit
retraction, open-questions resolved) + NEW GOTCHA #21: export-gap
blindness -- 'no callers in the decomp' is not 'no callers in the
binary'; sweep the raw image for the operand pattern before declaring a
data path dead, and when a primary source disagrees with your decomp
sweep, suspect your evidence first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 10:54:11 -05:00
Joe DiPrimaandClaude Fable 5 4704ab3d41 #84: missiles ACCELERATE like the binary -- the pool flew at constant
rack-eject speed; the authored MissileThruster was never applied

The binary Missile hosts a MissileThruster subsystem: authored
acceleration for BurnTime seconds after the MuzzleVelocity eject.
Authored values decoded from BTL4.RES (type-15 missile model records,
reached from the AmmoBin's ammoModelFile via the type-1 MODELLIST
indirection): SRM 2.5s @ 600 u/s^2, LRM 10s @ 300 (climb 50), Streak
3s @ 300 (turn 360deg/s), NARC 10s @ 300; splash 30 all (port constant
was already right); authored drag ~0.001 = negligible.

The port pool flew every round at CONSTANT |MuzzleVelocity| -- SRMs 100
u/s, LRMs 30 u/s (10x slower than authored) -- the entirety of the
night-7 "missiles are very slow" report, and the driver of the
"explosion before the missiles arrive" perception (the salvo-lead
detonates while the slow spread rounds straggle in).

Fix: BTMissileThrustOf (mech4.cpp) lazily parses + caches the RES
thruster table (ModelList ids aliased to their type-15 member's
burn/accel); both launch paths (master FireWeapon + the replicant salvo
mirror -- peers see the same speed) resolve through the launcher's bin
and pass burn/accel into the pool; the advance integrates speed +=
accel*dt while burn remains, heading preserved.  Ballistic rounds
(autocannon/gauss) pass 0/0 -- unchanged.  Impact log now prints
v=/burnLeft= evidence.

Verified live (solo, BT_SPAWN_ENEMY + BT_AF_MISSILE): thrust table 8
entries cached; Black Hawk Streak resolve burn=3 accel=300; impacts at
v=277 u/s (was a constant 100).  Fun authenticity note: the Black
Hawk's "SRM6" fires strk (STREAK) ammo per its authored bin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 10:41:39 -05:00
Joe DiPrimaandClaude Fable 5 ba4af90d6b phase-13 log: night-7 session research state (collision economy, myomer system complete, coolant leaks, the seek 4.0-vs-4.10 drift) + open-questions entry for the seek player version-probe
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 09:55:42 -05:00
Joe DiPrimaandClaude Fable 5 c18929b23a KB: manual cross-check -- myomer seek 'Super Charge' top speed is 4.0-manual behavior DROPPED by the 4.10 binary (stat sheets: Normal 143 / Super Charged 182 / Gimped 40 kph; printed ratio 1.27 vs 4.10's vestigial gear ratio 1.43)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 09:34:42 -05:00
Joe DiPrimaandClaude Fable 5 58f49ed942 KB: the seek dial's real stake is TORSO TWIST (PowerWatcher zeroes twist while myomers not Ready); generator has no load model -- gears 1-3 identical on a healthy mech
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 09:25:54 -05:00
Joe DiPrimaandClaude Fable 5 cf8618bcb9 myomer system COMPLETE -- the 'WAVE 6 mover cutover' was a phantom built on
a misattribution; the authentic couplings are all live

THE CORRECTION [T1]: @004b9550/@004b95b8 ('ConnectToMover'/'Disconnect
FromMover') are MechWeapon::ConfigureMappables/ChooseButton -- their +0x31C
is the weapon TriggerState and **(mech+0x128) is the CONTROLS MAPPER's
button roster (the MECHWEAP.CPP assert string + the trigger-edge detector
@004b9608 sit adjacent in the image).  The binary has NO dynamic myomer->
speed feed: AvailableOutput has exactly TWO callers in the whole image --
RegisterMaxOutput (assembly) and the SeekVoltageGraph sampler -- and
speedEffect@0x31C is a published GAUGE attribute.

The authentic myomer system (now fully live):
1. drive heat (#85, this morning);
2. VITAL death -- myomers are the only authored vital=1 subsystem
   (BTL4.RES res+0x48 scan): destroying them kills the mech via the #80
   vital-crit path (this is also the ram-death mechanism -- myomers are a
   0.35-weight collision-rattle target);
3. the assembly-time gait cap: RegisterMaxOutput @004b8ef0 raises
   mech+0x7A0 to AvailableOutput(top gear) = base x ~1.4284 -- now called
   (idempotent max, per tick; the 0x358 layout lock leaves no latch room)
   with real OwnerBaseSpeed/OwnerMaxSpeed bridges; the old per-frame
   reverseSpeedMax2 heal that would have clobbered it is garbage-guarded;
4. the ENG-page power curve at correct absolute scale (real base speed);
5. electrical sourcing (low gears run on a browned-out generator).

REMOVED (inventions): the mechmppr `speedDemand *= speedEffect` demand
multiplier + BTMyomersDriveOf + the Myomers ConnectToMover/MoverAttach
family.  Damaged-but-alive myomers heat faster and drop the power curve
but do NOT slow the mech (gitea #75's premise is falsified by the binary;
the real damage slowdown is the LEG-damage gimp gait).  Seek gears change
heat/sourcing/graph -- never speed.

Verified live: a mech at myomer dmg=0.234 (speedEffect attr 0.766)
beelines at full commanded speed with dmgGain=1.234 heat; 0 faults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 09:17:31 -05:00
Joe DiPrimaandClaude Fable 5 31552c0444 #88 part 2: the central coolant pull-through -- HeatSink::DrawCoolant was a
return-0 stub; the Reservoir BAR now drops when a damaged mech leaks

The user watched the coolant panel's level bar during the leak bench and it
never moved -- correctly: a leaking subsystem drained only its own loop,
because the slot-14 top-up (DrawCoolant) was stubbed.  Disassembled the real
base @0x4add00: the draw RECURSES UP the sink linkage, scaling by
coolantFlowScale@0x15C per hop, terminating at the Reservoir's supply
(@0x4af3b0 -- clamp to [0, coolantLevel], drain the tank, return granted --
already faithfully ported as Reservoir::DrawCoolant).  The binary's terminal
hop is the bank's slot-14 override @0x4ae8b0 resolving its reservoir
connection @0x1D8 (the port's 'helper' member -- not vestigial after all);
the port's existing Reservoir-ctor Attach + Reservoir override terminate the
same chain equivalently (the extra bank hop scales by ctor-default 1.0).

Implemented the base recursion (null-guarded -- the binary calls through the
resolved link unguarded; authored topology always links) + a BT_COOL_LOG
[resdraw] probe at the supply.

Verified live: a destroyed myomer's damage leak pulled the central tank
6.0 -> 5.58 over ~30s ([resdraw] granted lines) -- Reservoir/CoolantMass,
the cockpit coolant bar, now visibly drops as a damaged mech bleeds
coolant, and DeathReset refills it on respawn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 08:47:55 -05:00
Joe DiPrimaandClaude Fable 5 a8a0042f28 #88: coolant leaks LIVE -- the #64 damageZone shadow was the single break
in the leak chain

HeatSink::UpdateCoolant prices the damage-driven leak from the QUALIFIED
engine member (Subsystem::damageZone @0xE0): coolantDraw = zoneDamage *
heatLoad, with the coolantActive hysteresis (= the ReportLeak attribute
the 19 authored leak watchers ride).  But the MechSubsystem ctor only
ever filled its re-declared SHADOW member (gitea #64, gotcha #1), so the
engine member stayed NULL forever, zoneDamage pinned 0, and a coolant
leak was STRUCTURALLY IMPOSSIBLE no matter how much damage landed --
Oracle's night-7 report exactly.  The #80 crit fix and the #83 collision
rattle write real subsystem-zone damage, but into an object the leak
reader could never see.

Fix: alias the ENGINE base member to the same zone in both MechSubsystem
ctors.  Every shadow reader keeps working (same object); the engine base
Subsystem::TakeDamage latent null-AV (#64 consequence 2) is disarmed;
the full #64 de-shadow sweep remains the long-term cleanup.

Verified live (solo, BT_COOL_LOG/BT_CRIT_LOG): collision rattle drove a
damaged Myomers to [cool] draw = dmg*heatLoad with the level draining;
sustained weapon fire rolled real crits ([critroll]) and produced 588
leak-pricing lines.  Authored routing extracted from BTL4.RES: collision
rattle targets HeatSinkBank 0.3 / Gyro 0.35 / Torso 0.25 / Myomers 0.35
(Condenser+Reservoir authored 0 for collisions); weapon crits select via
the zone's crit-entry list.  Known-remaining fidelity item (documented):
HeatSink::DrawCoolant (slot 14, central top-up) is still a return-0 TODO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 08:26:40 -05:00
Joe DiPrimaandClaude Fable 5 1570983fde #85: myomer drive heat LIVE -- the five Mech motion operands were return-0
shims, so the reconstructed integrator @004b8d18 accumulated zero forever

The integrator, its gates and the heat plumbing were all reconstructed and
running -- but OwnerVelocityMag/OwnerDriveMag/OwnerVelocityY/OwnerMotionGain/
OwnerMass were cross-family compile shims returning 0.0f, so the myomers
priced ratio^2 * damageGain * 0 every tick: ice cold at any seek (Oracle's
night-7 panel-confirmed report).

Operands re-grounded against the raw disasm + decomp and mapped to NAMED
members via a complete-Mech-TU bridge (BTMechMyomerMotionSample, mech4.cpp),
per the databinding rule:
  +0x1C4 vec  -> localVelocity.linearMotion
  +0x82C vec  -> localAcceleration.linearMotion (the authentic 15-ring
                 smoothed AccelerationLastFrame the port already maintains)
  +0x20C      -> moverMass (the collision divert's cell; the old "motion
                 gain" label was a misread)
  *(+0x250)   -> environment gravity (FUN_00421e2c: vy -= **(+0x250)/tick),
                 via GetEnvironment()->gravityConstant
So the heat physics reads: 0.005 * 1/2 m v^2 (kinetic work, per-tick/no-dt
in the binary -- kept verbatim, frame-rate note documented) + 0.2 * m g
|vy| dt (climb power) + 0.2 * m |v||a| dt (acceleration power), all scaled
by ratio^2 and (1 + zone damage).  Restored the binary's fabs(v.y) (the
draft had signed vy -- descending would have COOLED).

Authored tuning extracted from BTL4.RES (18 mech records, one shared
tuning): VelocityEfficiency=0.995, AccelerationEfficiency=0.8, gears
0.3/0.5/0.7/0.9999 rec idx 2; myomer thermal profile 77/1000/2000,
conductance 1.9e5, thermalMass 2.5e5.

Live-verified (solo arena, BT_MYO_LOG + BT_HEAT_LOG): standstill generates
~0; hard circling drove Myomers T 77 -> 1225 (past the 1000 degradation
line) and the coolant loop conducted it back to ~520 equilibrium on
slowing; damageGain live at 1.048 from grind rattle.

Known-inert remainder (filed in open-questions): the climb term reads g=0
because Mover::localEnvironment is never populated in the port
(EnvironmentZone res type 23 unwired); the mover feed (speedEffect ->
locomotion) stays WAVE 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 08:14:05 -05:00
Joe DiPrimaandClaude Fable 5 37dd7f9663 collision-pricing audit: restore the binary's crash economy -- two fixes
A. KNOCKDOWN PACING: the 0.4s gBlockCooldown contact-hysteresis is gone.
It suppressed the crash knockdown for as long as contact was held, letting
the kinematic drive re-slam the obstacle at full commanded speed EVERY
FRAME -- continuous full-price crash self-damage, a healthy 65-tonner dead
in ~2-3s of grinding (the night-7 SAURON ram death, 110 priced frames at
|v|~34.7).  The binary needs no guard (crash threshold @0x4ab178 read from
the exe = 0.0, dispatch per blocked frame is authentic): the bmp clip
zeroes the drive, and a pressed mech re-triggers the knockdown at v>6.3
long BEFORE its grind can price anything -- the type-0 divert's 0.5-pt
free floor needs v>~18.5 at 65t.  Wall grinding is free taps + paced
staggers; only genuine fast arrivals pay.

B. RAW RAM DISPATCH: the *0.001 "ram economy normalization" (2026-07-12)
predated the #83 type-0 divert and became a double-normalization -- the
victim's divert scale (~4e-5) compounded with it, so NO physically
possible ram could clear the 0.5-pt free floor: rams were a no-op against
the victim while the rammer's own un-scaled crash path rattled for real.
The binary dispatches raw [T1 @part_012:15324]; the divert IS the
normalizer.  Also un-starves cultural-icon crunches (trucks now crush on
contact per their authored armor, not after ~19 bumps).

Bench (scratchpad/night7/mp_rampricing.sh):
- wall leg: 150s full-throttle wall push = 125 paced knockdown binds
  (~1.2s cadence, iv2 40-43), 4767 crash frames ALL under the free floor,
  0 rattle, 0 deaths (pre-fix: dead in seconds).
- ram leg: BT_GOTO=enemy + BT_GOTO_STOP=2 rams the peer; raw amount
  arrives byte-identical on the victim (66239.1 tx == rx), victim prices
  2.55 pts internal rattle (first ram damage to a victim in the port's
  history), pressed follow-ups free, 0 deaths both nodes, 108 knockdowns
  -> exactly 108 type-5 records on the observer (no skating regression).

KB swept: combat-damage.md (the 1.3e6 moverMass mis-attribution corrected
to the measured 60-90k tonnage scale), locomotion.md, rendering.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 02:48:35 -05:00
Joe DiPrimaandClaude Fable 5 054c3f2eee #94: replicant un-wreck now rides the DEATH-STATE EXIT edge -- the zone
falling-edge latch was blind to zone-less (collision) deaths

The peer wreck lifecycle listened to two different signals: wreck ON came
from the replicated death explosion (unconditional), wreck OFF from a
damage-zone falling edge (wasWrecked latch, armed only when a zone crossed
>= 1.0 on the observer).  The #83 collision damage prices as internal
rattle and never moves a zone, so a fresh mech killed by ramming could
NEVER un-wreck on peers -- the live respawned mech drove around wearing
the hulk (night-7 field report: SAURON's Loki, screenshotted by both
observers at the exact minute the log analysis places the ram death).

MechDeathHandler::Tick now tracks prevMode and fires the rebuild + warp
when the replicated MovementMode leaves the death modes {2,9} -- only
Mech::Reset ever does that, and the state rides every record header, so
the trigger is cause-agnostic.  The zone falling edge just refreshes the
cache.  Side effect fixed for free: the peer respawn warp vortex now
plays for zone-less deaths too (same gate).

Bench (scratchpad/night7/mp_zoneless.sh + the new
BT_SELF_DAMAGE_TYPE=collision harness option, which dispatches type-0
through the real TakeDamage divert): 14/14 zone-less deaths un-wrecked
(mode 9->1) with zero zone rises on the observer; explosive regression
leg 12/12 with zone destruction active.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
2026-07-31 02:10:04 -05:00
Joe DiPrimaandClaude Fable 5 e91d447405 #82 FINAL LAYER: the peer body-channel trn-lock -- a circling limper skated because the peer's turn-arm had no walk-demand yield and the trn speed exit read a dead replicant mapper cell
The master limped clean (218x gimp-cycle records, zero trn) -- a _ReturnAddress
trap on SetAnimationState(4) proved ALL peer trn arming came from the port's own
mech4 turn-step block, not the type-3 reader.  Two dead ends, one root:
- mech4 peer arm: gated on !wantsWalk (standSpeed < bodyTargetSpeed), exits trn
  when walking demand appears -- the leg twin's authentic precedence
  (part_012.c:12013, the Standing speed test outranks the turn test).
- mech2 body case 4: bspd reads bodyTargetSpeed on ReplicantInstance; the local
  mapper's speedDemand is a dead cell on a peer (reads 0 forever -> no exit).
Verified 2-node timeline: arena circle replicant 218x state-24 / 0x state-4
(was 116x trn churn), grass circle 235-clean, knockdowns bounded, 0 deaths.
Diagnostics kept: BT_TRNTRAP ra-trap, BT_ANIMIND_CAP, [gimpfeed], [replgimp]
extension.  KB: locomotion.md trn-lock entry + the dead-mapper-cell lesson.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRjrTQpJd6u9XyfnUbTB3v
2026-07-30 18:26:15 -05:00
Joe DiPrimaandClaude Fable 5 14ff3519ac #83: the COLLISION DIVERT -- type-0 damage prices as internal rattle, never armor
The crash self-damage reconstruction (9c83a46) exposed its missing second
half: the binary's TakeDamage hub DIVERTS damageType 0 (@0x4a0368) into
FUN_0049ffcc -- an export-gap distributor recovered by raw disasm -- so
collision damage NEVER reaches the zone/armor loop.  Without the divert the
mover's raw kinetic-energy figure (60-ton mech: ~1000+ per wall tap, 90k+ on
a hard slam) fell into the WEAPON loop: 'tapped a wall and died instantly'.

The distributor, byte-anchored: gate on the owning player's advancedDamage
copy (+0x268 -- the manual's 'splash/collision damage' technician setting);
scale = (2000/moverMass) / (100 km/h in u/s)^2 / (1 - elasticity^2)
(tbyte 1/3.6 @0x4a0148; mass @+0x20c; elasticity @+0x244); scaled < 0.5 is
FREE; else Round(2x) sub-hits of 0.5 land on random INTERNAL subsystems
(HeatSink family / Gyroscope / Torso, drawn by collisionCriticalHitWeight
@0x10C) via ApplyDamageAndMeasure (@0x4ac07c).  Bridges: family derivations
(heat/gyro/torso.cpp), BTPlayerAdvancedDamageOn (btplayer.cpp).

Verified: bhk1 wall test -- 94k slam = 4.2 rattle pts in 8 sub-hits, taps
free (0.049 < 0.5), armor untouched.  A/B: arena wall-grind now 0 deaths,
27 rattle events, the limper STAYS GIMPED and peers' replicants re-enter the
gimp cycle between bounded staggers; grass circling unchanged (198 replicant
gimp entries).  [crashdmg]/[colldmg] probes document the pricing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 17:04:24 -05:00
Joe DiPrimaandClaude Fable 5 9c83a468db #82 ROOT FIX: reconstruct the crash SELF-DAMAGE -- wall-grinding now hurts,
the knockdown storm self-limits, peers keep the limp

The 'peers see a limping mech skating' report decomposed into a wall-grind
KNOCKDOWN STORM: a gimped mech held against a blocking solid re-crashes each
time its gg cycle rebuilds speed (the gg ceiling is authentically the clip's
natural speed -- bhk1 ~14.9 u/s, ceiling+divisor @0x544/0x548, loader formula
byte-matched -- so iv2 ~222 >> the 40.0 threshold @0x4ab184), and every
type-5 KNOCKDOWN broadcast floors all peers' replicants into knockdown/trn
churn: gliding + leg-lifts = skating.  Turning and crushable cars were
A/B-exonerated (grass circling: 198 replicant gimp entries, 17 crunches,
zero broadcasts).

What the binary has that the port lacked -- recovered from the EXPORT GAP by
raw disasm (scratchpad/night6/gap_4a9770.txt, @4aa89f-4aaab4) -- is the crash
branch's tail: fallDirection = worldToLocal(damageForce) (FUN_0040879c, M^T v,
un-normalized), fallScalar = -(fallDirection . localVelocity.linearMotion),
and a self-dispatched TakeDamageMessage{0x64, zone=-1, the engine-computed
collision Damage verbatim}.  A wall crash COSTS ARMOR, so the grind degrades
the mech instead of looping forever.  That dispatch was the long-standing
'DEFERRED' note in the response policy; now reconstructed.

Falsified en route (comments + KB corrected, do not revive): the stagger's
FUN_004a4c54(1)/(0x20) 'action-request flags' feed NO drive suppressor --
image-wide sweep + full gap disasm show word[this+0x18] is read only by the
net record emitter; the bmp clip applies NO root motion (adv=0 measured); the
'gimp cap 5.8' figure was the BACK-cycle stride printed under a misleading
label (now ggCapL/R; @0x52c has no binary consumer).

Verified (mp_gimpAB rigs): arena1 wall-grind now 4 bounded strikes, limper
dies of its crashes and respawns cleanly twice (START->RESET, no strand);
grass circling unchanged-clean (198 replicant gimp states, 0 knocks).
Rig: pid capture race fixed (a missed winpid left a stale node holding the
-net port -> null runs), per-config sweep added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:18:28 -05:00
Joe DiPrimaandClaude Fable 5 19fe1e5fa2 KB: the wall-grind knockdown storm IS the surviving #82 skating -- A/B-isolated
Four-config matrix (mp_gimpAB.sh, [animind] replicant-state verdict):
grass straight/circle = clean replicant limp (198 gimp entries; 17 car
crunches during it -- crushable path broadcasts nothing, cars exonerated);
arena1 straight = one wall strike, replicant recovers into the limp;
arena1 circle = wall GRIND -- the bmp stagger clip's ~6.5 u/s root motion
re-strikes the wall (6.5^2 > iv2 threshold 40), each rebind re-broadcasts
the type-5 knockdown (26x in the user's run), and every peer's replicant
lives in knockdown/recovery churn: gliding + trn leg-lifts = "skating".
Turning while gimped is innocent; single strikes are benign.

Authentic fix filed in open-questions: reconstruct the consumer of the
stagger's action-request flags (FUN_004a4c54 bits 1/0x20 -- the drive
suppressor while the bmp clip plays); the rebind guard at mech4.cpp:6861
is already marked bring-up-pending exactly that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:41:56 -05:00
Joe DiPrimaandClaude Fable 5 4e89baeff3 bench parity: local nodes now launch exactly like a player's shortcut
Local benches had drifted from the shipped launchers and every divergence
cost a debugging session:

  * no BT_START_INSIDE -> benches opened in the EXTERNAL CHASE camera while
    every player starts in the COCKPIT.  The first first-person look at a
    bench window was misread as a broken HUD.
  * novice bench eggs  -> every SHIPPED egg is expert; novice gates off the
    entire heat model, crits and jams, so bench combat was not field combat.
    (Verified: the aligned node now boots experience=3 simLive=1
    heatModelOn=1, where it used to boot 0/0/0.)
  * 1-LP affinity pins -> starved the gauge executive and produced a false
    "the comms panel never counts deaths" reading.  Two LPs per node keeps
    the documented single-box jitter fix without the starvation.

scratchpad/night6/bench_common.sh now owns the contract (bt_player_env,
bt_launch, bt_expert_egg, bt_novice_egg) copied verbatim from play_solo.bat,
with bt_assert_player_env warning if the shipped bat ever drifts.  All three
4-node benches source it.  The limp bench keeps a novice egg -- expert crits
the aimed leg and the mech turns-but-never-moves -- and now says so loudly.

Also: BT_SHOT_EVERY=<n> headless backbuffer capture (btl4vid.cpp), the tool
this was diagnosed with.  It must sit above the warp phase-0 early-out since
that fn is the per-frame alpha-pass hook.  OS screen-capture is off-limits:
a foreground-lock failure photographed the user's browser instead of the game.

KB: build-and-run.md gains the bench-parity rule + the capture diag;
cockpit-view.md gains the measured aspect/FOV finding (the surround's 2.05:1
view collapses vertical FOV to 31.5 deg vs the pod's 46.8, so fixed canopy
geometry covers 68.6% of the lower half -- NOT a regression: the shipped 643
binary reports identical geometry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 13:30:30 -05:00
Joe DiPrimaandClaude Fable 5 494f32efb4 #45 ROOT CAUSE + FIX: the gauge executive starves in MP -- panels froze at healthy fps
Instruments repaint via the background task pump, which authentically runs
ONLY in the frame's leftover time with a floor of ONE pump per frame; the
gauge renderer is 1 of ~7 round-robin tasks and each turn advances ONE gauge
of ~140 active, with the rate mask advancing once per full sweep.  On a busy
MP mission the foreground eats the whole frame budget, the floor becomes the
norm, and the whole instrument stack rotates once in MINUTES at perfect fps:
comms-panel K/D stuck at 0, recharge tickers frozen, while the tallies and
their replication underneath were exactly right.  Measured: 4-node bench at
70-90 fps gave the PilotList ~2 Execute turns in six minutes.

Fix, both env-tunable, authentic behavior restorable:
  BT_BG_MIN      (APPMGR.cpp, default 32, 0=authentic)  minimum background
                 pumps per frame regardless of slack;
  BT_GAUGE_BATCH (GAUGREND.cpp, default 32, 1=authentic) gauges advanced per
                 gauge-renderer turn (a visit is only a rate-mask check
                 unless the gauge is due).
Verified 4-node self-damage bench: sweeps 0.006/s -> 18-20/s, PilotList ~10
Exec/s, ALL FOUR panels tracked every death live (0->11) within ~1s, bg cost
2-4 ms/frame, respawn ledger clean (40 cycles, 0 swallow / 0 mismatch).

Also: BT_PERF now reports gaugeTurns/sweeps/active alongside bgTasks;
BT_AF_PERIOD now throttles the missile autofire group too (unthrottled spam
trips the documented FailureHeat all-weapons brick, which froze run 1 of the
cross-fire bench).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 11:22:47 -05:00
Joe DiPrimaandClaude Fable 5 bfa1b04990 #81 follow-ups: peer warp must not stomp the POV vortex; panel draw probe; mech3 ghost symbols
Two field observations from watching the 4-node stress, both run down:

1. "A respawn happened without the blue vortex" -- REAL.  The translocation
   effect is ONE global slot shared by the POV lifecycle and the world-anchored
   peer sphere (a port extension).  A peer's un-wreck arriving while the local
   pilot's own collapse/wait/expand was in flight overwrote gWarpPhase/gWarpPOV
   and killed the POV vortex.  Invisible before the respawn fix only because
   overlapping respawns barely existed; now they are routine.  Fix: the POV
   lifecycle owns the slot -- BTStartWarpEffect self-skips while it is active
   ([tloc] peer warp SKIPPED).  2-node bench: 20/20 POV collapse+expand pairs,
   9 peer spheres played, 11 correctly skipped; solo: 8/8 pairs unchanged.

2. "Comms panel counted no deaths" -- panel machinery CORRECT; the 4-node
   zeros were the bench's own CPU crush (4 core-pinned instances starved the
   PilotList to <0.6 Hz, so rows redrew minutes-stale).  Added the arbiter:
   [score] panel DRAW slot/pilot/kills/deaths edge log (BT_SCORE_LOG) -- the
   2-node rerun drew 0->9 / 0->11 live on both nodes, local AND replicated
   (SBMIRROR rows confirm owner->replicant tally flow on all 4 stress nodes).

Also: the mech3 offline-authoring stubs declared every <Subsystem>::DefaultData
as Entity__SharedData while the real statics are Simulation__SharedData (this
engine derives Entity FROM Simulation) -- ~20 ghost symbols /FORCE silently
resolved to garbage.  DefaultData half fixed (SubsystemDefaultData now returns
the true common base Simulation::SharedData); the CreateStreamedSubsystem stub
signatures remain wrong (nested SubsystemResource* + ResourceFile*), are
cold (no callers), and are tracked in open-questions + gotchas §6 stub-typedef
corollary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:10:29 -05:00