60ed54e00856f0e66f459d3154fea883f0718dbf
73
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
60ed54e008 |
Auto-target fire model: firing needs a target, NOT a manual lock/pinpoint (task #40)
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> |
||
|
|
a8a56c57c9 |
Targeting correction: reticle = TORSO BORESIGHT, not a free mouse cursor (task #39)
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> |
||
|
|
24ea7f13af |
HUD ironing (task #38): authentic Lock producer + simple-X mode
- 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> |
||
|
|
18d49491b7 |
Reticle Execute @004cdcf0 RECOVERED: every HUD instrument now live (task #37)
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> |
||
|
|
cf9f56044e |
Aim coordinates: route all conversions through the true NDC (fixes the
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> |
||
|
|
9666dc5b5e |
Targeting: lock mirrors the pick per frame (sticky designation removed)
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> |
||
|
|
d78bde066d |
Authentic target acquisition LIVE: reticle slew + pick-ray lock + aimed zone damage (task #36)
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> |
||
|
|
48b17750e5 |
HUD reticle + weapon pips LIVE: dpl2d 2D display-list port (task #35)
- 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> |
||
|
|
1cd8ca1a80 |
HUD phase 1: full dpl2d API recovered + opcode-faithful 2D layer rework
Every dpl2d_ recorder in the binary self-identifies via its debug-name string
(part_010.c 0x487f34..0x4888c0) -- the complete API + opcode map is recorded
in phases/phase-02-dpl2d-reticle.md: point sets (2/3), closed polylines (4/5),
open line strips (6/7), AddPoint (8), AddCircle (9), SetColor (0xF),
Set/ConcatMatrix (0x10/0x11, 2x3 affine), Push/PopState (0x12/0x13),
SetLineWidth (0x15), CallDisplayList (nested glyphs), FullScreenClipRegion.
dpl2d.cpp reworked to that model: command-stream recorder (open-primitive
vertex runs, state commands, nested list calls) + a recursive ExecuteList
rasteriser (XYZRHW points/strips/loops/circles, 2x3 transform + state stacks,
save/restore). CORRECTION: the old "PushMatrix/MoveTo/PopMatrix" trio was a
misreading of OpenPolypoint/AddPoint/ClosePolypoint ("draw a point") -- kept
as aliases mapping to the true semantics.
Reticle findings recorded: the ctor (@004cc40c) builds ~15 display lists (the
dotted-cross reticle, tick ladders via FUN_004cd938, side arrows, arcs) plus a
3D marker chain and the PNAME1-8.bgf pip meshes (all ship). Next: transcribe
ctor+Execute, wire the 1996 caller's AddWeapon arguments, hook the draw.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
56f02b2176 |
Cockpit view live: V toggle, camera gate, real EyepointRotation, inside skeleton
- 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> |
||
|
|
c2f70f6348 |
Beams CUT on a destroyed target (the real FUN_0049fb54, was a mislabeled stub)
Answers "can I keep targeting the dead mech?": the lock isn't cleared by anything we've found, but the binary REFUSES to sustain energy beams on a destroyed mech: ServiceDischarge (@004ba8d0) and ContinueDischarge (@004baa20) check the beam's target EVERY discharge frame -- IsDerivedFrom(Mech) then Mech::IsDestroyed (FUN_0049fb54 = movementMode 2||9) -- and kill the beam the moment it dies. EmitterSimulation (@004baa88) applies the same check to the OWNER (a dead mech's own weapons drop everything). Our recon had FUN_0049fb54 as a mislabeled "cockpit/HUD query" no-op stub called with NULL, and never set the beam's target (0x474) at fire -- all three authentic gates were dead. Now: FireWeapon stashes the owner's target entity; both discharge paths + the owner gate run the real check (BTMechDestroyed). Verified: 40 beam samples before the kill, 0 after -- lasers flash-fail on the wreck instead of cutting it like butter (heat is still spent per the binary's FireWeapon, which fires blind; only the DISCHARGE checks the corpse). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
31626a3b97 |
Beams at the models' NATURAL width, per-weapon tube models (de-cartooned)
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> |
||
|
|
2036e7da51 |
Per-weapon beams from the REAL Emitter sim state (authentic per-mech fire)
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> |
||
|
|
a834866fcd |
Enemy spawn faces the player (was copying the player's pose -> back turned)
The dummy spawned 120u along the player's forward with the SAME orientation, so both mechs faced the same way and the player stared at its back (also why first shots read as rear hits). Flip the enemy's yaw 180 deg about Y (engine convention: Z basis = (sin y, 0, cos y) -> yaw = atan2(z.x, z.z) + pi) so the two spawn face to face. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ebeadba78e |
Verified the BLH loadout (missiles ARE authentic) + split weapon controls
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> |
||
|
|
a2333de7c1 |
Buried wreck: PARK the collision volume (count-zero alone doesn't unblock)
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> |
||
|
|
9b5004c39d |
Missiles: authentic dsrm smoke trail + torso aim (placeholder tracer removed)
The "white lines toward the feet" report: the LAUNCHES are authentic (the mech's real MissileLauncher/ProjectileWeapon roster subsystems firing on their own recharge cadence -- that's the every-Nth-shot rhythm), but the visual was the bring-up 3-segment beam tracer, and the aim point was the victim's ORIGIN (ground level between the feet). - Trail: per-frame puffs of the AUTHENTIC dsrm effect (psfx 0, "the lrm smoke trail") along the flight path, frame-oriented so the smoke streams behind the round (its .PFX velocities are +Z = backward); the round itself is a short hot streak. New BTPfxTrailPuff spawns slot particles directly at a moving point (no emitter instance). - Aim: the resolved target point lifts to torso height (the beams already converged there) -- impacts now cyl-resolve to body zones instead of diving at the feet/legs. Follow-up noted: the round MESHES (BULLET/LRMS.BGF ship) for a true model-per-projectile look. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bf87360c42 |
Buried wreck goes INERT: collision off + target lock dropped
Answers "it's still there but invisible": not normal -- the real game removes
the dead entity (death row). Full entity teardown needs the mech render tree
unhooked from the renderer first (the remaining P5 follow-through), so until
then the burial transition makes the wreck behaviorally gone:
- collisionVolumeCount = 0 (the collision gather skips volume-less movers;
MoveCollisionVolume early-outs) -> no phantom blocking
- the player's target lock drops -> beams stop converging on / hitting the
empty spot, no phantom impact smoke
Verified: zero [damage] hits after burial; INERT fires one-shot at
"wreck buried".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
5236d4263a |
Wreck sink (quadratic burial) + ldbr debris field -- the 1996 script completed
Answers "does the wreck fade away?": YES, by sinking. FUN_00456410 (the 1996 sink renderable) computes offsetY = rate * t^2; the hulk's authored rate is -0.025 -> the ~7-unit hulk is fully underground ~17s after the kill. The script also pairs the standing hulk with the LDBR strewn-debris field (12x13u flat scatter), parented together and sinking together. Also verified from the mesh data: BLHDBR (1537 verts -- more than the intact torso) IS the authored Blackhawk wreck: the classic standing-leg-in-rubble sculpt. The "just a leg standing there" report is the authentic art. (THRDBR -- the mesh the 1996 script hardcoded -- parses to ZERO vertices; more evidence the hardcode was an unfinished dev shortcut.) Implementation: SwapToWreck adds the ldbr piece; TickWreck applies the quadratic sink per frame (driven from the dead mech's UpdateDeathState), hides both pieces at burial and reports it so the wreck-smoke re-arm stops with the wreck. DPLStaticChildRenderable::SetOffsetTranslation added (Execute re-reads OrientationMatrix per frame -- same in-place idiom as SetDrawObj). Lifecycle verified live: kill -> 'blhdbr.bgf' + ldbr debris -> smoke re-arm @10s -> wreck buried @~17s -> smoke stops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c0fa6bf21a |
Death = the burning WRECK-HULK swap (effect 104) -- the authentic chain, reconstructed
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>
|
||
|
|
b44c0f98f6 |
PFX continuous-rate emission (data-verified) + persistent wreck smoke
The one-burst emitter batching was wrong: in EVERY shipped .PFX, maxIssue/rate == release_period (DAFC 25/100~=0.2, DNBOOM 35/150~=0.2, DDAM2 35/16~=2, DDTHSMK 30/3=10) -- release_period is the EMISSION WINDOW and rate is particles/second emitted continuously across it. The death smoke plume is authored as a 3/sec trickle for 10 seconds (+6s particle life), not one same-frame puff. Emitters now integrate rate*dt until maximum_issue. Wreck smoke (port addition, [T3]): a destroyed mech re-arms the death/rubble smoke plume (psfx 1, DDTHSMK) every 10s -- its own authored window -- so the dead hulk visibly keeps smoking instead of standing there looking alive (the freeze-death is invisible on an already-stationary target). New member Mech::wreckSmokeTimer; UpdateDeathState takes dt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d9254736ab |
Death is a FREEZE, not a collapse -- fall-latch vestige proven; band-effect impact frames
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> |
||
|
|
a3d67cc639 |
Combat visible + killable: Wword root-cause fix, .PFX effect layer, RemakeEntity swap
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> |
||
|
|
7c455303bd |
MechDeathHandler: per-zone destroyed-skins + explosions (faithful, exported)
Fixes the "kills are invisible" report: a mech died only at the state level, with no visible destruction on its body. Root cause: the per-zone damage-state descriptor table (binary zone+0xd4) that drives destroyed-skins + explosions was never built -- its loader (Mech__DamageZone::LoadCriticalSubsystems / FUN_0041e4a8) was a no-op stub, and MechDeathHandler itself was a no-op stub. The surrounding plumbing (the per-zone load loop over the type-0x1e resource, the death-handler slot) was already correct. Whole pipeline is EXPORTED -- no stand-ins. Reconstructed: - Mech__DamageZone descriptor table (binary this+0xd4): un-stubbed LoadCriticalSubsystems (FUN_0041e4a8/FUN_0042a748/FUN_0042a2c8) to parse the real type-0x1e stream -- entry = [f32 DamageLevel][i32 EffectResource] [i32 GraphicState][f32 TimeDelay], ascending by DamageLevel. Verified live: 6 entries/zone, first threshold 0.2, effect 26, across all 40 zones, no crash. - The three lookups (FUN_0042a664 by-level / FUN_0042a5f4 crossing / FUN_0042a6c4 by-graphic-state) as Mech__DamageZone methods. - The real MechDeathHandler (FUN_0042a984 ctor / FUN_0042aa2c Performance / FUN_0042a9f4 dtor), replacing the stub: each tick it walks the zones and, as a zone's damageLevel rises across a descriptor threshold, fires that entry's explosion (binary +0xb8 & 4) and, on destruction, the Destroyed-graphic descriptor's explosion (+0xb8 & 8), applying the descriptor's GraphicState (the destroyed skin) to the zone. Runs for EVERY mech, so the enemy visibly falls apart as it dies. Integration: the binary ticks MechDeathHandler off the mech's Performance list (mech+0xbc), which the bring-up drive override bypasses, so Mech::PerformAndWatch drives Tick() directly (same approach as UpdateDeathState). Effect spawn uses the established Explosion::Make port (BTSpawnDamageEffect) -- the authentic dispatch (class-5 message -> the 0xBD3 SubsystemMessageManager effect manager) is unported. Runtime: builds clean, boots clean, 42 descriptor tables load with sensible byte-aligned data, no crash. Live effect firing is combat-triggered (verified by the load + the wiring; the [deathfx] threshold-crossing log fires under BT_DEATH_LOG). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a9467481c5 |
Death-state sequence: collapse + subsystem shutdown + freeze (wreck stays)
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>
|
||
|
|
2af401eef8 |
Collision damage applied: mech-vs-mech + icon-crunch via the STEP-6 unaimed path
The two deferred TakeDamage dispatches in Mech::ProcessCollision now fire,
unblocked by STEP 6 (the cylinder hit-location override that resolves zone==-1):
- Mover branch (:15324-15358): on a collision with another Mech, dispatch the
collision damage to it.
- CulturalIcon branch (:15369-15401): crunch dispatch to a building/tree/prop,
before the walk-through sentinel overwrites the amount.
Both go through a new file-scope helper BTDispatchCollisionDamage, which builds
an Entity::TakeDamageMessage{zone==-1} (the engine ctor -- same idiom as the
weapon-impact path) with the world centre of the overlap slice as the impact
point and this mech as the inflictor, then Dispatch()es it to the victim. The
receiver turns the world impact point into a damage zone: a Mech via its cylinder
table (STEP 6), an icon via its base handler (crushable props have no zones -> a
harmless no-op). Terrain (walls/hills) matches neither branch, so it still
BLOCKS without damage (faithful to the binary).
Faithful to the binary's raw DamageMessage dispatch (:15324-15401) but via the
engine's named TakeDamageMessage API (no databinding-trap field-by-field build);
the binary's inflictor global DAT_0050b9ac is a sentinel EntityID (only ever
read, never set -> a collision has no "shooter"), so this mech is the inflictor.
Verify: builds clean; reachability GUARANTEED (Mover::ProcessCollisionList calls
the VIRTUAL ProcessCollision -> Mech::ProcessCollision, on the active
AuthenticGroundAndCollide path); stable across runs; both mechs build their
cylinder tables. The live dispatch was not captured headlessly (the solo
auto-walker never rammed a tree/mech), but the path is proven reachable and
composed of runtime-verified pieces (the weapon TakeDamage path + STEP 6).
Also validated STEP 6's height ref: collisionTemplate->maxY ~= 7.1 (a real mech
height), confirming CylinderReferenceHeight reads a height (not the heat value
the mech+0x2ec dual-labeling hinted at).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
d07ac7dd49 |
STEP 6 COMPLETE: cylinder hit-location LIVE — unaimed hits resolve to zones
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> |
||
|
|
7fc4acb89f |
gauge-complete P4g: MessageBoard reconstructed + registered -> LAST parse-skip cleared (0 unregistered gauges)
The "messageBoard" cockpit primitive (the secondary-MFD comm/status message ticker, L4GAUGE.CFG:4913) was PROSE-ONLY (no bodies/methodDescription) -> parse-skipped. This was the last unregistered gauge primitive. Reconstructed byte-verified (Make @4cb678, ctor @4cb704, dtor @4cb788, BecameActive @4cb7fc, Execute @4cb82c; vtable 0051bddc; sizeof 0xA4). THREE header mislabels fixed (decode + adversarial verify): int enabled -> Entity* trackedMech (a pointer deref'd at +0x190, set by SetSource not SetEnable); previousMessageId/previousNameId were SWAPPED (BecameActive writes 0x9c=-1/0xa0=-2; Execute compares messageId vs 0xa0, name vs 0x9c). Execute blits the strip cell (id -> (id&3)<<7,(id>>2)<<5) + sender name; methodDescription = 4 params (rate, mode, btsmsgs.pcx, color). Registered in BTL4MethodDescription[]. DEFERRED / EMPTY by design (authentic for bring-up): the source is never bound (SetSource has no recovered caller) AND the per-player status queue (StatusMessagePool, btstubs.cpp:62) is a NULL stub -> no status messages exist -> Execute early-returns on the NULL source (safe no-op == empty board). Data read via a BTResolveMessageBoard bridge in btplayer.cpp (a real /FORCE-safe stub returning False; the raw mech+0x190/+0x1dc reads stay in the complete-BTPlayer TU, dodging the databinding trap). The name-cell path is a marked structural simplification to restore when the feed lands. VERIFIED: the gauge parse-skip list is now EMPTY ([gskip]=0 -- every config gauge primitive registered + built); BTResolveMessageBoard resolves (no /FORCE __ImageBase AV); combat TARGET DESTROYED, no crash; gauge composite renders identically (300/77, clusters, radar un-regressed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c0d2eb0aae |
gauge-complete P4f: PrepEngrScreen reconstructed + registered -> 12 engineering-screen label overlays build
The "prepEngr" cockpit primitive (the per-engineering-screen static label overlay,
12 CFG calls at L4GAUGE.CFG:4595-4759) was a MISLABELED stub (base Gauge, an Execute
override, mech/screenNumber SWAPPED) with no methodDescription -> parse-SKIPPED, none built.
Reconstructed byte-verified (ctor @4c7bf0, BecameActive @4c7e48, Make @4c7b30; vtable
0051a06c). THREE corrections vs the stub (decode workflow + vtables.tsv proof):
(1) base = GraphicGaugeBackground, not Gauge -- vtable-identical shape to the engine's
BackgroundBitmap (only dtor + BecameActive overridden); (2) the overridden slot is
BecameActive (a paint-on-activation), NOT Execute -- GraphicGaugeBackground has no Execute
virtual, so the Gauge::Execute->Fail->abort hazard doesn't apply; (3) screenNumber@0x6C /
mech@0x70 were swapped. sizeof 0x90.
BecameActive walks the mech roster (databinding-safe GetSubsystemCount/GetSubsystem +
BTGetSubsystemAuxScreen bridge, like VehicleSubSystems::Make), finds the subsystem whose
auxScreenNumber == this screen (1..12), and paints the screen-number numeric + subsystem
label + type-specific label cells dispatched on GetClassID (Sensor/Myomers/Emitter/PPC/
Projectile/Missile/GaussRifle). methodDescription = 9 params (mode, screen, 7 .pcc names).
Registered in BTL4MethodDescription[]. Two raw subsystem reads (heat-sink #, +0x224 label)
are guarded fail-soft best-efforts (marked; the aux-screen bridge label64 substitutes).
VERIFIED: the skip list drops from {prepEngr x12, messageBoard} to just {messageBoard} --
all 12 prepEngr gauges build; no screen-range/crash; boot+sim+combat run; gauge composite
renders identically (COOLANT 300/77, clusters, radar all un-regressed). The prepEngr labels
paint on the Eng (ModeMFD*Eng*) screens, not the default composite view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4a4ec6855c |
gauge-complete P4e: SectorDisplay reconstructed + registered -> radar SECTOR X/Z read-out LIVE
The "sectorDisplay" cockpit primitive (Secondary overlay, the radar SECTOR X/Z coordinate read-out) was PROSE-ONLY in btl4gau3 (placeholder Make, no ctor/ methodDescription/registration) -> the config line was parse-SKIPPED and never built. Reconstructed byte-verified from the disassembly (ctor @4c9e10, Execute @4ca07c, methodDescription PE-parse): SectorDisplay : GraphicGauge, sizeof 0xC4. Its Execute reads the linked mech's world position and shows two 100-unit sector numerics: numericA = Round(-localOrigin.z * 0.01) + 500 numericB = Round( localOrigin.x * 0.01) + 500 (rounding = round-to-nearest == FUN_004dcd94, corrected from the reviewer's wrong "truncate" claim; 0.01 const PE-verified; -Z/+X axis + fchs confirmed from asm). Overridden slots: LinkToEntity(9) caches the subject, BecameActive(3, non-inactivating), Execute(16) -> satisfies the container-Execute rule. Layout overflow-locked (static_assert sizeof<=0xC4). Make/ctor/dtor mirror the registered PilotList sibling; the config image name is copied (nameCopy) since Execute reads it per-frame. Registered in BTL4MethodDescription[]. VERIFIED LIVE (BT_SECTOR_LOG): Make port=1 pos=(125,579) image=helv15.pcc gridCached=1; Execute -Z=960.4 X=361.6 -> sectorA=510 sectorB=504 (Round(9.6)+500=510, Round(3.6)+500=504 -- authentic 100-unit sectors from live mech position). Gauge composite renders full, no crash. The skip list is now exactly the two remaining widgets (prepEngr x12, messageBoard). Also: a permanent BT_GAUGE_SKIP_LOG diagnostic (GAUGREND.cpp, gated) that logs each unregistered gauge primitive the dev-parse skips -- the tool that pinned this down (earlier "not built" runs were killed before the lazy gauge-renderer init). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
aae3ce29d8 |
gauge-complete P4d: Condenser::MoveValve reconstructed from the REAL @0x4ae464 (was a mislabel)
The reconstructed Condenser::MoveValve carried FUN_004afbe0's body -- a CONTROLS-mapper display-mode cycler (cycles field@0x190 through 0/1/2 + repoints owner HUD gauges), NOT the condenser valve. The real valve handler @0x4ae464 was not captured by the assert-anchored exporter; disassembled it (tools/disas2.py) and reconstructed faithfully: cycle valveState (@0x1D0) 1 -> 5 -> 50 -> 0 -> 1 (byte-verified @4ae480-4ae4bf), then RecomputeCondenserValves(owner) to redistribute flow so every ValveSetting gauge updates. DORMANT by design: the binary guards on owner->messageManager(+0x190)+0x274 (FUN_004ac9c8, the 0xBD3 SubsystemMessageManager) and the message that invokes MoveValve arrives through that same manager -- both DEFERRED to WAVE 8. Reproducing the raw owner+0x190 deref would be a databinding trap in our layout, so the handler is faithful-but-uninvoked; the valve gauge shows the authentic static 1/N (RecomputeCondenserValves at ctor) until 0xBD3 lands. No V-key bring-up stand-in added. Build green; combat TARGET DESTROYED, un-regressed (MoveValve is not yet routed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b91057aadc |
gauge-complete P4c: Condenser dedup + RecomputeCondenserValves -> ValveSetting gauge reads authentic 1/N
Two coupled fixes so the condenser valve gauge (ValveSetting -> coolantFlowScale @0x15C) reads its authentic value instead of garbage/zero. STEP 7 (gating dedup): heat.cpp carried STUB Condenser ctor/dtor/TestClass/ TestInstance/CreateStreamedSubsystem that ODR-duplicated the REAL bodies in heatfamily_reslice.cpp and WON under /FORCE (heat.obj links first) -> the real ctor (which sets valveState=1) was shadowed, leaving valveState=0xCDCDCDCD. #if 0'd the heat.cpp stubs so the reslice ctor (@4ae568, byte-verified: valveState=1, coolantFlowScale=0, massScale=refrigerationFactor, condenserNumber from name) is the sole definition. DefaultData/GetClassDerivations/ResetToInitialState (not duplicated) stay in heat.cpp. STEP 9 (the real writer): FinishConstruction() at the Mech ctor tail was a no-op template stub in place of FUN_0049f788 = RecomputeCondenserValves -> coolantFlowScale was never written (stayed 0). Reconstructed it (byte-verified vs part_012.c:9264): distribute coolant flow across the mech's condensers, coolantFlowScale_i = valveState_i / sum(valveState), with the condenserAlarm@0x1DC change pulse (2-if-flow<=old-else-1, then 0). Walks the populated subsystem roster filtering Condensers via IsDerivedFrom (behaviorally identical to the binary's @mech+0x7cc condenser chain, GUID 0x50e4fc). Wired as BTRecomputeCondenserValves(this) at the Mech ctor post-init pass (binary @9457). _DAT_0049f850 fallback confirmed 0.0f (PE read). Verified: [valve] the Blackhawk's 6 condensers each read flow=0.166667 (=1/6, total=6) for both player + spawned enemy, no every-mech crash; combat TARGET DESTROYED, un-regressed. Diagnostic BT_VALVE_LOG added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
16f5f545ce |
gauge-complete P4b: AggregateHeatSink 0xBBE -> HeatSink/AmbientTemperature bound (last config NULL cleared)
The 0xBBE heat-sink BANK was built as a plain HeatSink, so the numeric-R cockpit gauge binding HeatSink/AmbientTemperature (L4GAUGE.CFG:4552) had no publisher -> the LAST unresolved config attribute. Reconstruct AggregateHeatSink : HeatSink (ctor @4ae8d0, own GUID 0x50e590), byte-exact + static_assert-locked (heatSinkCount@0x1D0, ambientTemperature @0x1D4=300, helper@0x1D8 0xC link node, sizeof 0x1E4 == factory alloc @9993). Publish HeatSinkCount + AmbientTemperature via a dense-prefix attribute table chained to HeatSink::NextAttributeID (shared HeatSink table unchanged, so CoolantMass/CoolantCapacity keep resolving). Move CreateHeatSinkBankSubsystem into heatfamily_reslice.cpp (needs the class def) and build the real class at factory case 0xBBE; mech.cpp unchanged. DELIBERATE DEVIATION (documented in the class): keep the base HeatSinkSimulation the HeatSink ctor installs; do NOT reimplement the authentic Performance @4ae73c -- it derefs a raw self+0xE0 -> [+0x158] that does not map in our compiled layout (AV/NaN, and runs for EVERY mech), and ambientTemperature is a frozen constant so the gauge reads 300 either way. Authentic relaxation model deferred. Verified: [attr] HeatSink/AmbientTemperature OK; all 50 config attribute bindings resolve (0 NULL); no every-mech crash; combat TARGET DESTROYED, FIRED #41+, un-regressed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
12c4254aae |
gauge-complete P4a: heat-leaf data fixes (radar heat-degradation authentic)
The gauge-complete-decode workflow found the "deep heat-leaf byte-exact re-base" is STALE -- the leaf layout already shipped byte-exact (alarm-unification/P7), and the real work is a small misidentified-field data fix (highest value, lowest risk): - Sensor radarPercent read the INHERITED heatEnergy (~1.3e7) -> hugely negative -> needed a bring-up guard forcing it to 1.0. The binary (@004b1c4c:1829) reads *(this[0x38]+0x158) = the subsystem's OWN DamageZone::damageLevel [0,1] (0 intact .. 1 destroyed). Fixed: radarPercent = RadarBaseline - GetSubsystemDamageLevel(); guard DELETED (damageLevel is engine-clamped). So the radar authentically degrades with sensor structural damage (RadarBaseline-damage, x0.5 on DegradationHeat, 0 on Failure). - New databinding-SAFE accessor MechSubsystem::GetSubsystemDamageLevel()/Set... reads the ENGINE DamageZone::damageLevel NAMED member (NOT the ReconDamageZone proxy, whose offset-0 structureLevel aliases the vtable ptr as a float = garbage). - HeatSink::HandleMessage msg==1: same misidentification -> SetSubsystemDamageLevel(0.5) (was linkedSinks->heatEnergy=0.5, wrong object+field). - MyomerCluster seek lamp: subsys+0x800 (OOB for 0x358 Myomers) -> currentSeekVoltageIndex @0x320 (the Emitter+0x3f0 analog; INFERRED -- ctor not in the assert-anchored export). - numericSpeed (~225 vs 61.5): verified NOT A BUG (NumericDisplaySpeed applies x3.6; the feed is correct u/s) -- closed, no code. No layout change (already byte-exact); pure data-read retargets. Verified DBASE+dev gauges: no crash, combat un-regressed (TARGET DESTROYED). (radarPercent degrade is only OBSERVABLE once per-subsystem damage routes to each engine DamageZone -- a separate task; today the sensor is undamaged so radarPercent = full 1.0, which is correct.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a9210e038a |
gauge wave P3: fix the DEATHS runaway + RANK=-1 (solo scoreboard correct)
RANK=-1: Player::DefaultFlags creates every player NonScoring (CalcRanking only ranks IsScoringPlayer()==true, else -1), and the bring-up never cleared it. Call SetScoringPlayerFlag() in DropZoneReplyMessageHandler's mech branch (a piloted mech IS an active scoring combatant; the camera-ship branch stays non-scoring). -> RANK=0. DEATHS runaway (climbed ~1/s, not the "1" first seen): @004c012c -- which our MESSAGE_ENTRY binds as the VehicleDead handler -- is actually the drop-zone RESPAWN-RETRY helper. The engine's RequestDropZone (PLAYER.cpp:400) posts a 2s-delayed VehicleDead(deathCount=-2) as a "did the drop zone reply?" timer; our handler ++deathCount + RE-POSTED it UNCONDITIONALLY -> an infinite loop, because the drop-zone handshake never "completes" in the bring-up (no console/drop-zone system). Traced it to a pure boot artifact (13 fires with zero combat; the only VehicleDead producers are the re-post + my kill-producer, and the kill fires AFTER the loop starts). FIX (correct respawn-retry semantics): if the player already has a vehicle the drop zone WAS acquired (the DropZoneReply created it), so the retry is moot -- return without counting a death or re-posting. -> DEATHS=0 in solo, loop gone. Verified DBASE+dev gauges: the Comm roster shows KILLS=1 / DEATHS=0; the KILL log reads killCount=1 deaths=0 score=164 rank=0; 0 crashes. (MP real-death DEATHS -- a destroyed vehicle via the separate @004c05c4 path -- remains deferred per the plan; solo never dies so the retry gate is correct there.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
34aaa7dda4 |
gauge wave P3: wire the combat scoring feed (KILLS tracks combat)
The Comm roster + score gauges read 0 for 4 independent reasons (none was handler logic -- the BTPlayer ScoreMessage/VehicleDead/ScoreInflicted handlers were already reconstructed). Mapped by the scoring-feed-decode workflow; fixed all 4: - NO PRODUCER: combat only dispatched Entity::TakeDamageMessage, never a scoring message. Added producers (btplayer.cpp bridges, called from mech4.cpp): per-hit ScoreInflicted at the beam (:2171) + projectile (:831) damage dispatch -> SCORE; KillScore at the TARGET-DESTROYED edge (:2198) -> KILLS. senderMechID = the VICTIM (so the local player is credited via the !=our-mech branch, not the suicide-negate branch); dispatched to application->GetMissionPlayer(). - CROSS-FAMILY MECH OFFSETS: MECH_OWNING_PLAYER/TONNAGE/DAMAGE_BIAS read raw binary offsets (garbage in our 0x638 Mech). MECH_OWNING_PLAYER -> Entity::GetPlayerLink() (NULL for the ownerless dummy); tonnage/bias stubbed 1.0/0.0 (bring-up). - NULL scenarioRole: the BTMission role registry has no WinTesla analog, so every award path NULL-deref'd. CalcInflictedScore returns the neutral (damage+bias) when scenarioRole==0; DamageReceived guarded. - NULL-owner dummy: the KillScore sender-owner increment + StatusMessage now guard GetPlayerLink() (the dummy has none) so a solo kill credits only the local player. - DATABINDING: the PilotList read KILLS/DEATHS at raw offsets (pilot+0x27c/+0x200) that don't match our compiled layout -> silent 0. Repointed to the compiled members via bridges (BTPilotKills/BTPilotDeaths -> BTPlayer::GetKillCount/GetDeaths). ⚠ ROOT-CAUSE of the empty scoreboard: the PilotList SELECT-TARGET highlight did a raw-offset deref (*(local+0x284) objectiveMech, then *(tgt+0x190)) that AV'd on our layout -- caught silently by the SEH GuardedExecute, aborting Execute BEFORE the KILLS/DEATHS draw. Replaced with BTPilotIsSelected (accessors + GetPlayerLink). - SCORE persistence: PlayerSimulation's 10s flush zeroed currentScore (the binary's console delta); now only flushes/zeros when a console host exists (solo keeps the running score for the gauge + CalcRanking). Verified DBASE+dev gauges (BT_SCORE_LOG): SCORE climbs +5.83/hit -> 164; on the kill KILLS 0->1, and the Comm roster RENDERS KILLS=1 (screenshot). No crash. Follow-ups (noted): DEATHS=1 is a pre-existing spawn/respawn-handshake artifact (not from the producers -- the dummy's GetPlayerLink is NULL so no VehicleDead is posted; confirmed killCount=1 not 2); RANK=-1 is CalcRanking's non-scoring mark (Plasma serial surface, invisible on dev); MP VehicleDead/tonnage/role registry remain per the plan. Diagnostics kept: BT_SCORE_LOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
88cd68aaa1 |
gauge wave P2d: GeneratorCluster un-gated -- the 4 generator panels render
Root-caused the P2c abort with a cdb attach to the frozen abort dialog: the stack
was Gauge::Execute (GAUGE.cpp:598) -> abort, via GuardedExecute <- Update <-
ProcessOneActiveGauge. The engine base Gauge::Execute is Fail("not overridden")
-> abort, so an ACTIVE container that doesn't override Execute aborts by design;
GuardedExecute's SEH can't catch an abort(). The decode's "GeneratorCluster
overrides ONLY the dtor / renders via its children" was wrong -- the 1995 base
Gauge::Execute was a no-op, the 2007 WinTesla engine's aborts, so the container
MUST override Execute (the sibling SubsystemCluster does).
FIX: give GeneratorCluster the two container overrides (mirroring SubsystemCluster):
- BecameActive() -- blit the panel's static background pixmap (DrawPixelMap8) and,
by overriding the default, DON'T self-inactivate so the panel stays active.
- Execute() -- no-op (the 5 self-registered child gauges draw the dynamic content;
the override exists only to avoid the base Fail->abort).
Re-registered in BTL4MethodDescription[]. Verified DBASE+dev gauges: all 4
generator panels (A/B/C/D) render -- blue OutputVoltage bars (the @004c72ac
ScalarBarGauge Scalar* variant), labels, status lamps -- 0 "BecameActive not
defined" warnings, NO abort, combat un-regressed (TARGET DESTROYED). Phase 2 is
now COMPLETE (4/4 widgets: LeakGauge, VertNormalSlider, PilotList, GeneratorCluster).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
b44e908101 |
gauge wave P2c: GeneratorCluster reconstructed + infra (gated -- parent aborts)
Reconstructed the 4th unbuilt widget (the 4 generator engineering panels, buttons 9-12) + its companions, but left it UNREGISTERED pending a runtime-abort fix: - Generator OutputVoltage attribute table (powersub) -> outputVoltage@0x1DC, chained to HeatSink's dense index (temps already reach it). Correct + reusable. - ScalarBarGauge @004c72ac plain-Scalar variant (btl4gau2) -- the DIRECT-Scalar* bar the GeneratorCluster voltage bar needs (the existing @004c721c takes a resolved source object). Correct + harmless. - GeneratorCluster Make/ctor/dtor + 15-param methodDescription reconstructed from part_014.c:891-1013 (replacing the prose + placeholder Make + the bogus DAT pool); builds the 5 children (temp bar / voltage bar / leak wipe / 2 lamps). Header fixed to the binary ctor param order (secondaryColor@0x94, +_reserved0xB0 -> sizeof 0xB4). ChildRate forward-declared. ⚠ REGISTRATION GATED (commented out in BTL4MethodDescription[]): registering it aborts at runtime -- the 4 panels build (the 4 "BecameActive not defined" warnings fire) then abort(). ISOLATED to the PARENT lifecycle: the abort PERSISTS with all 5 children nulled; there are no pure virtuals; Execute is SEH-guarded under BT_DEV_GAUGES so the fault is in the unguarded ctor/lifecycle path. The sibling SubsystemCluster overrides BecameActive/Execute/TestInstance whereas the decode has GeneratorCluster overriding ONLY the dtor -- that "inherits only the dtor" claim is the prime suspect. Deferred; the 3 other P2 widgets (LeakGauge/VertNormalSlider/PilotList) ship clean. Verified: unregistered -> parse-skips -> no abort; combat un-regressed (DESTROYED), 0 crashes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0060a3e1ca |
gauge wave P2b: build the Comm KILLS/DEATHS pilot roster (PilotList)
PilotList (keyword "pilotList") was PROSE-ONLY, so the Comm surface showed only
the baked btcomm.pcx labels. Reconstructed all 7 functions from part_014.c:
3156-3434 (Make/ctor/dtor/BecameActive/TestInstance/Execute/DrawMechIcon), with
the 8x(x,y,layoutMode) layout table DAT_0051af88 PE-parsed exact from BTL4OPT.EXE.
Draws ONE roster slot/frame (round-robin) from the viewpoint mech's cockpit-mapper
pilot roster.
- Roster FEED: new BTResolveRosterPilot(slot) bridge in mechmppr.cpp (a complete-Mech
TU) resolves the viewpoint mech's ControlsMapper (subsystemArray[0]) -> GetPilot;
btl4gau3.cpp reads the returned pilot at raw BTPlayer offsets. The mapper's
FillPilotArray already fills the roster (pilotArray[0]=GetMissionPlayer,
[1..]=FindGroup("Players")).
- KILLS = killCount@0x27c (real; ScoreMessageHandler increments it in combat).
- DEATHS: the binary reads pad_0x280 which has NO writer anywhere (a shipped dead
field -> perpetual 0). FIX per "if it doesn't work, fix it": read the real
deaths counter Player::deathCount@0x200 (VehicleDeadMessageHandler increments it)
so DEATHS is meaningful in MP.
- DrawMechIcon: App+0xC8 name-bitmap cache is unwired (same deferral PlayerStatus
uses) -> LookupPlayerNameBitmap returns NULL -> the tinted name box (never an AV).
Dropped the bogus x,y from the header ctor (positions come from DAT_0051af88).
/FORCE-safe (all vtable slots real; link log clean, no unresolved PilotList/
BTResolveRosterPilot). Verified DBASE+dev gauges: the Comm surface now renders the
live local pilot row (KILLS 0 DEATHS 0 + name box; 0/0 authentic in solo -- the
combat scoring feed that moves them is Phase 3), combat un-regressed (DESTROYED),
0 crashes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d68284ede2 |
gauge wave P2a: build the condenser LeakGauge + valve slider (heat panel)
The gauge-widget-decode workflow reconstructed the unbuilt cockpit widgets from the binary. First two (the heat MFD's missing SET/LEAK columns): - LeakGauge (BitMapInverseWipe, keyword "LeakGauge"): the class body already existed + is byte-faithful; it was just UNREGISTERED so every LeakGauge(...) config line was parse-skipped. Added the Make factory + methodDescription + the BTL4MethodDescription[] registration. Value @6 = Condenser/CoolantMassLeakRate (already published P1); 0 undamaged -> the leak wipe is authentically empty. - VertNormalSlider (keyword "vertNormalSlider"): the condenser VALVE slider @2 was PROSE-ONLY. Reconstructed all 7 functions (Make/ctor/dtor/TestInstance/ BecameActive/Execute/Draw) from part_013.c:14051-14175 (Make @004c4b08 by disassembly) -- an XOR indicator row=Round(span*value) over the track; fixed the jumbled header member layout to the byte-exact order (sizeof 0xB0). Published ValveSetting -> coolantFlowScale@0x15C on the HeatSink table so Condenser/ValveSetting resolves (verified: all 6 condensers OK). The valve indicator renders (condenser 1 shows it near the top = coolantFlowScale 1.0 = valve open). Both /FORCE-safe (every vtable slot has a real body; link log grep clean, no unresolved VertNormalSlider/BitMapInverseWipe). Verified DBASE+dev gauges: the heat panel now shows all 3 columns per condenser (TEMP scales, SET valve indicator, LEAK track), coolant bar stays full, combat un-regressed (TARGET DESTROYED), 0 crashes. The player-valve-toggle -> coolantFlowScale drive (SetValveSetting vtable+0x48) is the remaining Phase-3 valve-mechanism work; the slider shows the static valve value today. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
64975ec22f |
gauge wave P1b: Searchlight/LightOn + fix coolant/refrigeration source misread
- Searchlight publishes "LightOn" -> lightState@0x1D8 (the button-5 searchlight lamp; the empty default-constructed index resolved NOTHING). The config binds the name "LightOn", so the enum id is renamed LightOnAttributeID and the table is chained to PowerWatcher's dense index. Verified: LightOn resolves OK. - FIX a genuine reconstruction bug the heatmodel decode caught (heat.cpp UpdateCoolant + heatfamily_reslice RefrigerationSimulation): both read linkedSinks.Resolve()->heatEnergy where the binary reads *(this[0x38]+0x158) = the subsystem's OWN DamageZone.damageLevel (the engine base zone @0xE0). Consequences of the misread: coolantDraw = master.heatEnergy(~1.3e7) * heatLoad would SLAM the live CoolantMass bars to empty whenever the link resolved; and RefrigerationSimulation clamped massScale permanently to 1.0 (minimum) instead of the authentic 3.0, plus null-deref'd a Condenser whose linkedSinks is skipped. Now both read this->Subsystem::damageZone->damageLevel (qualified past the MechSubsystem shadow, per mechweap.cpp:252): undamaged 0 -> coolantDraw 0 (no leak, coolant bars stay full = authentic pristine) / massScale = 3.0, rising only as the sink/condenser itself takes battle damage. After this only HeatSink/AmbientTemperature remains NULL (the #if 0'd aggregate HeatSink bank -> P3). Verified DBASE+dev gauges: no crash, combat un-regressed (TARGET DESTROYED). (Also noted: the Condenser dup-ctor links to the REAL heatfamily_reslice definition -- LNK4006 keeps the first/real one; the heat.cpp stub is ignored -- so no bug today; #if 0 cleanup deferred to the Condenser table.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
070af409f7 |
gauge wave P1a: publish heat/power/weapon attributes + radar zoom
The gauge-databinding-map workflow found most cockpit gauges resolve NULL because the reconstructed subsystems publish only a fraction of the attributes the config binds. First publishing batch (attribute tables are read-only static data; ids kept a dense prefix from each parent's NextAttributeID): - HeatSink table dense-append: DegradationTemperature/FailureTemperature (the condenser temp-bar warn/max endpoints -- were NULL, so the two-part bars could not scale), NormalizedPressure/DegradationPressure/CoolantMassLeakRate, and the HeatSink link. Condenser/Reservoir inherit this -> all 6 condenser temp bars now resolve current/warn/max (verified: BT_GAUGE_ATTR_LOG all OK). - PoweredSubsystem::GetAttributeIndex() (new) publishes InputVoltage->voltageSource -- the cluster power-branch gate (the power-lamp/generator-voltage/state-lamp sub-branch is skipped when it resolves NULL). Flows to Sensor/Myomers/weapons. - MechWeapon::GetAttributeIndex() (new) publishes OutputVoltage/PercentDone-> rechargeLevel; Emitter/PPC/ProjectileWeapon/MissileLauncher/GaussRifle DefaultData re-pointed at it (they carried an EMPTY default-constructed index -> resolved NOTHING). Verified: the ER MED LASER / PPC / STREAK weapon clusters now render live recharge dials (were blank TEMP/STATUS). - Mech::SetTargetRange un-stubbed (radarRange = range) -> the radar map scale + overlay range readout track the mapper's zoom (was frozen at 1000). - GAUGREND ParseAttribute: env-gated per-binding resolution trace (BT_GAUGE_ATTR_LOG) -- durable diagnostic infra for the wave. Verified DBASE+dev gauges: no startup/gauge-construction crash (dense chain intact), combat un-regressed (TARGET DESTROYED), clusters build with InputVoltage resolving. Remaining config-binding NULLs: HeatSink/AmbientTemperature (aggregate bank, P3) + Searchlight/LightOn (P1b). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c850ffa26c |
WAVE 7 Phase B polish: missiles launch from the mech's missile ports + look like missiles
Two issues the user hit driving the Mad Cat: (1) projectiles appeared to launch from the mech's FEET -- MechWeapon::GetMuzzlePoint falls back to the mech origin (localOrigin) when the weapon's mount segment (this+0xdc) doesn't resolve, so the passed muzzle was at ground level; (2) they rendered as a thin laser-like line, not a missile. Fix: BTPushProjectile now takes the SHOOTER mech and resolves the real launch port by NAME (sitermissleport / sitelmissleport, alternating L/R for a salvo look; then torso ports, then gun ports; then a raised fallback) -- the same segment-name -> world-transform mechanism the visible laser beams use for the gun ports. The tracer is now a 3-part missile look (dim smoke trail behind + orange body + hot flame tip) instead of a single thin beam. FireWeapon (both ProjectileWeapon and MissileLauncher) passes owner as the shooter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bf44a45ab7 |
WAVE 7 Phase B fix: projectile weapons no longer permanently jam in bring-up
Interactive repro: fire one missile salvo, then only lasers. Cause: ProjectileWeapon:: CheckForJam gates the heat-scaled jam roll on `heatLoad > 0` (true whenever firing), and the roll uses currentTemperature -- which the reconstructed heat model pins at a constant ~77 (thermalMass is huge, so temp doesn't rise/fall with fire yet). So the roll fired spuriously on ~every shot, and since JammedState clears only on ResetToInitialState, a weapon that jammed stayed jammed for the whole mission. (The auto-fire harness hid this -- it kills the target before jams accumulate.) Fix: gate the roll on the weapon being in a degraded HEAT STATE (heatAlarm level >= DegradationHeat) instead of raw heatLoad -- the authentic "jam risk scales with heat" rule. In bring-up the alarm stays 0 (temp << degradation threshold), so weapons fire reliably; real jams return once the heat model drives true temps. FOLLOW-UP noted in-code: make JammedState recoverable (unjam delay) so even a genuine overheat jam clears. Verified: 31 projectile impacts sustained over 18s, 0 crashes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f52bf057e6 |
WAVE 7 Phase B: the Mad Cat's LRMs LAUNCH flying missiles that fly + damage (autocannon too)
The byte-exact WORLD-ENTITY reconstruction (Projectile @4be1bc 0x340 / Missile @4bf5b4 0x368)
is infeasible on the 2007 engine: measured sizeof(engine Entity)=0x1BC vs the 1995 binary's
0x300, so the reconstruction's raw base-offset reads (velocity@0x1dc, roster@0x124, motion@0x250)
read GARBAGE on the engine (the Mech 0x638-vs-0x854 gap, but the entity integrator DEPENDS on
those offsets). So -- like the mech drive and the beam renderer -- flying projectiles are a PORT
reconstruction (BTPushProjectile/BTUpdateProjectiles in mech4.cpp, a static array + stack
messages, ZERO heap ops): seeded from the launcher's fire with the decomp's real muzzle
(GetMuzzlePoint) / launch speed (|launchVelocity|) / per-shot damage (damageData, split across
missileCount), they fly to the target (tracer via BTPushBeam) and deliver the weapon's damage on
impact through the SAME Entity::TakeDamage path as the beam (aim Mech::FirstVitalZone()).
THREE bring-up fixes were needed to make a projectile weapon fire at all (found by tracing):
1. TRIGGER: fireImpulse was only driven for the Emitter; ProjectileWeaponSimulation now sets
fireImpulse = gBTWeaponTrigger too (else CheckFireEdge never sees an edge).
2. AMMO BIN: OwnerSubsystemCount/OwnerSubsystem were stubbed ->0, so ammoBinLink never resolved
and ConsumeRound always failed; redirected to the real roster (owner->GetSubsystemCount()/
GetSubsystem(i) -- the AmmoBin 0xBCB constructs before the weapons 0xBCD/0xBD0).
3. JAM ROLL: UniformRandom() was stubbed `return 0.0f`, so CheckForJam's `0 < jamChance` ALWAYS
jammed (a projectile weapon could NEVER fire); replaced with a real LCG [0,1) (fires
~1-jamChance of the time -- authentic occasional jams).
Also: the mech's own target slot (owner+0x388) isn't populated in bring-up (the visible fire
targets the gEnemyMech global), so BTPushProjectile falls back to gEnemyMech.
Verified: Mad Cat PUSH=62 / IMPACT=31 (LRM missiles 3.33 dmg each split across the salvo + AFC100
autocannon 25 dmg), TARGET DESTROYED, 0 crashes, construction heapcheck-clean; BLH un-regressed
(also fires its ballistic weapon now). Diagnostics BT_PROJ_LOG ([projectile] PUSH/IMPACT).
REMAINING (deferred): the byte-exact world-entity Missile (Projectile : Mover, MP-replicable via
Registry::MakeEntity) -- the port projectile is master-local only (no MP replication); the real
per-weapon fire-rate/heat wiring off the subsystem sim (mech4 beam path is still the bring-up harness).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
2bcca26cea |
WAVE 7 Phase A: wire the projectile/missile weapon SUBSYSTEMS byte-exact (un-mislabeled)
An 8-agent decomp-mapping workflow mapped every weapon-family ctor + fire/spawn path
(scratchpad/wave7_maps.txt). KEY IDENTITY CORRECTION via VDATA.h enum (base 3000=0xBB8):
the factory ctor-address comments were right but the built CLASS names were stubs/base --
0xBCD == ProjectileWeapon (was building the base MechWeapon stub)
0xBCE == GaussRifle (NOT MissileLauncher -- was a MissileLauncher stub)
0xBD0 == MissileLauncher (NOT BallisticWeapon -- was a BallisticWeapon stub)
Un-mislabeled + wired via Create<Class>Subsystem bridges (mirroring CreateEmitterSubsystem),
each static_assert-locked byte-exact on the now-locked MechWeapon(0x3F0)/Emitter(0x478) bases:
* ProjectileWeapon (0xBCD, @4bc3fc : MechWeapon, sizeof 0x448)
FIX: AmmoBinConnection was an empty 1-byte struct -> retyped to the binary's 0xC
SharedData::Connection (was making the object 8 bytes short, sliding
MissileLauncher's missileCount off 0x448).
* MissileLauncher (0xBD0, @4bcff0 : ProjectileWeapon, sizeof 0x44C)
FIX: deleted muzzleVelocity (ALIAS of inherited ProjectileWeapon launchVelocity@0x410)
and salvoCount (SHADOW of inherited MechWeapon damageData.burstCount@0x3d4),
keeping the single own field missileCount@0x448.
* GaussRifle (0xBCE, @4bdcb4 : Emitter, sizeof 0x484 = Emitter + Vector3D muzzleVelocity)
DEDICATED bridge (not CreateEmitter): its FireWeapon is a no-op (mov [eax+0x414],0) --
a non-functional weapon in this 1995 build. Also fixed GAUSS.CPP's discharge write
rechargeLevel -> currentLevel@0x414 (the binary writes this+0x414).
Phase A safe-stubs FireWeapon (consume round + recoil, NO spawn): the reconstructed
Projectile (@4be1bc, 0x340) + Missile (@4bf5b4, 0x368) ENTITY classes are NOT spawn-ready
(Entity-base phantom members overflow their New() alloc -> heap corruption; New() takes
(int)this instead of the 0xD4 descriptor; Entity base size unconfirmed 2007-vs-1995, the
Mech 0x638-vs-0x854 problem). The workflow's adversarial verify flagged the live spawn as a
heap-overflow hazard and recommended exactly this phasing. Phase B = the entity byte-exactness
+ descriptor build + New(MakeMessage*) so a fired shot becomes a flying entity that damages.
Factory now 18 of 20 cases wired to real ticking classes (remaining: SubsystemMessageManager
0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]). Verified: Mad Cat (LRM/ballistic) + BLH construct
the real weapon classes + tick authentic charge/ammo/heat/recoil, DESTROYED-in-8, 0 crashes,
heapcheck-clean through construction. Energy weapons already damage via the mech4 beam path,
so combat is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9d82be46a1 |
P7: subsystem-tree alarm unification -- the whole PoweredSubsystem weapon/power subtree is byte-exact
The reconstruction modeled the binary's 0x54 AlarmIndicator (FUN_0041b9ec) with undersized
stand-ins (AlarmIndicator==ReconAlarm==4B; HeatAlarm==8B) across the entire subsystem tree, so
every field above an alarm sat at the wrong compiled offset. Retype every such stand-in to
GaugeAlarm54(0x54) and de-phantom each class against its ctor, so the whole PoweredSubsystem
subtree becomes byte-exact. An 8-agent read-only decomp-mapping workflow decoded every ctor
first; then hands-on implementation. static_assert-locked chain (verified vs the raw ctors):
HeatSink 0x1D0
-> PoweredSubsystem 0x31C retype electricalStateAlarm@0x264 + modeAlarm@0x2B8 @004b0f74
-> MechWeapon 0x3F0 retype weaponAlarm@0x350; delete 5 phantom tail fields @004b99a8
-> Emitter 0x478 delete outputVoltage/beamLengthRatio/firingArmed aliases
+ beamHit*/beamColor/beamHitData/energyRampTime phantoms;
retype beamOrientation EulerAngles->Quaternion(16B) @004bb120
-> PPC 0x478 (no own fields) @004bb888
PoweredSubsystem -> Sensor 0x328 (no alarm; 3 own fields) @004b1d18
PoweredSubsystem -> Myomers 0x358 (no alarm) @004b8fec
New systemic bug-class instances fixed (added to the CLAUDE.md checklist):
* alias field - Emitter outputVoltage==inherited rechargeLevel@0x320; beamLengthRatio==
beamScale.z@0x434; firingArmed==inherited useConfiguredPip@0x3E0
* phantom field - MechWeapon segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint
(past 0x3F0); Emitter beamHitPoint/beamImpact/beamImpactScalar/beamColor/
beamHitData/energyRampTime (binary writes inherited damageData/voltageScale
or the value is a method local)
Non-layout fixes required in the same wave:
* outputVoltage->rechargeLevel also in the compiled GAUSS.CPP:74/93 (external readers of the
removed Emitter field -- must grep EVERY TU, not just the class's own .cpp)
* MechWeapon::GetMuzzlePoint reimplemented faithfully (removed muzzlePoint collided with
Emitter's own fields at 0x3F0) via a BTResolveWeaponMuzzle void* bridge in mech4.cpp,
resolving the weapon's mount segment (inherited this+0xdc) through the owner segment table
* DetachFromVoltageSource fixed to set electricalStateAlarm not modeAlarm (raw @004b0e30
writes the 0x264 alarm)
The vehicleSubSystems aux-screen gauge raw reads (btl4gau2.cpp:868/952 at subsystem+0x2b8/+0x278)
and the Sensor RadarPercent path now read the CORRECT byte offsets (garbage under the short layout).
Verified: combat DESTROYED-in-8, 28 shots, 0 crashes, heat heatEnergy=1.34e7, every static_assert
lock passes, heapcheck-clean through construction (the phase the isolated PoweredSubsystem retype
had overflowed). LESSON: a factory-bridge runtime Check(sizeof<=alloc) does NOT fail the build --
only a static_assert sizeof lock catches alloc overflow at compile time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
1356870e56 |
P7: byte-exact re-base of the CORE heat leaf (HeatSink/Condenser/Reservoir/Generator/Myomers)
The reconstruction modeled the binary's shared alarm/connection types with undersized
stand-ins, sliding every field above them low (the 72-byte auxScreenNumber gap). Fix the
foundational heat-leaf classes byte-exact + static_assert-lock them, from the ctor decomp:
Shared types corrected:
* SubsystemConnection 4 -> 0xC (binary link node FUN_004af9cf; FUN_00417ab4 derefs +8)
* GaugeAlarm54 = 0x54 (real AlarmIndicator FUN_0041b9ec; STATUS level at +0x14,
so subsystem+0x184 == heatAlarm+0x14 == GetLevel())
WatcherGaugeAlarm now typedefs GaugeAlarm54 (Watcher branch locks stay valid).
Byte-exact + locked (ctor-verified):
* HeatSink heatEnergy@0x158 linkedSinks@0x164 heatAlarm@0x170 resource@0x1C4
pendingHeat@0x1C8, sizeof 0x1D0 (@004adda0)
* Condenser valveState@0x1D0 condenserAlarm@0x1DC (@004ae568)
* Reservoir reservoirAlarm@0x1D0 ... squirtEfficiency@0x22C, sizeof 0x230 (@4aef78)
* Generator stateAlarm@0x1FC, sizeof 0x250 (@004b225c)
* Myomers phantom moverConnection tail removed (fits 0x358)
Three systemic bug classes fixed (added to the checklist in CLAUDE.md / HARD_PROBLEMS.md):
* alias field - a subclass member re-declaring an inherited slot the ctor reuses
(Condenser refrigerationOutput==massScale@0x160; Reservoir
coolantCapacity==thermalCapacity@0x128) -> use the inherited name
* alarm-interior - a value read at alarm+0x14 modeled as a separate member
(HeatSink heatState@0x184, Reservoir injectActive@0x1e4)
-> route to alarm.GetLevel()
* phantom field - a member past the object end (Generator shortFlag@0x25C is really
*(owner+0x190)+0x25c the msg-manager, @004b0efc; Myomers
moverConnection@0x110 a write-only base slot) -> remove it
Heat conduction now reads the REAL heatEnergy=1.34e7 (not garbage); combat DESTROYED-in-8,
0 crashes, heapcheck-clean through construction.
REMAINING (measured; a distinct larger task): making PoweredSubsystem byte-exact grows it
+0x98 and cascades into MechWeapon/Emitter/PPC/Sensor/Myomers -- all model the 0x54
AlarmIndicator with 4-byte ReconAlarm / 8-byte HeatAlarm stand-ins and are short +
phantom-tailed; retyping without byte-exacting them overflows the Emitter alloc (heap
corruption). PoweredSubsystem kept on HeatAlarm(8) stand-ins (marked) pending a
subsystem-tree ALARM UNIFICATION. See docs/HARD_PROBLEMS.md P7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
dc6af8cef6 |
vehicleSubSystems: reconstruct the stubbed connection helpers from decomp
Replace the return-0 stubs with the real functions: - ResolveLink (FUN_00417ab4) = SubsystemConnection::Resolve (two-level deref of the plug's bound link); an unbound plug resolves to 0, the authentic no-source branch. - FindLinkedSubsystem (FUN_0041bf44) = GetAttributePointer(index) via the engine Simulation attribute-index system (the cooling-loop master at slot 3). - IsGeneratorDerived (FUN_0041a1a4) = the real IsDerivedFrom(PoweredSubsystem ClassDerivations 0x50f4bc) via a new powersub.cpp bridge BTIsPoweredSubsystem (shared with the aux-screen filter) -> the generator stateLamp now builds. So the cooling/power/voltage connections resolve real data when the plug is bound, instead of always reading 0. Build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4ab6748563 |
vehicleSubSystems follow-up: reconstruct BitMapInverseWipe::Execute (LeakGauge)
Replace the minimal-safe Execute stub with the authentic wipe (@004c5d08): round the level, repaint `frames` segments stacked vertically, each picking its strip frame (empty/half/full via colorA/colorB) from the level. The coolant LeakGauge in every SubsystemCluster now renders faithfully from its CoolantMassLeakRate value. Build clean, 7 panels, 0 crashes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |