User report: after killing the enemy, wandering around, weapons won't
fire. Not a lock requirement -- the trigger pulse generator (the
1,0,1,0 edge feed for CheckFireEdge) sat inside `if (gEnemyMech != 0)`.
Once the wreck buried and gEnemyMech nulled, the channels froze at
their last value -> no rising edges -> no fire, even with a valid
terrain pick in the target slot. Latent since the world-pick model made
no-enemy firing meaningful (task #41); the weapon-group split kept the
pulses in the same wrong place.
The pulses now run unconditionally (before the enemy block); the
weapon's own HasActiveTarget gate remains the only fire arbiter --
terrain downrange fires at the scenery, a true sky shot (nothing within
1200) authentically refuses.
Verified: BT_AIM=0.5,0.3 (terrain-only picks, enemy never aimed at) ->
sustained fire, 123 laser + 82 PPC beam samples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Interim pod-like grouping ahead of the authentic ConfigureMappables/
ChooseButton mapper channels (decomp-anchored in open-questions):
- Three fire channels like three pod buttons: gBTWeaponTrigger (lasers,
key 1 or SPACE), gBTPPCTrigger (PPCs, key 2), gBTMissileTrigger
(missiles, key 3 or CTRL). Keyboard only per user (mouse buttons
removed).
- EmitterSimulation picks its channel by classID (PPC 3028 vs Emitter
3016); BT_AUTOFIRE holds all three.
- The BT_BEAM_LOG sampler modulus 30 aliased to ONE weapon under the
new synchronized 5-beam volleys (5 | 30) -> 31 (coprime).
Verified: all 5 energy weapons fire (31 samples each), aimed hits land,
kill completes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
[hud] pip diagnostic logs each registered pip's classID/PipPosition/
WeaponRange/PipExtendedRange/PipColor. The live dump corrects the
"everything is 500m" assumption: BLH lasers red @500 (x3, ext=1),
missiles amber @800 (x2), PPCs blue @900 (x2) -- the pips sit at their
weapons' max-range marks on the range ladder (caret below a pip = that
weapon reaches), so 7 weapons read as 3 weapon-system groups. KB +
comments corrected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report: lock at spawn range (~120) but no damage until closing in.
The bring-up damage gate used kWeaponRange=100 -- a placeholder from
before the weapon reconstruction -- while the authored BLH weapon range
is 500 m. The binary's rule (FireWeapon @004bace8:7758 [T1]): damage
applies when dist <= the weapon's EFFECTIVE range = (1 - host-zone
damage) x authored WeaponRange -- the per-weapon targetWithinRange flag
UpdateTargeting computes each frame. The gate (and the [target] log's
IN WEAPON RANGE annotation) now reads that flag across the weapon
roster; kWeaponRange removed.
Verified: stationary at spawn range 120 -> IN WEAPON RANGE + 34 aimed
zone hits (was 0 until closing under 100).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The right-ladder caret reads range-to-pick, and with the world-pick
model the pick is usually the ground downrange -- so walking (cockpit
bob + turning sweeping the boresight across near/far terrain) made the
caret teleport frame to frame ("going all over the place", user
report). The binary rate-limits the DISPLAYED range: shown +=
clamp(true - shown, +-dt*500) -- a 500 m/s slide, applied in
HudSimulation, including toward the no-target 1200 default
(_DAT_004b7ed0 confirmed 0 = the abs() idiom). Implemented in mech4's
HUD feed where dt is in scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report: enemy takes hits, starts smoking, then beams pass through
-- no more damage, no death. Root cause: the death-effects dispatch
(blhdead explosion chain + wreck swap + KILL score) lived in the
SHOOTER's laser fire block kill check. A missile killing blow lands
frames later in BTUpdateProjectiles -- outside that block -- and after
task #41 the boresight pick skips a dead mech, so the fire block never
ran against it again: the death chain was silently skipped, leaving an
internally-dead, smoking, standing, invulnerable mech.
Fix: the dispatch moved to the VICTIM's own once-per-death transition
(UpdateDeathState's movementMode->9 moment) -- fires exactly once for
ANY kill source (laser, missile, collision). Killer id for the
Explosion message = lastInflictingID (the task-#31 bookkeeping). The
BT_ENABLE_TEARDOWN cdb harness was removed with the old block (findings
preserved in docs/HARD_PROBLEMS.md).
Verified headless (75s soak, missile-heavy kill): destroyed ->
'blhdead' id=22 -> wreck swap 'blhdbr.bgf' -> smoke re-arm -> sink ->
buried INERT; post-death picks fall through to terrain; 0 crashes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User video evidence (lasers firing at nothing) + the colleague's
torso-locked report + the binary all reconcile into one model:
The boresight pick hits the WORLD, and the target slot mech+0x388 holds
whatever entity is downrange:
- enemy mech under the boresight -> aimed target (hull point -> STEP-6
zone under the boresight, hotbox + lock ring, damage);
- else the TERRAIN downrange (BTGroundRayHit to 1200) -> the beam and
missiles fire at the scenery ("firing at nothing", as in the pod
videos), range caret reads the ground distance (authentic :5639), no
damage;
- else (sky) -> no target, and the weapon's own double 0x388 gate
refuses the discharge.
Binary evidence for non-mech targets: HudSimulation :5620 explicitly
handles a target WITHOUT damage zones (target->0x120 == 0) -- dead code
if only mechs were ever targeted. The pick is automatic every frame
(0x388: 11 reads / 0 direct stores in CODE).
Implementation: gBTTerrainEntity captured in MakeEntityRenderables
(Terrain::GetClassDerivations); mech4 pick = enemy PickRayHit else
ground ray -> terrain entity + ground point; damage/hotbox/lock only
for the mech pick; the task-40 enemy auto-converge REMOVED (facing away
now fires at the scenery, not magically at the enemy -- the user's
complaint). Verified all three states headless (BT_AIM 0,0 / .6,.5 /
0,-.8): aimed zone hits / terrain beams + 0 damage / no discharge.
Fourth (and evidence-complete) targeting model iteration; KB updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Colleague: you fire without a lock, at "nothing." Double-checked the
binary:
- The weapon fire path is DOUBLY gated on mech+0x388 != 0:
EmitterSimulation (FUN_004baa88:7689) only calls FireWeapon with a
target, and FireWeapon (FUN_004bace8:7727) wraps its whole body --
including beamFlag(+0x46c)=1 -- in the same check. So no target => no
beam, literally (cannot fire into truly empty space). [T1]
- BUT a capstone scan of the entire CODE section finds 11 READS of
+0x388 and ZERO direct stores: the target is written INDIRECTLY (a
message/selector), i.e. AUTO-acquired -- there is no manual lock to
fire. The spinning-ring LOCK (HudSimulation 5619-5634) is a separate,
stricter state.
So the colleague is right that no lock is needed; my port was wrong to
gate firing on a pinpoint boresight-on-hull pick. Fix: mech+0x388 = the
enemy whenever it is ALIVE (auto-target); firing needs only that. A
pinpoint hull hit upgrades the shot to aimed-zone damage + the lock
ring; off-hull with the enemy present, the beam converges on centre
mass (body hits). Third correction from over-reading the RP-shared
Reticle struct (after sticky-lock and mouse-cursor).
Verified: BT_AIM="0.5 0.2" (off-hull) -> fires + lands damage (was 0
before); BT_AIM="0 0" (centred) -> HOT-aimed zone hits. KB swept;
checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Colleague flagged that targeting is torso-locked with no fine cursor.
Correct: the pod stick's yaw drove the TORSO TWIST
(MechControlsMapper -> HUD::SetFreeAimSlew(stick_x) @cockpit+0x28C,
gated on torso-horizontal-enabled; hud.hpp:167) and reticlePosition
@HUD+0x1FC is COMPUTED by HudSimulation from the mech pose quaternion +
target geometry, zeroed to centre with no target (part_013.c:5680) --
never a free-floating cursor. The engine Reticle struct is general
(shared with Red Planet); BT drives it from the torso boresight.
Fix:
- Removed the mouse-cursor slew (a mis-sourced stand-in from the
RP-shared struct). The crosshair is now the torso boresight:
BTTwistToReticleX(torsoTwist) = tan(twist) projected through the live
per-axis projection. Dead-centre on the fixed-torso BLH
(TorsoHorizontalEnabled=0); you aim by steering the whole mech.
- BT_AIM="x y" retained as the headless test harness.
Verified: face-to-face spawn -> HOT lock + aimed zone hits (36);
BT_FORCE_TURN circling -> 31 no-target vs 1 HOT (steering off-target
drops the lock, as it must). KB swept (combat-damage / gauges-hud /
open-questions); checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- LOCK rules transcribed from the exported HudSimulation
(part_013.c:5619-5634): lock requires a target AND your own HUD host
zone damage < 0.75 (_DAT_004b7ec4 -- a shot-up targeting computer
drops lock) AND the targeted zone damage < 1.0 (_DAT_004b7ec8;
whole-mech target checks zone 0 -- a wreck's dead zone can't
re-lock). Box and ring are now separate signals like the binary:
gBTHudLockState 1 = target held (hotbox draws), 2 = LOCKED (+ the
spinning ring).
- SIMPLE-X mode: the ctor's [0x99] minimal-reticle list transcribed
(@4689-4705: green +-0.02..0.08 cross on the aim translate); Draw
switches master <-> simple X on the PrimaryHudOn element bit (0x20),
completing the recovered Execute's state-list logic.
- Canopy diagnosis sharpened (open-questions): blx_cop is the torso
segment's inside-skeleton mesh enclosing the eye; the black box =
the skeleton render branch missing its texture + PUNCH cutout (the
canopy windows are punch texels). A rendering-branch task; stays
hidden by default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one un-exported gap in the reticle chain, read via capstone
(tools/disas2.py; the annotated disasm preserved at
reference/decomp/reticle_execute_004cdcf0.disasm.txt). Draw() is now a
transcription of the real per-frame logic, and the HUD attr-table names
(hud.hpp ids 4/5/6/8/A/B/C/D) are CONFIRMED by their Execute usage:
- Range ladder: BAR from ladder-top to the caret + caret translate.
- The bottom 21-tick tape is the TORSO-TWIST indicator (NOT heading):
deflection = -/+(span/2) x (RotationOfTorsoHorizontal / twist limit),
attrs 4/5/6. Fixed-torso BLH reads centred -- authentic static.
- The circle-with-stem is the COMPASS (attr 0xD, rad->deg rotation) at
(botX, botY - 3*tickMajor - 0.03), with the THREAT trail (attr 0xC)
in its rotated frame: 0.05-unit attack-direction marks, fresh < 2s
red, expiring at 6s. Port feed: player TakeDamage pushes the impact
direction (dormant vs the passive test dummy).
- Pips (composed into subB6): hidden when destroyed (attr 1 == 1), LIT
when the fire cycle is LOADED (attr 0x1c == 2; port source
rechargeLevel >= 1) else the dark charging ring; filtered by the
weapon-GROUP bits (weaponMode & elementMask&0xF). Range plays NO part
(the stored TargetWithinRange slots are never read by Execute).
- Lock ring: subB9 at frame centre SPINNING 4 deg/frame while locked
([0x9d] is the spin matrix, not a heading list).
- Target HOTBOX: a rectangle hugging the projected extents (x+-4 around
the top-centre hotbox point, +1/-11.5 vertical; baked K=2.8145 -- the
port projects through the live per-axis projection), switching to the
edge arrows past +-1.6 or behind (BTProjectHotBox, L4VIDEO).
- Reticle state Off/On + PrimaryHudOn full-HUD/simple-X switch, aim
translate on slew move, 3D marker + PNAME player-name mesh identified
(chain still deferred).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sideways lock offset)
The windowed present stretches the fixed 800x600 backbuffer into the
client area while the projection's aspect follows the CLIENT (resize
rebuild) -- so the dpl2d/reticle frame, the client frame and NDC only
coincide for a backbuffer-shaped window. The mouse mapping and the
square-frame ray shortcut (cx = rx*tanHalfFov) were exact at screen
centre and drifted outward with a side-flipped sign: aiming left of the
enemy locked as if right of it (user report).
- BTSetAimProjection now publishes BOTH proj scales (P11 carries the
aspect) + the backbuffer size (the dpl2d frame). NOTE: GetViewport at
the loop top holds the PREVIOUS frame's last pass viewport (gauge
passes shrink it) -> use the renderer's own GetWidth/GetHeight.
- BTGetAimRay / BTProjectToReticle convert reticle <-> NDC per axis
(ndc_x = rx * vh/vw; camera x = ndc_x/P11) -- exact for any window
shape and FOV.
- New BTClientToReticle undoes the present stretch for the mouse
(client px -> viewport px -> reticle coords); mech4 uses it instead
of the raw client-height mapping.
- 1Hz aim telemetry in the [target] log (hits/noRay/noPick): verified
100% pick hit rate once the projection is live (the pre-visibility
mission-load seconds report noRay -- that was the "flicker").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sticky lock made aim irrelevant after the first acquisition (the
crosshair starts on the face-to-face enemy, so it locked at spawn and
never let go -- played as auto-targeting). The binary treats no-target
as a frequent live state (fire path re-checks 0x388!=0 at every step,
part_013.c:7689/7727; HUD range feed has a permanent no-target default,
:5636), so the target slots now mirror the reticle pick each frame:
locked while the crosshair is ON the mech, no lock otherwise. Keeping
the crosshair on the enemy is the gunnery.
Verified headless: BT_AIM="0 0" -> HOT + aimed zone hits; BT_AIM="0.6
0.4" -> no lock, zero damage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine Reticle model (MUNGA/RETICLE.h [T0]) reconstructed end to end:
- Mech::targetReticle is a real Reticle member bound to the TargetReticle
attribute (0x1d), per the RP VTV analog (VTV.h targetReticle).
- Crosshair slew: mouse -> client rect -> reticle coords (the pod stick
free-aim channel's dev-box stand-in); BT_AIM="x y" pins it headless.
LMB fires lasers / RMB missiles (alongside SPACE/CTRL).
- Pick ray: the ACTIVE eye publishes pos + LookAtRH basis (BTSetAimCamera,
L4VIDRND view-write site) + the render loop publishes proj._22;
BTGetAimRay builds the world ray, Mech::PickRayHit slab-tests it against
the collision template's ExtentBox via the engine's BoundingBox::HitBy
(local frame; clips the Line at entry) -> world hull point.
- Designation: the mech under the crosshair designates (sticky; re-hover
refreshes; cleared when the target leaves the roster at burial); the
entity target slots 0x37c/0x388/0x38c feed the whole weapon path.
- Aimed fire: while HOT the impact point is the PICKED hull point -> the
STEP-6 cylinder lookup resolves the zone under the crosshair (verified:
center-aim -> head-band zone 13 dominant). Off-crosshair the sticky
designation converges on center mass.
- HUD: the aim group draws at the slewed position ([0x9a] translate,
contained by push/pop); the designator ring tracks the target's
projected point (subB9 hot / subB8 designated, BTProjectToReticle);
edge arrows when off-screen/behind.
- AUTHENTIC gating: no fire arc exists in the binary (FireWeapon fires
whenever HasActiveTarget, part_013.c:7758) -> BT_FIRE_ARC is now an
explicit OPT-IN presentation clamp; the hardwired gEnemyMech lock and
the projectile path's gEnemyMech fallback are removed.
- Fixed en route: every renderable rebuild stomped mCamera back to the
chase eye (start-inside silently lost the cockpit camera; the aim feed
exposed it). BTL4VideoRenderer::mViewInside persists the chosen view.
Verified headless: BT_AIM="0 0" -> HOT lock, pick hits the hull face at
exact range, aimed zones resolve; BT_AIM="0.8 0.3" -> no lock, zero
damage, zero missile launches; kill chain completes to wreck + smoke.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- dpl2d API fully recovered from the binary recorders (@487f34-488630):
opcode model (points/lines/polyline/circle/color/width/matrix/push-pop),
CallList = INLINE include (state persists to caller), centered coordinate
frame (unit = half viewport height). game/reconstructed/dpl2d.cpp rework.
- BTReticleRenderable ctor @004cc40c transcribed with the authentic
calibration (originX .35, originY .25, scaleY .5, 0..1200m right range
ladder, bottom heading tape, FUN_004cd938 tick ladders, lock rings,
turn arrows); range caret slides from the live target range fed by the
mech4 targeting step (BTSetHudTargetRange).
- Weapon pips: the binary gate is IsDerivedFrom(0x511830 =
MechWeapon::ClassDerivations) [T1: part_014.c:5386 hard-aborts on missing
weapon attrs; part_012 counts + roster ORs capabilityFlags@+0x334] so ALL
7 BLH weapons register (3 lasers + 2 PPCs + 2 MissileLaunchers). Pip A
(lit, authored PipColor) on TargetWithinRange, else dark ring B.
- AddWeapon @004cdac0 store map corrected to the verified order
(part_014.c:4827-4837); both state attrs are literally named
"SimulationState" (strings @51d526/51d577) -> weapon simulationState.
- Mech roster this[0x1ef] renamed poweredSubsystems -> weaponRoster
(0x511830 is MechWeapon, not PoweredSubsystem=0x50f4bc); derivation-tag
table added to context/decomp-reference.md.
- Draw hook BTDrawReticle after the 3D scene, cockpit view only. Binary
Execute @004cdcf0 is an un-exported gap -> Draw dynamics [T3], tracked
in context/open-questions.md with the blx_cop canopy + PNAME pip meshes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every dpl2d_ recorder in the binary self-identifies via its debug-name string
(part_010.c 0x487f34..0x4888c0) -- the complete API + opcode map is recorded
in phases/phase-02-dpl2d-reticle.md: point sets (2/3), closed polylines (4/5),
open line strips (6/7), AddPoint (8), AddCircle (9), SetColor (0xF),
Set/ConcatMatrix (0x10/0x11, 2x3 affine), Push/PopState (0x12/0x13),
SetLineWidth (0x15), CallDisplayList (nested glyphs), FullScreenClipRegion.
dpl2d.cpp reworked to that model: command-stream recorder (open-primitive
vertex runs, state commands, nested list calls) + a recursive ExecuteList
rasteriser (XYZRHW points/strips/loops/circles, 2x3 transform + state stacks,
save/restore). CORRECTION: the old "PushMatrix/MoveTo/PopMatrix" trio was a
misreading of OpenPolypoint/AddPoint/ClosePolypoint ("draw a point") -- kept
as aliases mapping to the true semantics.
Reticle findings recorded: the ctor (@004cc40c) builds ~15 display lists (the
dotted-cross reticle, tick ladders via FUN_004cd938, side arrows, arcs) plus a
3D marker chain and the PNAME1-8.bgf pip meshes (all ship). Next: transcribe
ctor+Execute, wire the 1996 caller's AddWeapon arguments, hook the draw.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- V toggles the authentic cockpit eyepoint <-> the chase camera. Both eyes
coexist; DPLEyeRenderable::Execute now writes the VIEW only when it IS
mCamera (unconditional writes let the last-executed eye stomp the toggle).
- EyepointRotation is a real zeroed EulerAngles member on Mech (was bound to
the shared junk attrPad -- the eye composes this attribute into the view
EVERY frame, so the cockpit camera was rotated by garbage: the canted
horizon, then the black screen). The eye-slew systems write it later.
- The cockpit eye mounts at the eyepoint's REST position with a clean upright
forward basis on the tree root (the live joint chain fed it the site tilt +
torso pose; authentic pitch/yaw is the deferred gyro eye chain).
- The inside view swaps the player to the INSIDE skeleton mesh set
(SkeletonType_A): 19 body segments hide, exactly one mesh remains --
blx_cop.bgf, the authentic cockpit canopy shell around the eyepoint. It
currently renders as a black enclosure (the black-screen report), so it is
HIDDEN pending its interior/punch-material rendering (BT_INSIDE_COCKPIT=1
shows it for that work). Damage gstates respected in both directions;
RemakeEntity keys off the DISPLAYED skeleton.
- Dev: BT_START_INSIDE=1 starts in the cockpit view.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answers "can I keep targeting the dead mech?": the lock isn't cleared by
anything we've found, but the binary REFUSES to sustain energy beams on a
destroyed mech: ServiceDischarge (@004ba8d0) and ContinueDischarge (@004baa20)
check the beam's target EVERY discharge frame -- IsDerivedFrom(Mech) then
Mech::IsDestroyed (FUN_0049fb54 = movementMode 2||9) -- and kill the beam the
moment it dies. EmitterSimulation (@004baa88) applies the same check to the
OWNER (a dead mech's own weapons drop everything).
Our recon had FUN_0049fb54 as a mislabeled "cockpit/HUD query" no-op stub
called with NULL, and never set the beam's target (0x474) at fire -- all three
authentic gates were dead. Now: FireWeapon stashes the owner's target entity;
both discharge paths + the owner gate run the real check (BTMechDestroyed).
Verified: 40 beam samples before the kill, 0 after -- lasers flash-fail on the
wreck instead of cutting it like butter (heat is still spent per the binary's
FireWeapon, which fires blind; only the DISCHARGE checks the corpse).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "cartoonish" beams: the port drew the ermlaser tube INFLATED -- a 3.0x
glow + 0.9x core two-layer hack, 13x the authored radius. Parsed the beam
models: ERMLASER/MLASER 0.22u radius, SLASER 0.11, LLASER 0.32, PPC 0.62 --
thin pencil beams with an authored per-class size progression; the PPC bolt
is genuinely ~3x fatter with its own 62-vert shape.
Now: ONE draw per beam at natural model scale (width multiplier 1.0), using
the weapon's OWN tube model (ermlaser for lasers, ppc.bgf for PPCs; the
s/m/llaser tubes load for other mechs' loadouts). BTPushBeamKind carries the
model selector; BTPushBeam keeps its signature (kind 0). Tint = 40+215x the
authored PipColor; the thin natural tubes stay under additive saturation so
the scrolling grit reads without a hand-dimmed core layer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The visible beams now come from each weapon's own live state instead of the
hardcoded single-look stagger: FireWeapon arms beamFlag + dischargeTimer (the
weapon's authored DischargeTime), ServiceDischarge ends the window, recharge
gates cadence -- so volley-vs-stagger patterns, cadence and colours all emerge
from each mech's real loadout (BLH: 3 lasers + 2 PPCs). Per weapon: live
muzzle (its own mount; gun-port ordinal fallback when the mount doesn't
resolve -- GetMuzzlePoint returns the feet otherwise), the fire's stored world
endpoint, and the authored PipColor (lasers red 1,0,0 / PPC blue 0,0,1
straight from the .SUB data). PPC draws as a thicker, brighter bolt.
Filter is by EXACT classID (Emitter 3016 / PPC 3028) -- the derivation check
matched shared recon-stub chains (a Sensor and the MissileLauncher passed and
drew garbage beams from misinterpreted offsets).
The target slot (MECH_TARGET_POS) now carries the TORSO aim point (all
consumers -- beam endpoints, missiles, reticle -- want center-mass, not the
ground between the feet); the projectile fallback lift moved to match.
Accessors: Emitter::BeamOn/BeamEndpoint, MechWeapon::MuzzlePoint/PipColor.
BT_BEAM_LOG=1 samples the live per-weapon beam state.
Verified: [beam] PPC mz=(...,4.49,...) end=(...,7,...) rgb=(0,0,1) -- gun-port
muzzles, torso endpoints, authored colours; kill chain intact (10 hits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dummy spawned 120u along the player's forward with the SAME orientation,
so both mechs faced the same way and the player stared at its back (also why
first shots read as rear hits). Flip the enemy's yaw 180 deg about Y (engine
convention: Z basis = (sin y, 0, cos y) -> yaw = atan2(z.x, z.z) + pi) so the
two spawn face to face.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BT_ROSTER dump (the shipped subsystem stream is the authority): the pod
Blackhawk mounts 5 Emitters (two laser types) + 2 MissileLaunchers each with
its own AmmoBin -- the periodic missile volleys are the real loadout firing
on its own recharge, NOT a placeholder weapon.
What WASN'T authentic: one key firing everything (the bring-up collapse of
the controls mapper). Interim split toward the real weapon groups:
SPACE = energy weapons (lasers)
CTRL = missile launchers (the projectile-weapon channel)
New gBTMissileTrigger pulse; projweap's fireImpulse moves onto it; the
BT_AUTOFIRE harness drives both channels. The full controls-mapper weapon
grouping stays the deferred authentic reconstruction.
Also: BT_ROSTER=1 one-shot loadout dump (class name + id per subsystem).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mech-vs-mech gather (Mover::GetCurrentCollisions) tests
mover->collisionVolume DIRECTLY -- it never consults collisionVolumeCount --
so the previous inert fix left the stale box blocking at the wreck spot.
The count-zero now serves only to stop MoveCollisionVolume from re-placing
the box, and the box itself is parked 100km underground where it intersects
nothing. Verified: the autodrive harness, previously pinned at ~7m from the
enemy, drives straight through the spot after burial (341,155 -> 992,-971).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "white lines toward the feet" report: the LAUNCHES are authentic (the
mech's real MissileLauncher/ProjectileWeapon roster subsystems firing on
their own recharge cadence -- that's the every-Nth-shot rhythm), but the
visual was the bring-up 3-segment beam tracer, and the aim point was the
victim's ORIGIN (ground level between the feet).
- Trail: per-frame puffs of the AUTHENTIC dsrm effect (psfx 0, "the lrm
smoke trail") along the flight path, frame-oriented so the smoke streams
behind the round (its .PFX velocities are +Z = backward); the round itself
is a short hot streak. New BTPfxTrailPuff spawns slot particles directly
at a moving point (no emitter instance).
- Aim: the resolved target point lifts to torso height (the beams already
converged there) -- impacts now cyl-resolve to body zones instead of
diving at the feet/legs.
Follow-up noted: the round MESHES (BULLET/LRMS.BGF ship) for a true
model-per-projectile look.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answers "it's still there but invisible": not normal -- the real game removes
the dead entity (death row). Full entity teardown needs the mech render tree
unhooked from the renderer first (the remaining P5 follow-through), so until
then the burial transition makes the wreck behaviorally gone:
- collisionVolumeCount = 0 (the collision gather skips volume-less movers;
MoveCollisionVolume early-outs) -> no phantom blocking
- the player's target lock drops -> beams stop converging on / hitting the
empty spot, no phantom impact smoke
Verified: zero [damage] hits after burial; INERT fires one-shot at
"wreck buried".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answers "does the wreck fade away?": YES, by sinking. FUN_00456410 (the 1996
sink renderable) computes offsetY = rate * t^2; the hulk's authored rate is
-0.025 -> the ~7-unit hulk is fully underground ~17s after the kill. The
script also pairs the standing hulk with the LDBR strewn-debris field (12x13u
flat scatter), parented together and sinking together.
Also verified from the mesh data: BLHDBR (1537 verts -- more than the intact
torso) IS the authored Blackhawk wreck: the classic standing-leg-in-rubble
sculpt. The "just a leg standing there" report is the authentic art.
(THRDBR -- the mesh the 1996 script hardcoded -- parses to ZERO vertices;
more evidence the hardcode was an unfinished dev shortcut.)
Implementation: SwapToWreck adds the ldbr piece; TickWreck applies the
quadratic sink per frame (driven from the dead mech's UpdateDeathState),
hides both pieces at burial and reports it so the wreck-smoke re-arm stops
with the wreck. DPLStaticChildRenderable::SetOffsetTranslation added
(Execute re-reads OrientationMatrix per frame -- same in-place idiom as
SetDrawObj).
Lifecycle verified live: kill -> 'blhdbr.bgf' + ldbr debris -> smoke re-arm
@10s -> wreck buried @~17s -> smoke stops.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user was right: a dead mech turns into a pile of wreckage. The authentic
1995 chain, recovered end-to-end:
kill -> the victim's per-mech DEATH ModelList ('blhdead'/'lokdead'/'owndead'/
'thrdead', .RES ids 22-25) -> its authored effects: 104 (the WRECK script) +
1007 (dnboom big explosion) + 1001 (ddthsmk rubble smoke plume) + a damage-
smoke burst (3/4/5/15).
Effect 104 = ExplosionScripts case 4 (part_008.c:2663, LIVE in the 1996 binary;
the "disabled" warning is case 6): loads the destroyed hulk + flamesml/flamebig
flame meshes with sweep flicker. Every mech ships its hulk (BLHDBR/MADDBR/
LOKDBR/... + GENDBR generic); the 1996 script hardcoded thrdbr.bgf (dev
shortcut) -- we use the victim's own.
Reconstructed as BTL4VideoRenderer::SwapToWreck: hide every segment mesh, hang
"<prefix>dbr.bgf" on the tree root (pending-swap if death precedes tree build);
routed from the engine's ExplosionClassID dispatch (effect 104 ->
BTSwapMechToWreck(explosion->GetEntityHit())). The kill now fires the
authentic 'blhdead' resource (manual 7+1 pfx calls removed -- the list carries
1007/1001 itself). Verified live:
[death] firing authentic death list 'blhdead' id=22
** effect_number = 104
[BTrender] wreck swap: victim -> 'blhdbr.bgf'
Follow-ups noted: mesh flames + hulk settle (cosmetic), DeathSplash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-burst emitter batching was wrong: in EVERY shipped .PFX,
maxIssue/rate == release_period (DAFC 25/100~=0.2, DNBOOM 35/150~=0.2, DDAM2
35/16~=2, DDTHSMK 30/3=10) -- release_period is the EMISSION WINDOW and rate
is particles/second emitted continuously across it. The death smoke plume is
authored as a 3/sec trickle for 10 seconds (+6s particle life), not one
same-frame puff. Emitters now integrate rate*dt until maximum_issue.
Wreck smoke (port addition, [T3]): a destroyed mech re-arms the death/rubble
smoke plume (psfx 1, DDTHSMK) every 10s -- its own authored window -- so the
dead hulk visibly keeps smoking instead of standing there looking alive (the
freeze-death is invisible on an already-stationary target). New member
Mech::wreckSmokeTimer; UpdateDeathState takes dt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task #32 (death collapse animation) resolved by decomp evidence -- BT 4.11 has
NO collapse animation; the movementMode 5-8 fall latch (clips 0x1c-0x1f) is an
engine-lineage vestige:
(1) the clip-table loader FUN_004a80d4 fills slots 0x00-0x1b + 0x20 (bmp
knockdown) and returns -- slots 0x1c-0x1f are never written;
(2) mech+0x63c..0x648 appear in NO exported function;
(3) no fall clip exists in the shipped 27-clip set;
(4) firing the latch would bind resource id 0 -- a StaticAudioStream -- as
keyframes.
Authentic death modes are the FREEZE modes (IsDestroyed == mode 2||9), so
UpdateDeathState now settles straight to 9 (was a [T3] mode-5 guess that would
trip the garbage latch in the binary). The death READ = freeze + dnboom +
ddthsmk smoke plume + destroyed skins + shutdown; the wreck stands.
Also: damage-band effects orient toward the ATTACKER (impact frame) via
lastInflictingID -- which was declared but never written (recon gap); now
maintained by Mech::TakeDamageMessageHandler, unblocking the DamageZone LOD
same-attacker redirect too.
KB corrected + proofs recorded (combat-damage "Death SEQUENCE", open-questions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'can't kill the enemy / no visible damage' cluster, root-caused and fixed
faithfully:
- STEP-6 unaimed path was INERT: the cylinder table was 'cached' at Wword(0x111)
-- the recon ABSORBER bank (stores nothing, reads 0) -- so every unaimed hit
silently no-op'd. Promoted to the named member Mech::damageLookupTable
(binary this[0x111], was mislabeled ammoExpended). New gotcha class recorded
(reconstruction-gotchas §2) + sweep; 2 dead multiplayer branches logged.
- Fire path migrated off the stale vital-zone aim onto the completed STEP-6
unaimed dispatch (zone=-1 + beam entry point -> cylinder resolves the
exterior zone). No more invisible 1-shot kills; death via the authentic
cascade (~14 center-mass hits). Wreck stays TARGETED on kill (beams stop on
it); scoring latches off.
- SendSubsystemDamage AV fixed: unbound critical-subsystem plug guard (43
unbound plugs/mech logged as an open question -- the binding itself is a gap).
- RemakeEntity (render damage swap): the 1996 render state machine's missing
Remake state, reconstructed as an in-place SetDrawObj mesh swap keyed by each
segment's damage-zone graphic state (tree dtor doesn't cascade -> never
rebuild). Destroyed arms/guns visibly wreck (the only variants the RES
registers).
- BT .PFX particle layer (L4VIDEO.cpp): the 1995 explosion/damage effect layer,
unported since 2007 (DPLIndependantEffect/ReadPSFX/ExplosionScripts all
stubs). Parses the authentic VIDEO/*.PFX definitions via the [pfx_day]
psfxN mapping; premultiplied blending renders BOTH families from the same
data (additive-style fire + occluding smoke -- DDAM2 is 30% grey, DDTHSMK
ramps negative: impossible additively); depth-sorted billboards with a
radial-masked grit sprite; impact-frame orientation (.PFX offsets are
authored mech-local, -Z = out of the struck armor toward the shooter) for
weapon hits AND damage bands (via lastInflictingID, now maintained -- was
declared but never written). Both effect-number encodings route (raw dpl
<100 + WinTesla 1000+slot carried by the band resources). Death fires the
authentic dnboom (7) + ddthsmk smoke plume (1).
- Effects anchor at the impact point / damaged zone's segment, not the mech
origin (no more fire at the feet).
- Dev force-input gates BT_AUTOFIRE / BT_AUTODRIVE for headless fire-chain
verification; BT_PFX_ADD=1 flips the particle blend for A/B.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the "kills are invisible" report: a mech died only at the state level, with
no visible destruction on its body. Root cause: the per-zone damage-state
descriptor table (binary zone+0xd4) that drives destroyed-skins + explosions was
never built -- its loader (Mech__DamageZone::LoadCriticalSubsystems / FUN_0041e4a8)
was a no-op stub, and MechDeathHandler itself was a no-op stub. The surrounding
plumbing (the per-zone load loop over the type-0x1e resource, the death-handler
slot) was already correct. Whole pipeline is EXPORTED -- no stand-ins.
Reconstructed:
- Mech__DamageZone descriptor table (binary this+0xd4): un-stubbed
LoadCriticalSubsystems (FUN_0041e4a8/FUN_0042a748/FUN_0042a2c8) to parse the
real type-0x1e stream -- entry = [f32 DamageLevel][i32 EffectResource]
[i32 GraphicState][f32 TimeDelay], ascending by DamageLevel. Verified live:
6 entries/zone, first threshold 0.2, effect 26, across all 40 zones, no crash.
- The three lookups (FUN_0042a664 by-level / FUN_0042a5f4 crossing / FUN_0042a6c4
by-graphic-state) as Mech__DamageZone methods.
- The real MechDeathHandler (FUN_0042a984 ctor / FUN_0042aa2c Performance /
FUN_0042a9f4 dtor), replacing the stub: each tick it walks the zones and, as a
zone's damageLevel rises across a descriptor threshold, fires that entry's
explosion (binary +0xb8 & 4) and, on destruction, the Destroyed-graphic
descriptor's explosion (+0xb8 & 8), applying the descriptor's GraphicState (the
destroyed skin) to the zone. Runs for EVERY mech, so the enemy visibly falls
apart as it dies.
Integration: the binary ticks MechDeathHandler off the mech's Performance list
(mech+0xbc), which the bring-up drive override bypasses, so Mech::PerformAndWatch
drives Tick() directly (same approach as UpdateDeathState). Effect spawn uses the
established Explosion::Make port (BTSpawnDamageEffect) -- the authentic dispatch
(class-5 message -> the 0xBD3 SubsystemMessageManager effect manager) is unported.
Runtime: builds clean, boots clean, 42 descriptor tables load with sensible
byte-aligned data, no crash. Live effect firing is combat-triggered (verified by
the load + the wiring; the [deathfx] threshold-crossing log fires under BT_DEATH_LOG).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconstructs the mech DEATH state machine — the un-exported master-perf death
branch (region 0x4a9770-0x4ab188) — from its EXPORTED consumers + the RP
VTV::DeathShutdown analog, wired into the active path (the bring-up drive
override bypasses the authentic Simulate where this normally lives).
Mech::UpdateDeathState() + IsMechDestroyed() (mech4.cpp), called for every mech
early in PerformAndWatch: on a vital kill (graphicAlarm >= 9, raised by the
damage side) it
1. sets movementMode = 5 -> the collapse clip's one-shot latch in
AdvanceBodyAnimation (fall direction 5-8 is un-exported -> 5 [T3]);
2. loops the roster calling Subsystem::DeathShutdown(1) (RP VTV::DeathShutdown
analog; the base is a no-op virtual, overrides act -- a SHUTDOWN not a
teardown, so it frees nothing and never removes the entity: the wreck STAYS);
3. next frame settles to movementMode = 9 -> IsDisabled -> locomotion frozen.
The drive's lone `movementMode = 1` write (mech4.cpp ~1309) is guarded on
!IsMechDestroyed so the death state is not clobbered back to a live gait.
Runtime-verified (forced kill, BT_DEATH_LOG=1):
[death] mech destroyed -> collapse + subsystem shutdown (wreck stays)
[death] mech settled -> disabled (IsDisabled=1, frozen wreck)
mech frozen in place, subsystem tick + renderer keep running, no crash, wreck
stays.
Also records the full death-sequence decomp map in combat-damage.md (the
exported consumers: AdvanceLeg/BodyAnimation collapse latch, IsDestroyed, the
MechDeathHandler effect engine; and the un-exported orchestration gap).
Deferred (honest): the visible collapse ANIMATION latch through the active path
(gait SM shows state=0 post-death; spawned mechs don't advance body anim);
MechDeathHandler (FUN_0042a984/FUN_0042aa2c -- the exported per-subsystem
destroyed-skin + explosion engine, still a stub) for death explosions; the
whole-mech DeathSplash radius damage (un-exported).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two deferred TakeDamage dispatches in Mech::ProcessCollision now fire,
unblocked by STEP 6 (the cylinder hit-location override that resolves zone==-1):
- Mover branch (:15324-15358): on a collision with another Mech, dispatch the
collision damage to it.
- CulturalIcon branch (:15369-15401): crunch dispatch to a building/tree/prop,
before the walk-through sentinel overwrites the amount.
Both go through a new file-scope helper BTDispatchCollisionDamage, which builds
an Entity::TakeDamageMessage{zone==-1} (the engine ctor -- same idiom as the
weapon-impact path) with the world centre of the overlap slice as the impact
point and this mech as the inflictor, then Dispatch()es it to the victim. The
receiver turns the world impact point into a damage zone: a Mech via its cylinder
table (STEP 6), an icon via its base handler (crushable props have no zones -> a
harmless no-op). Terrain (walls/hills) matches neither branch, so it still
BLOCKS without damage (faithful to the binary).
Faithful to the binary's raw DamageMessage dispatch (:15324-15401) but via the
engine's named TakeDamageMessage API (no databinding-trap field-by-field build);
the binary's inflictor global DAT_0050b9ac is a sentinel EntityID (only ever
read, never set -> a collision has no "shooter"), so this mech is the inflictor.
Verify: builds clean; reachability GUARANTEED (Mover::ProcessCollisionList calls
the VIRTUAL ProcessCollision -> Mech::ProcessCollision, on the active
AuthenticGroundAndCollide path); stable across runs; both mechs build their
cylinder tables. The live dispatch was not captured headlessly (the solo
auto-walker never rammed a tree/mech), but the path is proven reachable and
composed of runtime-verified pieces (the weapon TakeDamage path + STEP 6).
Also validated STEP 6's height ref: collisionTemplate->maxY ~= 7.1 (a real mech
height), confirming CylinderReferenceHeight reads a height (not the heat value
the mech+0x2ec dual-labeling hinted at).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Mech per-impact hit-location resolver (the cylinder damage table) is now
functional, wired, and runtime-verified [T2]. Unaimed (zone==-1) hits — the
collision-damage path — now resolve an impact point to a damage zone via the
authentic height x angle grid + weighted dice roll, instead of dropping.
dmgtable.cpp/.hpp was a non-functional skeleton on no-op ReconTable/stream
shims; backed it with real std::vector storage and fixed 5 latent runtime bugs:
- ReadEntries now consumes the leading cell name-string ([i32 len][len+1])
- PieSlice ctor reads rotateWithTorso into the correct member
- SelectSlice direct-indexes (was int lookup on a float-keyed table)
- ResolveHit returns the zone (chains SelectSlice -> SelectZone)
- real MemoryStream::ReadBytes (was a variadic no-op)
mech.cpp ctor: replaced the empty-name StandingAnimation stub with the real
load — FindResourceDescription(dzRes->resourceName, type 0x1d) -> stream ->
new DamageLookupTable, cached at mech[0x111]; ~Mech deletes it.
Mech::TakeDamageMessageHandler override registered (MESSAGE_ENTRY overlays
Entity's by ID): on invalidDamageZone, resolve via the table then base-route;
aimed reticle hits pass through unchanged.
Three named accessors (no databinding-trap raw reads): WorldToLocal
(localToWorld.MultiplyByInverse), CylinderReferenceHeight (standingTemplateMaxY
== collisionTemplate->maxY == binary mech+0x2ec[+0xc]), TorsoHeading via a
BTGetTorsoTwist bridge in torso.cpp (Torso::CurrentTwist == torso+0x1d8;
torso.hpp cannot be included into mech.cpp — subsystem-stub collision).
Stream format + geometry + roll + handler were all byte-verified against the
shipped BTL4.RES type-29 resources (18 tables, exact consumption) and the
disassembly (FUN_0049eb54/e678/de14, glue 0x49ed0c, handler @0x4a037a).
Runtime: boots clean, "[cyl] table 'bhk1' layers=7" (exact byte-verified layer
count, found by name), mech spawns + walks, no asserts/AV/0xCDCDCDCD. Env gate
BT_CYL_LOG=1. Unblocks collision-damage application.
KB updated (combat-damage.md STEP 6 COMPLETE, open-questions.md marked done).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "messageBoard" cockpit primitive (the secondary-MFD comm/status message ticker,
L4GAUGE.CFG:4913) was PROSE-ONLY (no bodies/methodDescription) -> parse-skipped. This
was the last unregistered gauge primitive.
Reconstructed byte-verified (Make @4cb678, ctor @4cb704, dtor @4cb788, BecameActive
@4cb7fc, Execute @4cb82c; vtable 0051bddc; sizeof 0xA4). THREE header mislabels fixed
(decode + adversarial verify): int enabled -> Entity* trackedMech (a pointer deref'd at
+0x190, set by SetSource not SetEnable); previousMessageId/previousNameId were SWAPPED
(BecameActive writes 0x9c=-1/0xa0=-2; Execute compares messageId vs 0xa0, name vs 0x9c).
Execute blits the strip cell (id -> (id&3)<<7,(id>>2)<<5) + sender name; methodDescription
= 4 params (rate, mode, btsmsgs.pcx, color). Registered in BTL4MethodDescription[].
DEFERRED / EMPTY by design (authentic for bring-up): the source is never bound (SetSource
has no recovered caller) AND the per-player status queue (StatusMessagePool, btstubs.cpp:62)
is a NULL stub -> no status messages exist -> Execute early-returns on the NULL source (safe
no-op == empty board). Data read via a BTResolveMessageBoard bridge in btplayer.cpp (a real
/FORCE-safe stub returning False; the raw mech+0x190/+0x1dc reads stay in the complete-BTPlayer
TU, dodging the databinding trap). The name-cell path is a marked structural simplification to
restore when the feed lands.
VERIFIED: the gauge parse-skip list is now EMPTY ([gskip]=0 -- every config gauge primitive
registered + built); BTResolveMessageBoard resolves (no /FORCE __ImageBase AV); combat TARGET
DESTROYED, no crash; gauge composite renders identically (300/77, clusters, radar un-regressed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "prepEngr" cockpit primitive (the per-engineering-screen static label overlay,
12 CFG calls at L4GAUGE.CFG:4595-4759) was a MISLABELED stub (base Gauge, an Execute
override, mech/screenNumber SWAPPED) with no methodDescription -> parse-SKIPPED, none built.
Reconstructed byte-verified (ctor @4c7bf0, BecameActive @4c7e48, Make @4c7b30; vtable
0051a06c). THREE corrections vs the stub (decode workflow + vtables.tsv proof):
(1) base = GraphicGaugeBackground, not Gauge -- vtable-identical shape to the engine's
BackgroundBitmap (only dtor + BecameActive overridden); (2) the overridden slot is
BecameActive (a paint-on-activation), NOT Execute -- GraphicGaugeBackground has no Execute
virtual, so the Gauge::Execute->Fail->abort hazard doesn't apply; (3) screenNumber@0x6C /
mech@0x70 were swapped. sizeof 0x90.
BecameActive walks the mech roster (databinding-safe GetSubsystemCount/GetSubsystem +
BTGetSubsystemAuxScreen bridge, like VehicleSubSystems::Make), finds the subsystem whose
auxScreenNumber == this screen (1..12), and paints the screen-number numeric + subsystem
label + type-specific label cells dispatched on GetClassID (Sensor/Myomers/Emitter/PPC/
Projectile/Missile/GaussRifle). methodDescription = 9 params (mode, screen, 7 .pcc names).
Registered in BTL4MethodDescription[]. Two raw subsystem reads (heat-sink #, +0x224 label)
are guarded fail-soft best-efforts (marked; the aux-screen bridge label64 substitutes).
VERIFIED: the skip list drops from {prepEngr x12, messageBoard} to just {messageBoard} --
all 12 prepEngr gauges build; no screen-range/crash; boot+sim+combat run; gauge composite
renders identically (COOLANT 300/77, clusters, radar all un-regressed). The prepEngr labels
paint on the Eng (ModeMFD*Eng*) screens, not the default composite view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "sectorDisplay" cockpit primitive (Secondary overlay, the radar SECTOR X/Z
coordinate read-out) was PROSE-ONLY in btl4gau3 (placeholder Make, no ctor/
methodDescription/registration) -> the config line was parse-SKIPPED and never built.
Reconstructed byte-verified from the disassembly (ctor @4c9e10, Execute @4ca07c,
methodDescription PE-parse): SectorDisplay : GraphicGauge, sizeof 0xC4. Its Execute
reads the linked mech's world position and shows two 100-unit sector numerics:
numericA = Round(-localOrigin.z * 0.01) + 500
numericB = Round( localOrigin.x * 0.01) + 500
(rounding = round-to-nearest == FUN_004dcd94, corrected from the reviewer's wrong
"truncate" claim; 0.01 const PE-verified; -Z/+X axis + fchs confirmed from asm).
Overridden slots: LinkToEntity(9) caches the subject, BecameActive(3, non-inactivating),
Execute(16) -> satisfies the container-Execute rule. Layout overflow-locked
(static_assert sizeof<=0xC4). Make/ctor/dtor mirror the registered PilotList sibling;
the config image name is copied (nameCopy) since Execute reads it per-frame.
Registered in BTL4MethodDescription[].
VERIFIED LIVE (BT_SECTOR_LOG): Make port=1 pos=(125,579) image=helv15.pcc gridCached=1;
Execute -Z=960.4 X=361.6 -> sectorA=510 sectorB=504 (Round(9.6)+500=510, Round(3.6)+500=504
-- authentic 100-unit sectors from live mech position). Gauge composite renders full,
no crash. The skip list is now exactly the two remaining widgets (prepEngr x12, messageBoard).
Also: a permanent BT_GAUGE_SKIP_LOG diagnostic (GAUGREND.cpp, gated) that logs each
unregistered gauge primitive the dev-parse skips -- the tool that pinned this down
(earlier "not built" runs were killed before the lazy gauge-renderer init).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reconstructed Condenser::MoveValve carried FUN_004afbe0's body -- a CONTROLS-mapper
display-mode cycler (cycles field@0x190 through 0/1/2 + repoints owner HUD gauges), NOT
the condenser valve. The real valve handler @0x4ae464 was not captured by the
assert-anchored exporter; disassembled it (tools/disas2.py) and reconstructed faithfully:
cycle valveState (@0x1D0) 1 -> 5 -> 50 -> 0 -> 1 (byte-verified @4ae480-4ae4bf), then
RecomputeCondenserValves(owner) to redistribute flow so every ValveSetting gauge updates.
DORMANT by design: the binary guards on owner->messageManager(+0x190)+0x274 (FUN_004ac9c8,
the 0xBD3 SubsystemMessageManager) and the message that invokes MoveValve arrives through
that same manager -- both DEFERRED to WAVE 8. Reproducing the raw owner+0x190 deref would
be a databinding trap in our layout, so the handler is faithful-but-uninvoked; the valve
gauge shows the authentic static 1/N (RecomputeCondenserValves at ctor) until 0xBD3 lands.
No V-key bring-up stand-in added.
Build green; combat TARGET DESTROYED, un-regressed (MoveValve is not yet routed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two coupled fixes so the condenser valve gauge (ValveSetting -> coolantFlowScale
@0x15C) reads its authentic value instead of garbage/zero.
STEP 7 (gating dedup): heat.cpp carried STUB Condenser ctor/dtor/TestClass/
TestInstance/CreateStreamedSubsystem that ODR-duplicated the REAL bodies in
heatfamily_reslice.cpp and WON under /FORCE (heat.obj links first) -> the real
ctor (which sets valveState=1) was shadowed, leaving valveState=0xCDCDCDCD.
#if 0'd the heat.cpp stubs so the reslice ctor (@4ae568, byte-verified: valveState=1,
coolantFlowScale=0, massScale=refrigerationFactor, condenserNumber from name) is the
sole definition. DefaultData/GetClassDerivations/ResetToInitialState (not duplicated)
stay in heat.cpp.
STEP 9 (the real writer): FinishConstruction() at the Mech ctor tail was a no-op
template stub in place of FUN_0049f788 = RecomputeCondenserValves -> coolantFlowScale
was never written (stayed 0). Reconstructed it (byte-verified vs part_012.c:9264):
distribute coolant flow across the mech's condensers, coolantFlowScale_i =
valveState_i / sum(valveState), with the condenserAlarm@0x1DC change pulse
(2-if-flow<=old-else-1, then 0). Walks the populated subsystem roster filtering
Condensers via IsDerivedFrom (behaviorally identical to the binary's @mech+0x7cc
condenser chain, GUID 0x50e4fc). Wired as BTRecomputeCondenserValves(this) at the
Mech ctor post-init pass (binary @9457). _DAT_0049f850 fallback confirmed 0.0f (PE read).
Verified: [valve] the Blackhawk's 6 condensers each read flow=0.166667 (=1/6, total=6)
for both player + spawned enemy, no every-mech crash; combat TARGET DESTROYED,
un-regressed. Diagnostic BT_VALVE_LOG added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 0xBBE heat-sink BANK was built as a plain HeatSink, so the numeric-R
cockpit gauge binding HeatSink/AmbientTemperature (L4GAUGE.CFG:4552) had
no publisher -> the LAST unresolved config attribute.
Reconstruct AggregateHeatSink : HeatSink (ctor @4ae8d0, own GUID 0x50e590),
byte-exact + static_assert-locked (heatSinkCount@0x1D0, ambientTemperature
@0x1D4=300, helper@0x1D8 0xC link node, sizeof 0x1E4 == factory alloc @9993).
Publish HeatSinkCount + AmbientTemperature via a dense-prefix attribute table
chained to HeatSink::NextAttributeID (shared HeatSink table unchanged, so
CoolantMass/CoolantCapacity keep resolving). Move CreateHeatSinkBankSubsystem
into heatfamily_reslice.cpp (needs the class def) and build the real class at
factory case 0xBBE; mech.cpp unchanged.
DELIBERATE DEVIATION (documented in the class): keep the base HeatSinkSimulation
the HeatSink ctor installs; do NOT reimplement the authentic Performance @4ae73c
-- it derefs a raw self+0xE0 -> [+0x158] that does not map in our compiled layout
(AV/NaN, and runs for EVERY mech), and ambientTemperature is a frozen constant so
the gauge reads 300 either way. Authentic relaxation model deferred.
Verified: [attr] HeatSink/AmbientTemperature OK; all 50 config attribute
bindings resolve (0 NULL); no every-mech crash; combat TARGET DESTROYED,
FIRED #41+, un-regressed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gauge-complete-decode workflow found the "deep heat-leaf byte-exact re-base" is
STALE -- the leaf layout already shipped byte-exact (alarm-unification/P7), and the
real work is a small misidentified-field data fix (highest value, lowest risk):
- Sensor radarPercent read the INHERITED heatEnergy (~1.3e7) -> hugely negative ->
needed a bring-up guard forcing it to 1.0. The binary (@004b1c4c:1829) reads
*(this[0x38]+0x158) = the subsystem's OWN DamageZone::damageLevel [0,1] (0 intact ..
1 destroyed). Fixed: radarPercent = RadarBaseline - GetSubsystemDamageLevel(); guard
DELETED (damageLevel is engine-clamped). So the radar authentically degrades with
sensor structural damage (RadarBaseline-damage, x0.5 on DegradationHeat, 0 on Failure).
- New databinding-SAFE accessor MechSubsystem::GetSubsystemDamageLevel()/Set... reads the
ENGINE DamageZone::damageLevel NAMED member (NOT the ReconDamageZone proxy, whose
offset-0 structureLevel aliases the vtable ptr as a float = garbage).
- HeatSink::HandleMessage msg==1: same misidentification -> SetSubsystemDamageLevel(0.5)
(was linkedSinks->heatEnergy=0.5, wrong object+field).
- MyomerCluster seek lamp: subsys+0x800 (OOB for 0x358 Myomers) -> currentSeekVoltageIndex
@0x320 (the Emitter+0x3f0 analog; INFERRED -- ctor not in the assert-anchored export).
- numericSpeed (~225 vs 61.5): verified NOT A BUG (NumericDisplaySpeed applies x3.6; the
feed is correct u/s) -- closed, no code.
No layout change (already byte-exact); pure data-read retargets. Verified DBASE+dev
gauges: no crash, combat un-regressed (TARGET DESTROYED). (radarPercent degrade is only
OBSERVABLE once per-subsystem damage routes to each engine DamageZone -- a separate task;
today the sensor is undamaged so radarPercent = full 1.0, which is correct.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RANK=-1: Player::DefaultFlags creates every player NonScoring (CalcRanking only
ranks IsScoringPlayer()==true, else -1), and the bring-up never cleared it. Call
SetScoringPlayerFlag() in DropZoneReplyMessageHandler's mech branch (a piloted mech
IS an active scoring combatant; the camera-ship branch stays non-scoring). -> RANK=0.
DEATHS runaway (climbed ~1/s, not the "1" first seen): @004c012c -- which our
MESSAGE_ENTRY binds as the VehicleDead handler -- is actually the drop-zone
RESPAWN-RETRY helper. The engine's RequestDropZone (PLAYER.cpp:400) posts a
2s-delayed VehicleDead(deathCount=-2) as a "did the drop zone reply?" timer; our
handler ++deathCount + RE-POSTED it UNCONDITIONALLY -> an infinite loop, because the
drop-zone handshake never "completes" in the bring-up (no console/drop-zone system).
Traced it to a pure boot artifact (13 fires with zero combat; the only VehicleDead
producers are the re-post + my kill-producer, and the kill fires AFTER the loop
starts). FIX (correct respawn-retry semantics): if the player already has a vehicle
the drop zone WAS acquired (the DropZoneReply created it), so the retry is moot --
return without counting a death or re-posting. -> DEATHS=0 in solo, loop gone.
Verified DBASE+dev gauges: the Comm roster shows KILLS=1 / DEATHS=0; the KILL log
reads killCount=1 deaths=0 score=164 rank=0; 0 crashes. (MP real-death DEATHS -- a
destroyed vehicle via the separate @004c05c4 path -- remains deferred per the plan;
solo never dies so the retry gate is correct there.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Comm roster + score gauges read 0 for 4 independent reasons (none was handler
logic -- the BTPlayer ScoreMessage/VehicleDead/ScoreInflicted handlers were already
reconstructed). Mapped by the scoring-feed-decode workflow; fixed all 4:
- NO PRODUCER: combat only dispatched Entity::TakeDamageMessage, never a scoring
message. Added producers (btplayer.cpp bridges, called from mech4.cpp): per-hit
ScoreInflicted at the beam (:2171) + projectile (:831) damage dispatch -> SCORE;
KillScore at the TARGET-DESTROYED edge (:2198) -> KILLS. senderMechID = the
VICTIM (so the local player is credited via the !=our-mech branch, not the
suicide-negate branch); dispatched to application->GetMissionPlayer().
- CROSS-FAMILY MECH OFFSETS: MECH_OWNING_PLAYER/TONNAGE/DAMAGE_BIAS read raw binary
offsets (garbage in our 0x638 Mech). MECH_OWNING_PLAYER -> Entity::GetPlayerLink()
(NULL for the ownerless dummy); tonnage/bias stubbed 1.0/0.0 (bring-up).
- NULL scenarioRole: the BTMission role registry has no WinTesla analog, so every
award path NULL-deref'd. CalcInflictedScore returns the neutral (damage+bias)
when scenarioRole==0; DamageReceived guarded.
- NULL-owner dummy: the KillScore sender-owner increment + StatusMessage now guard
GetPlayerLink() (the dummy has none) so a solo kill credits only the local player.
- DATABINDING: the PilotList read KILLS/DEATHS at raw offsets (pilot+0x27c/+0x200)
that don't match our compiled layout -> silent 0. Repointed to the compiled
members via bridges (BTPilotKills/BTPilotDeaths -> BTPlayer::GetKillCount/GetDeaths).
⚠ ROOT-CAUSE of the empty scoreboard: the PilotList SELECT-TARGET highlight did a
raw-offset deref (*(local+0x284) objectiveMech, then *(tgt+0x190)) that AV'd on our
layout -- caught silently by the SEH GuardedExecute, aborting Execute BEFORE the
KILLS/DEATHS draw. Replaced with BTPilotIsSelected (accessors + GetPlayerLink).
- SCORE persistence: PlayerSimulation's 10s flush zeroed currentScore (the binary's
console delta); now only flushes/zeros when a console host exists (solo keeps the
running score for the gauge + CalcRanking).
Verified DBASE+dev gauges (BT_SCORE_LOG): SCORE climbs +5.83/hit -> 164; on the
kill KILLS 0->1, and the Comm roster RENDERS KILLS=1 (screenshot). No crash.
Follow-ups (noted): DEATHS=1 is a pre-existing spawn/respawn-handshake artifact
(not from the producers -- the dummy's GetPlayerLink is NULL so no VehicleDead is
posted; confirmed killCount=1 not 2); RANK=-1 is CalcRanking's non-scoring mark
(Plasma serial surface, invisible on dev); MP VehicleDead/tonnage/role registry
remain per the plan. Diagnostics kept: BT_SCORE_LOG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root-caused the P2c abort with a cdb attach to the frozen abort dialog: the stack
was Gauge::Execute (GAUGE.cpp:598) -> abort, via GuardedExecute <- Update <-
ProcessOneActiveGauge. The engine base Gauge::Execute is Fail("not overridden")
-> abort, so an ACTIVE container that doesn't override Execute aborts by design;
GuardedExecute's SEH can't catch an abort(). The decode's "GeneratorCluster
overrides ONLY the dtor / renders via its children" was wrong -- the 1995 base
Gauge::Execute was a no-op, the 2007 WinTesla engine's aborts, so the container
MUST override Execute (the sibling SubsystemCluster does).
FIX: give GeneratorCluster the two container overrides (mirroring SubsystemCluster):
- BecameActive() -- blit the panel's static background pixmap (DrawPixelMap8) and,
by overriding the default, DON'T self-inactivate so the panel stays active.
- Execute() -- no-op (the 5 self-registered child gauges draw the dynamic content;
the override exists only to avoid the base Fail->abort).
Re-registered in BTL4MethodDescription[]. Verified DBASE+dev gauges: all 4
generator panels (A/B/C/D) render -- blue OutputVoltage bars (the @004c72ac
ScalarBarGauge Scalar* variant), labels, status lamps -- 0 "BecameActive not
defined" warnings, NO abort, combat un-regressed (TARGET DESTROYED). Phase 2 is
now COMPLETE (4/4 widgets: LeakGauge, VertNormalSlider, PilotList, GeneratorCluster).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconstructed the 4th unbuilt widget (the 4 generator engineering panels, buttons
9-12) + its companions, but left it UNREGISTERED pending a runtime-abort fix:
- Generator OutputVoltage attribute table (powersub) -> outputVoltage@0x1DC, chained
to HeatSink's dense index (temps already reach it). Correct + reusable.
- ScalarBarGauge @004c72ac plain-Scalar variant (btl4gau2) -- the DIRECT-Scalar* bar
the GeneratorCluster voltage bar needs (the existing @004c721c takes a resolved
source object). Correct + harmless.
- GeneratorCluster Make/ctor/dtor + 15-param methodDescription reconstructed from
part_014.c:891-1013 (replacing the prose + placeholder Make + the bogus DAT pool);
builds the 5 children (temp bar / voltage bar / leak wipe / 2 lamps). Header fixed
to the binary ctor param order (secondaryColor@0x94, +_reserved0xB0 -> sizeof 0xB4).
ChildRate forward-declared.
⚠ REGISTRATION GATED (commented out in BTL4MethodDescription[]): registering it
aborts at runtime -- the 4 panels build (the 4 "BecameActive not defined" warnings
fire) then abort(). ISOLATED to the PARENT lifecycle: the abort PERSISTS with all 5
children nulled; there are no pure virtuals; Execute is SEH-guarded under BT_DEV_GAUGES
so the fault is in the unguarded ctor/lifecycle path. The sibling SubsystemCluster
overrides BecameActive/Execute/TestInstance whereas the decode has GeneratorCluster
overriding ONLY the dtor -- that "inherits only the dtor" claim is the prime suspect.
Deferred; the 3 other P2 widgets (LeakGauge/VertNormalSlider/PilotList) ship clean.
Verified: unregistered -> parse-skips -> no abort; combat un-regressed (DESTROYED),
0 crashes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PilotList (keyword "pilotList") was PROSE-ONLY, so the Comm surface showed only
the baked btcomm.pcx labels. Reconstructed all 7 functions from part_014.c:
3156-3434 (Make/ctor/dtor/BecameActive/TestInstance/Execute/DrawMechIcon), with
the 8x(x,y,layoutMode) layout table DAT_0051af88 PE-parsed exact from BTL4OPT.EXE.
Draws ONE roster slot/frame (round-robin) from the viewpoint mech's cockpit-mapper
pilot roster.
- Roster FEED: new BTResolveRosterPilot(slot) bridge in mechmppr.cpp (a complete-Mech
TU) resolves the viewpoint mech's ControlsMapper (subsystemArray[0]) -> GetPilot;
btl4gau3.cpp reads the returned pilot at raw BTPlayer offsets. The mapper's
FillPilotArray already fills the roster (pilotArray[0]=GetMissionPlayer,
[1..]=FindGroup("Players")).
- KILLS = killCount@0x27c (real; ScoreMessageHandler increments it in combat).
- DEATHS: the binary reads pad_0x280 which has NO writer anywhere (a shipped dead
field -> perpetual 0). FIX per "if it doesn't work, fix it": read the real
deaths counter Player::deathCount@0x200 (VehicleDeadMessageHandler increments it)
so DEATHS is meaningful in MP.
- DrawMechIcon: App+0xC8 name-bitmap cache is unwired (same deferral PlayerStatus
uses) -> LookupPlayerNameBitmap returns NULL -> the tinted name box (never an AV).
Dropped the bogus x,y from the header ctor (positions come from DAT_0051af88).
/FORCE-safe (all vtable slots real; link log clean, no unresolved PilotList/
BTResolveRosterPilot). Verified DBASE+dev gauges: the Comm surface now renders the
live local pilot row (KILLS 0 DEATHS 0 + name box; 0/0 authentic in solo -- the
combat scoring feed that moves them is Phase 3), combat un-regressed (DESTROYED),
0 crashes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gauge-widget-decode workflow reconstructed the unbuilt cockpit widgets from
the binary. First two (the heat MFD's missing SET/LEAK columns):
- LeakGauge (BitMapInverseWipe, keyword "LeakGauge"): the class body already
existed + is byte-faithful; it was just UNREGISTERED so every LeakGauge(...)
config line was parse-skipped. Added the Make factory + methodDescription +
the BTL4MethodDescription[] registration. Value @6 = Condenser/CoolantMassLeakRate
(already published P1); 0 undamaged -> the leak wipe is authentically empty.
- VertNormalSlider (keyword "vertNormalSlider"): the condenser VALVE slider @2 was
PROSE-ONLY. Reconstructed all 7 functions (Make/ctor/dtor/TestInstance/
BecameActive/Execute/Draw) from part_013.c:14051-14175 (Make @004c4b08 by
disassembly) -- an XOR indicator row=Round(span*value) over the track; fixed the
jumbled header member layout to the byte-exact order (sizeof 0xB0). Published
ValveSetting -> coolantFlowScale@0x15C on the HeatSink table so Condenser/ValveSetting
resolves (verified: all 6 condensers OK). The valve indicator renders (condenser 1
shows it near the top = coolantFlowScale 1.0 = valve open).
Both /FORCE-safe (every vtable slot has a real body; link log grep clean, no
unresolved VertNormalSlider/BitMapInverseWipe). Verified DBASE+dev gauges: the
heat panel now shows all 3 columns per condenser (TEMP scales, SET valve indicator,
LEAK track), coolant bar stays full, combat un-regressed (TARGET DESTROYED), 0 crashes.
The player-valve-toggle -> coolantFlowScale drive (SetValveSetting vtable+0x48) is
the remaining Phase-3 valve-mechanism work; the slider shows the static valve value today.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Searchlight publishes "LightOn" -> lightState@0x1D8 (the button-5 searchlight
lamp; the empty default-constructed index resolved NOTHING). The config binds
the name "LightOn", so the enum id is renamed LightOnAttributeID and the table
is chained to PowerWatcher's dense index. Verified: LightOn resolves OK.
- FIX a genuine reconstruction bug the heatmodel decode caught (heat.cpp
UpdateCoolant + heatfamily_reslice RefrigerationSimulation): both read
linkedSinks.Resolve()->heatEnergy where the binary reads *(this[0x38]+0x158) =
the subsystem's OWN DamageZone.damageLevel (the engine base zone @0xE0).
Consequences of the misread: coolantDraw = master.heatEnergy(~1.3e7) * heatLoad
would SLAM the live CoolantMass bars to empty whenever the link resolved; and
RefrigerationSimulation clamped massScale permanently to 1.0 (minimum) instead
of the authentic 3.0, plus null-deref'd a Condenser whose linkedSinks is skipped.
Now both read this->Subsystem::damageZone->damageLevel (qualified past the
MechSubsystem shadow, per mechweap.cpp:252): undamaged 0 -> coolantDraw 0 (no
leak, coolant bars stay full = authentic pristine) / massScale = 3.0, rising
only as the sink/condenser itself takes battle damage.
After this only HeatSink/AmbientTemperature remains NULL (the #if 0'd aggregate
HeatSink bank -> P3). Verified DBASE+dev gauges: no crash, combat un-regressed
(TARGET DESTROYED). (Also noted: the Condenser dup-ctor links to the REAL
heatfamily_reslice definition -- LNK4006 keeps the first/real one; the heat.cpp
stub is ignored -- so no bug today; #if 0 cleanup deferred to the Condenser table.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gauge-databinding-map workflow found most cockpit gauges resolve NULL
because the reconstructed subsystems publish only a fraction of the attributes
the config binds. First publishing batch (attribute tables are read-only static
data; ids kept a dense prefix from each parent's NextAttributeID):
- HeatSink table dense-append: DegradationTemperature/FailureTemperature (the
condenser temp-bar warn/max endpoints -- were NULL, so the two-part bars could
not scale), NormalizedPressure/DegradationPressure/CoolantMassLeakRate, and the
HeatSink link. Condenser/Reservoir inherit this -> all 6 condenser temp bars
now resolve current/warn/max (verified: BT_GAUGE_ATTR_LOG all OK).
- PoweredSubsystem::GetAttributeIndex() (new) publishes InputVoltage->voltageSource
-- the cluster power-branch gate (the power-lamp/generator-voltage/state-lamp
sub-branch is skipped when it resolves NULL). Flows to Sensor/Myomers/weapons.
- MechWeapon::GetAttributeIndex() (new) publishes OutputVoltage/PercentDone->
rechargeLevel; Emitter/PPC/ProjectileWeapon/MissileLauncher/GaussRifle DefaultData
re-pointed at it (they carried an EMPTY default-constructed index -> resolved
NOTHING). Verified: the ER MED LASER / PPC / STREAK weapon clusters now render
live recharge dials (were blank TEMP/STATUS).
- Mech::SetTargetRange un-stubbed (radarRange = range) -> the radar map scale +
overlay range readout track the mapper's zoom (was frozen at 1000).
- GAUGREND ParseAttribute: env-gated per-binding resolution trace (BT_GAUGE_ATTR_LOG)
-- durable diagnostic infra for the wave.
Verified DBASE+dev gauges: no startup/gauge-construction crash (dense chain intact),
combat un-regressed (TARGET DESTROYED), clusters build with InputVoltage resolving.
Remaining config-binding NULLs: HeatSink/AmbientTemperature (aggregate bank, P3) +
Searchlight/LightOn (P1b).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues the user hit driving the Mad Cat: (1) projectiles appeared to launch from the
mech's FEET -- MechWeapon::GetMuzzlePoint falls back to the mech origin (localOrigin) when the
weapon's mount segment (this+0xdc) doesn't resolve, so the passed muzzle was at ground level;
(2) they rendered as a thin laser-like line, not a missile.
Fix: BTPushProjectile now takes the SHOOTER mech and resolves the real launch port by NAME
(sitermissleport / sitelmissleport, alternating L/R for a salvo look; then torso ports, then
gun ports; then a raised fallback) -- the same segment-name -> world-transform mechanism the
visible laser beams use for the gun ports. The tracer is now a 3-part missile look (dim smoke
trail behind + orange body + hot flame tip) instead of a single thin beam. FireWeapon (both
ProjectileWeapon and MissileLauncher) passes owner as the shooter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>