Commit Graph
40 Commits
Author SHA1 Message Date
CydandClaude Fable 5 6b9a54089c BT410 5.3.104: turn-in-place -- the mech STEPS around at a standstill instead of pivoting like a statue
The dispatcher (master perf @0x4aa505-0x4aa588, from the raw disasm -- the
function Ghidra never decompiled), re-hosted at the gait seam in Simulate:

  From Standing, arm the trn clip when the mech is TURNING (the binary's
  operand is |angularVelocity| > 1e-4; at a standstill the turn rate is
  exactly walkingTurnRate, so the test is turnDemand * walkingTurnRate),
  the speed demand sits in the FULL sub-walk band [0, standSpeed], the
  model carries a trn clip (turnCapable, real since 5.3.103), and the
  wind-down debounce is clear.

  BOTH channels arm on the same frame -- the lockstep weld.  Arming only
  the leg lets the body enter its next cycle frames apart and the two walk
  cycles run permanently out of phase (the donor's rhythmic-gait-skip
  finding).  Each trn clip plays one figure and drops back to Standing via
  the case-4 group, so a held turn re-arms clip after clip.

  The debounce is the master perf's own shape: legResetLatch cleared at the
  top of every frame (@0x4a9bff), set by the advancer's wind-down, honoured
  here -- a walk that wound down THIS frame cannot re-enter as a turn until
  the next.

LIVE (new pod_render_trn.conf: throttle 0, turn 0.5, exterior clips):

    pos PINNED at (-641.043, 0, -243.722) across the whole run
    yaw sweeping the full circle
    spd oscillating +/-0.001347

That last number is the proof of authenticity: it is the trn clip's authored
root translation -- the foot shuffle.  The mech turns by STEPPING, not by
having its yaw slid underneath it, and the stride-driven speed model shows
the steps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 14:55:36 -05:00
CydandClaude Fable 5 c85bf98814 BT410 5.3.103: the INTERIOR clip set, the duck, and why state 4 was always the turn
Three finds from one suffix table, closing the last mech2-family loader:

THE 'i' SET.  The 4-char table at .data:0050d94d is the 3-char gait table
with an 'i' appended -- swri, wwri, ... squi, sqdi, trni.  These are the
INTERIOR clips: the gait as seen from inside the cockpit, animating the
inside-view skeleton (they never bind the hip -- you cannot see your own
hip from the seat).  LoadLocomotionClipsExt (@004a86c8) is byte-identical
to LoadLocomotionClips over that table, and is now its reconstructed twin.

THE VIEW DISPATCH (ctor tail @0x4a1674 region): a REPLICANT loads the
exterior set -- you see it from outside -- while the MASTER loads the
interior set.  And the binary carries its own dev override: the L4VIEWEXT
env var forces the exterior set on a master for external-camera work.
Which is exactly what our render-bridge verification rigs need, so
pod_render_joints/limp.conf now set it -- the 1995 authors shipped the
switch our test harness wanted.

THE TAIL THE 5.3.93 LOADER MISSED: two more optional sets past the limp
probes.  squ/sqd resolve into slots 3 and 2 with squatCapable -- the DUCK:
state 2 plays the squat-down, holds ducked at state 1, state 3 stands back
up (three state numbers the machines always handled but never had clips
for).  And trn resolves into SLOT 4 with turnCapable -- which is WHY state
4 is turn-in-place.  The slot map explains the state machine, again.

VERIFIED both paths live: L4VIEWEXT rig run measures the exterior set
bit-identically to 5.3.93 ('mad': standSpeed=5.23428 walkStride=18.5085
revStride=56.0462); the authentic default (master -> interior) measures
THE SAME CONSTANTS from the 'i' clips -- as it must, the mech covers the
same ground from either view, only the joint content differs -- proving
every madXXXi clip resolves.  No fault on either run.

turnCapable is now real, which unblocks the turn-in-place dispatcher
(master perf) as the next locomotion piece.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 14:44:58 -05:00
CydandClaude Fable 5 05392e227b BT410 5.3.102: ProcessCollision -- trees fall, walls do not, and both sides of a ram hurt
The per-contact responder (@004abb40), overriding the engine's protected
virtual per the MECH4.NOTES.md decode:

  THE SEPARATING GATE: a contact whose relative velocity points away from
  the surface ((normal . rel) < -1e-4) is skipped entirely -- no damage
  while pulling apart.

  MECH-VS-MECH: another mech takes the collision damage too, zone -1, its
  own cylinder resolving where the ram landed.  Both sides of a ram hurt,
  and both route through the 5.3.100 economy, so a ram rattles rather than
  executes.

  THE CRUSHABLE SENTINEL: a CulturalIcon takes its crunch dispatch, and one
  WITHOUT the StoppingCollisionVolume flag then sets the amount to exactly
  0.00123f -- Simulate's response block reads that value as "the move
  stands", restores the post-snap saves, and drives on through.  Trees
  fall; walls do not.

  THE CONTACT ACCUMULATOR feeds the audio-bound collisionState indicator
  (0->1 fires the impact sound once per contact, not per frame) and
  captures the impact speed for the authored volume/brightness scales.
  [T3 simplification, tagged in source: the authentic InitialHit/Slide
  split keys StaticBounce's OUT coefficients against an unidentified
  literal @0x4ac048.]

LIVE, one arena run, all three behaviours in the wild and zero deaths:

    [crash] CRUNCH (crushable) at (371.551,-102.862)     <- drove through it
    [colldmg] rattle 1.72691 pts in 3 sub-hits
    [crash] BLOCK dmg=47969.6 iv2=932.659 KNOCKDOWN      <- wall still walls
    [crash] BLOCK dmg=630 iv2=12.5 (x5, free taps)       <- grind still free

STAGED remainder, named in MECH4.NOTES.md: the gyro crunch feed (torque 0.4
/ impulse 0.2 -- the gyro is a feel-wave stub), and the authentic
InitialHit/Slide split pending the @0x4ac048 literal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 14:24:32 -05:00
CydandClaude Fable 5 e40abc86ea BT410 5.3.100: the collision economy -- armor is never touched by a crash, and the wall stops one-shotting the mech
The 5.3.99 open question is closed the way the binary closes it, not with a
divisor.  Two pieces:

THE DIVERT (damage hub @0x4a0368): type-0 Collision damage NEVER reaches the
armor zones.  Our handler was routing wall crashes through the same
cylinder-lottery path as enemy fire, which is why 24k-point kinetic numbers
one-shot vital zones.  The binary short-circuits them into

THE DISTRIBUTOR (@0049ffcc): the manual's "collision damage" technician
setting.  Gated on the owning player's advanced-damage flag; priced against
a 100 km/h reference impact --

    scale  = (2000 / mass) / (100 km/h)^2 / (1 - elasticity^2)
    amount = raw * scale;   under 0.5 the tap is FREE
    n      = Round(amount * 2) sub-hits of amount/n each

-- and applied as rattle crits to random INTERNAL subsystems (HeatSink /
Gyroscope / Torso families only), drawn by cumulative
collisionCriticalHitWeight against a [0,1) roll.  An un-won roll lands
nowhere; the binary does not normalize the weights, so neither do we.

LIVE, same arena mission as 5.3.99's four-death run:

    [colldmg] rattle 0.677919 pts in 1 sub-hits (scale=3.6e-05)
    [crash] BLOCK dmg=18831.1 iv2=711.428 KNOCKDOWN
    [crash] BLOCK dmg=331.878 iv2=12.549        (x6, pressed against a wall)
    deaths: ZERO (was four)

An 18.8k-point crash becomes 0.68 points of internal rattle; the low-speed
wall grind prices as free taps under the 0.5 floor; the knockdown stagger
still fires above the velocity threshold.  Walls now block, stagger, and
rattle -- they no longer execute.

Plumbing: MechSubsystem::CollisionCritWeight() accessor (the weight was
already streamed at +0x10C, protected), Mech::DistributeCollisionDamage
declared, random.hpp include.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 14:08:37 -05:00
CydandClaude Fable 5 4ace2571d9 BT410 5.3.99: walls are real -- frame rejection, crash self-damage, the knockdown stagger, and a pricing question left honestly open
The response half of the ground model (binary @4aa6cf-4aab0b), after the
5.3.98 snap:

  WALLS BLOCK BY FULL FRAME REJECTION, never slide or climb: any blocking
  contact restores the start-of-frame position and zeroes the velocity.

  THE CRASH HURTS: the accumulated collision Damage is dispatched at our own
  mech, zone -1 -- through the same cylinder lottery as enemy fire -- which
  is what makes wall-grinding self-limiting in the original.

  A HARD HIT STAGGERS: past the binary's |v|^2 > 40 threshold (@0x4ab184),
  both gait channels bind the bump clip (slot 0x20, the reason the clip
  array outgrows the name table) and recover to Standing at its end.  Both
  channels together, per the donor's foot-slip finding: staggering only the
  leg channel desyncs the two cycles permanently.

