05392e227bcbe98a27bc79ba803b2ef9e10d314e
38
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |