Commit Graph
841 Commits
Author SHA1 Message Date
Joe DiPrimaandClaude Opus 5 832bec0966 hud.cpp: byte-ground the HudSimulation constants -- all five were stand-ins under guessed names
Found chasing the Thor "no range finder" report: _DAT_004b7ec4 was documented
as two incompatible things -- the 0.75 LOCK damage threshold (mech4.cpp:6325)
and a "heat threshold for HUD page visibility" valued 0.0f (hud.cpp:59).

The .rdata settles it (reference/decomp/section_dump.txt):

     4b7ec0  8be55dc3 0000403f 0000803f 0000c842
     4b7ed0  00000000

  _DAT_004b7ec4 = 0.75f    _DAT_004b7ec8 = 1.0f
  _DAT_004b7ecc = 100.0f   _DAT_004b7ed0 = 0.0f    _DAT_004b7f90 = 0.0f

mech4.cpp was right on both thresholds.  hud.cpp's whole tuning block was
wrong -- every entry a 0.0f/500.0f stand-in, and three of five names named the
wrong mechanism:

  * ec4/ec8 are the fire-control LOCK limits (own HUD host zone < 0.75 damage,
    targeted zone < 1.0), NOT heat/page-visibility.  A shot-up cockpit drops to
    "target held, no lock"; a dead zone cannot be re-locked.
  * ed0 is the shared ZERO -- the right-hand side of the range-slide Abs()
    idiom (`dt * 500.0 <= 0.0` picks the sign) and of an `== 0.0f` test at
    @0x28C.  The 500 m/s slide rate is an IMMEDIATE (0x43fa0000).  The old
    "MaxTorsoSlew = 500.0f" read that backwards.
  * f90 (FlickerFloor 0.0f) was the only correct entry.  Its decay RATE is the
    object's own @0x298, not a constant -- the step-6 banner said "up to
    MaxTorsoSlew (500/sec)" and is corrected too (hud.cpp:229 already had it
    right, so the file disagreed with itself).

All four wrong constants were DEAD (zero code uses; MaxTorsoSlew appeared only
in a comment), so this changes no behaviour -- it stops the next reader
trusting them.  Renamed to what they are: LockOwnZoneDamageLimit,
LockTargetZoneDamageLimit, RangeBias, HudZero.  Builds clean.

GAP FOUND, filed not fixed: HudSimulation subtracts _DAT_004b7ecc (100.0f)
from RangeToTarget@0x1EC every frame while the timed flag @0x22C is set
(timer @0x21C accumulates to @0x1D8, then both clear).  Our targeting step
does the 500 m/s slide but never this bias, so the authentic timed -100 m
range offset is missing.  What sets @0x22C is unidentified.  -> open-questions.

KB swept: no context/ or docs/ file repeated the wrong constants (gauges-hud's
0-1200 ladder / 500 m/s / pegs-at-1200 claims are all correct); the error was
confined to hud.cpp.  gauges-hud.md gains the byte-grounded table + the gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 06:11:59 -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 5b7e481913 scoring: mech+0x354 is VESTIGIAL -- label it so nobody "finishes" it
MECH_DAMAGE_BIAS(m) returned 0.0f under a comment reading "bring-up: factor =
0*bias+1 = 1", which invites a future session to wire it up.  Auditing
Mech::Reset settled what it actually is, and 0.0f turns out to be EXACT:

  * mech+0x354 has exactly ONE writer in the image -- Mech::Reset (@0049fb74,
    part_012.c:14340).  Nothing touches it during play.
  * Reset computes mean(zone + 0x158) across every damage zone, AFTER the zone
    heal has already zeroed those cells.  So it is ~0 the moment it is written,
    stays ~0 for the mech's whole life, and is recomputed as ~0 next respawn.
  * It has exactly ONE reader -- CalcInflictedScore (@004c052c,
    part_013.c:19055) -- as `avg * role.damageBias + 1.0`.

So the factor is 1.0 for the entire game and the stand-in reproduces the
binary exactly.  0x358 and 0x35c are the same computation over subsystem zones
and have NO reader at all.

This also raises confidence in the night-13 scoring work: the 505.88 kill award
was not right DESPITE a missing term -- the term genuinely is 1.0.  Wiring
0x354 to live accumulated damage would silently inflate every inflicted and
kill award, and both chart-verified numbers (+1 a damage point, +500 a kill)
assume 1.0.

Comment rewritten at the macro; combat-damage.md carries the same finding [T1].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-08 00:19:20 -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 03c4d55672 #142 crouch: duckState is a THREE-state posture -- the strip is an animation
Operator confirmed on screen: bduck.pcc is a real duck ANIMATION, and stepping
duckState 0->1->2 plays it.  So the attribute is not a flag:

    0 = standing        1 = moving between        2 = crouched

