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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
A peer's replicant mech slid around as a frozen statue: DeadReckon moved
localOrigin but nothing animated the legs. Reconstruct the replicated-motion
display loop: after DeadReckon, derive a SIGNED speed demand from the
replicated velocity (the master publishes worldLinearVelocity in every update
record, Mover::WriteUpdateRecord MOVER.cpp:759; the replicant stores it in
updateVelocity.linearMotion :712; sign = forward dot against the -Z facing),
write it to controlsMapper->speedDemand (the LEG channel's live demand
source), and advance the LEG state machine for its JOINT writes only --
travel stays with DeadReckon, so position always follows the master and the
clip cadence matches the replicated speed.
Two replicant-only initialization holes found live (the master's equivalents
are primed per-frame in the PLAYER-only drive block):
- globalTimeScale/idleStrideScale read 0 -> clip advance time dt*0, the leg
machine engaged (state 11) but the clip froze (legCycle stuck 0);
- reverseSpeedMax2 (the run-cycle rise-clamp @0x7a0; LoadLocomotionClips does
NOT set it) read debug-heap garbage -> legCycleSpeed clamped to -4.3e8 on
entering run state 13. Primed both; legCycleSpeed sane-banded once.
VERIFIED 2-node (BT_GOTO driver + BT_REPL_LOG observer): the replicant runs
the full gait lifecycle -- stand -> walk(5,7) -> run(10,12), legCycle 7.5->34
tracking the master's speed, wind-down(8), Standing(0) when the master stops.
Solo un-regressed (goto->aim->kill chain intact, no crash).
projectedVelocity source-swap (the binary publishes the BODY channel's
smoothed projection instead of the actual travel velocity) evaluated and
DEFERRED: it risks a verified working replication feed for a smoothing
nuance; recorded in multiplayer.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report: the ground shadow painted OVER the mech's feet. Root cause: the
flat quad + big depth-bias (-0.004 ~= 2-4 world units) workaround for slope
burial -- a bias big enough to beat the TERRAIN also beat the FEET in the
depth test, and the shadow drew in the late alpha-blend pass (after the mech),
so z-arbitration was the only layering control.
Fix, four co-dependent parts (user-verified live: feet on top, opaque,
survives inclines/declines):
- TILT (mech.cpp UpdateShadowJoint): apply the SKL's own contract for
jointshadow ("apply terrain angle to pitch and roll") -- quad up aligned to
the surface normal via an orthonormal basis fed to the ENGINE's own
LinearMatrix->EulerAngles conversion. Hand-derived Euler signs are exactly
how the prior tilt attempts "dug into the hillside"; the engine conversion
is convention-proof. ~35 deg cliff-guard cap.
- SAMPLER (mech4.cpp): surface gradient from the collision probes PLUS
BTVisualGroundLift per probe -- the quad hugs the VISIBLE terrain, whose
lift varies across a slope; world->local rotation by the TRUE yaw from
localToWorld, not the drift-prone gDriveHeading mirror (the task-#48 bug
class). BT_SHADOW_LOG traces the normals.
- DRAW ORDER (new PASS_SHADOW, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw
between the STATIC terrain and the DYNAMIC opaque bodies -- the classic
decal order. The mech, drawn after, z-passes over the shadow: the bias
can never paint the shadow over the feet, structurally.
- BIAS pairing (L4D3D.cpp): tilt on (default) -> decal epsilon -0.0008;
BT_SHADOW_TILT=0 flat fallback -> the old -0.004. BT_SHADOW_BIAS overrides.
KB (locomotion.md): the four-part arrangement recorded as co-dependent, with
the gait-desync-symptom diagnostic hint retained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User-reported recurrence of "legs stutter and lose sync". Live [sync] showed
advSum/legSum drifting 5->92 during walk-up churn, then locking. Reproduced
headlessly (BT_FORCE_OSC throttle feathering + BT_FORCE_TURN + the new
per-frame BT_SYNC_LOG probe) and root-caused in two parts:
- SEED: the bring-up turn-in-place trigger arms only the LEG channel, so the
walk re-entry lands one frame apart -> a persistent clip phase offset.
- AMPLIFIER: with offset clips the two end-of-clip callbacks fire on different
frames; a demand change landing between them picks DIFFERENT next states
(one channel winds down, the other keeps walking) -> opposite-phase churn
(observed bs=6 ls=7 -- body on walk-R, leg on walk-L).
Under v4 (display+travel = BODY) the out-of-phase LEG pose showed through on
every frame the body didn't write joints (Standing/wind-down) = the visible
stutter/pop.
DISASM GROUND TRUTH (neither Advance fn has a static decomp caller; found by
byte-scanning the CODE section for e8 calls):
- master perf (0x4a9b5c gap) -> AdvanceLegAnimation @0x4aa399 (air @0x4aa388)
-> -dist/dt into localVelocity (+0x1cc): the LEG drives LOCAL travel and
(writing last) the displayed pose;
- IntegrateMotion (0x4ab1c8, body advance @0x4ab312, caller FUN_004ab430 =
the projected-origin updater) -> -dist/dt into projectedVelocity (+0x2a0):
the BODY is the dead-reckoning PROJECTOR -- locally invisible.
v4's reading of FUN_004ab430 as the travel source was wrong.
FIX: advance body FIRST (projection; its writes get overwritten), leg LAST
(displayed pose); travel = legAdv under the two-channel split. Display ==
travel through the LEG by construction -- body-channel drift can no longer
become visible, structurally (not by input-symmetry luck).
Verified: trn+feathering repro clean; straight drive locked (adv==proj);
solo goto=enemy kill chain intact; MP 2-node drive-to-range cross-pod kill
intact (241 hits, DESTROYED); no crashes. KB corrected (locomotion.md roles
+ P3_LOCOMOTION.md v5 entry). New gated instruments: BT_SYNC_LOG,
BT_FORCE_OSC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BT_GOTO beeline never actually closed on a target -- three separate faults:
1. It was gated behind `if(gBTDrive.forced)`, and `forced` was set ONLY by
BT_AUTODRIVE -- a chicken-and-egg where the block that would drive was gated
on the drive already being on. So BT_GOTO alone just turned in place (the
"stall"). Fix: BT_GOTO enables forced mode itself AND supplies its own
forward throttle (a beeline must drive, not only steer), cutting the throttle
inside a stop radius (weapon range for "enemy", so it holds + shoots).
2. Steering used the gDriveHeading SCALAR MIRROR (seeded to 0), but MP pilots
spawn facing a non-zero heading -> it steered the WRONG way. Fix: heading
error from the mech's ACTUAL forward (localToWorld's -Z axis), signed angle
to the target.
3. The goto turn was routed through controlsMapper->turnDemand, which zeroes out
in -net mode (the mapper key-bridge only shapes the local viewpoint mech
there) -- the heading froze and A drove straight past B. Fix: apply the goto
turn to the orientation integration DIRECTLY (if gBTGotoActive, turn=
gBTGotoTurn), after the mapper read.
Verified 2-node one-box: A with BT_GOTO="enemy" ALONE (no autodrive) drives
dist 1673->231m, holds at weapon range, mechPicks=59, autofire -> B hdlr=192,
DESTROYED -- a fully automated human-style cross-pod KILL via the real beam
path (not the FORCE_DMG hook). Solo un-regressed (plain autodrive still drives;
goto="enemy" resolves the enemy, mechPicks=59, no crash). BT_GOTO_LOG traces
the beeline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The aim/pick ray was the active RENDER EYE's view direction, which made a
mech unable to aim at a distant enemy:
(a) it inherited the mech BODY pitch -- on a slope the body tilts ~8 deg to
conform to terrain, so the boresight pointed that far DOWN and the pick
ray hit the ground ~50-100m short of the target (groundPicks, never
mechPicks); and
(b) in CHASE view (the default) it was the elevated behind-the-shoulder
camera, so the ray started over the mech's shoulder and flew high.
Fix (L4VIDRND DPLEyeRenderable::Execute, the BTSetAimCamera publish):
- LEVEL the boresight: drop the view-direction pitch and rebuild an upright
basis (world +Y up) -- the guns fire along the mech's gyro-stabilized
HORIZONTAL heading, not the terrain-pitched body. Reticle X still carries
the torso twist (BTTwistToReticleX); reticle Y any elevation.
- ANCHOR the ray origin at the mech's TORSO (myEntity->localToWorld
translation + ~5) instead of the render eye, so it is correct in BOTH the
chase (default) and cockpit views.
Verified end-to-end: solo, enemy 120m ahead, BOTH chase and cockpit view ->
[target] "MECH under boresight ... mechPicks=59 groundPicks=0" -> beam ->
*** DESTROYED. Also proved PickRayHit resolves a REPLICANT when aimed at it
(hit=1 at 757m, 2-node run).
mech4: BT_GOTO="enemy" test aid now chases the nearest peer of ANY instance
(solo dummy OR replicant), not just replicants.
Still open (locomotion, not aim/MP): BT_GOTO/BT_AUTODRIVE stall short of the
enemy, so an AUTOMATED 2-node kill via the real beam isn't demonstrated yet
(the FORCE_DMG hook still is). A human can now aim correctly in the default
view. Solo un-regressed; build clean; KB validator clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-measured interactive -net combat on a 2-node run. The KB's "aim ray dead /
drive dead in -net" was STALE: A's aim ray is live (noRay=0, the active eye
publishes the aim camera) and A drives (BT_AUTODRIVE/BT_GOTO move it). The real
blocker to a human cross-pod kill via the beam path is NOT the netcode:
- Boresight inherits the mech BODY pitch. The aim ray = the active eye's view
dir; on sloped terrain the body pitches to conform and the boresight points
~8 deg down ([EYE] up=(-.03,.99,-.14) == [pick] ldir.y=-0.145 every frame),
so at range the ray hits terrain ~50-100m short of the enemy (mechPicks=0).
- Drive/goto stalls ~700m short before closing to pick/weapon range.
- PickRayHit vs a replicant is thus unconfirmed, but the replicant localToWorld
IS correct (lstart magnitude == true A->B distance; the DeadReckon +
localToWorld=localOrigin block already tracks the replicated position).
Change: add a gated BT_GOTO="enemy" test mode (beeline toward the nearest live
replicant) -- MP spawn coords vary per run so a fixed "x z" can't reliably face
the peer. KB (multiplayer.md) updated with the corrected findings + the real
remaining work (torso-pitch/level-boresight aim ray + drive-to-range).
Solo un-regressed; btl4.exe builds; KB validator clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full cross-pod KILL now works: instance A (host 2) shoots B's mech (host 3)
dead over the network; B runs the death transition on its own screen. Verified
on a 2-node one-box run (BT_MP_FORCE_DMG on A): B's mech takes ~36 remote hits
of 12 and is DESTROYED, with wreck swap + explosion.
ROOT CAUSE (one bug): B's own MASTER mech was INVALID. Entity::Receive(Event*)
does `if(!IsValid()) event->Defer()` -- so a cross-pod-delivered TakeDamage that
arrived, resolved to B's real mech, and Posted, was DEFERRED forever; the handler
never ran. The reconstructed Mech::Make force-set ValidFlag only for replicants
(the P6 bring-up hack for the partial MakeReady handshake); a locally-created
master never got validated.
FIX (one line): Mech::Make now sets ValidFlag for the master too -- the
reconstructed ctor builds the mech synchronously, so it's safe. Nothing else
changed: targeting, the virtual victim->Dispatch, the replicant reroute, the
wire, receive, and id resolution were ALL already correct.
Two prior root causes were BOTH mis-diagnoses from the WRONG messageID:
Entity::TakeDamageMessageID is 18, not 21. Every earlier probe filtered
msgID==21, never matched a real TakeDamage, and mis-correlated the stray 21
hits' 3:19 / classID-48 against the real 3:22 hit -- inventing (a) a "cross-TU
Entity layout divergence" (disproven: offsetof identical in both TUs, sizeof
444, entityID@0x17C) and (b) a "localID corrupted on the wire". Both FALSE; the
id is correct end-to-end (3:22 = B's master).
- game/reconstructed/mech.cpp: validate master + replicant at Make.
- game/reconstructed/mech4.cpp: BT_MP_FORCE_DMG cleaned to a concise gated
2-node cross-pod reproducer (real virtual Dispatch path).
- context/multiplayer.md, reconstruction-gotchas.md: corrected root cause +
the validity-defers-cross-pod gotcha. Engine diagnostics reverted to clean.
Solo un-regressed (the solo enemy was already force-valid; the change only ADDS
ValidFlag). btl4.exe builds; full mission loop clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prior commit (c78662a) filed cross-pod damage failure under a
"cross-TU Entity/EntityID layout divergence" (and the P5 base-region
audit). A compile-time offsetof probe injected into BOTH translation
units disproves that: game/reconstructed AND engine/MUNGA compute an
IDENTICAL Entity layout --
sizeof(Entity)=444 offsetof(entityID)=380 (0x17C)
offsetof(ownerID)=388 offsetof(simulationFlags)=32
offsetof(Mech,entityID)=380 (Entity subobject at 0)
So Entity::Dispatch reads this->entityID at the SAME offset mech4's
GetEntityID() does -- there is no per-TU read difference, and the
logged 3:22 (mech4 candidate) vs 3:19 (engine Dispatch) cannot be one
object read two ways. Those lines were different objects/messages,
mis-correlated. The distinct, REAL P5 "base-region layout divergence"
(HARD_PROBLEMS.md) is about raw-offset stomps at this+0x2d4..0x2f0 --
far above entityID -- and is unrelated to #47.
Changes:
- Revert the committed BT_MP_NET engine diagnostics (ENTITY/EVENT/
NTTMGR/RECEIVER/L4NET) to the clean a9c3e96 baseline -- those are
the very instruments that produced the mis-correlated data.
- mech4 BT_MP_FORCE_DMG hook: dispatch via the real m->Dispatch(&td)
path (no false id stamping); comment records the offsetof finding.
- context/multiplayer.md: layout-divergence RULED OUT; 3:22/3:19 marked
unconfirmed; leading hypotheses reframed as H2 (wire host-relative
(de)serialization) vs H3 (replicant id != master's registered key),
to be distinguished by a 2-node run with per-message correlation.
Solo un-regressed (mech walks + targets, 0 faults). Cross-pod delivery
remains open, but the investigation is redirected off the wrong (large,
structural) layout-audit path onto the EntityID wire/id-assignment path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Traced the dispatched-message delivery end to end with BT_MP_NET /
BT_MP_FORCE_DMG. Everything works except one wire step:
- A's Entity::Dispatch reroutes the replicant's TakeDamage
(application->SendMessage(ownerID=3, EntityManager, msg)); POST-Dispatch
the message carries entityID=3:22 (host:local) -- the replicant's own
ID, matching B's master (GetEntityID()==3:22). VERIFIED sent.
- On B the message ARRIVES, GetEntityPointer finds an entity, Posts it,
the event drains (ProcessEventTask = ProcessOneEvent(0)), Event::Process
runs, Receive finds+calls a handler. VERIFIED the full deliver chain.
- BUG: B receives entityID=3:19, NOT 3:22 -- the localID dropped by
exactly the hostID (3) between A's send and B's receive. So
GetEntityPointer(3:19) returns the WRONG entity (classID 48, not the
mech 0xBB9), whose base handler ignores the unaimed hit -> 0 damage.
Auto-replicated UPDATE records (msgID 18) arrive with the correct 3:22
and find the mech, so the corruption is specific to the dispatched-
message wire path/direction.
Next: the host-relative EntityID (de)serialization on the dispatched-msg
path (RoutePacket / packet EntityID encoding) -- diff vs the update path
which translates correctly. Diagnostics retained (all BT_MP_NET-gated,
off in solo -- verified: solo 22 hits, 0 probe noise).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
STEP 1 -- target any peer mech: DONE + verified. A live-mech registry
(BTRegisterMech/BTDeregisterMech from the Mech ctor/dtor) collects
every mech -- player, dummy, and peer replicants. The boresight
world-pick walks BTGetTargetCandidates (all mechs != shooter, closest
ray hit) and the whole fire/damage block retargets from the solo
gEnemyMech to the picked hotTarget; missile/projectile validation
generalised via BTIsRegisteredMech. Verified one-box: instance A
enumerates B's mech as a live ReplicantInstance (20 zones, ownerID=3).
Solo un-regressed (pick->damage->kill clean, 28 hits + DESTROYED).
STEP 2 -- cross-pod damage: dispatch + engine reroute are CORRECT and
invoked with a valid owner, but the dispatched message doesn't reach
the master. Entity::Dispatch (ENTITY.cpp:244) reroutes a replicant's
msg via application->SendMessage(ownerID,...); BT_MP_FORCE_DMG proves A
dispatches at B's replicant with ownerID=3, yet B takes 0 STEP-6
damage. Corrects the KB's [T3] "Dispatch already reroutes": the reroute
FIRES; the break is downstream in the transport/delivery of a
dispatched entity-message (update records flow fine -- net-rx works).
Next MP task = that wire delivery. Diagnostics BT_MP_LOG /
BT_MP_FORCE_DMG retained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
- 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>
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>
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>