Commit Graph
180 Commits
Author SHA1 Message Date
arcattackandClaude Fable 5 48c9c8444f Fire VISUALS wave: the authored firesmoke sheet, vertex-alpha effect cards, the case-4 wreck dressing
The "fireballs like the demo vids" arc, decomp/content-grounded end to end,
plus the live-play UX batch verified over the same sessions:

FIRESMOKE SHEET (the PFX fireball fix): every firesmokeN_scr_tex in BTFX.VMF
maps the SAME 64x64 tileable noise image bintA (variants differ only in SCROLL
rate) and firesmoke1_mtl colours it through the "fiery" ramp (0.3,0.1,0.1)->
(0.9,0.7,0.3).  The particle layer now bakes ramp(lum(bintA)) as its sprite
colour (noise detail in alpha) and SCROLLS it at firesmoke1's authored rate
via a texture-transform; the port's radial soft-edge mask moved to a second
CLAMPed stage so the WRAPPED scroll rolls flame through the sprite without
scrolling the edge away.  Old grit x radial bake kept as the no-BINTA fallback.
Impact hits, damage-band smoke and death booms all ride this layer.

AUTHORED TEXTURE SCROLL in the model path: the BMF TEXTURE records carry
SPECIAL " SCROLL u0 v0 du dv" (tag 0x2037); the draw path always supported
per-op scrolling (SetTextureScrolling) but the BGF loader never parsed it, so
every scrolling material rendered frozen.  Wired TexRef -> MatInfo -> batch ->
L4TEXOP.doScroll: the flame cards (flamebig/fire5) now roll fire noise.

VERTEX-ALPHA EFFECT CARDS (the "twisted drill bit of fire" fix): FLAMEBIG's
verts carry authored float RGBA -- white-hot base (1.0,0.99,0.97) -> dark-red
tip fading to alpha -0.2 (the DPL clamp convention).  The loader kept a flat
batch colour and drew it OPAQUE = a solid orange spike.  Corpus sweep: exactly
14 shipped BGFs carry vertex alpha, ALL effect cards (flames, MUZFLASH,
EXDISK_A/B/C, TMST_A/B/C, beam models, DECLOUDS).  Such batches now keep the
authored per-vertex gradient and route to the alpha-blend pass, unlit,
colour = texture x gradient, alpha = the vertex fade; sky objects excluded
(drawAsSky + alphaTest passes NEITHER pass filter -- DECLOUDS stays in the
sky pass).  MUZFLASH/EXDISK render correctly for free when the muzzle-model
work lands.

WRECK DRESSING (the 1996 ExplosionScripts case-4 transcription): pieces spawn
HIDDEN and reveal 0.25s after the boom (the InstanceSwitch delay, behind the
dnboom flash); flamebig hangs over the pile, Y-BILLBOARDED at the camera
(SetOffsetYaw + a camera-pos getter -- the dpl_SetDCSReorientAxes analog);
the MakeDCSFall settle arms at the reveal with the two authored rates (hulk/
debris -0.025 t^2, fires -0.01 t^2 -- the flames ride above the sinking pile
and die with it at burial).  EMPTY-PLACEHOLDER hulk guard: THRDBR.BGF is a
153-byte zero-geometry stub that "loads fine" -- vertex-count check now routes
it to the gendbr fallback (a Thor wreck was invisible).  Hulk content census
recorded: AVADBR==MADDBR==VULDBR geometry (palette-only prefix diffs),
RAPDBR==SNDDBR==STIDBR byte-identical -- wreck variety is materials + the
dressing, not unique piles.

LIVE-PLAY BATCH: muzzle resolve uses the named segmentIndex (raw +0xdc read
was layout garbage); forward launch frame (authored MuzzleVelocity +Z vs the
mech's -Z facing); dock-bottom single window (gauge strip appended below the
world viewport, 1100x600 default, BT_DEV_GAUGES_WINDOW=1 restores the separate
window); portrait sec surface unrotated CW; ammo counters live via typed
bridges (BTAmmoBinCountPtr/BTAmmoBinFeeding/BTWeaponAmmoBin -- raw bin+0x180
and a hand-rolled link walk were garbage); fourth fire key ('4' = Pinky);
panel/arc probes de-aliased (%61 prime).

KB: rendering.md (vertex-alpha card family + scroll), combat-damage.md (hulk
census + THRDBR stub), gauges-hud.md (ammo bridges).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:04:29 -05:00
arcattackandClaude Fable 5 bb795e2805 MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified:

COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity
per contact and ProcessCollisionList walks EVERY touched solid per frame; with
2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a
walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy).
Fix: frameEntryWorldVelocity restore per contact (damage always priced at the
real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes
the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry.
Gotcha #16 (engine-facility drift class).

MISSILES: peer-visible salvos (the launcher record extension carries a salvo
counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone
never serialized), the authentic arc (authored MuzzleVelocity vector + the
Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78),
world-impact bursts (rounds detonate on cave geometry instead of phasing
through), contact-only damage (flight-cap expiry = fizzle, no more teleport
damage), live re-lead, and ballistic (unguided) shells for autocannons.
projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6
corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded.

RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244
is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation
(never the translation); worldToView now Invert(view) built rotation-first.
CulturalIcons sorted out of the moving grid (the phantom red pips were map
props), visible-radius culls on all three draw passes, live pip verified at
|delta| x ppm px.  Gotcha #17 (verify the FUN_ body, not its call shape).

WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means
FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X
lamps, the panel look, the children enable and the ready-lamp gate (which had
NEVER executed).  Polarity chain corrected end-to-end (failedState, fed by real
damage saturation).  Root cause of the freezes: MFD page-mode gating -- the dev
composite shows ALL pages at once, so off-page dials legitimately stopped; under
BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio
untouched).  The SEH gauge guard now names its kills; repaint-heal resets the
incremental arc after panel repaints; [panel]/[arc] probes added.

COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole
Player__StatusMessage queue; wired the binary's one producer -- the kill branch,
victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS
counted via the observed-death tally (each node scores every pilot from locally
observed events, the same model as the KILLS credit) and the -2/-1 engine seed
clamped for display.

DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480,
true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering,
BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar
(first second-chassis live outing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:24:15 -05:00
arcattackandClaude Fable 5 dd27238ceb Gauges: the CLOSEOUT wave -- radar pips, Myomers dedup, attribute parity, ledger sweep (tasks #14-#17)
TASK #17 -- AUTHENTIC RADAR SYMBOLOGY (the cross-blip stand-in retired).
The recon proved the 'missing pip raster set' never existed as BT game code:
pips are the ENGINE's L4GaugeImage vector-stroke system (T0 source in tree,
L4GAUIMA.cpp == FUN_0046f0c0 line-for-line), the 'pip table' is L4Warehouse::
gaugeImageBin keyed by Entity::resourceID, and BTL4.RES ships 110 type-0x12
shapes (every mech/vehicle/building/tree).  The six btl4rdr stubs are wired
to the real facilities: contacts draw their authentic model silhouettes with
LOD selection, the target gets the binary's 4px-inflated box highlight, and
player-name labels resolve via Mission::GetSmallNameBitmap (the prebuilt
64x16 egg rasters keyed by the player's bitmapindex).  DECODED: the invented
'VideoObject' was Entity::owningPlayer all along (+0x190; nameID =
playerBitmapIndex@0x1E0, target = BTPlayer::objectiveMech@0x284 -- new
bridge BTPlayerObjectiveMechOf); the 'LabelledEntity' class is Landmark
(cultural.h; label path dormant -- no landmark content ships, no runtime
landmarkID writer).  The L4GREND BT_DEV_GAUGES warehouse guard is removed
(its AV had a different culprit, below); resource type 18 corrected to
GaugeImageStream in decomp-reference (was mislabeled 'ModelList').

TASK #14 -- the Myomers ODR duplicate ELIMINATED: the powersub.cpp/hpp
'Myomers' (classID 0xBC3 -- actually Sensor) is retired whole; it duplicated
?DefaultData@Myomers@@ against the real class (dumpbin-verified) and /FORCE
picked the winner by link order.  The real Myomers (0xBC6) now chains
PoweredSubsystem's handler set (ids 4-8) and publishes its SEVEN binary
attributes (@00511588: SpeedEffect/Current/Recommended/Min/MaxSeekVoltage-
Index/SeekVoltage/OutputVoltage) -- the old empty unchained index starved
the Myomer engineering panel of every resolve.

TASK #16 -- attribute parity: MechWeapon publishes the FULL binary table
@0x511890 (11 entries; ids renumbered to binary truth -- the port aliases
had squatted the binary's DistanceToTarget/TargetWithinRange ids; the
streamed TriggerState 0x13 binding unchanged).  Binary names resolved two
TODO members: pipState -> estimatedReadyTime (attr 0x1A), and the EXT-model
flag is the binary's RearFiring (0x1B).  ThermalSight LightState published.
HUD (offset conflicts) + missile-side tables (id encoding suspect)
documented for a re-dump instead of publishing blind.
  THE CRASH THIS EXPOSED [T2, cdb-verified]: gotcha #11's dense-table gap
is a LATENT AV, not a guaranteed one -- the old table's 0x0D..0x12 gap
(task #5) survived on heap luck; the renumber reshuffled allocations and
Find() AV'd on a garbage entryName in WeaponCluster's PercentDone resolve.
Fixed with five named PAD entries (the mech.cpp attrPad idiom) + a
static_assert locking the pad base to PoweredSubsystem::NextAttributeID.
Gotcha #11 amended with the proof.

TASK #15 -- stale-ledger sweep: GAUGE_COMPOSITE ('composite not yet built',
Reservoir shadow, PlayerStatus/vehicleSubSystems 'remaining', valve-dormant-
until-0xBD3, sensor guard, the superseded 'Heat MFD near-static' reframe --
all banner-corrected), gauges-hud frontmatter, L4VB16 + powersub comments.

Verified live: 50/50 config attribute bindings resolve, 0 NULLs, 0 parse
skips, mech spawns and simulates 31/31 subsystems, no cross-blip fallbacks,
the pip cache fills through entity registration without the old guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 00:57:45 -05:00
arcattackandClaude Fable 5 80cadc98c7 Cockpit: the COOLANT VALVE control -- the heat-management triad completes (task #13)
The pilot's third heat tool: Condenser::MoveValve (id 4, the Condenser handler
table @0x50E52C -- PE-verified, exactly one entry) registered via the
order-proof function-local accessor and GUARDED by the real FUN_004ac9c8
(task #12's BTPlayerRoleLocksAdvanced rookie-role bridge -- the old deferral
note blamed the 0xBD3 messmgr, a stale misattribution, swept).  Each press
cycles the valve 1 -> 5 -> 50 -> 0 -> 1 and RecomputeCondenserValves re-shares
the fixed total coolant flow across all six condensers -- opening one valve
wide starves the others: the pilot's cooling-triage lever.

Desktop: 'C' cycles the selected condenser's valve (BT_VALVE_SLOT picks the
roster slot; default Condenser1).  BT_VALVE_TEST scripts a headless press.

Verified live: press -> "[valve] condenser#1 valveState=5 flow=0.5 (total=10)"
-- exactly the authored redistribution math (5/(5+5x1)), through the real
role gate.

The heat-management triad is now complete and all-authentic:
  G + fire buttons  = regroup weapons across buttons     (task #6)
  F5-F8 / F9        = reassign weapon generators / mode  (task #12)
  C                 = coolant valve triage               (task #13)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:33:27 -05:00
arcattackandClaude Fable 5 d7b900d108 Cockpit: GENERATOR POWER ROUTING -- assign weapons to generators (task #12)
The pilot's second heat-management tool: the PoweredSubsystem message table
@0x50F4EC (ids 4-8) registered and implemented -- SelectGeneratorA-D
(@004b099c..@004b0a74: FindGeneratorByNumber roster walk on generatorNumber
@0x1E0 -> AttachToVoltageSource re-tap with tap accounting -> modeAlarm
Connected) and ToggleGeneratorMode (@004b0abc: Manual -> Auto -> detach+Manual
cycle).  Weapons inherit via the MechWeapon handler chain.  Desktop: F5-F8
assign the selected weapon (BT_CONFIG_SLOT) to Generator A-D, F9 toggles
reconnect mode; BT_GENSEL_TEST scripts a headless verify.

Verified live end-to-end: dispatch -> handler -> re-tap ("PPC_1 ->
GeneratorD (tapped)") -> the charging I^2R physically moved (GenD cold ~90K
baseline -> ~1570K carrying the PPC; GenA relieved), stable over a sustained
autofire soak with thermal-breaker trips.

FOUR defects found and fixed on the way [all T1/T2]:
- THE e17 HEAT EXPLOSION: Generator::SourceLevel misread *(this[0x38]+0x158)
  as linkedSinks->heatEnergy; it is the engine-base DamageZone @0xE0 ->
  damageLevel [0..1] (the same named-member pattern as the bank radiator's
  zone read).  A breaker-restarting generator emitted (1 - 4e8) x 10000
  volts; squared through the customers' I^2R feed, one restart blew the
  whole thermal network to e17.  Authentic: a damaged generator yields
  proportionally less voltage.
- FUN_004ac9c8 is NOT "IsDamaged": raw body = owner -> mech+0x190 player ->
  roleClassIndex(+0x274) == 0 -- the ROOKIE-role lockout for advanced
  cockpit systems.  New bridge BTPlayerRoleLocksAdvanced (NULL player =
  unlocked [T3]; bring-up role 2 = unlocked).  The old stand-in gated the
  handlers off permanently (healthy subsystems have simulationState==1).
- MESSAGE_ENTRY tables must be FUNCTION-LOCAL statics inside the accessor:
  as namespace-scope arrays they are read by other TUs' static-init chains
  before their own initializers run -- Build copies zeros and every id in
  the table is silently dropped (ids 9/10 only worked by TU-order luck;
  both tables relocated; gotcha recorded, reconstruction-gotchas #9).
- The AutoConnect hunt scanned GetSegment() -- the raw @004b0bd0 walks the
  subsystem ROSTER (+0x124/+0x128); EntitySegments were being cast to
  Subsystems (the classic +0x128 gotcha).

Also: the dense handler table's GAP slots (skipped ids) are uninitialized
heap -- the name-based Find strcmp-walks them and AVs; diagnostic probes use
the id-based Find only (the 1995 binary's own tables carry the same holes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:16:48 -05:00
arcattackandClaude Fable 5 092408041c Cockpit: the CONFIG-MODE weapon-regrouping session + the live-play fire fixes (tasks #6+#11)
TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and
live-verified (hold-configure -> tap fire button -> toggle membership ->
release-commit):
- MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton
  @004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept)
  + the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/
  MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch).
- Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration /
  +0x40,+0x44 AddOrErase evt/dir.  No "secondary vtable @0050f498", no
  "CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP).
  L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the
  NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon
  latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO
  mapper implements the toggle (Thrustmaster no-ops = can't regroup).
- MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID
  (ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the
  destination to &fireImpulse (@004b99a8).
- ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table;
  buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale
  was wrong).  Finding [T1]: the shipped binary NEVER enables this gauge (no
  SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable.
- Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon);
  BT_CONFIG_TEST scripts a headless verify.  @004afbc4 corrected to its real
  Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups).
- Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries
  (PercentDone..WeaponState) -- "TriggerState is the only one" was wrong.

TASK #11 -- the user-reported live-play regressions (both real bugs):
- PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the
  victim on its OWN 0.3s cadence whenever fire was held -- desynced once the
  authentic recharges landed.  Retired (the real impact visual flows from
  each discharge via the messmgr SubmitExplosion).
- WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window
  (+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike
  jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness,
  and the weapon sticks in Loading at level ~10000 forever (observed live).
  Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick
  -- also fixes the I^2R integral over-heating generators on big steps.
  NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap).
- Duty-cycle measured (max-rate autofire): generators equilibrate in the
  degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker
  never trips solo, PPCs sustain ~11.8-damage full-charge fire.  The slow
  kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's
  counters (this regrouping UI, generator reassignment, coolant valves) are
  the reconstruction queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:39:35 -05:00
arcattackandClaude Fable 5 4e63a7b6c3 Heat: THE AUTHENTIC ELECTRICAL MODEL -- weapons recharge from generators (task #10)
Task #10 set out to fix "scrambled linked-sink routing"; a [heat-link] attach
log proved the routing was NEVER scrambled (every subsystem links its authored
condenser exactly; the "pools in Condenser1" read was the diagnostic-sampler
aliasing trap).  The real defect: emitter.cpp's local FUN_00417ab4 stub
returned NULL, so the whole electrical model was inert and the E7 force-charge
recharged every emitter in ONE frame (~0.3s cycle, 1501 fires/90s, ~1.7e9
heat/s -- the "runaway").

Landed authentically [T1: disasm + byte-verified constants]:
- Emitter ctor @004bb120: seekVoltage = authored fraction x generator
  ratedVoltage (10000); EC = energyTotal/(seekV^2 x 0.5); voltageScale@0x310 =
  (RechargeRate / -ln(1 - 1e-4 x seekV)) / EC -- charge reaches seekV[rec] in
  EXACTLY the authored RechargeRate (PPC 5s, ERL 4s, SRM 3s, ERM 2s) cold.
  Owner-flags ctor gate (the usual gotcha; the this-flags read never armed).
- PoweredSubsystem::ChargeTimeScale (@004b0d50, was a =1.0 stub): voltageScale
  x (1 + thermalResistivity x srcTempRise) -- hot generators charge slower.
  ("voltageScale is never read back" was wrong; corrected + swept.)
- TrackSeekVoltage @004ba838: charging I^2R -> the GENERATOR's pendingHeat
  (~3.5e8/full PPC charge) -- generators self-heat, conduct to their authored
  condensers, and throttle further charging.  The feedback economy closes.
- FailureHeat consumers found: this+0x184 == 2 gates BOTH weapon families
  (@004baa88 emitter: reset firing + hold charge 0; @004bbd36 ballistic:
  recoil=rechargeRate + alarm 7).  Emitter::GetFaultState un-stubbed.
- ProjectileWeaponSimulation @004bbd04 opens with call 0x4b0bd0 (disasm) --
  launchers now run the powered/heat step (their firing heat previously
  accumulated in pendingHeat forever).
- heat.cpp: per-instance BT_HEAT_LOG census (the old shared-static 1-Hz
  sampler aliased); [heat-link] attach log; the stolen-else Verify restored.

Verified live (120s max-rate autofire): PPC ~470-500 (was 55,000), bank
plateaus ~600 and sheds to ambient, generators 1100-1400, ERMLaser
self-regulates at the authored 2000 failure threshold (shutdown-cool-resume).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:59:29 -05:00
arcattackandClaude Fable 5 4ed2bbc293 Heat: authentic 1e7-unit heat + the ambient radiator land (task #9)
- Emitters: heatPortion closed form = heatCostToFire x 1e7 x (charge/seekV)^2
  (PPC 1.1e8 -> +632K on its own sink); the missing projectile/missile heat
  adds (raw -- resources author pre-scaled 1e7 units).
- The bank's AMBIENT RADIATOR @4ae73c reconstructed (the system's ONLY heat
  exit; conductance x 0.1 x HeatSinkCount -- _DAT_004ae974 float80 = 0.1) +
  the link-attach guard corrected (skip = the 0xBBE bank, not Condenser; the
  inversion blocked condenser->bank links and closed the system).
- Constant corrections (all byte-verified float80s): coolant epsilons
  0.0025/0.003/1e-4 (was a single 1e-4 serving three sites);
  CoolantCapacityScale 0.05 (was 1.738).
- Verified: heat flows + exits; max-rate autofire overheats weapons into the
  authentic heatLoad range-cutout (thermal spam unsustainable by design).
- KNOWN REMAINING: linked-sink routing scrambled (heat pools in Condenser1;
  authored map says PPC->C4/C6) -- the heat-stream offset audit, filed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:21:37 -05:00
arcattackandClaude Fable 5 fd055281a8 Combat: THE AUTHENTIC DAMAGE ECONOMY -- authored per-weapon amounts through the real fire chain (task #8)
Three root causes, all fixed:
1. kDamageScale was 1.0 -- _DAT_004bafbc is an x87 float80 = 1e-7, cancelling
   the ctor's x1e7: damagePortion = authored DamageAmount x (charge/seekV)^2
   (closed form at fire time; madcat AC=25/LRM=50/ERLL=6, bhk1 PPC=12/SRM=35).
   The observed "0.25" was the degenerate EC=1 fallback, never authored data.
2. CheckFireEdge NaN latch: TriggerState carries ControlsButton INTS; the
   release value (-65) is a negative NaN.  The binary's x87 unordered compare
   read it as "released"; IEEE-correct float compares latched the edge
   detector shut after the first release -- the reason the emitter discharge
   chain NEVER fired in-game.  Fixed with bit-pattern sign compares.
3. The weapon-side submission LIVE: Emitter::FireWeapon fills damageData
   (amount + damageForce=target-muzzle [the gyro directional-bounce feed] +
   impact) -> MechWeapon::SendDamageMessage (@004b9728 real body) ->
   messmgr consolidation with per-weapon records + explosion bundling.
   The mech4 bring-up damage block + flat kShotDamage retired to diag hooks.
Bonus: LODReuseHysteresis 0.82 -> 0.33 (double misread).  Zone model
verified byte-exact (no change).  Solo end-to-end: 5-record volleys (2 PPC
+ 3 ERML with authored amounts), per-weapon zone granularity, explosions,
kill.  Heat stays bring-up scale pending the heat-calibration audit [T3].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:32:24 -05:00
arcattackandClaude Fable 5 77190c93e5 Combat: the AUTHENTIC consolidated damage delivery -- TakeDamageStream through the message manager (task #7 tail)
ConsolidateAndSendDamage @0049b784 fully implemented: the beam path submits
into AddDamageMessage; each messmgr tick builds ONE Entity::
TakeDamageStreamMessage (id 0x13; wire-verified 0x34+4+Nx12) with the
common impact record + appended {type, amount, subsystemID} entries and
Dispatches it at the victim -- the T0 handler (ENTITY.cpp:817) re-splits
into per-record TakeDamage; replicants reroute cross-pod.  Real
ResolveExplosionID (firing weapon's +0x3E4, guarded) + SubmitExplosion via
the Explosion::Make port; TWO chain-purge bugs fixed (the "iterator dtor
clears it" assumption was wrong -- records re-applied every tick, observed
1->2->3->4 double-counting).  @004b9728 identity corrected (SendDamageMessage,
not DrawWeaponPip).  Verified solo: 25 clean single-record consolidations ->
kill -> death transition.  Residue: weapon-side submission awaits the
damage-economy reconciliation (authored 0.25-scale vs kShotDamage=12).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:30:30 -05:00
arcattackandClaude Fable 5 afefaeece5 Mech: THE 0xBD3 UNTANGLE -- real SubsystemMessageManager in [0x10d], the mapper's one true home is roster slot 0 (task #7)
Binary census [T1]: mech+0x434 has exactly 2 writers (ctor init + factory
case 0xBD3) and ONE reader (@0x4b984b in MechWeapon::SendDamageMessage
@004b9728) -- pure message-manager semantics, nothing mapper-like.  The
factory case now builds the real SubsystemMessageManager (ctor @0049bca4,
0x130, ConsolidateAndSendDamage Performance); the misbuilt MechControlsMapper
squatter is evicted.  Every mapper consumer re-pointed to MappingMapper()
(roster slot 0 = the binary's **(mech+0x128); SetMappingSubsystem @0049fe40
touches ONLY slot 0 -- its [0x10d] mirror removed).  Non-viewpoint mechs get
a slot-0 demand LATCH (base mapper, [T3] accommodation -- the binary leaves
slot 0 NULL off-viewpoint but never drives/animates those paths; ours does).
Verified: solo drive+gait clean; MP replicant gait through the latch (full
run lifecycle); 3x kill/respawn cycle with type-6 records on the observer.
Remaining (task #7 tail): the messmgr's consolidated TakeDamageStream send +
weapon-path routing (@004b9728 real body).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:16:27 -05:00
arcattackandClaude Fable 5 8ed6184d65 Combat: AUTHENTIC weapon groups -- streamed per-mech button bindings + the real fire chain (task #5)
The recovered system: fire channels = LBE4ControlsManager buttonGroups
(0x40/0x45/0x46/0x47); default groups = the per-mech type-6 controls-map
resource in BTL4.RES, installed by the T0 CreateStreamedMappings the port
already called -- it needed only the TriggerState attribute (id 0x13 PINNED
to the binary value; fireImpulse@0x31C is the binary's TriggerState) and an
input feed.  Keyboard/harness now push press/release edges into the button
groups; the gBT*Trigger bypasses, per-type keyboard split and 1,0 pulse
hack are retired -- weapons sharing a button fire TOGETHER (madcat Trigger
= 4 weapons).  Myomers @4b9550/@4b95b8 misattribution corrected (they are
MechWeapon ConfigureMappables/ChooseButton).  Verified 2-node: kill through
the authentic chain (12 hits vs ~36 pre-groups).  Config-mode session
(regrouping UI) = the remaining stage, KB-scoped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 13:06:29 -05:00
arcattackandClaude Fable 5 4554ea543a KB: locomotion turn-rate note -- authored rates stream correctly post task #4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:36:39 -05:00
arcattackandClaude Fable 5 63206d7f19 Model record: the authoritative 200-byte overlay -- struct skew fixed, floor-25 era retired (task #4)
Two size errors (animationPrefix char[12] vs 4 bytes; a FICTITIOUS 64-byte
skeletonName block -- the record has no skl name) skewed every member after
word 0x10 by +2/+15 words and pushed the fieldXX block past the record end.
Full writer map recovered (FUN_004a2da8 + Mover FUN_004238bc + Reticle
FUN_00435ac8 chains) + runtime reader map (ctor pass @0x4a24d1-0x4a2968,
capstone-verified).  Struct rewritten as the exact overlay (static_assert-
locked); all Pass-2 consumers renamed to real fields; retired: ctor +
replicant + master-gait floor-25 hacks, the throttle guard, the fictitious
classID stamp.  Verified: accel=30 superStop=10 throttleAdj=1 (was ~1/OOB/0),
deadbands byte-identical via named fields, full walk->run lifecycle clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:36:12 -05:00
arcattackandClaude Fable 5 001ca16af9 KB: crit-propagation landing recorded (task #2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:11:21 -05:00
arcattackandClaude Fable 5 87c25b9206 Combat: critical-subsystem plugs BOUND -- zone destruction damages carried subsystems (task #2)
The binding was in the zone ctor all along: Ghidra dropped the two arg
pushes @0049d0e1 (Slot::AddImplementation(subsystemArray[streamedIndex])),
making it read as a bare Resolve().  DZSlot stand-in -> engine SlotOf<T>;
SendSubsystemDamage rewritten to the recovered @0049c9a8 body (allotment
into the subsystem's OWN private zone; vital -> graphicAlarm 9); CriticalHit
-> real ApplyDamageAndMeasure; parentArtifactZone.Add revived (LOD damage
averaging); videoObjectFlag renamed vitalSubsystem (+0xE4).  BT_CRIT_PROBE
diag added.  Verified: 66 plugs bound/mech; probe-destroyed zone -> crits
damaged/DESTROYED, statusAlarm + destroyed-skin chain fire; MP kill + solo
un-regressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:10:43 -05:00
arcattackandClaude Fable 5 a652ddcdbc MP: authentic update-record deadbands streamed from the model record (task #3)
Binary ctor @0x4a26a7-0x4a26ef: mech+0x768/76c/770 <- raw record words
0x26/0x27/0x28 (UpdatePositionDiffrence / UpdateTurnVelocityDiffrence /
UpdateTurnDegreeDiffrence x pi/180 [const @0x4a2d44]).  Read at RAW record
offsets -- the Mech__ModelResource struct is layout-skewed (field98 gave
-1.5 where the record holds 0.3; audit filed as task #4, also explains the
old forwardCycleRate "floor 25" hack).  Sender triggers now use the
authored constants: pos-deadband type 0, quat-Y delta + yaw-rate delta +
stopped-turn edge type 4 (binary @0x4aad35/@0x4aac2b/@0x4aac6c).
Verified 2-node: madcat streams 0.3/0.045/3deg, type-4 spam gone, walking
replicant clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:55:25 -05:00
arcattackandClaude Fable 5 e8b9f71025 KB: task #1 landings -- update-record channel DONE, movementMode=simulationState identification swept
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:40:44 -05:00
arcattackandClaude Fable 5 c9f0c2a7f1 MP: the Mech-level update records @0x4a0c2c/@0x4a1232 -- all 9 types byte-exact (task #1)
- Writer transcribed from the recovered disasm; reader rewritten from
  part_012.c with every Wword absorber promoted to named engine/port members
  (updateOrigin/updateVelocity/projectedOrigin/projectedVelocity/nextUpdate/
  lastUpdate + bodyTargetSpeed/latches/alarms).  Wire sizes verified live:
  0x14/0x20/0x2c/0x78.
- movementMode UNIFIED with Simulation::simulationState (binary mech+0x40 =
  StateIndicator@0x2c currentState) -- death/limbo/airborne now replicate in
  every record header.  Three mislabels of the same binary fns retired:
  SetInstanceFlags + RequestActionFlags -> Mech::ForceUpdate (updateModel |=
  mask, 0xfe03 disabled filter); IsNetworkCopy -> IsDisabled.
- Senders wired byte-exact: gait transitions Force(8), knockdown Force(1|0x20),
  death Force(1|0x40), Reset Force(0x1f) + binary zero-set, perf-loop
  deadbands (speed type 2, orientation type 4, heat type 7).
- 2-node verified: types 2/3/4 flow while driving; kill -> type-6 record
  (simState=9) -> the OBSERVER's replicant runs the wreck sink loop with no
  double death transition; respawn 0x1f burst snaps + un-wrecks the peer;
  walking replicant un-regressed (run 12, cycle tracking); solo clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:38:49 -05:00
arcattackandClaude Fable 5 3c34ae6de6 KB: full staleness audit + sweep -- 35 verified corrections across 16 topics, 15 new glossary terms
Adversarially-verified audit (18 agents) against the task #46-#56 landings:
MP/current-state understatements, superseded punch readings, retired env
gates, dead paths, line-cite drift; env-gate hub table completed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:56:51 -05:00
arcattackandClaude Fable 5 2abfd51350 KB: sweep stale MP open-questions -- cross-pod combat + replicant gait are DONE; the real gap is @0x4a0c2c
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:25:13 -05:00
arcattackandClaude Fable 5 1ace2e3cb4 Gyro: BT_GYRO_TRACE per-frame integrator trace -- oscillation verified vs authored constants (task #56)
25-hit MP autofire trace: kick -> damped oscillation -> settle, peaks well
inside clamps, no NaN/drift; measured Y:X frequency ratio 7.75x matches
sqrt(springK.y/springK.x) exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:56:55 -05:00
arcattackandClaude Fable 5 8af278a3f5 Cockpit: unlit map-independent frame colour (task #55)
User report from the MP/cavern session: the canopy frame rendered flat light
gray -- unlike the dark frames in the polar exports.  Root cause: the frame
colour was routed through the LIT material path (Diffuse+Ambient), so it
multiplied with each map's lighting -- polar's dim purple light happened to
look right, cavern-night's ambient 0.8 washed it out to gray.

The canopy is unlit interior structure: the frame batches now go through the
renderer's pure-emissive UNLIT branch (black diffuse + emissive = the frame
colour), constant on every map.  The exact original value is not recoverable
(texture-less ramp = no texel luminance to index); default (0.13,0.12,0.15)
[T3] matches the near-black frame in pod gameplay footage.
BT_COP_FRAME="r g b" overrides; BT_COP_RAMP_L retired.

Verified: cavern-night and polar-day now show the identical dark frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:42:22 -05:00
arcattackandClaude Fable 5 27959990ea View: the INI viewangle is the HORIZONTAL field -- authentic 60x47 frustum (task #55)
Community reports said the camera looked too far back and the cockpit frames
were never this revealed; the user's hypothesis (our projection widens the
view) was exactly right.  The original Tesla projection call, preserved in a
comment at DPLRenderer::SetView, is

  dpl_SetViewProjection(view, -1, -aspect, +1, +aspect, 1/tan(viewangle/2))

i.e. BTDPL.INI's viewangle=60.0 is the HORIZONTAL field of view with the
vertical derived by aspect: the pod's frustum was 60 x 46.8 deg at 4:3.  The
port fed the 60 to D3DXMatrixPerspectiveFovRH as the VERTICAL fov -> 75 x 60
at 4:3 (worse on widescreen): the world rendered ~25% farther away and far
more canopy was visible than the pod ever showed.

Fix: BTFovYFromHorizontal(viewangle, aspect) at all five projection sites
(main, per-frame resize, sky pass, WM_SIZE rebuild, fallback) -- the
horizontal field stays the authentic 60 deg at any window shape.  The aim
boresight reads the live projection (gBTAimP22) and adapts; combat
re-verified (kill chain, no NaN).  Cockpits now show the frames at the
pod's framing: viewports dominant, frames at the periphery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:17:07 -05:00
arcattackandClaude Fable 5 96b8892e7c Cockpit: the authentic punch STENCIL-CUT -- all 8 cockpits real (task #55)
The user caught the inversion (blocked viewports, see-through frames); the
answer came from the rasterizer board itself: an i860 disassembly of the
VREND.MNG firmware decoded the damageize handler (@0xf040f6f8) -- cmd 0x20
(vr_damage_action) writes the dpl_Punchize token triple onto the punch
geogroup's first three geometry chunks IN FILE ORDER:

  chunk1 = the detailed window-aperture shapes -> an invisible per-pixel MASK
  chunk2 = the coarse hull -> the ONLY colour-drawn geometry (where not masked)
  chunk3 = byte-identical hull twin -> the damage-reset record, never visible

Visible canopy = hull-minus-apertures.  Both earlier readings were wrong ways
around the same three chunks: dropping all punch left floating fragments (no
hull); hiding the duplicate pair hid the HULL and drew the MASK -- the exact
inversion observed.

Port: bgfload tags per punch patch {non-paired pmesh = mask (role 1),
first twin = hull (role 2), second twin = skip}; L4D3D DrawMesh runs
mask->stencil-1 (no colour/z-write, z-tested), hull->stencil-NOTEQUAL,
mask->stencil-0; device D24X8 -> D24S8 + stencil clear.  The cut never
touches z, so later-drawn entities show through the apertures.
BT_COP_PLATES=1 disables (diag).

In-game, all 8 mechs on pure defaults: connected dark frames with clear
viewports -- thor (the footage mech) shows the central viewport + surrounding
frame exactly as filmed.  Combat regression clean (kill + no NaN).

Arena textured punch uses the same firmware kit; the black-texel alpha path
there is a working approximation, flagged in the KB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 08:53:22 -05:00
arcattackandClaude Fable 5 8431e69b1a Cockpit: duplicate damage-plate rule -- connected frames on 7/8 mechs (task #55)
The user's observation was right twice over: cockpit elements can't float
disconnected, and the missing punch geometry is STRUCTURE, not glass.  The
TRICODER.H decode (dpl_Punchize tokens = {dmg_set, undmg2enbl|texture,
dmg_clear}) shows PUNCH marks geometry for the board's per-triangle DAMAGE
machinery -- battle damage punches holes through it; undamaged it renders
opaque.

The per-mesh discriminator: every *_cop PUNCH patch contains exactly one pair
of BYTE-IDENTICAL duplicate PMESHes -- solid damage-state cover plates, not
visible undamaged structure.  Rendering them was the "solid black canopy";
dropping whole punch patches left the floating fragments.  The loader now
hides just the within-patch duplicate pairs (FNV hash over the raw pmesh
chunk; BLX duplicates its main lattice ACROSS patches, so cross-patch copies
stay) and renders the rest of the punch lattice opaque, DOUBLE-sided (the old
single-siding + per-face inward winding existed only to fight the plates'
black box; with the plates hidden it was moot and its centroid heuristic was
fragile on the tight canopies -- removed).  BT_COP_PLATES=1 shows the plates,
BT_COP_SINGLE=1 restores single-siding (diag).

In-game on pure defaults: madcat dome + connected framed cockpits with window
apertures on bhk1 / owens / sunder / vulture / avatar.  Residual: thor + loki
still show an oversized central block vs the offline authored-eye prediction
-- under investigation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 08:25:50 -05:00
arcattackandClaude Fable 5 9414268138 KB: sweep the superseded punch-patch claims (task #55 resolution)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 07:50:08 -05:00
arcattackandClaude Fable 5 8057c37e5b Cockpit: punch patches ARE the windows -- all 8 mechs authentic (task #55)
Resolved the non-Madcat blocked views. Rendering every *_COP shell offline
from its mech's AUTHORED eye (SKL [jointeye] translation -- incl. the Thor's
+1.13 offset cockpit) with faces coloured by punch flag showed the answer
plainly: the PUNCH-tagged patches sit exactly over the window apertures in
all 8 mechs. On these texture-less black-diffuse shells dpl_Punchize renders
the tagged geogroup as holes -- the punch patches are the transparent
WINDSHIELDS, and the visible frame is only the non-punch geometry.

The loader now drops the punch batches of _cop meshes (bgfload finish();
BT_COP_KEEPPUNCH=1 keeps them for diagnosis). In-game sweep on pure defaults:
- madcat: the dark dome frame (matches gameplay footage)
- thor / owens / sunder: authentic brace/dash/pillar remnants
- bhk1 / loki / vulture / avatar: authentically FRAMELESS (all-punch
  canopies -- the pod showed no frame for those mechs)

(An early-session test that "disproved" drop-punch ran with the broken
pre-inverse eye; its conclusion was wrong and is corrected in the KB.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 07:49:33 -05:00
arcattackandClaude Fable 5 946323b696 Gyro: damage fan-out FUN_004b2980 -- the cockpit hit-BOUNCE is live (task #56)
Re-disassembled the unexported gap 0x4b2980-0x4b2d8b byte-exact (capstone over
section_dump.txt) and reconstructed Gyroscope::ApplyDamageResponse:
- zero-damage + Collision(type 0) no-op; hit direction = Damage::damageForce
  or a RANDOM horizontal fallback when ~zero (per-component sign roll then
  value roll -- binary-legal until senders fill damageForce)
- direction rotated by the yaw-only torso-twist frame (placeRot=(0,twist,0)
  -> euler->matrix, the FUN_00408744 raw row-dot reproduced verbatim)
- per-type scaling amount / damageMultiplier[type] * damageResponse[type]
  .{trans,pitchRoll,yaw,vibration}; Explosive alone multiplies burstCount;
  each clamped at 1.3f
- four kicks: impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
  vibrationDirection@0x390) and verticalImpulse(yaw, dir)

Layout corrections (byte-verified): gyro+0x390 is ONE Vector3D vibration axis
init (0,1,0) -- was mis-split as scalars animationOffset/Scale/Phase; mech
+0x5c4 is a FLOAT rumble-period countdown (gyroRumbleTimer) -- was mis-typed
int clipLoadGuard.

Call sites wired (binary-gated):
- take-damage hub: mech.cpp TakeDamageMessageHandler, FIRST action (@0x4a0264
  in hub FUN_004a0230) -- an invalid-zone hit still bounces
- crushable-icon crunch: mech4.cpp, torque 0.4 along the bounce delta-v + up
  impulse 0.2 (@4aa81e/@4aa86c)
- firing recoil: projweap.cpp FireWeapon, damage>3 -> impulse (0,0.6,-1.5)
  x damage/16 (@4bc136-4bc19c) via the new Mech::GetGyroSubsystem accessor

DEFERRED (recorded in KB with the byte recipe): the alternate-gait engage
jolt + 0.4s rumble (@4aa158-4aa365) -- its gates are [T3]-flagged and it
mutates the #49/#50-stabilized gait machine.

Verified live: [gyro-dmg] fires per hit with correct type scaling; eyePosition
shows the damped bounce (ramp to ~0.015u, oscillating Y, decay to zero); kill
chain un-regressed; no NaN.

Also recorded in KB: the OFFSET-COCKPIT verification -- Thor's authored
jointeye tranx=+1.13 matches its canopy X-center exactly AND our in-game eye
reproduces it to the hundredth (the placement model is correct per-mech); and
the playable roster is 8 models (strider/raptor/firstrtr/blkjack have assets
but no model resource -- BT_FORCE_MODEL with those names = NULL-resource AV,
cdb-verified, not a code bug).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 07:36:25 -05:00
arcattackandClaude Fable 5 cb85517ede Gyro: byte-exact re-enable -- ctor map, integrators, jointeye writers (task #56)
The Gyroscope subsystem is LIVE again (was a stub since the NaN revert).
Reconstructed byte-exact from @004b3778 / @004b2ec0 / @004b30ec / @004b33e0 /
@004b34ec + the byte-verified dispatch in the unexported Mech performance
FUN_004a9b5c (calls @0x4aaf74/0x4aaf83):

- ctor field map corrected: springConstant@0x1E8 / dampingConstant@0x1F4 were
  mislabelled (the old 'eyeOrientation = r->springConstant' poison line), the
  0x254-0x2B3 block (externalPitchPtr, work matrix, placement) was missing so
  everything after was mis-offset, and clamps/accumulators were 0xCD fill --
  every field now initialised per the binary; 33 offsets static_assert-locked,
  sizeof == 0x3D0 exact
- integrators: state-minus-target displacements, componentwise damping
  OVERWRITE (FUN_004086d0 is a component multiply, not a cross), position step
  without dt, clamp to [posSpring, negSpring]; IntegrateBody's X/Z-crossed
  force + integration terms
- writers: WriteMechJoint drives ONE node ('jointeye', type 5): TRANSLATION =
  eyePosition spring, ROTATION = bodyOrientation spring; WriteEyeJoint is a
  multiplicative sway attenuator on 'jointlocal' (post-anim only)
- dispatch moved out of GyroscopeSimulation into the Mech performance tail
  (GyroFrameJointWrite bridge, mech4.cpp) with the binary gates; gyro<->torso
  pitch link wired (gyro+0x258 = &torso currentTwist) via complete-type-TU
  bridges, retiring the SubProxy::linkTarget landmine
- Mech ctor: deathAnimationLatched/legResetLatch were never initialised --
  0xCDCDCDCD gated the writers off silently

Runtime-verified: joints resolve (both BallTranslation), Performance installs,
WriteMechJoint finite from frame 0 (no NaN), Madcat cockpit un-regressed,
combat targeting healthy. Empirical: spring targets are SYMMETRIC -> eye
equilibrium (0,0,0), clamps +/-0.1-0.15u -- the gyro is the hit-BOUNCE
mechanism, not a steady eye offset (hypothesis disproven, recorded in KB).

Pending (task #56 tail): the damage->gyro fan-out FUN_004b2980 (unexported
gap) so hits actually kick the springs; the mech+0x3F0 overspeed sway model;
the torso-pitch EyepointRotation writer (FUN_004b66b4) + glance-look states.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 06:47:22 -05:00
arcattackandClaude Fable 5 abaa145b6f Cockpit: authentic eyepoint + canopy lattice render (task #55)
The first-person cockpit now shows by default and matches gameplay footage
(verified on the Madcat). Two decomp-verified reconstructions:

EYEPOINT (FUN_004579a8 + caller part_014.c:5525, FUN_004c22c4):
- eye offset = siteeyepoint GetBaseOffset (not GetSegmentToEntity, not the
  upright hack), parented on the PARENT segment DCS (btl4vid.cpp)
- live view = affine INVERSE of the eye world matrix (L4VIDRND Execute) --
  replaces the hand-rolled LookAtRH whose +Z-forward/+Y-up row guess aimed
  some mechs into the canopy; combine order fixed to baseOffset * R
- chase camera basis + aim boresight re-expressed for the inverse convention
  (row2 = back); boresight now derives from the view matrix (the stale rows
  aimed the pick ray at the sky = no target, no discharge)

CANOPY (*_COP.BGF, dpl_Punchize geogroups):
- every shell is an open strut lattice (38-59% boundary edges, all 12 mechs);
  rendered single-sided with per-face INWARD winding (l/r torso patches are
  mirrored, so no global winding works) the struts are the dark frame and the
  openings show the world
- frame colour = the 'softer' ramp near its dark end (texture-less unlit ramp;
  BLXSKIN.BMF has no texture -- the old "punch texels" theory was wrong)
- scoped by filename (meshIsCop); wrong drop-punch + view-cut paths removed
- BT_FORCE_MODEL=<name> forces the player mech for per-mech bring-up

KB: new context/cockpit-view.md topic; punch split corrected in bgf-format;
gotchas 14 (LookAt axis guess) + 15 (per-patch edge namespaces); env gates +
eye/punch addresses in decomp-reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 06:46:54 -05:00
arcattackandClaude Opus 4.8 7b0b4f64ab Diag: add BT_SHOT backbuffer dump + correct/park the -net render glitch (task #53)
Scouted the "-net node render glitch". Two prior framings were wrong and are
corrected in the KB: it is NOT device contention (retracted earlier), and it is
NOT a from-load glitch -- the trigger is a RESPAWN (user correction). It's a
post-respawn render-rebuild fault on the MP path.

Could not reproduce in 9 automated 2-node loads + a forced death/respawn (all
clean). Debug uninit is a deterministic 0xCDCDCDCD, so the intermittency implies
a race -- and the task #52 respawn fixes (LoadMission re-entry, viewpoint relink,
death-latch) plausibly already closed it. Parked as WATCH; re-open if it recurs.

Tooling (general, off by default): BT_SHOT=<path> dumps a per-instance backbuffer
PNG from the world-render path (L4VIDEO.cpp) every 90 frames -- lets us capture a
specific node's frame without foregrounding the window. Used with
scratchpad/hunt53.sh (N-run divergence hunter) to verify a fix if the glitch
returns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:48:54 -05:00
arcattackandClaude Opus 4.8 0bfb3d4ab3 Warp: restore the translocation-vortex look + fix texture-scroll precision collapse (task #52)
The death/respawn "blue whirlwind" (tsphere) now matches the original cabinet
photo (capture.png): a smooth spinning lavender vortex with a bright core.

Root cause of the long-standing "radial spokes" artifact was NOT the warp code
but a general engine bug: L4D3D::SetTextureScrolling computed its texture-matrix
offset as scrollDelta * absolute_time, which grows unbounded and collapses UV
float precision -> a smooth scrolled cloud shatters into grainy radial steps.
Wrapped with fmodf(..., 1.0f) (identical under REPEAT tiling, full precision).
This also cleans the scrolling bexp beam grit and any other SCROLL material.

Visual reconstruction (verified against the real 45-vtx TSPHERE.BGF bicone,
offline-rasterized then ported):
  - view ON-AXIS (eye centred on the throat) + spin in place -> concentric rings
    (decomp FUN_00453dc4 does spin-about-local-Z + submit; the port had stubbed it)
  - bintA cloud through a WIDE lavender ramp at full contrast (drawn as SKY);
    no geometry "bands", no log-polar twist, no off-axis tornado (all discarded)
  - tessellate the 12-facet bicone smooth; isotropic + trilinear; ramp baked into
    the texture and drawn SELECTARG1(TEXTURE) to avoid double-tinting

Env knobs (BT_WARP_*) default to the verified values; BT_WARP_SELFTEST/SELFSHOT
are an off-by-default visual-verification harness (backbuffer frame dump).

Docs: new context/translocation-warp.md (geometry/material/visual/lifecycle/env);
reconstruction-gotchas.md gains the accumulated-time precision-collapse bug class;
rendering.md / multiplayer.md / decomp-reference.md / CLAUDE.md cross-linked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 14:53:50 -05:00
arcattackandClaude Opus 4.8 a35f321ba1 Warp: exact POVTranslocateRenderable replica (collapse/throb/reveal + world mask)
Stop approximating -- replicate POVTranslocateRenderable::Execute() behaviorally,
byte-for-byte on the constants and transitions (engine T0, L4VIDRND.cpp:1763-2076),
per the user's "replicate the effect exactly".  The five prior piecemeal attempts
each fixed one facet and broke another because they left out the WHOLE mechanism.

Full state machine (btl4vid.cpp), driven by two decoupled events (NOT the
SimulationState trigger -- that dial also drives camera/POV+targeting, the f053535
regression; SetIsDead is a separate pure-render flag, verified safe):
  - LOCAL DEATH -> InitialCollapse: scale (pct_left*100)+1 over 1.3s (world visible),
    then SetIsDead(true) + WaitForReincarnate.  (btplayer VehicleDeadMessageHandler,
    deathCount==-1, local-guarded.)
  - WaitForReincarnate: world BLACK (mask), scale 1, Lissajous throb
    (cos(t*3.33), sin(t*2.5)) x2.0 -- the "dances around playfully".
  - LOCAL RESPAWN -> ExpandReveal: SetIsDead(false) + scale (pct_used*150)+1 over
    1.0s -- blasts open, world revealed ("shoots off into the distance").  (btplayer
    DropZoneReplyMessageHandler, local-guarded.)

Draw EXACTLY as the engine (isDeathDraw -> drawAsSky): PASS_SKY, OPAQUE,
CULLMODE=CW, z-test ON (L4VIDEO.cpp:7526/7568-7570/7693).  This kills the "glitchy
funnel" -- opaque = one write/pixel (no translucent double-blend), CW = one winding
of the double-sided mesh (no coincident z-fight), z-on = the expand shell is
occluded/revealed by the returning world.  Placement = Scale * inverse(view) with
NO recenter (the mesh is authored off-origin so the eye sits inside, authentic);
dropped the wrong mCullCenter recenter and the Z-off overlay hack.  Colour is the
"sky" ramp (per prior commit); no geometry spin (myRotateY is dead) -- swirl is the
texture scroll.  SetIsDead reached via a BTSetWorldDead bridge (L4VIDRND.cpp).

STUCK-BLACK SAFETY (the one new risk of decoupling mask-on from mask-off): a 12s
WaitForReincarnate render-loop timeout un-masks if no respawn arrives, plus
BTWarpForceUnmask on the mission-ending + no-DropZones abort paths.  Smoke-verified
2-node: collapse->wait(masked=1)->expand(masked=0), ends masked=0, no crash.

The peer-observer warp stays a world-anchored translucent alpha draw (port
extension; the authentic effect is POV-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:43:54 -05:00
arcattackandClaude Opus 4.8 062c66999e Warp polish: animate scroll, recenter, no depth-clip, expand-only respawn
Four fixes for the user's "static / above the viewport / cavern clips it / doesn't
shoot off" report, each root-caused:

1. STATIC swirl -> now animates.  The material's SCROLL (0.0 0.0 0.1 0.5) only
   reaches the draw via a per-texture .met file (L4D3D.cpp:640) that tsphere lacks,
   and the ramp-bake path leaves doScroll=false -- so the swirl was frozen.  Set the
   authored scroll rates (u -0.1/s, v +0.5/s) + REPEAT wrap on the sphere's draw-ops
   at load, so SetTextureScrolling churns the swirl (the "spins around").

2. "Above the viewport" -> recenter.  tsphere.bgf is modelled with its centre at
   y=+8.25 (logged: center=(0,8.25,0.47) r=21.5); scaled x100-150 that threw the
   sphere ~1000 units overhead.  Translate the mesh's mCullCenter to the origin
   before scaling so it sits ON the eye/anchor.  (Added public-ish mCullCenter use;
   l4d3d.h already exposes it.)

3. Cavern clip -> Z-test off.  The arena is an enclosed cavern; a depth-tested 150x
   sphere gets occluded by the ceiling/walls.  Draw the POV overlay with ZENABLE off
   (ZWRITE already off, so no depth pollution).  BT_WARP_ZTEST=1 restores it.

4. "Fades in then disappears" -> expand-only.  A respawn now starts in the EXPAND
   phase (1->150 over 1s) so the swirl immediately bursts and blasts outward ("shoots
   off into the distance").  The collapse (100->1) is the DEATH warp, not the
   respawn.  BT_WARP_COLLAPSE=1 plays the full collapse->expand.

Build + render + no-crash verified 2-node; expand warp fires POV/world.  Still
deferred: the SetIsDead world-mask (black-out that makes the expand "reveal" the
world) -- carries the camera-coupling risk, add scoped if wanted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:09:21 -05:00
arcattackandClaude Opus 4.8 a9d1534292 Warp: faithful POVTranslocateRenderable -- eye-centered, ramp color, no spin
Deep-dived the authentic engine effect (L4VIDRND.cpp POVTranslocateRenderable, T0)
and reworked our one-shot to match it, fixing the user's "blob from my own POV /
not aligned to my orientation" + "gray, not blue" reports:

1. PLACEMENT was the misalignment. The engine parents the sphere to the VTV/eye
   ("rotated and scaled around the VTV", L4VIDRND.cpp:1812) -- it is centered on and
   oriented to the viewpoint. We world-fixed it at the mech's feet, so from your own
   camera it sat off-axis and, once expanded, engulfed the eye as a shapeless blob.
   Now: the own-respawn warp (BTStartWarpEffectPOV, from btplayer) uses
   world = Scale(s) * inverse(view) -> centered on + carried by your eye (the tunnel
   wraps you). Observing a PEER (BTStartWarpEffect from mechdmg) stays world-anchored
   at the peer's point.

2. GEOMETRY SPIN was inauthentic. myRotateY/myRotateYSpeed are set in the engine
   ctor but NEVER applied -- the swirl's rotation is entirely the material's texture
   SCROLL (bintA, SCROLL 0.0 0.0 0.1 0.5). Dropped our world-Y geometry spin.

3. COLOR (gray) fixed at the source. The blue/white is the "sky" RAMP remapping the
   grayscale bintA cloud per-texel (argb=lerp(0,0,0.6 -> .99,.99,.99, luminance),
   L4D3D.cpp:480). The port bakes exactly this but GATED it off for normal-bearing
   meshes; tsphere has normals so it arrived raw gray, and a MODULATE tint can't add
   the blue floor (it only multiplies) -> the wash-out. Un-gate the ramp for
   tsphere_mtl (bgfload.cpp) so the bound texture is already the blue-white swirl;
   draw it MODULATE by white (no tint), alpha from TFACTOR.

4. SCALE to authentic 100 (collapse) / 150 (expand end) -- correct now that it's
   eye-centered (you're meant to be inside it as it collapses through you and blasts
   back open, the "blast off into the distance").

Still deferred (higher risk / SimulationState-coupling trap): the WaitForReincarnate
Lissajous wobble + the SetIsDead world-mask (black-out during the dead hold). Noted
for follow-up; the placement/color/motion are the parts the user flagged.

Smoke-verified 2-node: own respawn logs POV(eye), observer logs world-anchored, no
crash. BT_WARP_ADDITIVE / BT_WARP_COLOR / BT_WARP_SCALE remain as live tuning knobs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:42:02 -05:00
arcattackandClaude Opus 4.8 160b78e38d Respawn: swirl warp, cockpit-not-black on respawn, observer sees peer un-wreck
Three respawn-visual bugs the user saw, each grounded in the engine/decomp:

1. Warp = flat blue BLOB, not the demo's swirly blue/light-blue/white shimmer.
   Ground truth (content/VIDEO/MAT/DAY/BTFX.VMF): tsphere_mtl = a SCROLLING bintA
   texture (tsphere_scr_tex, SPECIAL SCROLL) + EMISSIVE {0.7,0.5,1} + RAMP "sky"
   (dark-blue->white).  The swirl IS the scrolling texture; the colour is the
   emissive/ramp.  Our draw did COLOROP=SELECTARG1/ARG1=TFACTOR, which REPLACES
   every texel with one flat colour -> the blob.  Fix: MODULATE the (bound,
   scrolling) texture by a TFACTOR set to the authentic EMISSIVE hue (0xB380FF)
   so the swirl survives and reads blue-white.  DrawMesh's cached SetTexture
   (L4D3D.cpp:1215) leaves our MODULATE ops standing since textured meshes drew
   first.  Additive glow, all state saved/restored.

2. First-person view BLACK on the dying/respawning mech until V.  SetViewInside's
   body-hide + '_cop' canopy suppression + viewSkeleton update were gated !wrecked,
   and RebuildMechRenderables (respawn un-wreck) restored the full OUTSIDE torso
   with no '_cop' rule -> the cockpit eyepoint ended up wrapped in opaque geometry.
   Fix: factor the per-segment mesh selection into ApplyViewSkeleton(viewpoint,
   inside) shared by SetViewInside AND RebuildMechRenderables, so respawn re-asserts
   the inside skeleton + '_cop' hide; record viewSkeleton even while wrecked.  Only
   the mech the local camera views FROM gets the inside treatment (a replicant is
   always outside).

3. OBSERVER never saw the peer respawn -- the peer's wreck sat forever.  The wreck
   appears incidentally via rising damage-zone replication -> MechDeathHandler::Tick
   -> BTRemakeMechModel (one-way).  The un-wreck+warp ran master-only in Mech::Reset.
   Fix (reuses the existing damage channel, no stream-framing change): Mech::Reset
   also ForceUpdate(DamageZoneUpdateModelFlag) so healed zones cross; Tick handles
   the FALLING edge -- on a ReplicantInstance whose zone heals from destroyed, call
   BTRebuildMechModel + BTStartWarpEffect once (wasWrecked latch).  This is the port
   analog of the binary's type-0 graphic-state -> ResetPose un-wreck hook
   (Mech::ReadUpdateRecord case 0); the full Mech::WriteUpdateRecord death record
   (type 6) is deferred (would touch the netcode framing).

Smoke-verified headless (2-node): victim respawns intact + warp; observer logs
"replicant un-wrecked + warp" at the peer's spawn point; no crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:09:27 -05:00
arcattackandClaude Opus 4.8 d07dd0bf4e Respawn visuals: warp was in the wrong render pass; render never un-wrecked
Two render bugs behind "warp never appears" + "respawned mech stays a sunk wreck":

1. Warp invisible: the tsphere was drawn in PASS_ALPHABLEND, but DrawMesh skips
   any op whose alphaTest != (pass==PASS_ALPHABLEND) (L4D3D.cpp:1045) and
   tsphere's op is opaque -> every op skipped, nothing drawn. Flag the sphere's
   ops for the alpha pass, and draw it as an explicit translucent-blue ADDITIVE
   glow (state saved+restored) so it reads as a warp shimmer, not an opaque ball
   or (as shipped) nothing. Tint via BT_WARP_COLOR.

2. Render stays wrecked on respawn: SwapToWreck hides the body + hangs a sinking
   dbr hulk with render_tree.wrecked a one-way latch; Mech::Reset healed the sim
   but nothing reversed the render, so the reborn mech kept rendering as the sunk
   hulk. Add RebuildMechRenderables (the heal direction of RemakeEntity): drop the
   hulk/debris, clear wrecked, restore every body segment to its now-intact mesh.
   Mech::Reset calls it via BTRebuildMechModel.

Smoke-verified: tsphere loads with its op alpha-flagged; on each respawn
"[BTrender] respawn: rebuilt intact model (12 segs restored, hulk dropped)"; no
crash. Both are local render (the respawning node's own screen); remote-observer
replication of the warp+un-wreck rides the deferred WriteUpdateRecord death path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 07:26:48 -05:00
arcattackandClaude Opus 4.8 247e51e1e1 Reset-based respawn: reuse+heal the mech, not create-a-new-one (task #52)
The respawn glitches (2 mechs, on-fire respawn, camera-inside, can't-control,
wreck-never-disappears) all traced to one architectural divergence: our respawn
SEVERED playerVehicle on death and CREATED a new mech, leaving the old as a
permanent wreck and building a duplicate viewpoint whose old render tree was
never torn down.

The authentic engine (FUN_0049fb74 + RPPlayer) REUSES the same mech entity: on
respawn Mech::Reset heals it and moves it in place. Implemented faithfully,
adapted to our layout (the 1995 raw offsets map to different 2007 engine fields,
so reset the equivalent named members, not the offsets):
- Mech::Reset (real, was a reposition-only stub): reposition + kill dead-reckon
  (projectedOrigin/projectedVelocity/updateVelocity + our relocated gait
  accumulators) so the replicant stops lerping to the death spot; clear the
  death latch (movementMode=1, graphicAlarm=0); Heal every damage zone
  (new Mech__DamageZone::Heal: full structure, intact skin); DeathReset
  (vtable+0x28) every subsystem; ForceUpdate to broadcast.
- btplayer.cpp: VehicleDead no longer severs playerVehicle; the respawn re-post
  gates on the mech still being dead; DropZoneReply resets the EXISTING mech in
  place (heal+move) instead of creating a new one, then fires the warp. Warp
  moved to the shared placement (initial drop-in + respawn).

Verified 2-node: mech entity ID stays 3:22 across 3 deaths (reused, not a new
3:32); each Reset logs alive=1, 20 zones healed, 33 subsystems reset; A sees ONE
mech (no wreck+new pair). Warp fires each respawn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:13:44 -05:00
arcattackandClaude Opus 4.8 efa7aeb683 KB: retract the GPU-contention claim -- the -net node glitch is MP-path-specific (two solo instances both clean)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 22:21:35 -05:00
arcattackandClaude Opus 4.8 6ac960c883 KB: one-box 2-instance render glitch is a two-D3D9-apps-on-one-GPU artifact (solo is clean)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 22:14:40 -05:00
arcattackandClaude Opus 4.8 f0535356bc Warp: decouple from player SimulationState (fix camera/targeting regressions)
Pulsing the player's SimulationState (DropZoneAcquired->Translocated) to trigger
the translocation sphere regressed everything else that dial drives in our
reconstruction -- the camera flipped to inside-view, targeting/firing gated off,
shadow pass glitched. That dial is load-bearing; co-opting it was wrong.

Replace with a self-contained render one-shot: btplayer.cpp respawn calls
BTStartWarpEffect(dropZoneOrigin); the effect plays its own collapse->expand and
touches nothing but the render. No SetSimulationState, no DropZoneLocation write,
no per-entity renderable walk (the tree's BTTranslocationRenderable objects are
now inert). Scale capped 30/40 (authentic 100/150) so the sphere -- centred on
your own reinsertion -- doesn't envelop the camera; tunable via BT_WARP_SCALE.

Smoke-verified 2-node: warp fires on each respawn (collapse 31->5, expand ->35),
repeats, respawn cycle intact, no crash. Camera/targeting restored (state pulse
gone).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:58:38 -05:00
arcattackandClaude Opus 4.8 63c1c5a460 Implement the translocation sphere -- the "blue warp" respawn effect (task #52)
BTTranslocationRenderable was a no-op stub; now reconstructed over the engine's
POVTranslocateRenderable: loads tsphere.bgf and runs a collapse-on-arrival /
expand-on-reveal sphere (scale 100->1 over 1.3s, then 1->150 over 1.0s, rotating)
keyed on the player's SimulationState dial. Drawn direct from the render loop by
BTDrawTranslocationSpheres (beside BTDrawBeams, PASS_ALPHABLEND) -- the same
accommodation the beams/reticle use.

The asset loads by FILENAME (tsphere.bgf), not through the RES table -- which is
why every resource-name search missed the effect for three rounds.

Trigger wiring: btl4vid.cpp MakeEntityRenderables builds the sphere for the LOCAL
player (the authentic wiring builds it only for replicants + a POV fade for self,
but peer player-attribute replication isn't wired -- on a replicant SimulationState/
DropZoneLocation read uninitialised). btplayer.cpp pulses SimulationState
DropZoneAcquired->VehicleTranslocated at respawn (a 1.4s flip timer stands in for
the engine's +1s drop-zone re-post) and writes the respawn origin into the
DropZoneLocation attribute.

Verified 2-node (BT_TLOC_LOG): on respawn the sphere collapses (~100->5) then
expands (->150) at the valid drop-zone origin, deduped to <=2 active, respawn
cycle un-regressed, no render errors. Visual appearance (colour/size) still needs
a live look; the authentic see-others'-spheres path needs player replication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:41:57 -05:00
arcattackandClaude Opus 4.8 6cd5f0b940 KB CORRECTION: the blue whirlwind IS in the game — the translocation sphere (tsphere.bgf)
My earlier 'not a shipped asset' verdict was wrong. The effect is POVTranslocateRenderable
(engine) / BTTranslocationRenderable (our port), which loads tsphere.bgf BY FILENAME (not via
the RES table, so every resource-name search missed it) and runs a collapse-on-death /
expand-on-respawn sphere keyed on the SimulationState dial. Wiring already exists in
btl4vid.cpp; the renderable is a no-op stub in btstubs.cpp. That's why it never shows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:15:48 -05:00
arcattackandClaude Opus 4.8 ef961a13c9 KB: deeper whirlwind sweep — string table, class registry, effect/camera systems all negative
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:43:42 -05:00
arcattackandClaude Opus 4.8 9abc839a43 KB: archive-check verdict — no whirlwind/warp recovery asset in REL410
Searched the full pod install (nick-games/tesla4pod_extracted REL410/BT;
BTL4.RES byte-identical to ours) and the whole nick-games archive: no
whirl/warp/tunnel/vortex asset by filename or RES resource name, no such
psfx in BTDPL.INI. The decomp drop path (DropZoneReply FUN_004bffd0) fires
no effect — only Mech::Reset + the level-2 "translocated" cockpit alarm.
BTPLAYER source is absent from all archives (binary-only). RP411 analog
uses InitialStasisFlag (sim gate, no visual). Verdict: the pod build's
respawn is mech-redrop + translocated alarm; the "blue whirlwind" is not
a shipped REL410 asset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:35:56 -05:00
arcattackandClaude Opus 4.8 83b3f31957 Whirlwind respawn: authentic death->drop-zone->recreate cycle (task #52)
Death transition (mech4) now dispatches VehicleDead(-1) to the owning
player; BTPlayer::VehicleDeadMessageHandler restructured to the authentic
@004c05c4 three-way branch: -1 = death bookkeeping + sever playerVehicle
(wreck stays) + 5s re-post; >=0 = engine drop-zone hunt -> DropZoneReply
-> CreatePlayerVehicle (new mech); -2 = the acquire probe. Guarded on a
DropZones group so a zone-less mission stays dead instead of aborting.

Three latent bugs the respawn path exposed, all fixed:
- IsMechDestroyed latched on graphicAlarm>=9 alone; a later leg hit on the
  wreck rewrites the alarm to 4/3, un-latching -> the death transition
  re-ran (double kill/score, abort). Now latches on movementMode 2||9.
- Score handlers dereferenced the severed playerVehicle during the dead
  window; guarded.
- The console score flush routed a NetworkClient::Message through the
  player's Entity::Dispatch, which stamps entityID past the smaller struct
  -> /RTC1 stack overflow. Sent via application->SendMessage instead.
- Renderer LoadMission re-entry (per viewpoint-make) re-read the env INI;
  its light block Fail'd on stale sceneLightCount. Reset it in
  DPLReadEnvironment so the respawn's second read is clean.

Verified 2-node self-drive: B killed repeatedly by A respawns each time
(wreck stays), ticks + reloads + takes fresh damage on the new mech, no
abort/hang across multiple death->respawn cycles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:26:55 -05:00
arcattackandClaude Fable 5 e430e474cb Death gate: a destroyed mech takes no pilot input (ghost-drive fix)
User report from 2-window play: after dying, the pilot could still steer the
wreck around invisibly.  Zero throttle/turn/fire and the forced harnesses at
the gBTDrive source when IsMechDestroyed() -- deliberately INPUT-only, so the
gait/animation machinery keeps running and the death collapse plays out.

Also recorded in the KB: the killer's screen shows the wreck SWAP but not the
death SINK (needs the Mech-level death/knockdown update records, disasm
preserved from task #51), and the respawn-cycle scouting (type-12 intro
scripts are binary ref-records; adrop; DropZone + Mech::Reset machinery) for
the recovery-whirlwind reconstruction (task #52).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:18:11 -05:00
arcattackandClaude Fable 5 29035028fd Cross-pod beams: replicate emitter discharge via subsystem update records
User report: lasers only visible on the window firing them.  The peer's
replicant emitters never learned the master fired.

THE AUTHENTIC PIPE (decomp-verified):
- FUN_0041c350 (the "beam keepalive" ServiceDischarge/ContinueDischarge call)
  does TWO things: queue the LOCAL deferred beam-effect callback (@0x4bac0c)
  on the app+0x34 manager -- our per-weapon render walk already plays that
  role -- and set the subsystem DIRTY bit, which maps to the 2007 engine's
  updateModel / ForceUpdate().
- Replication rides SUBSYSTEM UPDATE RECORDS inside the mech's update message:
  the roster walk already hands the entity's stream to every subsystem's
  PerformAndWatch; Simulation::WriteSimulationUpdate serializes each requested
  updateModel bit; Entity::UpdateMessageHandler routes received records by
  subsystemID to the subsystem's ReadUpdateRecord.  All engine machinery --
  the missing pieces were the Emitter's serialize/apply pair + the triggers.

CORRECTIONS to the dormant task-33-era transcriptions (never exercised --
nothing ever set updateModel -- so the latent misreads never surfaced):
- The weapon-family VTABLE SLOT MAP was swapped: slot 6 = ReadUpdateRecord,
  7 = WriteUpdateRecord, 9 = TakeDamage (evidence: Mech hierarchy symmetry +
  body semantics; @004ba568 resolves an EntityID at rec+0x30 through the
  entity index -- record semantics, not Damage).  Renamed across MechWeapon /
  Emitter / ProjectileWeapon; the real Emitter::TakeDamage @004bafc8 is
  undecoded (inherits MechWeapon for now).
- Emitter/MechWeapon Write: `*record = 0x38/0x18` is the record LENGTH, not
  recordID; rec+0x30 is the TARGET's EntityID (GetEntityID()), not a colour --
  the old `CopyColor(targetEntity+0x184)` was also a databinding trap.
- OVERRIDE-SIGNATURE TRAP: the decls used each class's own shadowing
  UpdateRecord typedef as the param type, silently NOT overriding the engine
  virtual (the base ran instead; nothing would ever have serialized).
  Base-typed params (Simulation__UpdateRecord*), casts inside.
- Emitter::ReadUpdateRecord reconstructed (@004ba568): target EntityID resolve
  (drop unknown non-null targets), MechWeapon alarm apply chain, beam fields.
- ServiceDischarge/ContinueDischarge: ForceUpdate() per keepalive tick + one
  final record at beam end (turns the peer's beam off).
- Mech::DrawWeaponBeams extracted from the player-only drive block so the walk
  runs for REPLICANTS (+ per-mech gun-port cache -- the process-wide statics
  would have served the player's segment pointers as the replicant's muzzles).

VERIFIED 2-node: A fires 57 volleys -> 225 emitter records -> B applies all
225 -> B draws 414 beams (PPC blue / laser red, from A's replicant's own gun
ports).  Solo un-regressed (150 beams, kill chain, no crash).

Also preserved: the full Mech::WriteUpdateRecord @0x4a0c2c recovery
(reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) with all 9 record
types decoded (pose/alarm/leg-state+heat with the body-channel write-through
re-sync, knockdown, death, impact, movementMode) -- transcription deferred;
it replicates remote knockdown/death/heat and was not needed for beams.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:04:34 -05:00
arcattackandClaude Fable 5 a110041a70 Fix "can't turn in MP": pilotArray[1] overrun stomped controlMode
User report from the first live 2-window session: throttle worked, A/D turn
did nothing.  [mppr] showed the input REACHING the mapper (stickX=-1) but
turnDemand=0 and controlMode = pointer garbage (73583232 / 217478632 --
different per instance), from frame 1, MP only (solo mode=0).

Hunted with a cdb hardware write-breakpoint (ba w4 armed at ctor time on
&controlMode, compiled offset +0x128 from the [mode-ctor] probe).  Caught the
writers red-handed: MechControlsMapper::BuildPilotArray / FillPilotArray.

ROOT CAUSE -- a shrunk-span array: the binary reserves a FIXED 10-slot pilot
block @0x15C..0x183 ((pilotArrayBuilt@0x184 - 0x15C)/4 = 10); the recon
declared `Pilot *pilotArray[1]` ("variable length"), so every write past
slot 0 stomps the members declared after it.  In MP, FillPilotArray wrote the
PEER's Player pointer into slot 1 = over controlMode -> the turn shaping
dispatched on pointer garbage -> turnDemand 0.  MASKED in solo: the zero-loop
overrun wrote 0 there, and 0 == BasicMode.  This was ALSO the real cause of
task #48's "turnDemand zeroes out in -net" observation (worked around then
with the direct goto-turn; attribution corrected in the KB).

Fix: pilotArray sized to the binary's own 10-slot span; ctor zeroes all
slots; BuildPilotArray clamps pilotCount to capacity (local + 9 remotes; an
8-pod game fits).  Diagnostics kept (BT_MODE_LOG ctor probe; [mppr] line now
prints mapper/&mode).

VERIFIED: MP session holds mode=0 throughout, turnDemand flows nonzero, the
mech turns (heading -1.33) and closes on the peer; solo kill chain intact.
KB: new "shrunk-span array" gotcha (reconstruction-gotchas 5) with the cdb
ba-w4 recipe; multiplayer.md task-#48 note corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:16:46 -05:00