LIVE, one arena run, no fault -- and the run turned into an unplanned
full-system exercise: 8 wall contacts (three at knockdown force, iv2
745-932; one low-speed bump correctly blocked without stagger), and the
self-damage cascaded through vital zones to MECH KILL, pilot notification,
respawn HEAL-and-place, four times over.  The death and respawn machinery
all held.

OPEN, stated rather than tuned away: THE PRICE IS TOO HOT.  Wall crashes
land 14k-24k damage points against a game where a PPC is ~12 and an upper
torso carries ~124 -- a hard crash one-shots vital zones through the
lottery.  BT411's economy audit hit the identical symptom ("tapped a wall
and died instantly") and traced part of it to compounded StaticBounce
reflections across multi-solid frames.  The fix lives in the
Mech::ProcessCollision override (with the crushable-icon sentinel and the
gyro crunch), which is the NEXT increment -- the pricing stays as the
engine computes it until that override is reconstructed from the binary,
because guessing a divisor would be tuning, not archaeology.  The [crash]
log line prints amount + iv2 under BT_MECH_LOG as the audit trail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 13:55:47 -05:00
CydandClaude Fable 5 ed04ba2d4e BT410 5.3.98: the ground snap -- the mech walks the arena's actual terrain, and there is no gravity
The probe half of the master performance's ground model (binary
@4aa630-4aa6cc, via BT411's raw-asm decode), placed after Simulate's position
integration:

  place the collision volume; drop a probe from the volume's authored bottom
  (origin.y + collisionTemplate->minY); ask the zone's box tree what surface
  lies under it (BoundingBoxTreeNode::FindBoundingBoxUnder); and if the query
  hits (h > 1e-4), put the origin ON that surface exactly --
  origin.y -= (h - lift).

The design is worth stating because it is not the obvious one: THERE IS NO
GRAVITY ANYWHERE IN THE MECH.  Placement is absolute each frame.  Walking
up-slope rides the lift window (an implicit step allowance), walking off a
roof drops instantly, and a probe MISS holds Y -- so a falling-through-the-
world runaway is structurally impossible rather than merely guarded against.

The engine side needed NOTHING reconstructed: Mover's own ctor already
builds collisionTemplate/collisionVolume from the model's BoxedSolidStream
resource and the zone tree was live all along.  One include (boxsolid.hpp --
mover.hpp only forward-declares BoxedSolid, so the extents were an
incomplete type).

LIVE, arena run, no fault -- the y track tells the story by itself:

    y = 20 -> 11.747 -> 10 -> 10 -> 0 -> 19.4049 -> 0

Spawn deck, down a ramp (caught mid-slope), an intermediate level, the arena
floor, up onto a structure, and off it.  Every previous run had y pinned at
its spawn constant for the whole mission.

Deferred with the collision increment: frame rejection on blocking hits, the
crushable-icon sentinel, the crash clip (SetLegAnimation 0x20) on hard
impacts, and the gyro crunch feed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 13:41:39 -05:00
CydandClaude Fable 5 aadc372307 BT410 5.3.97: combat damage now limps the mech -- and a fresh leg hit can no longer one-shot it
Two corrections to the damage model's leg branch, both read straight off the
binary's own control flow (part_012.c:6900-6945, the _DAT_0049c99c/9a0
compares), closing a delta the notes had carried as staged:

  THE KILL GATE WAS WRONG.  Our reconstruction felled the mech whenever a
  leg zone reached 1.0.  The binary only does that when the mech is ALREADY
  LIMPING (mode 3/4): a first leg hit -- even one that takes the zone
  straight to 1.0 -- produces a LIMP, and the fall needs a second blow to
  land while the gait is already gimped.  Two-stage leg death, exactly the
  BattleTech flavour, and measurably more forgiving than what we shipped.

  THE ALARM WAS RIGHT.  The limp/kill signal writes AlarmIndicator::SetLevel
  at mech+0x2c -- the statusAlarm, exactly as our MECHDMG had it (levels 3/4
  limp, 9 down).  Confirmed rather than changed.

AND THE MISSING LINK STAGED: nothing promoted statusAlarm 3/4 into the
movement mode the gait keys on.  The binary's own damage code READS mode 3/4
to decide "already limping", so the promotion demonstrably exists -- its
writer is the master performance (mech4 @004a9b5c, the function Ghidra never
decompiled).  Until that lands, Simulate promotes status 3/4 -> simulation
state 3/4 (guarded so a death mode is never overwritten), marked STAGED with
the evidence.

The chain is now END TO END: enemy fire -> cylinder table -> leg zone
crosses 0.5 -> statusAlarm 3/4 -> movement mode -> the limp advancers
replace the normal gait -> the drag leg shows on the wire -> the demand
clamp caps the throttle.  Live regression: the normal mission's speed
sequence is bit-identical a third time (7.31972, 26.6726, ...); the
promotion block is inert until real leg damage arrives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 13:27:02 -05:00
CydandClaude Fable 5 7c2363d89f BT410 5.3.96: the LIMP is real -- all twelve mech2 functions reconstructed, and the drag leg shows up on the wire
The "gimp" family had two conflicting donor readings -- limp vs jump-jet --
flagged in 5.3.94 rather than guessed at.  The binary settles it: LIMP.  The
mode test in every gimp function is mech+0x40 in {3,4}, the same values the
damage model documents as "limp gait graphic (left 3 / right 4)"; there is no
jump-jet control anywhere in the pod cockpit; and the donor's port routing
("graphicAlarm 3/4") was the same field under a different name.  Its
"run-jump clip" mech3 annotations were the misreading that started the murk.

RECONSTRUCTED, completing all 12 of mech2's census functions:

  GimpLegClipFinished  @004a7970    GimpBodyClipFinished @004a6344
  AdvanceLegAnimationGimp @004a71f4 AdvanceBodyAnimationGimp @004a5bf8
  + the limp branch atop both normal *ClipFinished
  + the limp pick in Simulate (replaces the normal advancers while limping)

HOW A LIMP WORKS, now from the bytes rather than description:

  It replaces ONE stride.  Limping left, the right stride (6) hands off to
  the left limp figure (0x16 -> the self-cycling 0x18) while the other leg
  keeps its normal clips.  The asymmetry IS the limp.

  Both machines CLAMP THEIR DEMAND while in a cycle -- the leg machine
  writes the mapper's own speedDemand cell down to the damaged side's cap
  (new MechControlsMapper::SetSpeedDemand, matching the binary's direct
  mapper+0x128 write), the body machine clamps bodyTargetSpeed, both floor
  at zero.  A limping mech cannot command more than its figure carries, nor
  reverse out of a forward cycle.

  The limp advancers keep states 0x16-0x1b LIVE -- the normal advancers
  treat those as the reset group, which is exactly why the limp flavours
  must be selected while limping or the figure is neutralized mid-cycle.
  No death latch, no wind-down: the movement modes are exclusive.

  ALSO EXPLAINED IN PASSING: Ghidra's 3760-byte FUN_004a6344 -- the census's
  largest function -- is really THREE functions.  The two normal ClipFinished
  callbacks (@004a6928/@004a6d8c) are reached only via data pointers, so the
  decompiler folded them into the gimp-body machine's extent.

All movement-mode reads route through Mech::MovementMode() (mech+0x40 == the
simulation state), which honours a BT_FORCE_LIMP=3|4 dev hook so the gait
could be verified before the damage model's limp hook exists.

VERIFIED, two runs on the rig:

  NO-REGRESSION: the normal mission's speed sequence is BIT-IDENTICAL to
  5.3.95 (7.31972, 26.6726, 22.1601, ...).  The branch costs nothing.

  FORCED LEFT LIMP (new pod_render_limp.conf): the mapper still demands
  26.9; the hull lurches at 10-23.  And on the wire the healthy walk's tight
  pose-count pairs (650/649 ... 434/421) BREAK to a 3x asymmetry -- the
  drag-leg joint at 18 poses against its partner's 55.  The limp is visible
  in the data exactly the way it will be visible on screen.

STILL OPEN in this family: the damage hook (leg zone >= 0.5 -> mode 3/4, a
MECHDMG increment -- nothing sets the mode in real play yet), and
IntegrateMotion's remaining pieces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 13:19:10 -05:00
CydandClaude Fable 5 c578a9b790 BT410 5.3.95: the stride becomes the speed -- and the hull now breathes with the gait
Half of IntegrateMotion's job, taken now because two findings made it safe:

THE GAIT SLEW RATE IS THE AUTHORED ACCELERATION.  The binary's ctor sets
forwardCycleRate, gimpCycleRate and groundCycleRate all from the model's
maxAcceleration (rec+0x44 -- 30 for the Mad Cat) and airborneCycleRate from
superStopAcceleration.  That closes both of yesterday's unsourced-rate
worries at once: the ctor default of 1.0 (which would have taken ~27 seconds
to reach speed) is retired, and replacing the explicit acceleration model
with the stride rate preserves drive feel EXACTLY -- same slew rate, same
demand, different (authentic) path for the value to travel.