Everything else was already right -- asset, element (OneOfSeveralPixInt
@004c5204), factory registration, L4GAUGE.CFG:5001, and the attribute binding
(new [gauge] receipt confirms 'bduck.pcc' frames=3x1 attr=BOUND).  We were
writing a two-value flag into a three-frame strip, so frame 2 was unreachable
and the cockpit saw a snap: "it lights up and sticks, no animation".

The handler is back to the binary's exact write (duckState = 1, @0049fa00).
That value now MEANS the middle frame, so the press gives immediate visual
feedback and the earlier toggle divergence is retired.

Needed a separate duckRequest cell, which I tried twice to avoid:
  * duckState cannot be both the request and the display.  Settling it to the
    real posture destroys the request, so on the frame the squat clip parked
    the consumer read "crouched + pending" and issued the opposite direction --
    69 transitions from 2 presses, benched, twice.
  * reading the CACHED legAnimationState instead of the alarm made it worse:
    the cache refreshes only at the top of AdvanceLegAnimation, so right after
    SetLegAnimation it still reads the old state.  Read the alarm.
duckRequest is port-only, appended, never read by offset.

Also fixes a silent failure in the gauge factory: the missing-image path used
DebugStream -- the no-op ReconStream (project gotcha) -- so a strip that failed
to load reported NOTHING.  Now DEBUG_STREAM, plus an ungated one-line receipt
per element naming the image, frame grid, port and whether the attribute BOUND
or came back NULL.  That receipt is what proved the element was healthy and
sent me looking at the value instead of the plumbing.

Benched (crouch142.sh, madcat): 2 presses -> exactly 2 transitions,
SQUAT -> parked (settles to 2) then RISE (settles to 0).  Refusal while moving
still holds (posture=0, authentic per Lynx).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 23:35:37 -05:00
Joe DiPrimaandClaude Opus 5 fcd1a0ca8d #142 crouch: refuse-and-snap, not queue -- and honour the must-be-stopped rule
Fixes a regression I introduced in 59f53da.  That revision retried the request
when the posture gate was not ready, which is worse than the drop it replaced:
benched, a crouch tapped at a walk QUEUED for 41 seconds (41 [duck] WAITING
lines) and would fire the instant the pilot stopped -- while duckState stayed
1, so the cockpit symbol read "crouched" for the whole time a STANDING mech
walked around.  duckState is what the gauge strip draws; it has to tell the
truth.

Now: if the gate refuses, snap desired back to actual (duckState = duckActual)
and say so once, throttled.

This also confirms the authentic rule rather than assuming it.  Benched, a
crouch pressed while driving gives posture=0 and no squat -- exactly Lynx:
"When a mech STOPS, crouch button lowers its stance."  Immobilization while
crouched looks EMERGENT rather than gated: the leg channel parked in 'sqd'
produces no root motion to travel on, and the operator's read ("i think you
cant walk when you crouch") matches.  No [skate] in the driving case either.

Also guards re-issue: while 'sqd'/'squ' is playing (legAnimationState 2 or 3)
the transition owns the channel, so want != actual no longer re-fires
SetLegAnimation every frame.

Benched both cases:
  stopped  duckState -> 1 (crouch) -> SQUAT -> holds -> -> 0 (rise) -> RISE
  moving   REFUSED (not stopped): posture=0 ... duckState 1 -> 0
           0 squats, 0 queued waits

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 16:39:05 -05:00
Joe DiPrimaandClaude Opus 5 59f53da07b #142 crouch: duckState is the POSTURE the cockpit animation reads
The crouch symbol animation is fully present and we were starving it.

    content/GAUGE/BDUCK.PCC                         the 3-frame strip
    OneOfSeveralPixInt  @004c5204/@004c52d8         the element, reconstructed
    btl4grnd.cpp:144                                registered in the factory
    L4GAUGE.CFG:5001                                oneOfSeveralPixInt(
                                                      E,ModeAlwaysActive,
                                                      bduck.pcc,3,1,DuckState)
    ATTRIBUTE_ENTRY(Mech, DuckState, duckState)     attribute 0x37

A 3-frame mech symbol beside the CROUCH button, indexed by duckState -- the
standing<->crouching animation Lynx and Draco describe.  It never played
because the consumer zeroed duckState the frame after the press, in BOTH
directions, so the strip sat on frame 0 with a one-frame blip to frame 1.
That is the field report verbatim: "button flickers sometimes on press ...
state does not change.  Remains in stand mode."

THE ZEROING WAS OURS.  Every writer of +0x398 in the export is the
DuckRequest handler (=1) and Mech::Reset (=0).  FUN_004a9b5c -- the master
perf, which contains the address the old comment cited as "the DuckRequest
consumer (@0x4aa011)" -- does not reference 0x398 at all.  mech.hpp's own note
already said "duckState has NO code reader anywhere in the decomp ... whatever
consumes it consumes it through DATABINDING".  The databinding consumer is
this gauge strip, and we were clearing it behind the gauge's back.

Restructure: drive on DESIRED vs ACTUAL.  duckState is the desired posture;
the parked leg alarm is the actual.  Act only on a mismatch -- no re-fire, and
nothing clears the attribute.  A frame where mapPosture is not ready now
RETRIES (throttled [duck] WAITING) instead of silently dropping the request,
which retires the old "request consumed, posture=N" miss as well.

ONE DOCUMENTED DIVERGENCE: the handler now TOGGLES.  The binary writes a bare
1 and clears the cell only in Mech::Reset, with no per-frame reader, so a
second press could never rise -- and a pod pilot's second press must un-crouch
(Lynx: "Mech is immobilized until crouch is pushed again, and mech rises").
One cell, same meaning, noted at the site.

Benched (crouch142.sh, madcat):
  duckState -> 1 (crouch) -> SQUAT -> [holds 1 while crouched] ->
  duckState -> 0 (rise)   -> RISE
Value now persists across the crouched period instead of blipping, so frames
0/1 of the strip are reachable and stable.  Also removed the interim REQUEST
DROPPED receipt: after the restructure nothing is dropped, and a receipt that
says otherwise is a trap for the next session.

STILL OPEN on #142: no immobilization while crouched (Lynx) -- nothing gates
movement on duckState or the parked leg alarm.  A driven mech with a parked
leg channel is the [skate] signature (#52), so it may not be cosmetic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 16:31:20 -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 29b4d68ba6 scoring: the +1000 START grant -- BT's MissionStarting override was never ported
Seventh chart row.  BT overrides MissionStarting purely to seed the score, and
the override was missing, so MESSAGE_ENTRY(BTPlayer, MissionStarting) resolved
to the inherited engine handler (which only does the fade-in) and the grant
never happened.

    FUN_004bfbe8(player):
        base_MissionStarting(player);
        if (app->state == 4 && (player[0x29] & 0x40) == 0)
            player[0x1c8] = 0x447a0000;          // = 1000.0f

Both operands decode exactly against engine headers: application state 4 is
LaunchingMission (APP.h -- same enum whose 6 is EndingMission, already used by
the console flush), and simulationFlags bit 14 is NonScoringPlayerBit
(PLAYER.h: NonScoringPlayerBit = Entity::NextBit), so `(+0x29 & 0x40) == 0` IS
IsScoringPlayer().  Camera-ship/spectator players are non-scoring and correctly
get nothing.

CELL NOTE: the binary seeds the ENGINE cell (+0x1c8), not BT's own (+0x278) --
1995 carried two accumulators, which is why the KB suspected the pod's death
cost "may never have displayed".  Our port has one currentScore, so grant,
awards and death cost land together and the chart reads coherently.

Also resets the console watermark so a fresh mission REPORTS the grant rather
than a difference from last round's tally.

Benched: both players "[score] mission start: player N:1 seeded to 1000",
scores run 1001.98 -> 1908.64 with kills=1 (1000 + ~400 damage + 505 kill).

Also corrects a FOURTH copy of the dead-code claim, in btplayer.hpp's ScoreType
enum ("type 0 has NO scoring arm ... per-hit inflicted credit never existed").
Its byte-scan was right that no TABLE entry binds @004c0200 and wrong to
conclude unreachable -- the vtable Dispatch override calls it directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 11:43:54 -05:00
Joe DiPrimaandClaude Opus 5 98082e64a0 KB sweep: retire the three claims that WERE the scoring bugs
Night 13 turned up three confident KB/source notes that each closed off a
working path, and each one was the defect:

  1. context/combat-damage.md -- "@0x4c0200 is in NO table entry: dead code",
     used to justify retiring per-hit inflicted credit in build 787.  It is
     reached through BTPlayer's Dispatch override (vtable @00513300 slot 3).
     (corrected in 2772175)
  2. context/decomp-reference.md -- "shipped content authors NO role keys, so
     the cost is 0 in the field".  Wrong on both halves: the fields come from
     the role MODEL's GameModel record, not notation keys, and dfltrole
     authors killBonus=500 / deathPenalty=500 / dmgInf=1 -- the manual's chart
     verbatim.  The cost read 0 because the role was never bound.
  3. docs/RECONCILE.md -- "role registry has no WinTesla analog -> stubbed;
     base-set scenarioRole stands".  The base ctor sets it NULL, so nothing
     stood, and Mission::GetScenarioRole IS the analog.

Common shape worth remembering: all three asserted an absence (dead code, no
authored data, no analog) and none was re-tested against the binary before
being built on.  An absence claim in this KB should carry the check that
established it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 11:22:47 -05:00
Joe DiPrimaandClaude Opus 5 e0b91df3e1 scoring: CORRECTION -- the death cost was never missing; my arithmetic was
Retracts the "open item" claimed in 2fcce53.  An ungated [deathcost] receipt
at the block settles it:

  [deathcost] player 2:1 advDmg=1 role=bound penalty=500 scoreBefore=-779
              -> APPLYING

It fires once, on a self-kill, exactly as it does on a combat death.  There is
no combat-vs-self asymmetry.

WHY I GOT IT WRONG: the cost is dispatched by a DIRECT
Player::ScoreMessageHandler() base call, so it never reaches the BT matchlog.
I computed the total from the LAST matchlog row and found no -500 in it -- but
that row is emitted BEFORE the unlogged cost.  I had noted the bypass one
message earlier and still failed to apply it to my own sum.  The lesson is the
usual one: a value that cannot appear in the log you are reading is not
evidence of absence.

The receipt stays.  A debit that moves the player-visible score while being
structurally invisible to the forensic log is exactly the kind of thing that
should announce itself.

Chart status after this: -500 special-case death cost VERIFIED applying.  The
-1000 eject ROW remains unverified -- its components (self-damage credit,
negated self-kill award, death cost) are each verified, but no real punch-out
has ever fired in a bench, so the total is still arithmetic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 11:20:20 -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 b2498ca39a scoring: the type-0 arm must RETURN, not break -- it was clobbering scoreAward
Chasing the duplicate rows from 1324c81 (80 real awards + 80 reading
award=0.00).  Not a double delivery -- Entity::Dispatch sends exactly once on a
replicant.  It was the `break` I left in the delegating arm.

After delegating to ScoreInflictedMessageHandler, control fell into
ScoreMessageHandler's post-switch tail, where the LOCAL `award` is still 0:

    message->scoreAward = award;             // clobbered to 0
    BTMatchLog("SCORE", ... award=0.00 ...); // the phantom row
    Player::ScoreMessageHandler(message);    // base: currentScore += 0

Harmless to the total only because the value added happened to be zero -- but
it mutated a message on a shared path and ran a base handler for nothing.  A
later reader of scoreAward, or any side effect gained by that tail, would have
turned it into a real bug with no obvious cause.

ScoreInflictedMessageHandler is self-contained (accumulates, ForceUpdate()s,
logs its own receipt), so the arm returns.

Re-benched cross-node:
  rows on shooter's master  100, ZERO phantom rows (was 80 + 80)
  rows on victim's node     0
  running total             417.85, climbing continuously, no resets
  type-0 Verify rejects     0
  kill path                 intact (kills=1)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 10:00:24 -05:00
Joe DiPrimaandClaude Opus 5 1324c81719 scoring: land inflicted credit on the OWNER's machine (Steam + console safe)
Completes 2772175/e82f54c.  The interceptor restored the credit; this puts it
on the right node, so a player's score accumulates again.

The operator corrected two of my claims, and both were load-bearing:

1. Scores DID accumulate before build 787.  Checked: build 774 already had
   `currentScore = 0` in the console flush, so the flush was never eating
   score.  My "score zeroed every interval" theory is dropped.  The watermark
   from e82f54c stays only because it is harmless and keeps a master's own
   total intact across a flush -- it was not fixing a field bug.

2. The reroute works, and 774's own comment says so: the killer's player is a
   REPLICANT, so Entity::Dispatch reroutes to the owning host
   (ENTITY.cpp:244-251) and the credit lands on the killer's OWN machine.
   That is how kill credit has always crossed nodes.

So the earlier master-only gate was the right idea and failed for a reason I
guessed wrong.  A rerouted message arrives over the WIRE through Receive(),
which goes straight to the handler table -- the virtual Dispatch override is
never called on the receiving side.  Type 0 therefore landed in
ScoreMessageHandler's arm, which Verify-rejected it: award 0.00.

Fix is both halves:
  * Dispatch intercepts on a MASTER only -- local delivery stays exactly as
    @004bffa0 does it;
  * ScoreMessageHandler's type-0 arm DELEGATES to ScoreInflictedMessageHandler
    instead of Verify-rejecting -- wire delivery gets the same handler.
One accumulator, on the machine that owns the score.

Works for Steam today (no console tally exists -- btconsole.py/btoperator.py
handle no score at all) AND for a real operator console later: the console
flush is untouched and still ships authentic deltas under the owner's ownerID.

Benched (cross-node zone-walk kill):
  credit node      shooter's master only (victim's node banks 0)
  running total    253.60 and CLIMBING, no resets
                   (was: peaks ~35, snapping back every few seconds)
  type-0 rejects   0
