Commit Graph
154 Commits
Author SHA1 Message Date
Joe DiPrimaandClaude Opus 5 43777569f9 night13: close #148 as not-a-bug on the tracker
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 10:11:47 -05:00
Joe DiPrimaandClaude Opus 5 cacca58836 #148 is NOT A BUG: a peer mech does not tick before RunningMission -- by engine design
Chased to the bottom instead of stopping.  The answer is that there was
nothing to fix, and my bench was lying to me.

Entity::Execute (ENTITY.cpp:556, real engine source [T0]) calls PerformAndWatch
ONLY when the app state is RunningMission/EndingMission or the entity
IsPreRunnable(); otherwise it merely WriteSimulationUpdate()s.
Entity::DefaultFlags is DynamicFlag|MasterInstance -- no PreRunFlag.  Only
Player and Director add it, and Mech::Reset sets it for a reset MASTER ("a
reset master must tick").  A REPLICANT mech never gets it.

So a peer mech performs ZERO subsystem ticks until the round actually starts,
however much correctly-replicated data is arriving.  Measured on the observer:

     235  [perf-first] mech 3:161 master      <- own mech, immediately
     402  [torso-rec-rx]                      <- peer torso records arriving
    2754  [perf-first] mech 2:55 REPLICANT    <- peer's FIRST performance
    2758  [torso] PushTwist COPY              <- its torso ticks 4 lines later
    2761  [ent-exec] state=5                  <- RunningMission

The peer starts performing exactly at the RunningMission transition.  That is
the engine doing what it says.

WHICH MEANS THE PREFIX WAS A BENCH ARTIFACT.  BT_AUTOFIRE starts shooting
immediately, during WaitingForLaunch -- something no player can do in a real
match -- so those 60 leading salvos measured a peer whose torso had never run.
Every "ZZZZ...XXXX" pattern in this investigation was that, and the first X
lands within a few lines of the state transition.  #141's fix is unaffected and
remains verified: the segment-cache defect was real and mid-match.

Chain of things ruled out on the way, all measured:
  * record CADENCE is authentic -- sends on RATE CHANGE (payloads are the sweep
    extremes, rate flips sign), peer dead-reckons between them.  12 records for
    12 reversals is correct, not starved.  My "only 13 records" premise was wrong.
  * ComputeTargetTwist clamp -- limits load fine on the copy (+/-2.44346).
  * the torso's own executable flag -- restoring the engine's instance branch
    (f36f013) is a genuine fidelity fix but moved this by nothing.
  * the replicant entity IS offered to the performer, executable=1, from line
    171 -- 2500 lines before its first PerformAndWatch.  The gate was inside
    Execute, not the scheduler.

Adds [perf-first]: a ONE-SHOT per-mech receipt naming entity ID + instance at a
mech's first performance.  Every other per-frame receipt in mech4 is anonymous,
which is precisely why this took so long in a 2-node log -- master and replicant
lines were indistinguishable.  Name the mech.

Gotcha #29 records the bench-design rule this cost: judge a 2-node bench by
PREFIX vs INTERLEAVED, never by raw percentage, and check [ent-exec] state=
before suspecting replication.  missileframe.sh carries the same warning.

#148 to be closed as not-a-bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 10:11:14 -05:00
Joe DiPrimaandClaude Opus 5 bb6605d53b night13: #148 tracker correction script
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 09:57:00 -05:00
Joe DiPrimaandClaude Opus 5 eebe7e61a4 gotcha #28: hand-composing an engine-derived transform reads a stale cache (replicant-only)
The #141 bug class, written up so it is not re-introduced.  GetSegmentToEntity
recomputes ONLY when segmentModified is set; JointedMover::GetSegmentToWorld is
what sets it -- and the binary's own GetMuzzlePoint @004b9948 goes through it
(FUN_00424da8), so every muzzle query in the 1995 image performs the
joints->segments refresh.  Four port sites hand-composed instead, one of them
commented "the faithful FUN_004b9948".

Records the four rules the investigation actually cost:
 (a) never hand-compose; call GetSegmentToWorld
 (b) never force the dirty flag to fix a stale read -- that stand-in scored
     IDENTICALLY to the faithful fix while patching only one consumer
 (c) "peer POV only" geometry bugs = suspect a cache the local render pass
     refreshes for free, before suspecting replication (it was provably fine)
 (d) a partial-looking score: check PREFIX vs interleaved before calling it
     partial -- these were a clean prefix ending when the peer first had a
     twist to carry, so the fix was complete and "64% fixed" was wrong
 (e) the probe trap: one shared static sampled every Nth call hides one of two
     alternating instances entirely

#141 closed with the full write-up; #148 filed for the torso replication
cadence (13 records in a 5-minute run) which is a separate, real problem and
likely bears on #37 and #70.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 09:29:47 -05:00
Joe DiPrimaandClaude Opus 5 05d7b5890a #141 peer missiles: the launch frame read a STALE segment cache on replicants
Oracle: "missiles are firing in the direction the mech feet are facing ...
and then coming around to track the target", peer POV only -- the shooter's
own view is correct.

REPRODUCED AND MEASURED (scratchpad/night13/missileframe.sh, 2-node: only A
sweeps its torso and only A fires, so every REPLICANT line in B's log is the
mirror of one A salvo).  New [launchframe] receipt prints the yaw of the
launch forward vs the BODY forward on both nodes:

    master     n=165  |twistDelta| max=2.2962  mean=1.2283  >0.1rad: 100%
    REPLICANT  n=165  |twistDelta| max=0.0000  mean=0.0000  >0.1rad:   0%

segResolved=1 on BOTH, and segYaw == bodyYaw EXACTLY on the peer.

WHAT IT IS NOT.  Both sides already pass the mount segment (mislanch.cpp:363
master, :478 replicant mirror, both `GetSegmentIndex()` from task #67), and
the peer's torso data is fine end to end: records arrive (atUpd=2.44/-2.39,
rate 0.305), the copy extrapolates correctly (cur=-2.13987 target=-2.13987
copy=1), and the copy torso demonstrably writes its joint (PushTwist COPY
twist=-1.49601).  Hierarchy is identical too: same seg 18, same parentIdx 4,
non-null parent + joint subsystem on both.

ROOT CAUSE.  BTPushProjectile composed the frame BY HAND --
`mw.Multiply(seg->GetSegmentToEntity(), localToWorld)`.  But
EntitySegment::GetSegmentToEntity (SEGMENT.cpp:262) recomputes ONLY when
`segmentModified` is set, and the thing that sets it after a joint moves is
JointedMover::GetSegmentToWorld (JMOVER.cpp:136-146), which tests
AreJointsModified() and then marks every segment dirty.  Hand-composing skips
that, so you read whatever cache is sitting there.  On the MASTER that was
invisible -- the renderer/cockpit camera call GetSegmentToWorld for the local
mech every frame, AFTER the local torso pushes its joint, so the cache was
already correct.  A REPLICANT gets no such refresh: its cache stayed at the
BIND POSE, and the twist never reached the launch direction.

FIX.  Use the engine accessor, and set the joints-dirty flag first so it
actually refreshes (by fire time the frame's render pass has already consumed
and cleared it -- measured jointsDirty=0 on BOTH nodes).

RESULT (same bench):
    REPLICANT  max 0.0000 -> 2.1145   mean 0.0000 -> 0.8252   0% -> 64%

PARTIAL, and I am not claiming otherwise.  36% of peer salvos still read the
exact-zero stale signature while the master is 100%.  Forcing every per-joint
`jointModified` flag as well (GetSegmentToParent's own gate, SEGMENT.cpp:196)
was tried and moved the number by NOTHING -- 64% either way -- so the residual
is a different cause, most likely frame ORDER (the salvo mirror running before
the copy torso has posed that frame).  Cheap form kept.

Also fixes a SAMPLING TRAP in the torso probe: PushTwist sampled one shared
static every 30th call, and with a master torso and a copy torso ticking 1:1
every 30th call is always the SAME instance -- so the probe showed only the
local untwisted torso and hid the copy's writes entirely.  Now sampled per
instance-kind, which is what made the copy's correct joint writes visible and
moved the search downstream to the segment cache.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 08:14:25 -05:00
Joe DiPrimaandClaude Opus 5 1ae57398f1 #147 range caret: NaN poisons a process-lifetime static -- and the caret's input was never logged
Oracle: "no range finder on this drop" + a screenshot -- tick marks present,
moving caret absent, one drop, only tester affected.

Not the host, not the chassis, not his destroyed HUD.  From the four field
logs: he WAS hosting (`[lobby] host:` appears only in his log) but range
computed fine on his node (1806 nonzero samples) and the reticle built on all
6 drops; a second tester flew a Thor the same night without hosting and saw
nothing, and the ladder is shared HudSimulation/BTReticleRenderable, not
per-chassis content; his HUD was destroyed twice but for 11s and 26s only, and
a destroyed HUD costs the LOCK (_DAT_004b7ec4 = 0.75), not the caret.

THE DEFECT.  sShownRange -- what the caret binds to -- is a function-level
static in mech4's targeting step: one cell for the whole process, shared by
every mech, carried across drops, never re-seeded.  NaN is ABSORBING in

    step = trueRange - sShownRange;
    if (step >  maxStep) step =  maxStep;      // false for NaN
    if (step < -maxStep) step = -maxStep;      // false for NaN
    sShownRange += step;

so one poisoned frame makes it NaN for the life of the process.  The consumer
repeats the mistake -- BTReticleRenderable::Draw clamps with the same two
comparisons -- so NaN reaches AddPoint/ConcatMatrix and the caret + its bar
become degenerate geometry that STOPS RENDERING, while every static reticle
element including the tick marks still draws.  That is the reported symptom
exactly, and it is sticky until relaunch.

WHY NO LOG COULD SETTLE IT.  The caret's actual input had NO diagnostic
anywhere: BT_RANGE_LOG instruments the PICK (#4), and [target]'s `range=` is a
SEPARATE locally-recomputed Sqrt in the weapon-range check -- neither is
sShownRange or gBTHudRangeStorage.  Grepping the field logs for NaN returns
nothing because the poisoned variable was never printed.  Absence of the
signal was not evidence of absence.

FIX (4 parts):
  1. re-seed sShownRange when the viewpoint mech CHANGES, so a new drop starts
     at the binary's 1200 default.  Deliberately NOT on respawn -- that reuses
     the entity, and the binary does not reset the readout on respawn either.
  2. producer NaN trap -> re-seed to 1200 instead of propagating.
  3. NaN-safe consumer clamp (test x == x first) -> fall back to the authentic
     no-target peg rather than rendering nothing.
  4. BT_RANGE_LOG now prints the caret's real input at 1 Hz plus a
     "[range] NaN TRAPPED" receipt, so the next field log CAN settle it.

STATUS [T3 on the field link].  The defect and the symptom match exactly and
the fix stands on its own merits -- a process-lifetime static feeding unguarded
float geometry is a bug regardless.  But the causal link to Oracle's report is
INFERENCE: the NaN source is unidentified and this has not been reproduced.
Field-verify with BT_RANGE_LOG=1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 07:16:32 -05:00
Joe DiPrimaandClaude Opus 5 7b003243ae #146 respawn: release the DESKTOP throttle -- and close #137, which was never a bug
#137 ("respawn came back with MYOMERS heat MAXED", Oracle; "overheating
generator D", Sauron) sent us through a full two-sided audit of Mech::Reset
and the whole RTIS chain.  Both sides were correct.  The answer was in the
field log all along, one line after the reset:

    [respawn] Mech::Reset 3:30 healed+moved to (...) alive=1
    [techstat] ... every live condition CLEARED
    [techstat] Myomers condition 3 SET        <- Overheating, immediately
    [mppr] in thr=1 -> ...
    [gaitSM] cycleSpeed=14.6 state=12         <- already RUNNING
    [techstat] Condenser5 condition 3 SET     <- "dumping into coolant loop 5"
    [techstat] GeneratorD condition 3 SET     <- Sauron's generator D

The mech respawns STILL UNDER POWER and earns the heat honestly.  Two facts
close it:

1. condition 3 is an OPERATING flag, not an alarm.  Census over one match:
   LLaser_2 33 SET / 33 CLEARED, LLaser_1 31/31, SRM4 26/26, PPC_2 18/18 --
   every volley trips it and clears it.  EVERY subsystem is balanced
   (GeneratorD 5/4, Myomers 5/4, Condenser5 1/1; the extra SET is only the
   log ending mid-heat).  cond 6 BadPower behaves the same (Myomers 8/8).
   Nothing latches.  A post-respawn SET is not evidence of anything.

2. Mech::Reset's subsystem loop starts at index 2 and the ControlsMapper is
   index 0, so the throttle is never reset -- and the BINARY does the same.
   That is right for a pod: the throttle is a PHYSICAL lever still under the
   pilot's hand.  Respawning under power is authentic and stays.

Oracle's read that the myomer heat rate "felt right" was correct.

WHAT IS a real defect (#146), desktop only: the glass bridge merely EMULATES
that lever, with the static ramp accumulator sLever (mech4.cpp:3250) zeroed
ONLY by the X all-stop and a direction-crossing snap.  A pad/keyboard pilot
is physically holding nothing and cannot see the lever, so they respawned at
speed for no reason they could perceive -- and ate the heat load above.  The
Thrustmaster/RIO path was never affected: InterpretControls (@004d2150)
rebuilds throttlePosition every frame from the databound throttleForward.

Fix: queue the existing all-stop at Mech::Reset, reusing the proven path
(it already clears the zero-crossing detent too).  LOCAL VIEWPOINT MECH ONLY
-- gBTDrive is the local bridge's state and Reset also runs for replicants,
so an ungated write would all-stop the player whenever a REMOTE mech
respawned.  Pod-safe besides: with a RIO present the key bridge is off and
gBTDrive.throttle is never read.  BT_NO_RESPAWN_THROTTLE_RELEASE=1 reverts.

Benched 2-node (scratchpad/night13/throttlerespawn.sh): the release fires
1:1 with local respawns on both nodes independently (A 2/2, B 1/1) and never
spuriously.  HONEST LIMIT: the viewpoint gate was NOT stressed -- B ran
Mech::Reset 0 times for A's mech, so the remote-respawn path never fired.
The gate is correct by construction (the isPlayerMech idiom), not proven.
BT_AUTODRIVE cannot test the lever itself (forced mode reads forcedThrottle,
never sLever), and the zeroing path is the X button, proven in the field.

Also keeps BTReportHeatAtReset (heat.cpp, BT_HEAT_LOG): the [heat-t] census
runs on a 5s timer, far too coarse to sample AT the reset.  It is what
proved every roster subsystem including all six Condensers sits at T=77
start=77, and it corrected an earlier false negative from filtering on
IsDerivedFrom(HeatSink).

KB: context/decomp-reference.md gains the routine/self-clearing condition
semantics + this post-mortem, so it is not re-chased; cross-ref in
context/gauges-hud.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 01:23:35 -05:00
Joe DiPrimaandClaude Opus 5 1f0923747b Mech::Reset: restore the POSTURE clears the port had dropped (#142)
Oracle: "crouch wasn't resetting on respawn ... mechs always spawn standing".
Correct -- Mech::Reset (@0049fb74) stands the mech up and the port cleared
none of it:

    *(this+0x398) = 0             duckState
    Set_Alarm_Level(this+0x39c,0) legStateAlarm  -> standing
    Set_Alarm_Level(this+0x714,0) bodyStateAlarm -> standing
    *(this+0x650/0x654/0x658) = 0 death + leg/body reset latches
    *(this+0x5ac) = 1.0f          idleStrideScale

A pilot who died CROUCHED came back crouched -- leg parked in 'sqd' -- and now
that the cockpit strip works, showing the up-arrow "press to rise" frame on a
standing mech.

Benched (crouchrespawn.sh): A squats, dies while down, respawns -> legLvl 0
(standing) after Mech::Reset.  Weak but the failure mode (stuck legLvl=1) is
absent.  NB the first attempt was void: force-damage kept A dying before it
could crouch (legLvl 22/24 = death clips), so the run tested a STANDING death.
Switched to self-damage so the mech is stopped long enough to crouch.

Also carries the #142 gauge work: the crouch strip is a BUTTON-STATE indicator
(grey unavailable / orange down-arrow ready / orange up-arrow crouched),
decoded by rendering BDUCK.PCC rather than inferring it; and the gauge
factory's missing-image path no longer uses the no-op DebugStream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-08 00:11:00 -05:00
Joe DiPrimaandClaude Opus 5 0530366687 #142 crouch: the mech is fine -- it is a missing PANEL ANIMATION
Benched solo AND in MP on the same chassis: both presses reach
DuckRequestMessageHandler, zero drops, SQUAT -> squat clip parked -> RISE.
Locomotion is not the bug, and MP is not refusing it.

Added an ungated [duck] REQUEST DROPPED receipt at the consumer's silent miss.
The squatCapable==0 path skips the consumer entirely AND leaves duckState
latched at 1 with NO log today; the posture-gate miss logged only under
BT_DUCK_LOG, which no player sets.  Neither fired on madcat.

What the pilot sees, traced with BT_LAMP_LOG: the button lamp is momentary
press feedback, not state --

    PRESS   -> [lamp] 0x13 <- 0x3c
    SQUAT   -> mech crouches, clip parked
    RELEASE -> [lamp] 0x13 <- 0x14    <-- while still CROUCHED

so crouched and standing look identical.

Era testimony corrects the scope: the button should ANIMATE A MECH SYMBOL
beside it, standing <-> crouching (operator).  Lynx: 'When a mech stops,
crouch button lowers its stance and plays crouch animation.  Mech is
immobilized until crouch is pushed again, and mech rises.'  Draco concurs.

Two real gaps, neither fixed here:
  1. no immobilization while crouched -- nothing gates movement on duckState
     or the parked leg alarm.  NB a driven mech with a parked leg channel is
     the [skate] signature (#52), so this may not be cosmetic.
  2. no stance symbol -- no gauge element draws one, and the decomp carries no
     crouch/squat/stance/duck graphic string, so it is an authored IMAGE on the
     secondary MFD; find it in that gauge's element list, not by string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 16:16:12 -05:00
Joe DiPrimaandClaude Opus 5 61f21107b4 #108 THE EJECT GHOST: the death-edge latch tested the wrong field
One substitution, three field symptoms.  Mech::TakeDamageMessageHandler arms
the whole death tail -- kill report, VehicleDead, death blast -- from a
was-alive-at-entry latch:

    const int deathBlastArmed = !IsMechDestroyed();   // graphicAlarm >= 9

The binary tests movementMode 9|10 there (@0x4a0303).  The port swapped in the
graphic alarm and justified it: "the death transition sets mode 9 synchronously
with the structural flag on every path through here, so the edges coincide".
True of every DAMAGE path.  False of the one that matters:
Mech::EjectPilotMessageHandler raises graphicAlarm to 10 (the EJECT state)
BEFORE dispatching its self-damage, while movementMode is still 1.  So on an
eject the handler entered already reading "destroyed", the latch never armed,
and the death tail was skipped entirely -- including VehicleDead, which IS the
respawn trigger.

Everything the field reported on night 13 follows from that:
  * "they all self destructed with panic button and didn't respawn properly"
    -- no VehicleDead, so no drop-zone hunt, so no respawn;
  * the EJECT GHOST -- the peer wrecks the mech and never un-wrecks it, because
    the un-wreck rides the master's respawn.  Normal deaths replicated fine all
    along (9 deaths -> 8 un-wrecks, benched), which is why only ejects ghosted;
  * the manual chart's "-1000 ejecting" never materialised -- the negated kill
    award and the death cost both live in the tail that never ran.

Fix: use the binary's own predicate.  MovementMode is untouched by the eject's
alarm write, so the latch arms on an eject exactly as on a combat death.

WHY SEVEN RIGS MISSED IT: the punch-out was being REFUSED, not undelivered.
EvaluateEjectPermission (@0049fa1c) grants only on
  liveWeapons < ejectMinWeapons || liveGenerators == 0 || coolantFrac < 0.05
  || (leg-gimped && !simLive)
-- armour damage satisfies none of them, and every bench ejected a healthy
mech.  An [ejecttest] receipt (2 lines) proved the dispatch fired every time
and the handler declined; the "[eject] REFUSED (mech not crippled enough)" line
was sitting in the very first bench log, ungrepped.  BT_KILL_SUBSYS's
comma-list form ("GeneratorA,GeneratorB,...") was already built for this bench.

Verified 2-node (scratchpad/night13/ejectreal.sh), before -> after:
  PUNCH-OUT landed        0 (785 refusals)  ->  1, charge=500
  peer wreck-enters       1                 ->  1
  peer UN-WRECKS          0  (the ghost)    ->  1
  eject score (type=2)    absent            ->  award=-1000.00, score 1000 -> 0
  death cost              never ran         ->  APPLYING, penalty=500

That -1000 is the manual chart's eject row to the digit: killBonus 500 plus the
500 self-damage tally, negated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 15:38:50 -05:00
Joe DiPrimaandClaude Opus 5 2fcce53bb2 scoring benches: self-inflicted + deaths counter verified (night 13)
scoreself2.sh -- one rig covering three unverified items, using a SELF-DESTRUCT
to reach the same paths an eject does without the panic button that defeated
five earlier rigs.

VERIFIED:
  chart '-1 each self-inflicted point'  type=0 award=-40.00 x11, total -440
  self-kill negation (#134)             type=2 award=-539.00, kills NOT incremented
  deaths counter                        PLAYER_DEAD deaths=1 tally=1

OPEN, found by arithmetic: the -500 death cost fires on a COMBAT death (prior
run: victim total exactly -500.00) but NOT on a self-kill -- A's total is
exactly -440 + -539 = -979, with no -500 in it, despite advDamage=1 and the
role bound.  The cost is dispatched by a DIRECT Player::ScoreMessageHandler()
base call, bypassing the BT handler, so it never reaches the matchlog and only
the totals expose it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 10:29:34 -05:00
Joe DiPrimaandClaude Opus 5 a4bfb64ace scoring: BIND the scenario role -- one commented-out line zeroed the whole chart
BTPlayer::scenarioRole was never assigned.  The lookup sat commented out with
"the BT role registry (BTMission::GetRoleRegistry()->Lookup) has no WinTesla
analog, so the scenarioRole set by the base Player ctor stands" -- and the base
ctor sets it to NULL (PLAYER.cpp:680).  So it stood NULL forever.

Every scoring value the game has hangs off that pointer, and the shipped
content authors them correctly.  New ungated receipt in the ScenarioRole ctor
prints what a real mission loads:

  [role] 'Role::Default' model='dfltrole' killBonus=500 deathPenalty=500
         dmgInf=1 dmgRcv=0 bias=1 ff=1 return=1000

That IS the original manual's scoring chart -- +500 a kill, -500 a special-case
death, +1 per damage point.  With the pointer NULL every award multiplied
against zero: kills scored the damage tally alone (4.88), the eject charge read
0 (the field log's "PUNCH-OUT: charge=0 (role killBonus)" = #134's missing
penalty), and the death-cost block was skipped.

The analog DOES exist: Mission::GetScenarioRole(name) (MISSION.h:162) walks
scenarioRoleChain -- the same dictionary BTL4Mission fills via AddScenarioRole()
when it parses the role pages, whose own comment says the WinTesla base exposes
it.  Same lookup, same key.  Falls back to Role::Default when a creation
message names an unknown role (shipped content authors exactly one page), and
logs BOUND/NULL so this cannot fail silently again.

Benched cross-node:
  role binding    player 2:1 BOUND, player 3:1 BOUND
  KILL AWARD      505.88  (was 4.88)   <- chart's +500, verified
  death cost      victim total -500.00 <- chart's -500
  inflicted       still tracking, killer total 1017.32 kills=1

The -500 on an ORDINARY combat death is AUTHENTIC, not a bug: the binary's gate
is advancedDamageOn alone (@004c05c4 tail: `if (player+0x264 != 0) { -role+0x20 }`),
verified in the decomp.  It only shows now because the role finally binds.  It
also reconciles the chart's two death rows: an EJECT costs -500 (death) plus its
self-kill negating its own ~500 award = -1000, and an ammo death costs -500.

CORRECTION to my own earlier note: returnFromDeath=1000 is NOT the chart's
"+1000 starting the game" -- role+0x28 is a lives/return gate (`if (< 1)` ->
mission review, else respawn).  The 1000 is coincidence.  That row is still
unlocated and is most likely console-side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 10:21:15 -05:00
Joe DiPrimaandClaude Opus 5 27721754da scoring: restore the type-0 INTERCEPTOR -- per-hit inflicted credit was live all along
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
2026-08-07 08:15:22 -05:00
Joe DiPrimaandClaude Opus 5 4642129e76 #108 peer-side WRECK receipt: make ghosts countable
The un-wreck receipt had no partner, so counting ghosts in a field log meant
pairing it against

    [BTrender] wreck: 'thrdbr.bgf' missing -> gendbr.bgf fallback

which is a MISSING-ASSET WARNING, not a death -- it only prints for chassis
whose wreck model is absent.  Night 13's census found ONE ghost while testers
reported many, and there was no way to separate a real count from a chassis
accident.

Emit one ungated line for every REPLICANT entering the wreck state, symmetric
with the existing un-wreck line, so a log's ghost count is exactly
(wreck-enters minus un-wrecks) per entity:

    [wreck]   replicant H:E entered wreck state (mode X->9) at (x,z)
    [respawn] replicant H:E un-wrecked + warp   (mode 9->1) at (x,z)

Verified 2-node (200s, force-damage victim): 5 enters, 5 exits, exactly
paired -- while the old marker printed ZERO times in the same run.  That gap
is the point: five real deaths, invisible to what the census was reading.

Also lands the night-13 census tooling (ghostcensus.py) and the eject benches
that did NOT reproduce, with their failure modes in the headers so the next
attempt does not repeat them: five rigs failed to trigger a punch-out at all
(BT_BTNTEST never reached the mapper for 0x3D or 0x14; BT_EJECT_AT did not
fire either).  Panic-eject replication remains UNTESTED by bench.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 07:48:04 -05:00
Joe DiPrimaandClaude Opus 5 43d30ca7e4 #140 receipts: name the corruption case in a FIELD log
Two ungated one-liners, because no bench here reached the failing path and
the next playtest is a better instrument than more automation:

  [glasswin] destroy entry #N windows=M   -- N=2,M=0 is the double-destroy
  [glasswin] saved ... (live=L remembered=R) -- live=0 IS the corruption case
                                                (pre-cache that wrote a file
                                                holding only the plasma line)

Also lands the benches that did NOT reproduce it, with their failure modes
recorded in the headers so the next attempt does not repeat them:
layoutsave.sh (round trip -- passes on the fixed build), layoutteardown.sh
(graceful WM_CLOSE; still never reaches the dtor chain), layoutround.sh (MP
round boundary; the relay never started the mission inside the window).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 01:23:33 -05:00
Joe DiPrimaandClaude Opus 5 c04bec0a52 #140 glass_layout.cfg lost every MFD line on a desktop teardown
Regression from d213c98 (the pod PadRIO/panel coupling fix).

BTGlassPanels_Destroy calls SaveLayout FIRST, unconditionally, before it
looks at whether any windows are left.  d213c98 added a SECOND caller
(~LBE4ControlsManager) alongside the existing one in ~PadRIO, so on the
desktop path both run: ~LBE4ControlsManager does `delete rioPointer`, which
fires ~PadRIO -> destroy #1 saves the live windows and zeroes gWinCount ->
destroy #2 then rewrites the whole file from an empty list.

Why only the MFDs vanished, which is the detail that identifies it: external
windows (plasma) already cached a last-known rect (gExtern[].haveLast) and
were written from the cache; the per-display glass windows had no cache and
were simply skipped once their HWND was gone.  Hence the reported signature,
"all the MFDs and secondary lines missing, but plasma was still there".

The pod was never affected -- no PadRIO there, so only one destroy, and it
runs BT_GLASS_LAYOUT=load off a frozen master regardless.

Two fixes, because the guard alone would leave the trap armed for the next
teardown-ordering change:
  1. glass windows get the same remembered-geometry cache the extern windows
     have, kept ACROSS teardown.  The file is now monotonic -- a save can
     update a line or add one, never drop one.
  2. the teardown save is guarded on there being windows to report.

Also corrects the comment at the L4CTRL call site, which claimed the second
call was "a no-op on the PadRIO path".  That claim is what made it look safe.

Verified (scratchpad/night13/layoutsave.sh -- the tester's round trip, not a
single launch, since the report was "saved fine, reset on relaunch"):
  run 1  one "saved 8 window position(s)" line (was two, the second wiping)
         cfg holds all 7 glass windows + plasma
  run 2  "restored 7 window position(s)", cfg byte-identical after the trip

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 01:03:32 -05:00
Joe DiPrimaandClaude Opus 5 6a96fb6420 #52 the peer STANDING-LOCK: case 0's fallthrough was intercepted
A replicant could not start walking between gait-change records.  The port's
body case 4 (the task-#64 lockstep twin) is an INSERTION sitting between case 0
and the advance group; in the binary case 4 is a MEMBER of that group
(FUN_004a5678 @004a5678: case 2,3,4,5,8,... -- no turn block, no speed exit),
so case 0's fallthrough is meant to land on Advance().  The insertion caught it.

On a replicant that is not a race but an identity: case 0 arms walk iff
standSpeed < bodyTargetSpeed, and the inserted block resets iff standSpeed <
bspd -- where bspd IS bodyTargetSpeed for a replicant.  Same expression, so arm
and reset fire on the same frame, forever, and a peer parked at Standing with a
live replicated demand never cycles.  bodyCycleSpeed stays 0 while position
advances from dead reckoning: the skate.

This is the sequel to e91d447 (#82).  Before it the replicant branch read the
dead local mapper cell (0 forever), the exit never fired, and the fallthrough
worked BY ACCIDENT.  Fixing the dead cell closed the escape hatch.

Fix: case 0 -> goto advance_body_normally, the leg twin's own idiom, restoring
the binary's structure without touching the #64/#82 turn logic.
BT_NO_BODY_FALLTHRU=1 reverts.

Measured (2-node, scratchpad/night13/skatelock.sh):
  legacy  336 consecutive locked seconds, bspd=39.2324 bts=39.2324 every line
  fixed   0 locks, every pass
  master body-Standing samples 52 -> 21 (it locked too, invisibly at mj=0)
  turn-in-place intact: pivoter body state 4 x9 / leg state 4 x8, in lockstep

Diagnostics (both keepers): [skate] now carries bstate= -- the field lines
proved "both channels idle" but never named the state, which was the whole
answer; [bodySM]/[peergait] under BT_BODY_SM_LOG instrument the arm->reset pair
and a moving replicant's body channel.

NOT claimed: that this accounts for the night-13 field episodes.  That link is
inference -- locked + translating IS the skate signature by construction, but no
bench caught the two together.  bstate= settles it next playtest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 00:50:54 -05:00
Joe DiPrimaandClaude Opus 5 39144813a4 pod: kit carries PODTEST.EGG (it was never in the repo)
First real new-build drop on the cart exposed it: the kit pushed environ.ini
and glass_layout.cfg into the fresh 4.11.817 extract, but runpod.bat launches
`-egg PODTEST.EGG` and that mission only ever existed on the pod -- mkdist
ships git-TRACKED content only, so the new install had no mission to run.
Master copy now sits at C:\bt411\ with the other kit files and podkit.ps1
installs it.

Does not affect testers: play_solo/join/play_steam pick their own missions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 19:20:23 -05:00
Joe DiPrimaandClaude Opus 5 6fcff95010 pod: the HARDWARE RIO on COM1 (L4CONTROLS=RIO:COM1,KEYBOARD)
The real cockpit board now drives the cab instead of PadRIO, frozen in the
profile so all five tester launchers get it.  Nothing above the seam changed
-- the 109-mapping L4 control table installs exactly as on a desktop, and the
cab keeps the GLASS display stack.  BT_PLATFORM=pod is NOT the way to this;
that would drag in the 1995 gauge path.  RIO:COM1 -> \.\COM1 at 9600 8N1.

Evidence the link is real, not just "the port opened":
  RIO successfully initialized!
  FAILURE.LOG: 4 missing boards (Slot 3:0, 3:2, 4:0, 5:0), 16 dead lamps
  [ctrlmap] push stick x=0.0595238 y=0   <- physical stick outside deadband
A specific 4-of-many board inventory is the proof: a dead serial line reports
the WHOLE address space missing.  Reproduced after deleting FAILURE.LOG.  The
dead lamps are the boards this partial crash cart does not have.

Banner honesty: "GLASS (PadRIO ...)" was hardcoded, so a wired cab reported
PadRIO on the very line you read to check which device won.  It now names the
resolved one -- GLASS (hardware RIO; plasma off [L4PLASMA]).

Recorded in pod-hardware.md, including that RIO and PAD are mutually exclusive
(both assign rioPointer, last token wins) and that a CENTRED stick reads x=0,
which is indistinguishable from no data -- so the by-hand check of stick,
throttle, pedals, buttons and the Ranger calibration is still outstanding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 18:11:28 -05:00
Joe DiPrimaandClaude Opus 5 05d8164801 pod: make the frozen rig survive a NEW BUILD, not just a reboot
Both frozen files live inside the versioned install and neither ships in the
zip -- environ.ini is generated on first run, glass_layout.cfg is ours.  So
Nick extracting the next build would get a cab that comes up wrong with no
error anywhere, which is exactly the failure the freeze was supposed to end.

Masters now live at the stable C:\bt411\ (podprofile.ini, glass_layout.cfg,
podkit.ps1).  setup_pod.bat pushes them into the newest BT411_* folder -- run
once per extract.  runpod.bat resolves the newest install and applies the kit
itself, so the remote launch path needs no per-build edit.  Re-tuning means
editing the MASTER: the apply overwrites the install's copy on purpose, so a
moved panel has one place to look.

Also records, for playtesting on the cab: all five tester launchers set no pod
key at all, so each inherits the rig from environ.ini without knowing the pod
exists -- verified by running play_solo.bat on the cart (7 settings applied,
not 9, because the bat sets BT_PLATFORM and BT_START_INSIDE itself and the
real environment wins).  That is the case against a separate pod-only ini: a
second file would need every launcher to opt in.

Verified: kit re-applies idempotently, and the rewritten runpod.bat brings the
cab up correct -- 9 settings, GLASS, -fit borderless, all three surfaces on
their intended displays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 17:59:57 -05:00
Joe DiPrimaandClaude Opus 5 d96fa4f4e2 pod: BT_GLASS=1 is not a gate -- name the real one (BT_PLATFORM=glass)
BT_GLASS is a compile-time #ifdef; getenv("BT_GLASS") appears nowhere in the
tree.  The line rode along from the bring-up launcher into the frozen profile
and into the pod-hardware runbook, reading like the switch that turns the
glass path on.  It never did anything -- the rig worked because glass is the
DEFAULT profile when nothing is set.

Replaced with BT_PLATFORM=glass (the real spelling, so the cart does not lean
on that default) and noted why NOT BT_PLATFORM=pod: the pod profile selects
the 1995 multi-surface gauge path, which needs the NVIDIA horizontal span no
modern driver has.  Behaviourally identical -- both land gBTPlatformGlass=1.

Re-verified on the cart: 9 settings applied, GLASS profile, -fit borderless,
all three surfaces on their intended displays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 17:44:07 -05:00
Joe DiPrimaandClaude Opus 5 654277bb7b pod: freeze the ALPHA-MR rig in environ.ini (BT_FIT, L4PLASMA=NONE)
The cart's display config was carried by a launcher .bat, so it only came up
right if the game was started one particular way.  Move it into the file the
engine already reads before anything touches the environment.

  content/environ.ini  <- scratchpad/pod/podprofile.ini, merged idempotently
                          between markers by scratchpad/pod/mergeprofile.ps1

Two gates were missing for that to be enough:

  BT_FIT=1      the env spelling of -fit, so the borderless main view does not
                depend on one launcher's command line (shortcut, scheduled
                task and autostart all have to produce the same rig)
  L4PLASMA=     NONE / OFF / 0 -> no marquee at all.  Leaving it unset does
                NOT work: the GLASS profile force-defaults it to SCREEN, which
                drops a desktop plasma window on the cab's glass.  The boot
                banner now reports the live state instead of always claiming
                "plasma window".

Also lands the bring-up engine work this depended on: monitor:<name|index>
layout binding (device-bound, not pixel-bound -- desktop rects move when a
display re-enumerates), ",bare" implying frameless, rotation-aware radar
surface sizing, BT_GAUGE_SEC_ROT accepting 0-3 (it silently forced 3 for
anything but 1), 180-degree ExpandPlaneToBGRA, and the BT_POD_CHANMAP /
BT_POD_IDENT / BT_POD_CHANTEST identification gates.

Verified on the cart, build 4.11.813, launcher carrying none of it:
  [boot] environ.ini: 9 setting(s) applied
  [boot] platform profile: GLASS (PadRIO; plasma off [L4PLASMA])
  [cockpit] -fit: borderless 800x600
  [glasswin] radar rotation 0 (none)
  ... all three surfaces on their intended \.\DISPLAYn
No [plasmawin] line in an otherwise-logging run = the ctor never ran.

KB: pod-hardware.md gains the ALPHA-MR section (mapping, the two wiring
deviations, the frozen profile, the session-0 remote-work traps) and its RGB
SPLIT "OPEN: which way is the cart wired" is now SETTLED -- it is splitter
wired, the composite is what lit it.  glass-cockpit.md documents monitor:
binding, BT_FIT and L4PLASMA=NONE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 17:40:39 -05:00
Joe DiPrimaandClaude Fable 5 bef051e837 handoff: pod bring-up + RGB split
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 12:55:30 -05:00
Joe DiPrimaandClaude Fable 5 e179c7033f BT_POD_RGB: the authentic RGB-split output -- one window per VGA PORT
Implements what the pod actually does (pod-hardware.md THE RGB SPLIT): a VGA
port's R/G/B lines each drive a separate mono MFD monitor, so a window is not
one MFD -- it is one PORT carrying up to three.  BT_POD_RGB=1 collapses the
five MFD windows into the two ports the cab drives (Port A: Comm=red,
Mfd2=green, Heat=blue; Port B: Mfd1=red, Mfd3=green) and composites each
group's planes into the colour channels, leaving the radar on its own
full-colour port.  Channel comes from the live port (GetEnableID), never a
hardcoded table, so an Eng-page swap follows automatically; BlankColor planes
contribute nothing, exactly as on the pod.  Implies BT_POD_SURFACES (bare
640x480 pictures -- the cab's buttons are physical).

Verified locally (bare windows, "RGB COMPOSITE of 2 plane(s): Comm Mfd2") and
LIVE ON NICK'S CRASH CART over the tailnet: Port A -> DISPLAY4, Port B ->
DISPLAY2, radar -> DISPLAY1, all exact-fit.  Pod scratch kit included (ssh
helper, layout cfgs for both modes, launcher; the mission-egg launcher fix --
a bare MP.EGG exits the mission loop with no relay, and BT_FE_SOLO parks at
the menu, so neither lights the panels).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 12:55:05 -05:00
Joe DiPrimaandClaude Fable 5 e0e9dcc292 KB: decode the pod's RGB SPLIT -- one VGA port drives THREE mono MFDs
Answering 'how do the panels split RGB into 3 monitors' from primary sources
rather than inference:

- content/GAUGE/L4GAUGE.CFG (the authentic 1996 pod config) configures each
  gauge port with a bit-plane mask AND A COLOUR CHANNEL: Comm=red,
  Mfd2=green, Heat=blue on clut2 (the upper row); Mfd1=red, Mfd3=green on
  clut1 (the lower row, blue spare); sec/radar = full rgb, rotation 270 (the
  portrait CRT).  Eng1/2/3 are the engineering-page twins on the same
  monitors, swapped in/out via reconfigure() with 'blank'.
- L4GraphicsPort::BuildSecondaryColor (L4VB16.cpp) proves the mechanism at
  T0: it walks the palette entries owned by the port's bit group and writes
  exactly ONE component (RedChannel->Red, GreenChannel->Green,
  BlueChannel->Blue, AllChannels->whole triplet); BlankColor blanks the
  group.  So one palettized framebuffer emits three independent pictures on
  the R/G/B analog lines, and the splitter feeds each line to its own mono
  monitor -- which is also what the '1280x480 horizontally spanned' MFD
  surface actually is: two VGA outputs x three channels.

Port consequence recorded: the per-panel window path (BT_POD_SURFACES) is
right for per-panel outputs but WRONG for splitter-wired glass, which needs a
channel-composite mode (three planes -> one RGB image, pure primary tints).
ExpandPlaneToBGRA already does the per-plane half.  Open: how Nick's cart is
actually wired.  Also lands the pod bring-up scratch (ssh helper, layout cfg,
launcher, firestorm repo browser).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 12:35:39 -05:00
Joe DiPrimaandClaude Fable 5 51dff6bd5a handoff: #60 closed (census + re-export)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:12:20 -05:00
Joe DiPrimaandClaude Fable 5 5e17bb2262 handoff: Cyd merge + re-cut 801
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:51:14 -05:00
Joe DiPrimaandClaude Fable 5 3e69944293 handoff: cut 4.11.797
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:27:03 -05:00
Joe DiPrimaandClaude Fable 5 58c3db090b KB: crouch eye-drop residual was false -- measurement error, corrected
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 08:55:29 -05:00
Joe DiPrimaandClaude Fable 5 18418af8fd handoff: crouch complete
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 00:50:39 -05:00
Joe DiPrimaandClaude Fable 5 591d205b19 CROUCH complete: full cycle + MP replication verified
The WIP's 'pose does not hold' was a chain of bench-instrument errors, not a
code bug: every capture ran in COCKPIT view (the pilot cannot see their own
legs; the eye-height residual masked as reversion).  Joint probes prove the
park holds indefinitely (knee 1.138, root -2.219 steady); the 2-node bench
shows the observer's replicant fully crouched and held (duckmpA_031 -- the
type-3 state record carries it with zero new replication code); the second
scripted press (new BT_BTNTEST2 env) verifies RISE -> standing zeros.
MP button delivery confirmed mode-mask-clean (the one miss was round-start
jitter).  Diags added, all BT_DUCK_LOG-gated: SetLegAnimation re-arm tracer,
1 Hz joint probe, RIO press mode-mask, BT_TREE_LOG topology dump, and the
squat-park log.  RESIDUAL filed: pilot's own cockpit eye does not ride the
root drop (DPLEyeRenderable chain composition; cosmetic).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 00:50:12 -05:00
Joe DiPrimaandClaude Fable 5 d44fae2ae3 KB + handoff: searchlight reconstruction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 19:46:09 -05:00
Joe DiPrimaandClaude Fable 5 412053d5af Searchlight reconstruction -- the pod's night kit, both halves
The subsystem (sim/toggle/attribute/replication) was already complete; this
lands the missing VISUALS, decoded from MakeMechRenderables @004cef28 case
0xbd8 (raw pseudocode part_014):

- COCKPIT: the 1995 searchlight is a FOG SWAP -- the @00456778/@00456814
  watcher switches DPLRenderer::SetFogStyle between the authored fog= (lit)
  and nosearchlightfog= (dark) sets per map/time page in BTDPL.INI.  The
  engine kept the whole system under its real names; completed the stubbed
  plane application (currentFogNear/Far) and transcribed the watcher (with
  its inverted-cache seed) into TickSearchlight.  CONSEQUENCE: night now
  STARTS on the authentic dark set (near-plane 5u on arena pages) -- our
  builds had rendered the searchlight-ON fog permanently.
- EXTERNAL: spot.bgf beam cone hung on the searchlight SITE joint, shown/
  hidden from the replicated LightOn attribute (@0045612c watcher).  Site
  segments now build geometry-less DCS children (posed + parentable, as the
  1995 graph did) -- previously they were skipped entirely.
- searchlight.hpp: commandedOn @0x1DC identified as mountSegment (resource
  segmentIndex; the cone's mount joint).

Benches: searchfog.sh (solo cockpit: first-tick dark sync, F5/0x14 press ->
SetFogStyle(2), red-fog probe end-to-end), spotcone.sh (2-node: B's button ->
lightState replication -> A logs "[spot] cone SHOWN (seg 20)").  Cone look
(size/aim on the mount) pending an eyeball pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 19:44:23 -05:00
Joe DiPrimaandClaude Fable 5 b31c6c4527 handoff: score report tail reconstruction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 18:38:48 -05:00
Joe DiPrimaandClaude Fable 5 91bd28669e #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 <noreply@anthropic.com>
2026-08-05 18:36:58 -05:00
Joe DiPrima 06a8edbff2 bench: #89 gate-OFF negative verified (advDmg=0 -> gated, zero splash) 2026-08-05 17:04:58 -05:00
Joe DiPrima 5a6ee51f3a handoff: #89 death blast 2026-08-05 16:53:05 -05:00
Joe DiPrimaandClaude Fable 5 297127d0d7 #89 DEATH BLAST reconstructed: a dying mech splashes its neighborhood
The missing half of Advanced Damage, found by call-scanning
Explosion::SplashDamage @0042fad0: TWO callers, not one -- Missile::Perform
(the known #62 path) and 0x4a0bda, the UN-EXPORTED tail of
Mech::TakeDamageMessageHandler itself.  Raw disasm @0x4a07b8-0x4a0bda:
when the victim ENTERS dead(9)/eject(10) during the applications, the
binary sets the wreck burning (id 0x17, deferred -- handler not yet
reconstructed), spawns the death Explosion (model 0x31 -- our death-list
visuals stand in), and SPLASHES:
  gates : owning player's advancedDamageOn (+0x264) AND NOT
          suppressConsole (+0x258 -- eject sets it: PUNCH-OUTS NEVER
          BLAST, the authentic anti-suicide-bomb rule)
  damage: type 2 Explosive, amount = deathSplashDamage (mech+0x520),
          bursts = round(0.001 * moverMass * 15.0) -- scales with tonnage
  radius: deathSplashRadius (mech+0x524); per-victim falloff
          bursts/dist^1.25 in the shared core
Draco's collision-divert suspicion is settled: the blast is TYPE 2, the
divert never touched it -- the tail was simply never reconstructed.

Port: deathSplashDamage/Radius PROMOTED from the Wword scratch bank to
named Mech members (the bank is one GLOBAL array -- authored per-chassis
values were clobbered to the last-loaded mech); BTSplashCore split out of
the #62 weapon splash and shared; BTApplyDeathSplash + the death-edge arm
in the handler tail; BTPlayerConsoleSuppressed bridge (friend).

Bench (2-node, B parked 8.9u from a self-destructing A): blast fired with
authored madcat data (radius=50, amount=5, mass=75000 -> 1125 base
bursts), B took 73 bursts (falloff exact: 1125/8.91^1.25), cross-pod
delivery + cylinder spray verified on B's own log ([dmghit] type=2
burst=73 across zones).  ~365 damage at 9u -- Draco's 'double kills on
drops' economy restored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 16:52:32 -05:00
Joe DiPrima 85d6ef4a59 handoff: skate campaign closed, ready to cut 2026-08-05 13:58:03 -05:00
Joe DiPrimaandClaude Fable 5 6dd1212e0c #52 campaign close: full transition matrix PASSES at 2 nodes -- field instrument rides
Provocations run under the verified [skate] detector: sustained healthy
walking, 7x respawn-while-moving, gimp onset at speed, ~21s of sustained
GIMPED walking (aimed-leg self-damage: BT_SELF_DAMAGE_ZONE=dz_ldleg),
leg-destruction death (authentic: lvl 1.0 -> leg gone -> fall/death; the
mid-session "died of the gimp edge" reading was a capped-print artifact,
retracted), and respawn.  ZERO skate anywhere.  Peer gimp replication
VERIFIED live: observer reads gl=3 + sim=3 with the gimp bodyStates
cycling for the whole master limp window (#82 remains fixed).  Conclusion:
the field skating does not reproduce at lab scale; the detector + SKATE
matchlog record ship with the next cut and the field names the failing
case.  Bench scripts archived (skatebench3-7; 7 is the clean-room one --
the sed-derived chains dropped envs twice).

Also: BT_LAMP_LOG=1 joins the field bats (#135 -- lamp/annunciator edge
forensics; near-zero noise, answers leak-no-flash reports in one grep).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 13:58:01 -05:00
Joe DiPrima 869d885eb2 handoff: skate detector state 2026-08-05 12:41:57 -05:00
Joe DiPrimaandClaude Fable 5 9604f4c492 #52 SKATE detector (ungated field forensic) -- negative-verified; local repro eludes
The night-12 field logs eliminated record starvation (zero [ghost] during
three observed skating windows), so the bug lives in gait APPLICATION on
peers.  This adds the [skate] detector to the death-handler tick: a
replicant moving >0.08 u/frame for 90+ frames with BOTH animation
channels idle (legCycleSpeed + bodyCycleSpeed ~ 0) logs one line per
episode + a SKATE matchlog record carrying the discriminating inputs
(legCyc/bodyCyc/cmdSpd/destroyed/mode).

Honest history: the first build keyed on legCycleSpeed alone and
false-fired on every healthy movement phase -- the current peer
architecture poses joints from the BODY channel (s_peerLegCh=0,
AdvanceBodyAnimation mj=1), so legCycleSpeed==0 is NORMAL there.  Caught
same-session by the [gimpfeed] silence (AdvanceLegAnimation never runs
on peers); corrected to channel-agnostic before anything shipped.

Bench (skatebench2.sh, 2-node, autodrive walker + kill every ~40s):
7 death/respawn cycles, ZERO skate hits either side -- no false fires,
and light local conditions do NOT reproduce the field skating.  Next
provocations: leg-GIMPED walker (the #82 family transition) and 6-player
load; otherwise the detector rides the next cut and the field names the
failing case for us.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 12:41:55 -05:00
Joe DiPrima 6d7946a264 handoff: night-12 log scan results 2026-08-05 11:47:30 -05:00
Joe DiPrima 754518b587 handoff: #131 fixed with receipts + build ritual note 2026-08-05 11:13:17 -05:00
Joe DiPrimaandClaude Fable 5 546aabd5ba #131 false lock FIXED: miss-means-miss -- the pick answers only for drawn geometry
Night-12 field report (Ronin/Conn Man/Oracle, blackhawk-correlated): lock
ring lit with the reticle visibly off the mech + no-reg complaints.  Root
cause: TWO port stand-ins answered where the 1995 card (which cast against
the DRAWN geometry) would miss -- the pick's any-object sphere fallback and
the caller's whole-mech AABB fallback.  The regime that exposes them: a
LEVEL boresight over a SHORT mech -- the blackhawk's mesh tops out below
eye-ray height, so the ray clears every triangle but pierces the fat cull
spheres; the ring lights with the reticle above the mech's head (the
operator watched exactly this on the sweep bench).  Careful aimed-down fire
rides triangles, which is why Oracle's per-panel audit passed on the same
build.

Fix: MechSegmentPick returns 1=drawn-geometry hit / 0=TRUE MISS / -1=no
render tree; the sphere may answer ONLY for a mesh the reader cannot parse
(pm==0 -- currently none exist: counters objs/invFail/noTri all clean);
the AABB survives ONLY as the pre-tree replicant grace.  A readable mesh
the ray misses is a MISS -- no lock.

Verified (2-node vs bhk1 at 100u, all runs on force-relinked string-
verified exes after today's stale-link flake):
- LEVEL lock-sweep: 0 locks all run (pre-fix: lock band from 168 sphere
  answers; picksrc tri=0 sphereFB=168).
- DOWN-PITCHED sweep: locks return 100%% tri-sourced (tri=158 sphereFB=0),
  landing on real parts (rarm/ldleg/rdleg) with honest gaps.
- Full zone-walk matrix: tri=18874 sphereFB=0 box=0; victim took 156 hits
  across 16 zones incl. both side torsos -- combat un-regressed.

New instruments (all env-gated): BT_LOCK_SWEEP=<axis> torso pan (the
operator-visible lock-envelope bench), [locksweep] transition log,
BT_LOCK_ENVELOPE synthetic unit-sweep probe, [picksrc]/[pickbox] source
telemetry with objs/invFail/noTri localization counters.
Bench: scratchpad/night12/zonewalk_bhk.sh.

NOTE for the field: locking is now strictly TIGHTER (ring = reticle truly
on the machine).  If era testers feel the pods were more forgiving,
Draco's "slight lock linger" memory becomes a deliberate investigation
(sourced hysteresis), not an accidental sphere halo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 11:12:43 -05:00
Joe DiPrima f279e38707 handoff: night-12 field results -- 10 closed, 8 filed (#131-#138) 2026-08-05 09:35:24 -05:00
Joe DiPrima 8b887f6cec handoff: #124 per-panel correction 2026-08-04 17:58:09 -05:00
Joe DiPrimaandClaude Fable 5 1d3616cceb #124 CORRECTION: aimed hull hits resolve the struck dz_* PANEL -- players were right
The 08-03 zone rule (struck SEGMENT's SKL dzone, ALWAYS -> every aimed
torso hit = center torso) was wrong, and era players' pushback caught it.
Byte-level proof: MAD_TOR.BGF zone-tags the hull PER PANEL (dz_utorso x36,
dz_ltorso/rtorso x18, dz_dtorso x16, all four rear panels, searchlight);
the dpl hit result kept GEOGROUP granularity (dplHitGeoGroup, T0); and the
binary's segment->zone map @49db20 has NO runtime caller (raw call-scan:
sole caller = CreateStreamedDamageZone, load time) -- no segment-level
collapse mechanism exists.  Oracle's night-10 'only LCT gets hits' audit
was the BUG's fingerprint, not the pod's design.

Fix: MechSegmentPick attributes the struck triangle to its draw op (index
range) and takes the op's .DZM-bound zone -- the #87 armour-darkening
bindings, the same authored patch->zone mapping that already paints the
panels -- with the segment dzone as the untagged fallback.  ZoneAimPoint
now aims hull zones at their patch CENTROIDS (all hull zones previously
shared the chest cull-center), which also upgrades the zone walker.

Bench (2-node zone-walk vs spinning madcat, zonewalk_madcat.sh): every
hull panel resolves individually -- utorso 11/12 in-zone, no L/R
mirroring, all four rear panels register; misses are the panel facing the
shooter mid-spin (correct geometry, not misattribution).  Victim applied
254 hits spread across every panel family.  Was 102/102 hull aims ->
dtorso before the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:57:14 -05:00
Joe DiPrima 98234c8b15 handoff: 4.11.771 re-cut with Cyd's lamp sweep 2026-08-04 16:56:10 -05:00
Joe DiPrima 26b5743f9a handoff: 4.11.767 cut -- field-night checklist 2026-08-04 16:37:17 -05:00