SO: Simulate now takes currentBodySpeed = bodyStride / dt, the binary's own
relationship (IntegrateMotion @004ab1c8: local velocity z = -distance/dt).
The old acceleration model survives only as a [T3] fallback for a model with
no gait clips at all, so unverified fleet mechs stay drivable.

LIVE CURVE, arena run, no fault:

    spd = 7.32                          stepping off (stand->walk clip)
    spd = 26.67 22.16 27.31 30.54 ...   the run cycle around the demanded 26.9

The old model pinned spd at exactly 26.9022 every frame.  Now the mech steps
off slow and its hull speed oscillates within each stride -- heel-strike vs
mid-swing.  Cadence in the motion is not noise; it is the point.  The mech is
carried by its animation, which is what "the gait IS the locomotion" meant in
the 1995 design.

Also queued a correction from the raw decomp: IntegrateMotion's replicant
branch writes +0x598 with a VECTOR op, so motionEventName is likely a Point3D
rather than the CString the BT411 member map suggested -- flagged in the
sidecar for the dead-reckon increment rather than churned now.

Still staged from IntegrateMotion: the airborne pick, the dead-reckon fold,
the orientation integration, the turn-in-place dispatcher.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 10:33:05 -05:00
CydandClaude Fable 5 9a74f2aa09 BT410 5.3.94: THE LEGS WALK -- AdvanceLeg/BodyAnimation live, 16 of 22 joints animating in paired strides
The last link is in.  AdvanceLegAnimation (@004a5028) and AdvanceBodyAnimation
(@004a5678) reconstructed and wired into Mech::Simulate, and the first live
run put a walking gait on the wire:

    before   22 handles,  1 animating (the vehicle root)
    after    22 handles, 16 animating, ZERO 2-float records
             root 812 poses; six PAIRS at 650/649, 599/596, 563/563,
             542/540, 531/529, 434/421; three slow joints at 25

Six left/right pose-count pairs is six joints per leg cycling in alternating
strides -- the symmetry is itself evidence that the handed clip alternation
(Right, Left, Right, each clip one stride) is running correctly.  Chain, end
to end, every stage previously verified in isolation and now live together:

  mapper demand -> AdvanceLegAnimation (state machine) -> SelectSequence
  -> SequenceController::Advance (keyframes -> Joint::SetHinge/SetRotation)
  -> BTL4HingeRenderable (5.3.88 matrix transport) -> 12-float flush
  -> render bridge applies.  Mission drove itself clean, no fault.

RECONSTRUCTED FROM THE RAW DECOMP, NOT THE DONOR -- and the sidecar says why:
BT411's versions carry port-era replicant accommodations (its mapper cell
does not replicate; the binary's does) and a turn-in-place dispatcher it
relocated INTO the leg machine from mech4's master performance.  None of that
is 1995 code.  Here the leg version reads the mapper unconditionally, exactly
as decompiled, and state 4's ARMING stays where the binary has it -- in
mech4, not yet reconstructed.

THE TWO CHANNELS DIFFER MORE THAN THEIR ClipFinished TWINS DO, all
binary-verified: the leg version has the wind-down block, the turn-in-place
case and the "Standing Not Supported" guard; the body version has none of
those, its case 4 sits in the plain-advance group, and move_joints reaches
every body Advance AND its reset's Reset -- the caller decides whether the
body channel poses joints or only measures stride.  Wired accordingly: leg
poses, body measures (move_joints 0), body distance dropped at a seam marked
STAGED -- consuming it as the forward step is IntegrateMotion's job (mech4).

Also faithful: case 0 FALLS THROUGH so a freshly armed clip advances the same
frame it was selected; the plain group's Standing guard is unreachable via
that fall-through and catches direct entry only; each cycle plays its clip at
cycle/stride of the authored rate (a slow walk IS the walk clip played slow);
and the reverse cycle's caps are all negative with the advance ratio folded
positive at the end.

UNSOURCED, named in the sidecar rather than invented: idleStrideScale
(+0x5ac, defaults 1) and runSpeedMax (+0x7a0, the run cycle's upward cap --
LoadLocomotionClips does not set it; defaulted huge so it never binds until
its real writer is found).  ForceUpdate(8) is a STAGED no-op pending the
replication emitter.

Still deferred: the airborne Advance* flavours (@004a5bf8/@004a71f4, jump
jets) and the Gimp*ClipFinished limp machines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 09:25:56 -05:00
CydandClaude Fable 5 a87c49c158 BT410 5.3.93: the clip loader runs LIVE -- the Mad Cat measures itself, and a 1995 copy-paste bug ships again on purpose
LoadLocomotionClips + ResolveAnimationClip + MeasureClipStride + LoadClipSlot
reconstructed and WIRED into the ctor's GameModel block.  animationClips[] is
no longer an empty array: every gait slot resolves by the model's animation
prefix, and the locomotion constants are now MEASURED from the authored clips
instead of asserted as bring-up defaults.  First live run, arena mission:

  [mech] clips 'mad': standSpeed=5.23 walkStride=18.51 revStride=56.05
                      revSpeedMax=26.26 gimpSpeedMax=-4.23 gimpStride=-20.26
                      limpSet=1

That line carries three verifications at once: the prefix printing as text
proves the Mech__ModelResource layout is right at +0x40; the reverse figures
come out NEGATIVE exactly as the transition machines expect; and the mission
ran clean to live driving afterwards (703 log lines, no fault).

DRIVING FEEL CHANGED, deliberately: speedDemand at 0.6 throttle went 14.4 ->
26.9, because the placeholder top speed (30) gave way to the measured 56.05.
The Mad Cat is simply faster than the bring-up guess.  Authenticity arriving,
not a regression.

TWO BINARY BEHAVIOURS REPRODUCED ON PURPOSE, both documented at the function:

  The speed caps read keyframeData[keyframeCount] -- one entry PAST the last
  frame.  Fencepost is the binary's own (0x690 + 8 + [0x670]*0xc); whether the
  authored table has count+1 entries is unestablished, but the clips were
  authored against this read and the measured values are sane.

  The reverse-cycle stride is computed from STALE locals.  The decomp is
  unambiguous: bbr and bbl are both measured into local_8/local_c, and the
  divide's second terms come from local_10/local_14 -- still holding the
  RUN-LEFT figures.  gimpStrideLength = -((bbl + rrl_stale)/(...)).  A 1995
  copy-paste bug, shipped in every pod for thirty years, reproduced here with
  a comment pointing at the wwr/wwl block that shows the intended pattern.

  (And an earlier scare resolved: the negation IS in the binary -- the very
  next instruction is 0x350 = -0x350.  My first decomp window cut one line
  short and briefly indicted the donor's minus sign.)

ONE DELIBERATE DIVERGENCE, tagged [T3]: the binary dereferences every resolve
result unguarded -- a model missing a mandatory clip crashes on load.  Here a
miss stores NullResourceID (SelectSequence resolves it to an inert controller)
and the dependent measurement is skipped.  Keeps the boot alive on unverified
clip sets; revisit when the fleet's models are known-good.  Measurement binds
pass a NULL finished-callback (measurement parses, never plays -- the binary's
live pointers can never fire there).

Ctor additionally zero-initializes the whole gait channel -- globalTimeScale
defaulting to 1 specifically, because zero would silence every clip advance --
and fills the clip array with NullResourceID before the loader runs, so the
uninitialized-member class of bug (see 5.3.83) is closed here BEFORE the
consumers arrive.

MECH.HPP: the optional limp set carved out (hasGimpClips + 4 measured limp
figures + gyroRumbleTimer); reservedState 150 -> 140.

Next: the four Advance* entry points -- the last link before the legs move.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 12:23:04 -05:00
CydandClaude Fable 5 07eb74d1f7 BT410 5.3.80: joint articulation is live -- 22 nodes, and the twist reaches the board
RecurseSKLFile now builds a joint renderable for any node whose page name
resolves to a live skeleton Joint: HingeX/Y/Z -> HingeRenderable (watching
Joint::GetHinge), Ball -> BallJointRenderable (watching GetEulerAngles),
otherwise the static path.  Each holds the rest offset in one DCS and the live
rotation in a child DCS, and its Execute diffs the watched value and calls
DPL_FLUSH_DCS -- the engine's own mechanism (L4VIDRND.CPP:1026+).  The value
comes from the mech's JointSubsystem via ResolveJoint, so sim and renderer
read one source.  Gated on BT_JOINTS while it proves out.

  [skl] video\max.skl -> 26 nodes, 1 objects, 1 eye, 22 articulated

The bridge reported anim_abs=1 joints=0 twist=+0.00 before; it now reports
joints=1 twist=-0.86, matching the game's [torso] twist=-0.856.  With the mech
stationary, frames that differed by 0.0% now differ by 62-80%.

A crash it exposed: Mech::ResolveJoint passed segment->GetJointIndex()
straight to GetJoint unchecked, and a segment with no joint reports -1 --
GetNthImplementation then indexes [base + -1*4] and dies (guest 00426A1D).
Torso never hit it because it only asks for its own authored joint name; the
walk asks for every page.  Now bounds-checked against GetJointCount.

Open: the canopy does not stay rigid in the view, though it and the eye hang
off the same articulated node.  Cancelling the bridge's cage compensation
(CAGE_TWIST_SIGN=0) did not close it.  Leading hypothesis: SetupCull builds
worldToEyeMatrix from GetSegmentToWorld(siteeyepoint) -- the SIMULATION's
segment transform -- independent of the render tree, so the canopy follows our
render chain and the eye follows the sim's, and they diverge whenever one
carries the twist and the other does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 20:46:18 -05:00
CydandClaude Fable 5 14da9489c2 BT410 5.3.43: full pod rig -- geometry loads, ladder advances to the audio watchers
Under the documented rig (launch_pod.ps1 with the GL bridge up) the 'couldn't
load object' count went to ZERO.  .BGF loading was never a reconstruction
problem, only a missing bridge.

What blocks now is a series of authored AttributeWatchers: BTL4.RES binds
them BY NAME and the engine Fails outright on any that does not resolve, so
each is a name our subsystems must publish.  Five rungs climbed, each
run-verified: UnstablePercentage, SpeedEffect (Myomers table), AnimationState
+ CollisionState/CollisionNormal + ReduceButton (Mech), and the full Torso
table (all six authentic names mapped onto existing members).

The method that makes this cheap is banked: the shipped binary's string pool
carries each class's attribute names CONTIGUOUSLY in ID ORDER after the class
name, and in every case so far our member declarations sit in the same order
-- which confirms the layout and lets ids be pinned rather than guessed.
Intersecting those names with BTL4.RES gives the exact work list.

A THIRD miswired SharedData found on the way: Myomers carried Subsystem's
tables where it derives from PoweredSubsystem, the same defect as
MissileLauncher (5.3.33).  Worth a sweep across every subsystem.

Staged member TYPES are provisional and documented as such: AttributeWatcherOf
reads *(T*)attributePointer and the instantiation comes from the resource,
which the string pool does not reveal.  Nothing drives them yet, so settle the
types with the models that write them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 01:03:36 -05:00
CydandClaude Fable 5 91b043778e BT410 5.3.42: the 3-D pipeline runs to the geometry load -- two rungs climbed
RUNG 1: BTL4VideoRenderer::LoadMissionImplementation, Fail -> bring-up no-op.
Legal, not a cheat: the engine's own VideoRenderer version (VIDREND.CPP:259)
is a bare Tell and our working GaugeRenderer ships the same, so the renderer
comes up and runs its frame loop with an empty scene -- exactly what we want
to measure before writing content.  The authentic shape for the real body is
pinned by the surviving sibling header CODE/RP/RP_L4/RPL4VID.HPP (walk the
mission entities -> MakeEntityRenderables -> ReadSKLFile/RecurseSKLFile).

RUNG 2: Mech::EyepointRotation published.  The engine then Failed at
SetupCull, which fetches that attribute BY NAME off the viewpoint entity and
composes it with the siteeyepoint segment to build worldToEyeMatrix.  The
mech already HAD EulerAngles eyepointRotation with a getter -- only the
publication was missing.  One enum id, one table row.

The run now reaches the full mech build, constructs the renderer, boots the
board (~907K VPX wire transactions), loads the mission, runs the per-frame
cull and enters geometry loading with ZERO Fails and zero exceptions.  It
stops at 'couldn't load object buttee.bgf' / 'NULL instance' -- and that is
not a defect: the models are present and BTDPL.INI points at them, but .BGF
loading goes through the board to the external GL render bridge that the VPX
HLE tees over VPX_FIFOSOCK, and that bridge was not running.  'NULL instance'
comes from the prebuilt LIBDPL.LIB refusing to instance an object that never
loaded.

Next rung is the documented full rig (render-bridge/launch_pod.ps1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 00:18:10 -05:00
CydandClaude Fable 5 d67f8d5eda BT410 Phase 5.3.27: the attribute wave -- 48/50 cockpit bindings go LIVE
The gauges now read the real simulation: heat/power/sensor/mech attribute
tables published under the 1995 L4GAUGE.CFG spellings the widgets already
bind by name.  Temperatures, coolant mass/capacity/leak rate, condenser
valve settings, generator voltages and numbers, radar percent, and the
mech's radar/speed rows all resolve to live members.

THE NUMBERING IS PINNED: the chain publishes 2..0x0E so
PoweredSubsystem::NextAttributeID lands on the authentic 0x0F -- the
surviving SENSOR.HPP numbers RadarPercent off it and MechWeapon's
binary-pinned table starts at 0x12, so its pads shrank 16 -> 3
(0x0F..0x11) exactly as the 5.3.x header comment predicted.  Sensor's
authentic enum finally has its table defined; MechWeapon/Sensor rechain to
PoweredSubsystem and Reservoir/AggregateHeatSink to HeatSink so the whole
family is visible where the cfg expects it.

Verified with BT_GAUGE_ATTR_LOG: 48 OK / 2 NULL (was 33/17, and 0/50 at
the block's birth).  The two remaining have no member to bind --
HeatSink/AmbientTemperature (a sim constant) and Searchlight/LightOn (a
memberless subclass) -- and fall to the documented zero cell.  Gauge
fight (15/15 rounds, 127 zone hits), smoke and novice all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:09:08 -05:00
CydandClaude Fable 5 f65a3259ce BT410 Phase 5.3.25: scoring + the COMPLETE mission lifecycle -- egg to GAME-RC
Scoring roles bound (binary ctor tail @004c0bc8): the mission role dict ->
Player::scenarioRole; TEST.EGG's Role::Default = the baked dfltrole
(lives=1000, killBonus=500) -- authored data flowing.  The three score
handlers are live per the decomp derivations (@004c02e4 / @004c0200 /
@004c02a8 + CalcInflictedScore @004c052c; tonnage/bias staged); the
producers post damage scores per delivered hit and the kill credit from
the death transition with the latched killing-blow magnitude.  First live
kill: award=3.43, kills=1, score=123.6.

Out of lives -> the mission END: the binary's +10s review post sits in the
one decomp gap, so the staged stand-in enters the engine's own cascade
(StopMissionMessage +10s) -- EndingMission -> the 3s fade -> the second
Stop -> Application::Stop -> RunMissions exits.  The BTL4/L4 stop layers
run authentically (plasma score display off, egress lamps).

VERIFIED END-TO-END: a 2-lives egg (role-page return=2 override) -- death
#1 debits and respawns healed; death #2 is OUT OF LIVES; +10s later the
mission ends and BTL4OPT.EXE exits CLEANLY to DOS (GAME-RC prints).  The
full 1995 pod mission loop -- egg, boot, spawn, fight, score, die,
respawn, die, mission end, exit -- now runs from reconstructed source.
Zero faults; fight/smoke/novice regressions green.  Dev knob:
BT_PLAYER_PASSIVE=1 (piloted mechs hold force-fire).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:18:41 -05:00
CydandClaude Fable 5 623c872c92 BT410 Phase 5.3.24: the death -> respawn cycle -- mechs DIE and COME BACK
The full circuit, workflow-researched (5 parallel dossiers over BT411 + the
engine) and adversarially reviewed (3 lenses) before landing:

Mech side: the once-per-death transition in Simulate (freeze, motion kill,
DeathShutdown sweep, MASTER-only VehicleDead(-1) to the player link);
Mech::Reset @0049fb74 (reposition + heal every hull zone + the roster
DeathReset sweep + PreRun -- the reset-based respawn that REUSES the entity);
dead-owner terms in both weapon hard gates (wrecks fall silent).

Player side: the VehicleDead(-1) branch (deathPending dedup, deathCount
bump+stamp, scenario-role life debit, +5s re-post -> the engine drop-zone
hunt) and the DropZoneReply respawn branch (reset the dead mech at the
replied drop zone; probes on a live mech are moot).

Subsystem side: the engine base virtual DeathReset implemented across the
family -- MechSubsystem (zone heal + DestroyedState clear), MechWeapon (full
powered/thermal restore + fresh Loading cycle), AmmoBin (restock from the
ctor-cached count), HeatSink/HeatWatcher/PowerWatcher/Generator/Sensor/
MissileLauncher.

Review catches fixed before commit: AmmoBin restocked through the FREED
padBuffer pointer (use-after-free -> initialAmmoCount; MechSubsystem::
resource is now documented as never-deref-post-ctor); died-hot weapons
respawned at FailureHeat (now full thermal re-init); Generator tap counts
desynced across reset (preserved); Sensor skipped the base heal; PowerWatcher
inherited a statically-bound reset; cooling toggle + connect mode restored.

Verified: kill -> wreck (0 shots while dead) -> +5s -> drop-zone hunt ->
HEALED + placed at a real drop zone -> 134 shots after respawn incl. 12 SRM
(restock proof); unowned enemy wreck settles silently; fight/smoke/novice
regressions green, zero faults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:35:05 -05:00
CydandClaude Fable 5 2f0d3b6239 BT410 Phase 5.3.23: the cylinder hit-location table -- unaimed hits land WHERE THEY STRIKE
DMGTABLE born (type-29 streams, BT411 byte-verified format): the height x
angle grid -- DamageLookupTable rows by height, PieSlice cells by angle
(rotate-with-torso rows add the live twist), DamageZonePercentTable = the
cumulative hit-distribution roll (first threshold above the roll -- the
BattleTech dice scatter).  Loaded in the Mech ctor by the DamageZoneStream
member's NAME (mech+0x444, resident); Mech::TakeDamageMessageHandler
resolves invalidDamageZone hits through it (binary @0x4a0264 tail).

The missile fuse becomes the authentic unaimed producer: zone -1 + the
round's world position as the impact point (ENTITY3.HPP: "damage zones are
only valid via reticle based weapons").

Fight-verified on the authentic 7-row bhk1 table: flat-flying SRMs strike
low -> row 0 -> FEET AND LEG zones, side-correct by impact angle (+x right,
-x left), identical geometry spread across the cell distribution by the
roll.  17/17 missile lifecycle, zero faults; smoke + novice green.  Also
fixed: the ctor's reservedState zero-loop counted past the shrunken array.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:46:39 -05:00
CydandClaude Fable 5 bfff7fe874 BT410 Phase 5.3.22: the cockpit weapon-button column -- generator panel LIVE
The full per-receiver message chain, contiguous ids 3..0xb: PoweredSubsystem
4-8 (SelectGeneratorA-D + ToggleGeneratorMode, binary table @0x50F4EC)
chained on HeatSink's ToggleCooling(3); MechWeapon 9/10 (config buttons,
staged latch bodies); Emitter 0xb ToggleSeekVoltage (@0x511DB8).

SelectGenerator = release the old tap -> roster walk by generatorNumber ->
AttachToVoltageSource -> Connected; mode toggle cycles Manual->Auto->
(detach)->Manual; the seek dial steps the authored voltage ladder with wrap
(a step up re-enters Loading, charge persists).  All novice-locked.
Generator::UntapVoltageSource + PoweredSubsystem::DetachFromVoltageSource
added.  Dev harness: BT_PRESS_GEN=1..4 / BT_PRESS_SEEK.

Verified: PPC_1 retaps GeneratorB (tapped); seek index 2->3, target 9900V
(authored fraction x rated); novice presses INERT; missile fight 14/14 and
smoke green, zero faults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:36:32 -05:00
CydandClaude Fable 5 d4f0aef6e1 BT410 Phase 5.3.17: the destruction cascade -- zone deaths DESTROY things
Mech::DamageZone::TakeDamage is the full binary cascade (@0049c690): LOD
artifact router with same-attacker clustering (@0049c40c, hysteresis 0.33),
artifact parent level = mean of children, Energy-hit generator shorts
through the crit plug binding (novice-exempt), weighted CriticalHit
(@0049ccc4), zone-death allotment push (SendSubsystemDamage @0049c9a8) and
the segment-table destruction descent (RecurseSegmentTable @0049cad4 --
SIBS + DESCEND via the engine EntitySegment iterators).

Supporting bricks: Mech's OWN handler table with the TakeDamage override
(latches lastInflictingID @0x43c; gyro feed staged; cylinder table
deferred); friend class Mech__DamageZone (authentic); subsystem private
zones ARMED (structureReference + armorByFacing[5] normalized -- crits are
measurable); ApplyDamageAndMeasure; ForceCriticalFailure sets
DestroyedState so the weapon FSMs' GetSimulationState()==1 hard gate went
live; Generator::ForceShort + PoweredSubsystem::ForceShortRecovery.

Fight-verified (mutual BT_SPAWN_ENEMY + BT_FORCE_ZONE=8 concentrated fire):
arm zone to 1.0 -> sibling searchlight zone dies (Searchlight2/ThermalSight
crits destroyed, HUD degrading) -> DESCEND kills the gun zone -> PPC_2 +
ERMLaser_2 + Condenser6 DESTROYED and FALL SILENT (0 shots after death;
undamaged PPC_1 keeps firing); vital-zone hit = MECH KILL; PPC hit shorted
GeneratorD. This art has zero artifact zones (redirect=0 across all 20 --
router verified dormant-correct). Smoke + novice regressions clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:13:28 -05:00
CydandClaude Fable 5 76cfc64353 BT410 Phase 5.3.16: targeting + damage -- two-mech fights are LIVE
The simulation is now a FIGHT. Mech target slot (+0x388), authentic
Loaded->Firing gate (viewFireEnable && HasActiveTarget), UpdateTargeting
range refresh, and SendDamage via the engine TakeDamageMessage.

MECHDMG.CPP is BORN against the AUTHENTIC surviving CODE/BT/BT/MECHDMG.HPP:
the Mech ctor's Pass-3 hull zone fill constructs Mech__DamageZone per zone
(the Entity base only allocates the raw pointer array -- unfilled slots were
crash #1; a base-class fill parses each record short and skews the stream --
crash #2). Streamed ctor parses the full BT tail (flags / Scalar-weighted
criticals with the Master+Dynamic plug gate / LOD redirect table) and
normalizes the armor economy: scale[type]=1/(raw x armorPoints), legs
halved -- BT411-verified @0049ce50.

Live two-mech verification (BT_SPAWN_ENEMY harness in DropZoneReply):
20 named hull zones stream on both mechs; PPC lands 11.77/hit (12 x
chargeRatio^2, type 4), ER-M laser 3.43 (type 3), SRM6 salvos 5.83 x 6
rounds on independent zones (type 2, one message per missile -- burstCount
is NOT in the zone formula); repeat hits climb linearly, leg zones absorb
at half scale, zones reach 1.0 Burning/Destroyed. No-target and novice
regressions hold (weapons load but never fire without a target).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:21:43 -05:00
CydandClaude Fable 5 6451a0a38d BT410 Phase 5.3.14: AggregateHeatSink -- the bank + the ambient radiator (the heat EXIT)
The central heat bank is now its real class: the binary's 0x0BBE
AggregateHeatSink (the value our VDATA enum named HeatSinkClassID -- there is
no streamed plain HeatSink; the segment walk now builds the bank).

- Ctor (@4ae8d0): heatSinkCount from res +0xFC (bhk1 = 6, matching its six
  condensers); thermalConductance x 0.1 x count (231000 -> 138600); ambient
  setpoint 300 (the mission [mission] temperature overwrite joins the
  Mech-PlayerLink wave).
- RadiatorSimulation (@4ae73c) replaces the base heat step on the bank -- THE
  system's only heat exit: relax toward the ambient target with rate
  k = conductance x (1-damage) x (coolant/capacity) x flowScale / mass; tail
  tops the bank's coolant from the attached store via the DrawCoolant virtual
  (base 0 until the reservoir-attach wave). VERIFIED signed-correct: the bank
  warms 77 -> 300 from the cold start, then flips to actively radiating
  (-48K..-167K/step) once fired heat pushes it past ambient. Until now heat
  only ever POOLED in the central sink; the mech now genuinely sheds it.
- Reservoir master path: capacity = 0.05 x bankCount x streamed = 6 (the
  authentic tank), refilled.

Zero Fail; the expert economy regression stays green (202 heat events under
forced spam).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:54:21 -05:00
CydandClaude Fable 5 c01e57ab22 BT410 Phase 5.3.13: cockpit-button message layer -- valves, cooling, flush LIVE
The subsystem-family cockpit buttons now work through the authentic Receiver
dispatch -> per-class handler-table path. The id space decodes cleanly:
Receiver::NextMessageID == 3, so ToggleCooling = 3 on the HeatSink chain and
the per-class id 4 is MoveValve on a Condenser / InjectCoolant on the
Reservoir -- same number, different class, the binary's per-receiver-class
convention.

- HeatSink::ToggleCoolingMessageHandler (@004ad6f8, id 3): novice-locked,
  press-only; toggles coolantAvailable + coolantFlowScale together.
- Condenser::MoveValveMessageHandler (@4ae464, id 4): novice-locked; cycles
  the valve 1->5->50->0->1 and calls Condenser::RecomputeValves (@0049f788):
  every condenser's coolantFlowScale = valve / sum-of-valves. The ctor now
  streams the AUTHENTIC flowScale=0; the Mech ctor seeds equal shares once at
  spawn -- the flowScale=1 interim is retired.
- Reservoir::InjectCoolantMessageHandler (@4aee70, id 4): novice-locked;
  press arms the flush when the tank holds charge, release drops it.
- MechSubsystem::NoviceLockout() (@4ac9c8): owner -> playerLink -> experience
  == novice; unlinked mechs read unlocked.
- DEV harness BT_PRESS_VALVE / BT_PRESS_FLUSH: dispatch the REAL messages ~6s
  into the mission from Mech::Simulate.

VERIFIED: spawn shares 6 x 0.166667; one MoveValve press -> Condenser1
valve=5 flow=0.5, others 0.1 (valve/sum exact); flush arms via the real
button message; NOVICE locks both presses (valve lines stay spawn-only, zero
flush). Zero Fail throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:43:01 -05:00
CydandClaude Fable 5 e1e5a9a6db BT410 Phase 5.3.12: experience gates + coolant system -- novice/expert modes REAL
The player-experience system now gates the entire heat/jam economy, verified in
BOTH directions with a one-token egg edit (TESTNOV.EGG, experience=novice):
NOVICE = 212 fire cycles all pinned at T=77, zero jams, zero shutdowns (the
heat model authentically absent); EXPERT = the full economy (duty cycles, 119
shutdowns + 2 authentic jams under forced fire). Zero Fail in both.

- BTPLAYER: the flag block renamed to its TRUE semantics (simLive @0x25c,
  heatModelOn @0x260 -- the FUN_004ad7d4 master switch, advancedDamageOn pair,
  levelFlag26c/270, experienceLevel); the ctor rows were already
  binary-accurate (nov 0000 / std 1011 / vet 1111 / exp 1101); accessors +
  [exp] sentinel.
- HeatSink::HeatModelActive() + ProjectileWeapon::LiveFireEnabled(): owner
  mech -> Entity::GetPlayerLink() -> the BTPlayer flags, NULL-permissive.
  Gates: both HeatSinkSimulation phases, every weapon fire-heat dump, and
  CheckForJam is now the AUTHENTIC form (LiveFireEnabled + heatLoad<=0
  early-outs + the minJamChance floor; the interim heat-degraded gate retired).

- THE LOAD-BEARING FIX: Mech ctor SetValidFlag(). Every 1995 entity ctor tail
  marks itself valid; ours didn't -- Entity::Dispatch routes messages to an
  INVALID entity into the deferred event queue, so the PlayerLink bind (and
  every directly-dispatched mech message) silently never landed and the gates
  read a NULL player forever.

- THE COOLANT SYSTEM (authentic bodies, byte-verified constants: HeatLoadScale
  0.002 -> heatLoad now in [0,1]; equalize eps 1e-4; draw floor/ON
  0.0025/0.003): UpdateCoolant (damage-scaled draw -- an undamaged mech leaks
  nothing), BalanceCoolant (full clamp chain from ConductHeat), DrawCoolant
  base=0 with the RESERVOIR override as THE SOURCE; Reservoir reconstructed
  (capacity overlays thermalCapacity, CoolantSimulation + the full
  InjectCoolant flush distribution, BT_FORCE_FLUSH dev hook); Condenser
  RefrigerationSimulation (massScale = (1-damage)*refrigerationFactor >= 1 --
  the heat pump that chills the bank; valveState inits 1; digit-suffix number
  fix). Deferred: AggregateHeatSink family, cockpit-button handlers
  (MoveValve/ToggleCooling/InjectCoolant), TrackSeekVoltage charge model.

Research driven by a 4-agent workflow dossier over the BT411 RE (verbatim
bodies for every function above + the egg experience plumbing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 22:15:07 -05:00
CydandClaude Fable 5 57507cb15c BT410 Phase 5.3.10: power/heat wave -- the thermal + electrical economy is LIVE
Weapons dump firing heat into their own sinks, sinks conduct through the
Condenser bank into the central HeatSink, temperatures drive the degradation/
failure alarms, and every powered subsystem tracks its generator through the
electrical state machine. Verified: the authentic fire-discipline game -- a PPC
spikes 77->709K per shot, relaxes to 441K across its 5s reload, and sustained
fire climbs 441->674->838->960K, brushing the authored 1000K degradation
threshold. All calibration values match the BT411 audit exactly (central bank
1.39e6, PPC sink 174000, thresholds 77/1000/2000).

- HEAT: HeatSinkSimulation (@004ad924 absorb->T->load->conduct->alarm),
  ConductHeat/ComputeHeatFlow (@004ad8ac/@004ad9ec two-body equilibrium
  relaxation; flow==0 exactly at uniform T), UpdateHeatLoad (15-sample filter);
  heat-state enum + accessors; installed by the HeatSink ctor.
- WIRE-VERIFIED: HeatSink resource gains linkedSinkIndex -- THE missing
  ancestry int that shifted every descendant block +1 (voltageSourceIndex had
  been reading the linked-sink index: "power sources" appeared to be
  Condensers). Conduction topology wired from it at construction:
  weapons->Condensers1-6->central; Generators->Condensers; Reservoir->central.
- MECHWEAP resource: authentic pip tail (pipPosition int + pipColor 3 floats +
  pipExtendedRange int) per the BT411 verified overlay; with linkedSinkIndex
  this closes the whole +3 alignment mystery (ProjectileWeapon pad deleted).
  True bhk1 reads: PPC recharge 5.0s (not 1.0), discharge 0.99s, range 900.
- POWERSUB: ctor resolves voltageSourceIndex -> GeneratorA-D (wire-verified),
  taps via Generator::TapVoltageSource (-1 when full); PoweredSubsystemSimulation
  (@004b0bd0) electrical FSM (Starting/NoVoltage/Shorted/GeneratorOff/Ready);
  GeneratorSimulation partial (start/short-recovery timers).
- Weapons: power step at sim head; Loading recharge gated on electrical Ready
  (authentic @4bbdf5); FireWeapon dumps firing heat. HEAT UNITS: the chain is
  1e7-native with TWO authoring conventions -- energy weapons store small
  (PPC=11, x1e7 from the energy algebra), ballistics store native (SRM6=5.06e7,
  dumped raw; double-scaling it was the bring-up runaway-temperature bug).
- SENSOR: authentic gating (@004b1c4c) -- power step, electrical-Ready gate,
  heat-state switch (Degradation x0.5 / Failure 0). Verified Ready + 100%.

Deferred: coolant depletion/venting, the novice HeatModelOff gate, the charge
model (TrackSeekVoltage/voltage sag/I2R), weapon gate 1 + jam roll, the central
sink's forward-linked drain. Zero Fail across fire + neutral runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 23:55:48 -05:00
CydandClaude Fable 5 d422dfffc0 BT410 Phase 5.3.7: weapon view-fire gating + build header-stamp hardening
Completes the weapon-side half of the look commit:

- MECHWEAP: real rearFiring/viewFireEnable members (binary @0x334/@0x3E0) +
  accessors. Ctor resolves REAR-FIRING authentically (@004b99a8 tail): the
  mount SEGMENT site name is tested for the 'b' (back) marker -- only the back
  gun ports (sitelbgunport/siterbgunport) carry it. Spawn = forward-armed.
- MECH CommitLookState: re-arms every roster weapon per view (forward =
  non-rear, LOOK-BACK = rear-mounted, side/down = none) and flips the HUD
  reticle pip group with the authentic Reticle::Front/RearFiringWeaponsOn flags
  (BT411's raw "bits 1/2" resolved to their 1995 names).
- MECHWEAP: defined MechWeapon::MessageHandlers (0-entry set inheriting the
  Subsystem chain) -- it was declared-but-undefined, and the surviving CODE
  PPC.CPP binds the inherited name into PPC::DefaultData; the zero-filled
  common block was a latent NULL deref (same trap as Emitter::AttributeIndex).

Discovery: the TEST.EGG mech genuinely mounts TWO REAR LASERS (ERMLaser_2/3 on
the b-ports). Verified on a full-clean baseline (engine 181 + bt 42, 0 fail):
look-behind arms exactly the rear lasers, disarms the other five,
pipMask=0xfffffffe; neutral run holds forward-armed; zero Fail.

build410.sh HARDENED against the stale-obj layout-skew trap this wave exposed:
cc() skipped recompiles on obj-vs-.cpp timestamps only, so the MECHWEAP.HPP
member additions left emitter.obj on the OLD MechWeapon layout -- its
chargeLevel=0.0f write landed exactly on the new rearFiring (a silent cross-TU
struct skew, heisenbug-grade). cc() now honors header stamps: a source410
engine-header edit invalidates every bucket, a BT/BT_L4 header edit
additionally invalidates the game buckets. CODE/ is immutable, no stamp needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:43:38 -05:00
CydandClaude Fable 5 eb8bba3195 BT410 Phase 5.3.6: look-button state machine -- full eyepoint composition
Reconstructs the binary's five-state LOOK machine (controls mapper tail,
part_013.c:396-459) as proper members/methods:

- MECHMPPR: LookState enum (None/Left/Right/Behind/Down) + lookState/
  previousLookState (out of reserved[24] -> [22]). InterpretControls picks the
  state from the look buttons each frame and on a CHANGE calls
  Mech::CommitLookState. BT_FORCE_LOOK=<1..4> dev hook.
- MECH: authored look angles lookLeft/Right/Front/BackAngle (deg->rad from the
  GameModel resource, guarded) + lookPitch/lookYaw + CommitLookState(int):
  side looks yaw by the authored angle, look-behind = yaw pi + lookBackAngle
  pitch, look-down = lookFrontAngle pitch, forward = identity. The per-frame
  eyepoint compose in Simulate adds the live Torso elevation on top.
  (reservedState [208] -> [202].)
- Deferred inside the commit (needs MechWeapon viewFireEnable/rearFiring):
  per-view weapon fire re-arm + HUD pip group-mask flip.

Verified headlessly: model reads clean authored angles (L=60 R=-60 F=-10 B=0
deg -- ModelResource layout confirmed again); BT_FORCE_LOOK=3 fires ONE commit
([look] state=3 yaw=3.14159 pitch=0) and the per-frame compose holds eyeYaw=pi
with eyePitch=0.349066 (lookBackAngle + the 20deg-clamped elevation). Zero Fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:08:50 -05:00
CydandClaude Fable 5 87b1c048cf BT410 Phase 5.3.5: eyepointRotation -- torso elevation pitches the eye/aim-ray, not a joint
Investigated the next planned binding (elevation -> gun joints jointlgun/
jointrgun) before implementing it, and found it was the wrong target: BT411's
own reverse-engineering history records that NO gun/arm elevation joint exists
in the authentic engine. The pod's stick-Y aim pitches the COCKPIT EYE and the
weapon boresight directly (mech4.cpp @~5219, pixel-calibrated against real
screenshots) -- the torso geometry never tilts for elevation on this mech
family. Reconstructing a gun-joint binding would have been invented behavior.

- MECH.HPP: EulerAngles eyepointRotation member (out of reservedState, now
  [208]) + GetEyepointRotation() accessor.
- MECH.CPP: composed each frame in Simulate -- eyepointRotation =
  EulerAngles(Radian(Normalize(lookPitch+elevation)), Radian(Normalize(lookYaw)),
  Radian(0)), reading Torso::CurrentElevation() via sinkSourceSubsystem.
  lookPitch/lookYaw are 0 until the look-button wave (BTCommitLookState) lands.

Verified: BT_FORCE_ELEV=0.8 -> torsoElev and eyePitch both read 0.349066 (the
authentic 20 degree resource limit) every frame, exact match; neutral -> 0.
Zero Fail. Corrected TORSO.NOTES.md's prior (wrong) "next: gun joints" note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:51:28 -05:00
CydandClaude Fable 5 9fff0e969e BT410 Phase 5.3.4: Mech::ResolveJoint + skeleton-joint binding (torso twist)
Reconstructs the shared skeleton-joint resolver and binds the first subsystem
(Torso) to the live skeleton.

- Mech::ResolveJoint(name) (mech.cpp @00424b60): GetSegment(name) -> segment
  joint index -> GetJointSubsystem()->GetJoint(idx).
- Verified the JointedMover base streams the full skeleton headlessly:
  jointCount=19, 40 named segments (gun joints jointlgun/jointrgun, shoulders,
  hip, leg joints jointlthigh..jointrankle). BT_MECH_LOG [skel] summary;
  BT_SKEL_DUMP lists every segment/jointIdx.
- Torso resolves + binds its twist joint (ResolveJoint(torsoHorizontalJoint))
  and pushes currentTwist onto it via Joint::SetRotation (hinge->Radian,
  ball->EulerAngles yaw). The TEST.EGG mech has a fixed torso (horizJoint='',
  enabled=0) so its twist path is inert -- correct + guarded, zero Fail.

Next joint piece: elevation -> gun joints (jointlgun/jointrgun, the fixed-torso
aim mechanism) via weapon-joint binding; then gait leg animation. Both are
VISIBLE only under the renderer; headlessly the joint transforms are loggable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:17:37 -05:00
CydandClaude Fable 5 0cc7713865 BT410 Phase 5.3.2e: source authentic per-mech locomotion params from GameModel resource
The Mech ctor now reads walkingTurnRate / runningTurnRate / maxAcceleration /
throttleAdjustment from the GameModel resource (SearchList(GameModelResourceType)
-> ModelResource), replacing the bring-up-default constants for those fields.

Each read is sanity-guarded (out-of-band -> keep default) since BT411 flags parts
of the ModelResource layout as mis-decoded -- but the live values came back clean
and authentic: walkTR=75deg/s, runTR=50deg/s, maxAcc=30 u/s^2, throttleAdj=1.0
(walking turns faster than running; maxAcc matches the madcat note), confirming
our reconstructed struct layout is correct for these fields.

Verified: at walk speed the authentic 75deg/s tightens the turn circle to r~6.9
(=speed/turnRate); zero Fail. Still on defaults (pending LoadLocomotionClips):
reverseStrideLength (top speed) / walkStrideLength / reverseSpeedMax -- measured
from the walk/run animation clips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:52:20 -05:00
CydandClaude Fable 5 ebf0d8c23f BT410 Phase 5.3.2: DRIVABLE mech -- authentic control interpretation + locomotion
The mech now drives from the authentic control chain (faithful route). Verified
headlessly: full throttle -> walks straight at top speed (30 u/s) along heading;
throttle+turn -> walks a circle (radius = speed/turnRate); neutral -> holds pose;
zero Fail.

MechControlsMapper::InterpretControls (mechmppr.cpp @004afd10) -- installed as the
mapper's per-frame Performance (roster slot 0, ticks before the mech):
- speedDemand = topSpeed*throttle*fwdScale (reverse inverts); soft stick (square)
  / pedal (cube) response; Basic=stick turn, Std/Vet=pedal turn; speed clamped
  while turning hard. Reads owner stride via Mech accessors.
- BT_FORCE_THROTTLE/BT_FORCE_TURN dev hooks for headless verification.

Mech::Simulate drive -- consumes the mapper demands:
- accelerate currentBodySpeed toward speedDemand (maxBodyAcceleration);
- authTurnRate = lerp(walkingTurnRate, runningTurnRate) by ground speed + over-run
  falloff (mech4.cpp master-perf @0x4aa3d3);
- integrate heading via Quaternion::Add(prevPose, (0,turn*rate*dt,0));
- facing = world -Z basis (GetFromAxis(Z_Axis)); worldLinearVelocity = facing*spd;
- integrate position (increment-1 core).

MECH.HPP: 9 named locomotion members out of reservedState (now [211]) --
walking/runningTurnRate, reverse/walkStrideLength, reverseSpeedMax,
forwardThrottleScale, maxBodyAcceleration, body/currentBodySpeed. BRING-UP
DEFAULTS (authentic values come from the model resource + LoadLocomotionClips --
next refinement).

Deferred: gait-clip-exact advance + leg anim (needs animation subsystem/renderer),
model-resource sourcing, terrain drop, torso/free-look aim, telemetry filters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:46:30 -05:00
CydandClaude Fable 5 3364b65ae8 BT410 Phase 5.3.1: Mech::Simulate motion core -- mech integrates motion per-frame
Installs Mech::Simulate as the mech's per-frame Performance (was DoNothingOnce).
Reconstructs the load-bearing spine of the 1995 Simulate (mech4.cpp @004ab430):
integrate the body velocity into localOrigin (linearPosition.AddScaled(..,
worldLinearVelocity, dt)) and commit the Origin to the world transform with the
engine idiom (localToWorld = localOrigin, per ENTITY.cpp:988 / MOVER.cpp:850).

Uses the NAMED engine base Mover fields (localOrigin/localToWorld/
worldLinearVelocity) -- BT411's MechBaseLayoutCheck proved the WinTesla decomp's
raw this+0x100/0x260 offsets actually stomp those base fields; the clean 1995
build addresses them by name.

- MECH.HPP: Mech Performance typedef + SetPerformance + Simulate() decl.
- MECH.CPP: SetPerformance(&Mech::Simulate) at ctor tail; Simulate body.

Verified headlessly (BT_MECH_LOG "[sim] mech pos=" 1Hz): BT_DRIVE="5,0,10"
advances the mech +5.0/s x, +10.0/s z (y fixed) = exactly the injected world
velocity; no BT_DRIVE -> worldLinearVelocity is 0, mech holds spawn pose. Zero
Fail/Exception either way. BT_DRIVE is a retained dev hook for headless motion.

Deferred (locomotion wave): gait-cycle self-propulsion feeding worldLinearVelocity
(IntegrateMotion->AdvanceBodyAnimation, steered by mapper throttle/turn), heading
integration, terrain-height drop, cockpit telemetry filters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:45:24 -05:00
CydandClaude Fable 5 b8f5928887 Mech ctor PROVEN: constructs 33 subsystems + 7 weapons from real model data
Live trace (BT_MECH_LOG) on the pod TEST.EGG mech: '[mech] segment walk done:
subsystemCount=33 weaponCount=7' -- the segment walk instantiates the ENTIRE real
subsystem roster (31 subsystems + 2 sentinels, 7 weapons) from the actual streamed
model, no crash. The structural reconstruction is proven end-to-end on real data.

The crash is AFTER full construction, in MakeAndLinkViewpointEntity's attribute-
watcher setup (an AttributeWatcher resolving a subsystem attribute via
GetAttributePointer). Phase-5 frontier confirmed = per-subsystem AttributeIndex
tables + watcher wiring. Trace gated behind BT_MECH_LOG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:55:42 -05:00
CydandClaude Fable 5 b93c5d4bdd Mech phase 4: non-NULL default roster slots -- boot reaches the attribute binding
Unrecognised subsystem classIDs now get a base MechSubsystem (Laser/ParticleCannon
-> Emitter) so control/damage bindings that resolve a subsystemID find a real
subsystem, not a NULL plug. This moved the boot PAST the first Link::AddToPlug
NULL-deref; it now runs to Simulation::GetAttributePointer -- the subsystem
ATTRIBUTE system. Each reconstructed subsystem currently reuses the base
Subsystem::AttributeIndex, but the streamed control-mapping / gauge bindings
reference subsystem-specific published attributes (Sensor RadarPercent, Generator
OutputVoltage, Emitter ChargeLevel, ...) that aren't in the base index -> the
attribute lookup walks off the end. Reconstructing each subsystem's AttributeIndex
(+ plug/capability-chain wiring) is the phase-5 integration. BT: 42 ok.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:15:00 -05:00
CydandClaude Fable 5 77041a1539 Mech phase 4: SetMappingSubsystem + MechControlsMapper ctor -- mech links as viewpoint
SetMappingSubsystem installs the control mapper into roster slot 0 (the streamed
control-mapping binds DirectMappings to subsystemID 0). MechControlsMapper ctor
constructs (per-frame InterpretControls deferred to phase 5). With RIO controls,
MakeViewpointEntity now builds the MechRIOMapper, installs it, and gets PAST the
controls check -- the mech constructs AND links as the viewpoint entity. Boot now
advances into the mission-startup / control-binding path (next: a real NULL-deref
there, deeper than the staged Fails). BT: 42 ok.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:11:41 -05:00
CydandClaude Fable 5 c9aff18191 Mech phase 4: the Mech constructor + segment walk WORKS -- roster instantiates live
The Mech ctor now finds the model's subsystem-model-stream resource
(SearchList by SubsystemModelStreamResourceType), wraps it in a MemoryStream,
and walks the segments -- switching on seg->classID to  into the base Entity's subsystemArray, caching sensor/gyro/sinkSource/hud
and counting weapons.

KEY FINDING: the source410 VDATA ClassID values MATCH the 4.10 binary for the
core subsystems (0xBBD=Condenser, 0xBC3=Sensor, 0xBC5=Torso, 0xBD6=HUD, ...) --
BT411's 'mislabels' were its OWN wrong enum names. So the walk switches on the
real VDATA names cleanly. Unrecognised classIDs leave a NULL slot (roster stays
aligned) rather than aborting.

LIVE: boot now runs the full Mech ctor -- segment walk instantiates the roster,
no crash -- and advances PAST it to BTL4Application::MakeViewpointEntity, which
halts at 'Mech has no controls mapping!' (the slot-0 control mapper /
SetMappingSubsystem is the next brick). The largest function in the game
executes. BT: 42 ok; tree links clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:07:39 -05:00
CydandClaude Fable 5 4d175c959a Mech phase 3: named member layout + embedded-member construction
Replaced MECH.HPP reserved[331] with the named Mech-own members: cached subsystem
pointers (sensor/gyro/sinkSource/hud) + messageManager + weaponCount, the 5
capability chains (ChainOf<Subsystem*>), and the embedded status/animation state
(4 AlarmIndicators, Reticle, 3 StateIndicators, 2 SequenceControllers, NameFilter,
telemetryFilter[5]=AverageOf<Scalar>, 3 CStrings) + a reservedState pad for the
phase-5 per-frame fields. The subsystem ROSTER (subsystemArray/subsystemCount)
lives in the base Entity (GetSubsystem/GetSubsystemCount), so the Mech only caches
back-pointers.

The Mech ctor now CONSTRUCTS all embedded members (alarms Initialize'd, reticle
armed, state indicators sized, telemetry filters sized, sequence controllers
Init'd) -- compile-verified -- then halts at the segment walk (the remaining
piece). BT: 42 ok; tree links clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:00:51 -05:00
CydandClaude Fable 5 e610a2a3aa 4.10 reconstruction: real spawn factory path -> Mech ctor frontier
BTPlayer::CreatePlayerVehicle reconstructed (faithful; BT411's BT_SPAWN_XZ/
BT_SPAWN_ENEMY bring-up scaffolding dropped): builds the Mech::MakeMessage from
the mission game-model resource and hands it to MakeAndLinkViewpointEntity.
Mech::Make = new Mech(creation_message); a staged Mech ctor chains the real
JointedMover base (so the model skeleton/segments stream from BTL4.RES) then
Fails.

This validates the entire spawn factory chain live: CreatePlayerVehicle ->
MakeAndLinkViewpointEntity -> registry -> Mech::Make -> new Mech -> JointedMover
base ctor (streams the mech model) -> halts at the Mech ctor (mech.cpp:66).

The Mech ctor (@004a1674, 5690 bytes -- the largest function in the game) plus
the subsystem roster is the next MAJOR milestone, not a brick: it needs the full
1995 Mech member layout derived first (MECH.HPP is still reserved[331]; BT411's
decomp uses raw offset pokes that can't compile fresh), the subsystem class
hierarchy (only GAUSS/PPC/SENSOR survive in the 4.10 archive; the rest are the
measured "917 missing functions"), and the segment-table walk. Plan + scope in
MECH.NOTES.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:33:15 -05:00
CydandClaude Fable 5 5b35eb973c 4.10 reconstruction: BTL4OPT.EXE links clean and BOOTS to the first staged brick
The reconstructed tree now produces a runnable binary with the authentic
1995 toolchain (BC4.52 / tlink32 / DPMI32):

- BTL4.CPP main TU reconstructed from the 4.11 Ghidra decomp (FUN_0040109c)
  + the 4.10 binary's own string pool + surviving RPL4TOOL.CPP house style;
  probe_main.cpp scaffold retired (BTL4.NOTES.md documents every decoded call)
- L4NET.CPP staged: L4NetworkManager ctor/dtor + 10 vtable-pulled virtuals
  (standalone-benign ones no-op, network ones Fail loudly) + NetNub client
  globals (Net_Common_Ptr=NULL routes L4File to its plain-DOS path)
- build410.sh: libs now built in the AUTHENTIC makefile member order
  (MUNGA.MAK / mungal4.mak / BT.MAK / BTL4.MAK). Order is load-bearing:
  tlink emits static-init records in module pull order, and alphabetical
  order booted into a null-vptr crash (IcomManager::ClassDerivations
  constructing before parent NetworkClient::ClassDerivations). Also fixed
  stage_link to the proven 32-bit lib set (SOSDBXC+SOSMBXC, no WATTCPLG)
- BOXTREE.HPP MemoryBlock unify, BTL4GRND notify stubs, remaining engine
  backfills (audio/gauge/resource/stream TUs) that closed the deep ledger

Smoke test (DOSBox-X + 32RTM, copy of the pod BT tree, our exe swapped in):
  BattleTech v4.10
  BTL4Application::BTL4Application
  l4net.cpp(22): L4NetworkManager -- l4net.cpp not yet reconstructed
Static init, main, -egg parse, BTL4.RES load (version 1.0.6 check passes),
ApplicationManager and the BTL4Application ctor chain all execute real
reconstructed code; boot halts at the first staged Fail() as designed.
Next brick: the real l4net.cpp body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:05:53 -05:00
CydandClaude Fable 5 af80d52e22 source410: engine COMPLETE (165/165) - link reaches PE emission; deep ledger cut
The tlink32 campaign after the checkpoint: eleven engine bodies back-dated
from BT412 (rotation player team explode dropzone terrain cultural receiver
subsystm app l4gauge - the Application core included), the WinTesla-ectomy
handled by backdate.py's new unwrap rules (accessor-fn statics -> 1995 static
objects, decl rewrites, pointer->reference Derivation ctor, 2007 windowed-
gauge/console-marshal/idle-pump excisions), staged statics TUs opened for the
game classes (MECH/BTPLAYER/BTDIRECT/PROJTILE/MISSILE/BTL4MPPR .CPP + L4APP/
NETWORK engine statics).

State: every compiled symbol short of the shallow graph RESOLVED (the 52- and
17-symbol ledgers burned to zero); with 32stub.exe in place the linker reaches
full vtable closure and emits the deep ledger (~210 symbols, UNRESOLVED-
LEDGER.txt): remaining engine bodies (objstrm cstr gauge/gaugrend graphics
pixelmap palette resfile ray scnrole explosion-table), the DOS driver extern
layer (_SVGA*/_PCSerial*/_PCSPAK*/joystick/netnub/sosMIDI), the full L4App
body, and the real game-TU frontier (Mech::Make et al.). Recipe proven for
every category.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:25:49 -05:00