LOOSE END: each real award is followed by a duplicate row with award=0.00
(80 real + 80 zero).  Harmless -- the total is unaffected -- but it means the
report is delivered twice on the owner; not yet explained.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 09:52:26 -05:00
Joe DiPrimaandClaude Opus 5 e82f54c957 scoring: the score AUTHORITY is the operator console -- and our port has none
Follow-up to 2772175 (type-0 interceptor restored).  Benching the restored
credit exposed the next layer, and two of my attempts at it were wrong; both
are recorded so they are not retried.

FINDING [T1]: the binary flushes ConsolePlayerVTVScoreUpdate(ownerID,
currentScore) then does `param_1[0x9e] = 0` -- UNGATED.  So +0x278 is a console
DELTA, never a running total, and it does not matter which NODE computed one:
every delta is stamped with the scoring player's ownerID and the CONSOLE
accumulates.  That is almost certainly where the manual chart's "+1000 starting
the game" was seeded, which is why no game-side code grants it.

Our port has no console as score authority.  GetScore() (SCORE gauge),
CalcRanking() and the replicated Player__UpdateRecord all read +0x278 on the
OWNING node.  Damage is applied on the VICTIM's node, so block B dispatches the
inflicted report to the SHOOTER's player object there -- a REPLICANT -- and the
master's next update record overwrites it.  Benched: totals climb to ~35 and
snap back every few seconds.  THAT is the "scoring went screwy" report.

WRONG TURN 1 (reverted in spirit, kept only where harmless): blamed the console
flush and added a last-sent watermark so the console still gets deltas while
+0x278 keeps a total.  The resets were 2s apart, not on the 10s console
interval -- the timing was already in the data.  The watermark stays because it
does stop the FLUSH from zeroing a master's own total, but it was not the bug.

WRONG TURN 2 (reverted): gated the interception to MasterInstance so a
replicant would reroute to the master.  The message arrives, but the BT
extension fields (damageAmount@+0x24, senderMechID@+0x34) do NOT survive the
wire -- only the base scoreAward -- so every award computed 0.00.  That failure
is the clue to the answer: the kill report (type 2) credits cross-node
correctly precisely because its value rides scoreAward.

FIX SHAPE (not implemented -- landing it deliberately rather than guessing a
third time): compute the award on the victim's node, where the damage data
lives, and ship the RESULT in scoreAward the way the kill report already does,
instead of shipping the basis and recomputing on a machine that cannot see it.

State now = binary-faithful unconditional interception.  Re-benched: 79
inflicted rows, awards 0.98..25.00 all positive and tracking damage, 0 type-0
Verify rejections.  Cross-node banking still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 08:41:59 -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 d213c980a9 fix: MFD panels never appeared with the HARDWARE RIO (PadRIO coupling)
Nick ran the cab with the real board and the mission came up on the main view
with every MFD dark -- and not one [glasswin] line in the log.

BTGlassPanels_Create() was called only from the END OF THE PadRIO CONSTRUCTOR.
The panels began as the frames around the on-screen RIO button banks, so "the
buttons ride the device" was reasonable then; it is backwards on a real cab.
L4CONTROLS=RIO:COM1 means PadRIO is never constructed, so the MFD windows were
never created -- silently, since nothing is wrong from the display layer's
point of view.

The panels are a DISPLAY concern.  Creation moves to LBE4ControlsManager,
after the L4CONTROLS parse where every device branch converges, and a
symmetric BTGlassPanels_Destroy() goes in its destructor: ~PadRIO tore them
down, ~RIO knows nothing about them, and windows outliving the surfaces they
blit would crash on the next mission cycle.  Both calls are idempotent
(Create returns on gWinCount != 0), so the proven PadRIO path is unchanged
and simply arrives first.  Both guarded by BT_GLASS -- L4GLASSWIN is only in
the build when the gate is on.

