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: 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>
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>
The engine Reticle struct (the mech's "TargetReticle" attribute) is the
acquisition chain: slewable screen-space crosshair -> pick-ray -> the
entity + damage zone under it become mech+0x388/0x38c, the intersection
point mech+0x37c. Convergence-on-lock is authentic (FireWeapon has no
aim test); targetWithinRange = dist < (1-damage) x weaponRange
(FUN_004b9bdc). Port acquisition is a bring-up stand-in (hardwired lock
+ BT_FIRE_ARC cone) -> tracked as the next combat-fidelity step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user was right: a dead mech turns into a pile of wreckage. The authentic
1995 chain, recovered end-to-end:
kill -> the victim's per-mech DEATH ModelList ('blhdead'/'lokdead'/'owndead'/
'thrdead', .RES ids 22-25) -> its authored effects: 104 (the WRECK script) +
1007 (dnboom big explosion) + 1001 (ddthsmk rubble smoke plume) + a damage-
smoke burst (3/4/5/15).
Effect 104 = ExplosionScripts case 4 (part_008.c:2663, LIVE in the 1996 binary;
the "disabled" warning is case 6): loads the destroyed hulk + flamesml/flamebig
flame meshes with sweep flicker. Every mech ships its hulk (BLHDBR/MADDBR/
LOKDBR/... + GENDBR generic); the 1996 script hardcoded thrdbr.bgf (dev
shortcut) -- we use the victim's own.
Reconstructed as BTL4VideoRenderer::SwapToWreck: hide every segment mesh, hang
"<prefix>dbr.bgf" on the tree root (pending-swap if death precedes tree build);
routed from the engine's ExplosionClassID dispatch (effect 104 ->
BTSwapMechToWreck(explosion->GetEntityHit())). The kill now fires the
authentic 'blhdead' resource (manual 7+1 pfx calls removed -- the list carries
1007/1001 itself). Verified live:
[death] firing authentic death list 'blhdead' id=22
** effect_number = 104
[BTrender] wreck swap: victim -> 'blhdbr.bgf'
Follow-ups noted: mesh flames + hulk settle (cosmetic), DeathSplash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task #32 (death collapse animation) resolved by decomp evidence -- BT 4.11 has
NO collapse animation; the movementMode 5-8 fall latch (clips 0x1c-0x1f) is an
engine-lineage vestige:
(1) the clip-table loader FUN_004a80d4 fills slots 0x00-0x1b + 0x20 (bmp
knockdown) and returns -- slots 0x1c-0x1f are never written;
(2) mech+0x63c..0x648 appear in NO exported function;
(3) no fall clip exists in the shipped 27-clip set;
(4) firing the latch would bind resource id 0 -- a StaticAudioStream -- as
keyframes.
Authentic death modes are the FREEZE modes (IsDestroyed == mode 2||9), so
UpdateDeathState now settles straight to 9 (was a [T3] mode-5 guess that would
trip the garbage latch in the binary). The death READ = freeze + dnboom +
ddthsmk smoke plume + destroyed skins + shutdown; the wreck stands.
Also: damage-band effects orient toward the ATTACKER (impact frame) via
lastInflictingID -- which was declared but never written (recon gap); now
maintained by Mech::TakeDamageMessageHandler, unblocking the DamageZone LOD
same-attacker redirect too.
KB corrected + proofs recorded (combat-damage "Death SEQUENCE", open-questions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'can't kill the enemy / no visible damage' cluster, root-caused and fixed
faithfully:
- STEP-6 unaimed path was INERT: the cylinder table was 'cached' at Wword(0x111)
-- the recon ABSORBER bank (stores nothing, reads 0) -- so every unaimed hit
silently no-op'd. Promoted to the named member Mech::damageLookupTable
(binary this[0x111], was mislabeled ammoExpended). New gotcha class recorded
(reconstruction-gotchas §2) + sweep; 2 dead multiplayer branches logged.
- Fire path migrated off the stale vital-zone aim onto the completed STEP-6
unaimed dispatch (zone=-1 + beam entry point -> cylinder resolves the
exterior zone). No more invisible 1-shot kills; death via the authentic
cascade (~14 center-mass hits). Wreck stays TARGETED on kill (beams stop on
it); scoring latches off.
- SendSubsystemDamage AV fixed: unbound critical-subsystem plug guard (43
unbound plugs/mech logged as an open question -- the binding itself is a gap).
- RemakeEntity (render damage swap): the 1996 render state machine's missing
Remake state, reconstructed as an in-place SetDrawObj mesh swap keyed by each
segment's damage-zone graphic state (tree dtor doesn't cascade -> never
rebuild). Destroyed arms/guns visibly wreck (the only variants the RES
registers).
- BT .PFX particle layer (L4VIDEO.cpp): the 1995 explosion/damage effect layer,
unported since 2007 (DPLIndependantEffect/ReadPSFX/ExplosionScripts all
stubs). Parses the authentic VIDEO/*.PFX definitions via the [pfx_day]
psfxN mapping; premultiplied blending renders BOTH families from the same
data (additive-style fire + occluding smoke -- DDAM2 is 30% grey, DDTHSMK
ramps negative: impossible additively); depth-sorted billboards with a
radial-masked grit sprite; impact-frame orientation (.PFX offsets are
authored mech-local, -Z = out of the struck armor toward the shooter) for
weapon hits AND damage bands (via lastInflictingID, now maintained -- was
declared but never written). Both effect-number encodings route (raw dpl
<100 + WinTesla 1000+slot carried by the band resources). Death fires the
authentic dnboom (7) + ddthsmk smoke plume (1).
- Effects anchor at the impact point / damaged zone's segment, not the mech
origin (no more fire at the feet).
- Dev force-input gates BT_AUTOFIRE / BT_AUTODRIVE for headless fire-chain
verification; BT_PFX_ADD=1 flips the particle blend for A/B.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconstructs the mech DEATH state machine — the un-exported master-perf death
branch (region 0x4a9770-0x4ab188) — from its EXPORTED consumers + the RP
VTV::DeathShutdown analog, wired into the active path (the bring-up drive
override bypasses the authentic Simulate where this normally lives).
Mech::UpdateDeathState() + IsMechDestroyed() (mech4.cpp), called for every mech
early in PerformAndWatch: on a vital kill (graphicAlarm >= 9, raised by the
damage side) it
1. sets movementMode = 5 -> the collapse clip's one-shot latch in
AdvanceBodyAnimation (fall direction 5-8 is un-exported -> 5 [T3]);
2. loops the roster calling Subsystem::DeathShutdown(1) (RP VTV::DeathShutdown
analog; the base is a no-op virtual, overrides act -- a SHUTDOWN not a
teardown, so it frees nothing and never removes the entity: the wreck STAYS);
3. next frame settles to movementMode = 9 -> IsDisabled -> locomotion frozen.
The drive's lone `movementMode = 1` write (mech4.cpp ~1309) is guarded on
!IsMechDestroyed so the death state is not clobbered back to a live gait.
Runtime-verified (forced kill, BT_DEATH_LOG=1):
[death] mech destroyed -> collapse + subsystem shutdown (wreck stays)
[death] mech settled -> disabled (IsDisabled=1, frozen wreck)
mech frozen in place, subsystem tick + renderer keep running, no crash, wreck
stays.
Also records the full death-sequence decomp map in combat-damage.md (the
exported consumers: AdvanceLeg/BodyAnimation collapse latch, IsDestroyed, the
MechDeathHandler effect engine; and the un-exported orchestration gap).
Deferred (honest): the visible collapse ANIMATION latch through the active path
(gait SM shows state=0 post-death; spawned mechs don't advance body anim);
MechDeathHandler (FUN_0042a984/FUN_0042aa2c -- the exported per-subsystem
destroyed-skin + explosion engine, still a stub) for death explosions; the
whole-mech DeathSplash radius damage (un-exported).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Mech per-impact hit-location resolver (the cylinder damage table) is now
functional, wired, and runtime-verified [T2]. Unaimed (zone==-1) hits — the
collision-damage path — now resolve an impact point to a damage zone via the
authentic height x angle grid + weighted dice roll, instead of dropping.
dmgtable.cpp/.hpp was a non-functional skeleton on no-op ReconTable/stream
shims; backed it with real std::vector storage and fixed 5 latent runtime bugs:
- ReadEntries now consumes the leading cell name-string ([i32 len][len+1])
- PieSlice ctor reads rotateWithTorso into the correct member
- SelectSlice direct-indexes (was int lookup on a float-keyed table)
- ResolveHit returns the zone (chains SelectSlice -> SelectZone)
- real MemoryStream::ReadBytes (was a variadic no-op)
mech.cpp ctor: replaced the empty-name StandingAnimation stub with the real
load — FindResourceDescription(dzRes->resourceName, type 0x1d) -> stream ->
new DamageLookupTable, cached at mech[0x111]; ~Mech deletes it.
Mech::TakeDamageMessageHandler override registered (MESSAGE_ENTRY overlays
Entity's by ID): on invalidDamageZone, resolve via the table then base-route;
aimed reticle hits pass through unchanged.
Three named accessors (no databinding-trap raw reads): WorldToLocal
(localToWorld.MultiplyByInverse), CylinderReferenceHeight (standingTemplateMaxY
== collisionTemplate->maxY == binary mech+0x2ec[+0xc]), TorsoHeading via a
BTGetTorsoTwist bridge in torso.cpp (Torso::CurrentTwist == torso+0x1d8;
torso.hpp cannot be included into mech.cpp — subsystem-stub collision).
Stream format + geometry + roll + handler were all byte-verified against the
shipped BTL4.RES type-29 resources (18 tables, exact consumption) and the
disassembly (FUN_0049eb54/e678/de14, glue 0x49ed0c, handler @0x4a037a).
Runtime: boots clean, "[cyl] table 'bhk1' layers=7" (exact byte-verified layer
count, found by name), mech spawns + walks, no asserts/AV/0xCDCDCDCD. Env gate
BT_CYL_LOG=1. Unblocks collision-damage application.
KB updated (combat-damage.md STEP 6 COMPLETE, open-questions.md marked done).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cylinder table resource = type 29 DamageLookupTableStream, 18 in BTL4.RES (one per mech) ->
STEP 6 is FEASIBLE, not content-blocked. Name copied from a sibling resource (local_130+0xc).
RE phase complete; build (containers + handler + load + wire) is the next phase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
De-risk + format-reversal pass complete. The CylinderDamageZoneTable is a passive nested-list
height x angle grid: TABLE (FUN_0049ea48) -> ROWS-by-height (FUN_0049e740, cell key i*2pi/count,
_DAT_0049e810=6.2831855) -> CELLS-by-angle (FUN_0049deb0) -> zone. All 3 vtables minimal (dtor +
2 Node slots, NO lookup method) -> the lookup is entirely in the unexported
Mech::TakeDamageMessageHandler (handler-set entry, not virtual) = the disassembly target.
Full ctor/dtor/vtable address map + the 4-step build plan recorded in combat-damage.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the 'correct errors' convention: the cited FUN_004a0230/FUN_0049ed0c don't exist. The real
CylinderDamageZoneTable = a list of 0x30-byte entries (FUN_0049e740) from resource 0x1d; the recon
builds the table (FUN_0049ea48, mislabeled StandingAnimation @Mech[0x111]) with an EMPTY name -> 0
entries -> no-op. The lookup + Mech::TakeDamageMessageHandler override are NOT in the exported decomp
(need disassembly). Recorded in combat-damage + open-questions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>