Verified on the cart: hardware RIO up AND all three surfaces on their intended
displays in the same run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 19:05:04 -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 99956d54e3 tools: podshell_setup.ps1 -- one-shot remote-shell setup for the pod PC
OpenSSH server + a single authorized key + a private/domain-only firewall
rule, so bring-up can run over a tailnet instead of by hand through Chrome
Remote Desktop (CRD paints a canvas -- unreadable to tooling; text and logs
need a real shell).  Handles the Windows administrators_authorized_keys ACL
quirk, refuses to run on pre-Win10 (the period-pod case, which stays on the
clipboard/probe route), never opens the public profile, and prints the exact
ssh line including the Tailscale address.  Undo steps in the header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:48:13 -05:00
Joe DiPrimaandClaude Fable 5 38b08c96d1 podprobe: run-compatibility verdict + XP-safe .bat fallback
The crash cart may BE the period pod PC (that is where NVIDIA Horizontal Span
still exists), in which case the first question is not the display map but
whether btl4.exe can launch at all -- a modern MSVC toolset needs Win7 SP1+.
The probe now states the verdict outright, and podprobe.bat covers the case
where PowerShell/.NET is not present to run the probe in the first place
(wmic + dxdiag, both XP-era tools).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:35:38 -05:00
Joe DiPrimaandClaude Fable 5 67a4f09e86 POD MFD bring-up: BT_POD_SURFACES bare-panel mode + placement receipts + probe
Nick's crash cart is the first shot at driving the real MFD panels, so the
pieces the cab needs that the desktop glass path lacked:

- BT_POD_SURFACES=1 (L4GLASSWIN): crop every display window to its SURFACE
  (MFDs exactly 640x480, radar 480x640 portrait), drop the on-screen RIO
  button banks -- the cab's buttons are PHYSICAL, so drawing fake ones onto a
  real panel is exactly wrong -- go frameless, and skip the Flight Controls
  pad entirely (6 windows, not 7).  Per-window ",bare" in glass_layout.cfg
  for mixed rigs (idempotent with the global mode).
- Placement receipts: each window logs which PHYSICAL monitor it landed on
  ("[glasswin] 'Heat MFD' surface=Heat at X,Y 640x480 bare -> monitor
  \.\DISPLAYn (...)").  Nobody can see 7 surfaces at once on a cab, and over
  Chrome Remote Desktop you cannot see the panels at all -- the log is the
  only confirmation the map is right.
- tools/podprobe.ps1: run-on-the-pod topology probe (no install/admin) --
  GPUs, every monitor's virtual-desktop rect, EDID make/model, serial ports
  (the RIO board), session type, plus a PROPOSED glass_layout.cfg assigning
  the six surfaces to the non-primary monitors.  Self-tested here (correctly
  reports a single-monitor laptop and declines to map).

Local verify: 5 MFDs at 640x480 bare + portrait radar + Flight Controls
dropped, receipts printed.  KB: pod-hardware §MFD PANELS ON REAL HARDWARE
(incl. WHY the 1995 path is not the route -- NVIDIA Horizontal Span is gone
from post-XP drivers and per-adapter exclusive fullscreen is fragile over a
remote session) + glass-cockpit env table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:31:49 -05:00
Joe DiPrimaandClaude Fable 5 bf39b81b9d KB: document the #60 census/re-export aftermath across the context system
- CLAUDE.md front + project-overview: census/re-export DONE (93.5% coverage,
  41KB dark), polish list refreshed; the citation-policy warning rides the
  front matter so it is unmissable.
- locomotion §CROUCH addendum: the re-export CONFIRMS the hand transcription
  field-for-field, documents that Ghidra renders members as int-ARRAY indices
  ([0xfe] == byte 0x3f8 -- why offset-string greps of the export miss them),
  and records the AIRBORNE AUTO-RISE branch the raw pass missed (+ its fix).
- decomp-reference §7: citation policy (@ADDR, never part/LINE -- old
  citations resolve only against archive_2025export/), the array-index
  gotcha, and the full re-export toolchain (ghidra_reexport.sh incl. the 8.3
  short-path requirement, gapcensus, gapdiff).
- open-questions: the leads the re-export produced -- @0x4c0904 is the MASTER
  BTPlayer Performance (team-by-name resolve + EndMission post + score
  heartbeat; our @0x4c083c attribution needs a re-check) and the ~9KB
  l4splr|btmssn cluster that stayed dark THROUGH the fill (no call/data
  reference reaches it -- jump-table entry suspected).
- glossary: dark-region.
Section ordering fixed (addenda above Key Relationships); checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:18:48 -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 f44be87ab2 #60 PART 2: the RE-EXPORT -- dark code 90KB -> 41KB, coverage 87.3% -> 93.5%
Installed JDK 21 + Ghidra 12.1.2 (no admin, %LOCALAPPDATA%\bt411-tools beside
DXSDK/cmake; runner uses 8.3 SHORT paths because Ghidra's .bat expands
%JAVA_HOME% unquoted and the profile has a space).

New tooling: reference/ghidra_scripts/ExportGaps.java -- ExportAll's exact
output contract PLUS a gap-fill pass (force disassembly + createFunction at
E8 call targets outside functions, data->code pointers at a plausible
prologue, and the census's discovered starts; iterated to a fixpoint,
logged to gapfill_report.tsv).  tools/ghidra_reexport.sh (headless runner,
'reprocess' mode) and tools/gapdiff.py (score two censused exports);
gapcensus.py now censuses any export dir.

Results: 6267 -> 6472 functions (+205 created in 2 rounds: 195 census
starts, 6 call targets, 4 data pointers; 56.1KB newly covered), ZERO
decompile failures.  Dark real code 90.4 -> 40.8 KB (54.8% recovered);
game-side dark 53.1 -> 21.1 KB; regions 428 -> 321.  EVERY historically
dark function now has pseudocode -- including @0x4c05c4 VehicleDead, the
absence that opened this issue.

VALIDATION: the new pseudocode confirms this week's hand reconstruction of
the crouch field-for-field (mapPosture/duckState/squatCapable/myomerEff/
novice gate/SetLegAnimation/ForceUpdate/stability alarm) -- and exposed one
branch the raw pass missed: AIRBORNE AUTO-RISE (mode 3|4 && legState 1 ->
forced squ), now implemented in mech4.cpp and re-benched un-regressed.

PROMOTION: the re-export is canonical reference/decomp/; the previous export
is preserved at reference/decomp/archive_2025export/ so old
`part_0NN.c:LINE` citations still resolve (addresses are stable across both;
line/shard membership is NOT -- cite @ADDR).

New lead recorded: @0x4c0904 is the MASTER BTPlayer Performance (team
resolution, EndMission console post, score heartbeat) -- our @0x4c083c
PlayerSimulation attribution needs a re-check.  KB: source-completeness,
gotcha #20 (the rule is cheap now -- look it up), CLAUDE.md router/layout.
Log: phases/phase-04-gap-census.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:11:26 -05:00
Joe DiPrimaandClaude Fable 5 e69c0d7aa8 #60 GAP CENSUS: the export dark-region inventory (tool + report + KB)
tools/gapcensus.py (deterministic): index-vs-.text interval math, export
cross-check, function-start discovery inside dark regions (E8 call targets +
data-section code pointers), pad exclusion, TU attribution, repo-citation
flags. Output: reference/decomp/GAP_CENSUS.md + gap_census.tsv.

Headline: .text 892KB, index covers 87.3%; 428 dark regions = 90KB REAL code
(indexed-but-unexported = 0 -- the gap class is purely 'never indexed').
Game-side dark ~54KB: 66 regions visited by past digs, 159 NEVER TOUCHED.
Validation: all six historically-bitten dark addresses (VehicleDead,
ToggleLamp, death tail, master-perf, myomer integrator, duck consumer) land
inside census regions; the two most-cited regions are the two that produced
the most reconstructions.

Top uncharted leads (spot-checked real code): the ~9KB l4splr|btmssn cluster
(dispatch-table state machine -- likely BTMission's unexported heart); the
613B btplayer hole before the ctor (mission-review id-0x18 sender suspect);
btl4app tails; heat|mechmppr + mechweap|btplayer boundaries. Full log:
phases/phase-04-gap-census.md. Re-export half deferred (no local Ghidra;
scripts ready). KB: source-completeness census section + CLAUDE.md lookup
row + decomp-reference tools entry; consult the census BEFORE any 'absent
from the export' claim (gotcha #20).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 10:34:02 -05:00
Joe DiPrimaandClaude Fable 5 f49f35cd43 KB housekeeping sweep: end-of-arc staleness purge (2026-08-06)
Corrections (wrong claims fixed + swept):
- open-questions: the DuckState 'no CODE consumer, authentic [T1]' verdict
  RETRACTED (consumer = the unexported posture block; squat clips shipped in
  BTL4.RES all along); the searchlight fog swap re-marked DONE (was 'STILL
  DEFERRED'); crouch index pointer updated.
- combat-damage: old task-#60 kill-score section marked SUPERSEDED by the
  report tail (BTPostKillScore retired).
- pod-hardware: crouch census rows -> COMPLETE; gauges-hud: 07-20 note
  (generators 'unreconstructed') re-swept.

New knowledge:
- reconstruction-gotchas #20: EXPORT-GAP BLINDNESS (4 incidents, 2 falsely
  T1; the byte-scan/named-member/RES-TOC/manifest checklist) + the
  verification-viewpoint bullet in #13 (the crouch capture errors).
- decomp-reference: master posture/crouch block offsets (+0x3f8/+0x79c, the
  myomers cluster) + 8 new bench env gates.
- rendering: beam-material decode (btfx brighten) + look-pass acceptance.
- experience-levels: crouch novice lockout in the +0x25c consumer list.
- build-and-run: the stale-link build ritual; test-harness: bench-script
  gotchas (both previously oral tradition).
- glossary: export-gap-blindness; multiplayer: crouch/searchlight ride
  existing records; subsystems: searchlight visuals pointer.

Status refresh: CLAUDE.md front + project-overview current-state -> core
gameplay reconstruction COMPLETE (774->801), polish phase, #60 census next.
Ledger addenda: INPUT_PATH row 6 complete; RESPAWN_REARM VehicleDead-sender
note. .gitignore: content/ bench artifacts (pngs/logs/eggs/exe) out of
status. checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 10:22:50 -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 4beac48f31 Merge Cyd's glass-lamp-latency: #138 unfocused flash-rate fix + red Panic/Eject + plasma no-frame
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:48:21 -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 e1c3f2db6a Myomer factor: correct the false 'feeder unreconstructed' claim + wire the
crouch gate to the LIVE drive value

The myomer system was ALREADY COMPLETE (2026-07-31 seek audit): Performance
wrapper @004b8b9c, AvailableOutput @004b8ac0 (gear clamp x quadratic heat
degrade x (1 - zone damage)), and the master-perf chain walk + speedDemand
scale + turn freeze in mechmppr.cpp:990 -- the same @0x4a9cf2-0x4a9da4 bytes
the crouch dig re-decoded.  The 2026-08-05 banners calling the feeder dark
were an export-gap-blind grep (named members, not offsets).  Fixes:
mechmppr publishes the chain MAX into mech->myomerEffectiveness (the
binary's +0x79C home) so the crouch posture gate reads the live factor
(dead/overheated myomers now genuinely refuse squat/rise -- previously the
gate read a neutral 1.0 and never fired); the duplicate speedDemand multiply
in the posture block is removed (mechmppr's is the one application); banners
and locomotion.md corrected.  Squat re-benched green on the live wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:21:28 -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 dd70061e0e CROUCH reconstruction (WIP): the master posture/duck machine, decoded + wired
Raw-disasm of the dark master-perf region (@0x4a9cf0-0x4aa0af): the myomer
effectiveness factor (+0x79c, MAX over heatables' +0x31c, scales speedDemand
-- feeder @004b8be3 unreconstructed, neutral 1.0 [T3]), the posture selector
(+0x3f8: mode/novice/leg-state/myomer gates -- novices cannot crouch), and
the DuckRequest consumer (standing -> SetLegAnimation(2) 'sqd', ducked ->
SetLegAnimation(3) 'squ', ForceUpdate 8+1 ships the type-3 state record,
stability alarm flips, request consumed).  +0x1DC = mountSegment... er, the
searchlight learned that one; here: mapPosture @0x3f8 + myomerEffectiveness
@0x79c members land; value-space note (port normal mode == 1, binary 0).

VERIFIED: request->consumer chain fires ([duck] SQUAT), the sqd clip plays
(22kf/7joint, ends root -2.22 crouched -- clip data parsed from BTL4.RES,
squ is its exact mirror; loader slot map re-verified byte-exact).  OPEN: the
parked crouch pose does not HOLD on screen (reverts ~1 frame after clip end
with NO SetLegAnimation re-arm logged) -- the hold's render path is the
remaining dig; [duck] re-arm tracer left in SetLegAnimation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 23:09:51 -05:00
Joe DiPrimaandClaude Fable 5 b75bb4a04c Searchlight beam: the btfx brighten material class + BT_SPOT_SELF rig
SPOT.BGF decoded: a 7-vert cone from the mount, ~50u forward and ~35deg DOWN
(a ground-pool lamp, not an air beam), verts tinted cyan-white, material
class 'brighten' smuggling its additive factor in DIFFUSE.r (0.25) with a
warm emissive on the night page.  The loader had never met the class -- it
drew as an opaque dark-red blob.  Now: brightenFactor parsed (name-gated to
brighten*), batch -> L4DRAWOP.brightenAlpha, drawn in the blend pass as an
additive veil (dest += vertexRGB x factor), unlit.  [T3] tint compose
(vertex cyan vs night emissive warm) noted in the draw branch -- field
eyeball accepted the current look.

BT_SPOT_SELF=1 (bench-only): builds the cone on the own-cockpit tree so the
BT_CAM=face view can inspect it solo.  KB: view toggle is BACKTICK ('V' is
the rear-view hold since #68 -- the toggle skips bound keys); stale V-toggle
claims swept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:21:32 -05:00
Joe DiPrimaandClaude Fable 5 7d8f109241 FIX: restore rendering.md (truncated to empty by a crashed encode in d44fae2) + searchlight section with the perceived-strength correction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 20:01:36 -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