844edbd2aaeb44c653c9a77f46ecd8a70f18ad39
31
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a83995a755 |
Locomotion: wire the AUTHENTIC per-mech turn-rate lerp (task #64b)
The master-perf disasm (0x4aa3d3-0x4aa4ff) computes the yaw rate as lerp(walkingTurnRate@0x574, runningTurnRate@0x578) by ground speed, not a constant. Wired it into the drive, replacing the bring-up constant kDriveTurnRate: - walkingTurnRate/runningTurnRate are now REAL named members. The ctor's `Wword(0x15d)/(0x15e) = model->...TurnRate * DegreesToRadians` writes were NO-OPS -- Wword() returns a shared static scratch cell, so the rates were silently discarded. Now stored + read by name. - The drive (mech4.cpp) computes authTurnRate: base = walkingTurnRate; above reverseSpeedMax(0x538) lerp walk->run across [walkStride 0x534 .. topSpeed 0x34c] with an over-run falloff runningTurnRate/t^2; clamp >=0; zeroed in the death/limbo/airborne leg states (1/2/3) or when deathAnimationLatched(0x650) is set. Falls back to kDriveTurnRate if the model gave no rates. The lerp is a RUN-speed refinement -- below reverseSpeedMax (walk / turn-in-place) the rate is the constant walkTR base; runningTurnRate < walkingTurnRate (a running mech is less maneuverable). Verified headless (DEV mech, BT_TURN_LOG): walkTR 1.309 rad/s (75 deg/s), runTR 0.873 (50 deg/s), turn-in-place + walk yaw at walkTR, no crash. [T1 logic / T2 runtime] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
267059ab88 |
Damage economy RECONCILED: 3 stand-ins fixed, 3 paths confirmed authentic (task #60)
Full 5-path audit of the damage economy vs the decomp (5-finder + adversarial-verify workflow), resolving the KB self-contradiction the binary-coverage audit flagged. CONFIRMED AUTHENTIC as-is (no change needed): - Energy beam (emitter.cpp): damagePortion = authored DamageAmount x (charge/seekV)^2; the ctor x1e7 and fire x1e-7 cancel (_DAT_004bafbc dumped from the exe = x87 80-bit 1e-7 exactly). - Autocannon (projweap.cpp): full authored DamageAmount from resource +0x19C delivered unmodified; the 0.0625 at :667 is the shooter's own gyro recoil, not the round. - Zone-armor BASE model: damageLevel += amount x damageScale[type] (engine DAMAGE.cpp:379, called mechdmg.cpp:427), legs x0.5, 1.0=dead. 3 STAND-INS FIXED (all byte-verified against the decomp): - A. mechdmg.cpp:451 -- read the phantom `stance` member (no binary offset, zero writers -> perma-0), so the leg-shot-out -> fall/death branch was DEAD. Now MovementMode() (mech+0x40, @part_012.c:6910). - B. mechdmg.cpp:458 -- guarded the leg partial-failure graphic on the always-0 IsAirborne() stub where the binary calls IsDisabled() (@0049fb54 = movementMode 2||9). On a wreck the binary SUPPRESSES the write; the stub let it corrupt graphicAlarm 9->4/3 -- the task-#52 wreck-graphic bug, now fixed AT SOURCE (was only masked by the IsMechDestroyed latch). - C. mech4.cpp:1551 -- flat kShotDamage=12 fed as the kill-score damageAmount (the KB self-contradiction: task #8 claimed it retired, but it was live). The score handler @0x4c02e4 derives the whole kill award from it, so every kill scored identically regardless of weapon. Now lastInflictingDamage -- the real killing-blow magnitude, latched in TakeDamageMessageHandler (mech.cpp:624), mirroring the per-hit path (mech4.cpp:1207). The phantom `int stance` slot is reused for the new Scalar member (size-neutral, no layout shift); init 0 in the ctor. DEFERRED (task #60-D, documented): the missile CLUSTER model -- the port fires N flying rounds (net armor total authentic) vs the binary's ONE missile with a random burstCount cluster roll (loses cluster variance + single-zone concentration). Blocked on an OPEN decomp semantic (does burstCount multiply armor or only the gyro kick? settle at FUN_004bef78 -> FUN_004be078 -> EXPLODE.cpp:209-210). VERIFIED live: clean build; 2-node fight -> clean center-mass kill (no crash, kills 0->1); [zone-armor] dump confirms per-zone armor 50-140 + legs x0.5. NB the displayed POINTS score still reads 0 -- a SEPARATE open gap (scoreAward + role/team/tonnage multipliers unwired); fix C corrected the damage INPUT to that formula. KB swept: open-questions.md (self-contradiction resolved + task #60 summary + deferred missile item), combat-damage.md (damageScale is type-indexed not even/odd; task-#52 source fix; kill-score section), RECONCILE.md (missile = ONE spawn not N), stale comments in mechweap.cpp (SendDamageMessage is LIVE), mislanch.hpp, mechdmg.cpp (FUN_0049fb54 = IsDisabled). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2e9c78d604 |
Gait: the AUTHENTIC walk lean -- interior/exterior clip sets, level cockpit + leaning mechs (task #59)
The MadCat (and every leg-mech) leans -8deg into a walk / -11deg into a run --
an authored jointhip (hingex) pose in the EXTERIOR gait clips that the walk/run
CYCLE clips never rebind, so it HOLDS. The 1995 game keeps a separate INTERIOR
('i'-suffix) clip set for the mech you PILOT: those shake jointshakey (cockpit
rattle) and OMIT jointhip, so your own view stays level while everyone else sees
you lean. The port had this entirely dead -- the local cockpit mech pitched
-8deg into the ground (user-reported "staring at the ground").
ROOT CAUSE (two stacked bugs):
- The authentic ctor clip-set gate (@part_012.c:10308-10320) was reconstructed
as no-op stubs: LoadLowDetailBody/LoadHighDetailBody MISLABELED the
FUN_004a80d4/86c8 GAIT-CLIP loader addresses as a "body LOD" pair, so the gate
did nothing and a separate unconditional LoadLocomotionClips always loaded
EXTERIOR clips.
- LoadLocomotionClipsExt (the interior loader) was a stub aliased to the
exterior loader.
FIX:
- Reconstructed the real 4-char INTERIOR loader (byte-exact vs @004a86c8; the
'i'-suffix table dumped from the exe @0x10d74d: swri/wwri/wwli/...; all 18
base clips confirmed present in BTL4.RES for mad/blh/ava).
- Fixed the ctor gate: getenv("L4VIEWEXT") || (instanceFlags&0xC)==4 -> exterior
(replicant / forced external view); else -> interior (local cockpit master).
Removed the mislabeling no-op stubs.
- PORT ADAPTATION (MaintainViewClipSet, per-frame in PerformAndWatch): the port
never sets the replicant COPY bit (all MP mechs build as local masters --
heat-sim/scoring/torso-watcher-connect all run on both, by design), so the
ctor gate lands everyone on interior. Pick the set by VIEWPOINT instead: the
mech you pilot keeps interior (level cockpit), every other mech flips once to
exterior (the lean). Reloads only on a viewpoint-status change; the sets
share stride data so the swap is seamless. Model pointers stashed at ctor.
VERIFIED live (BT_HIP_LOG probe): your walking mech writes jointhip=0 (level);
the peer's replica in the other pod's view leans at exactly -8.0/-11.1deg --
the authored clip values. A/B control: L4VIEWEXT=1 forces the walking master
back to 74 sustained -8/-11 writes.
KB: locomotion.md ("NO walk lean" audit conclusion CORRECTED -- the lean is
authored + sustained + clip-set-specific), asset-formats.md (.ANI parses
pitch/yaw/roll for all joint types; KeyJointPos translation; cycle clips carry
no pitch), open-questions.md (item closed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
02cdfd6576 |
Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows. Three reconstruction fronts closed: THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up): - PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog MIRRORS the watched subsystem's electrical level (+0x278), brownout downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong. - The factory watcher-CONNECT pass reconstructed (vtable slot +0x38, @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes): watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the SubProxy::Start() no-op -- every watchdog sat at 0 forever. - MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f = permanent brownout) and PowerWatcher's Derivation chains its REAL base HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the whole Torso/Searchlight/ThermalSight family). - KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not "HeatSink"); the btl4gaug heat-widget gate now tests it via the BTIsHeatWatcher bridge. THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes): - The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in every twist-capable .SKL; the camera + canopy ride the same hinge subtree through HingeRenderable's live matrix-stack compose -- ALREADY WORKING in the port. The crosshair stays screen-centered (center IS the boresight); the twist reads on the tape carets/compass/radar. - The real bug was the port's gBTAimX = tan(twist) slew (the falsified "body-mounted view" model): the camera already carried the twist, so the crosshair counter-slid to hull-forward and the fire ray with it. Deleted; the pick ray inherits the twist from the yawing eye basis. - Two instrumentation traps documented (chase-eye-as-default-camera, BT_FORCE_TORSO clobbering real joints -> the hook now only fills unresolved ones); an over-correcting explicit eye compose was added on those false readings and retired the same day. CONTROLS + REPLICATION: - Q/E spring-center on release (the axis is a twist-RATE demand; the old hold-deflection model drifted forever); X also zeroes the axis and pulses the authentic torso Recenter (@004b6918). M cycles control mode via the real CycleControlMode body. - Torso update-record DIRECTION fixed: engine truth is Write=serialize / Read=apply; @004b6a78 is the READ (was mislabeled Write) and the missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C, twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD -140-degree ghost twist. - Marching-ghost desync: 4 Standing-case guards zero stale reverse cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate). - Kill credit rerouted to the OBSERVED killer (lastInflictingID -> killer's player link) -- kills count, target K/D populates. KB: subsystems.md (watcher chain), multiplayer.md (record direction), combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view re-correction), decomp-reference.md (new addresses + tag fix), open-questions.md (dead capability-roster loops 2-4, snapshot CD read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
065c114590 |
Impact-FX FORENSICS wave: the i860 specialfx engine, per-round detonations, the ram economy closed
The "we're off the rails" reinvestigation -- a 5-thread evidence workflow
(logs / PFX data / RES model lists / decomp / port audit) + the i860 firmware
decode, then surgical fixes. Every claim tiered; the wrong turns are on the
record in the KB alongside the corrections.
THE OLD-STYLE SPECIALFX ENGINE [T1, firmware-decoded from VREND.MNG]:
rebuilt scratchpad/i860dis.py (binutils opcode table); mapped the dispatch
(data+0xdd0c; sfx trigger 0xf040cda0, install 0xf040cdc0, step ~0xf0413698,
instance init 0xf04128d8); decoded the heat model EXACTLY: per 30Hz board
frame h *= cool_a; RGB_ch = h_old*(h_new*cook_ch - 0.25) + 0.25 (K=0.25 =
the ember floor @VA 0xF080; kill at h <= 1e-4 @0xF0A0); alpha fades cool_b
x RAW dt (PER-SECOND -- the x30 scaling made laser hits invisible); 7s cap;
y_off = the kill plane; "variance" is DEAD DATA (binary reads "variance",
INI authors "varience"). The 13 descriptors (PPCHit/LaserHit/MissileHit/
Chunks/Sparks/Fireball...) parse from BTDPL.INI and render via the BTPfx
layer -- heat bursts draw the FIERY sheet (brightness over fire), .PFX keeps
GRAYSCALE (authored colours: DNBOOM orange, DDAM gray).
HIT-PACKAGE CENSUS [T1, RES byte-verified]: ppchit=[8] lzrhit=[9] mghit=[7,11]
canhit=[11] mslhit=[10,12,1023] explode=[6]; all 8 mech death lists identical.
Effect routing corrected in BOTH consumers: <100 = specialfx, >=1000 = psfx.
"SILVER MIST x5" ROOT CAUSE: SHKWAVE.PFX (mslhit's 1023) authors maxIssue=5
relPeriod=0.2 rate=1 -- the ONE file where rate contradicts the window; the
emitter trusted rate -> 5 shells at exactly 1Hz. Emission rate now always
maxIssue/releasePeriod.
PER-ROUND DETONATIONS [T1 @004bef78]: every missile round spawns its own
ExplosionModelFile at ITS impact (hull + terrain) -- a volley ripples 12
fireballs like the demos; rack-tube launch spread [T3] (GUIDED rounds only --
the slot-0 deflection sent every AFC100 shell 3.4deg left/2.5deg down: the
phantom "4th gold beam"); replicant salvos detonate too (launcher index rides
the visual push). Missile damage bundles through the shooter's messmgr with
the launcher's subsystemID (mslhit fires at the consolidated point; the
binary's dedup CONFIRMED [T1 @0049b784] -- a workflow agent's per-record
claim REFUTED by direct decomp read).
DAMAGE-BAND SEMANTICS [T1]: MechDeathHandler fires the CURRENT band effect on
any damage rise (the binary's changed-flag coalescing) -- a mauled mech under
fire smokes/burns per hit; bands 3/4 are authored fire plumes.
THE RAM ECONOMY -- CLOSED (3 layers, measured live):
1. armor = POINTS (every zone: damageScale = 1/armorPoints, armor 50-140;
the [zone-armor] BT_DMG_LOG dump);
2. StaticBounce prices rams with authored moverMass ~1.3e6 -> ~59,000 pts
@10m/s; the binary dispatches it RAW but pod MP dropped it on the local
replicant (MECH.CPP:986 warns) -- ram damage was NETWORK-INERT; our
task-#47 replicant forwarding surfaced it as a one-shot. Port
normalizes x1e-3 to the point economy [T3];
3. contact EDGE (ramLastVictim/ramContactLinger): the binary's bounce made
separation implicit; our gait-derived velocity re-priced full rams at
60Hz (the respawn explode-loop). One bump = one hit; pressed = BLOCK.
PLUS: sfx size x0.5 + 1.25m visibility floor + hot-phase occlusion [T3];
particle pool 2048->8192 (missile traffic starved laser bursts -- the
"intermittent effects"); replicant beam aging (a lost beam-END record pinned
a stale beam on forever); UV-variant noise stamps (mask-safe mirror/swap);
no scroll on particle stamps (material-path only); BT_FX_TEST/fxshot.py
self-verification harness ([beam-draw]/[zone-armor]/[collide-tx] telemetry).
KB: rendering.md (specialfx engine + census + closeout), combat-damage.md
(ram economy + band semantics).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
bb795e2805 |
MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified: COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity per contact and ProcessCollisionList walks EVERY touched solid per frame; with 2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy). Fix: frameEntryWorldVelocity restore per contact (damage always priced at the real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry. Gotcha #16 (engine-facility drift class). MISSILES: peer-visible salvos (the launcher record extension carries a salvo counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone never serialized), the authentic arc (authored MuzzleVelocity vector + the Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78), world-impact bursts (rounds detonate on cave geometry instead of phasing through), contact-only damage (flight-cap expiry = fizzle, no more teleport damage), live re-lead, and ballistic (unguided) shells for autocannons. projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6 corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded. RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244 is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation (never the translation); worldToView now Invert(view) built rotation-first. CulturalIcons sorted out of the moving grid (the phantom red pips were map props), visible-radius culls on all three draw passes, live pip verified at |delta| x ppm px. Gotcha #17 (verify the FUN_ body, not its call shape). WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X lamps, the panel look, the children enable and the ready-lamp gate (which had NEVER executed). Polarity chain corrected end-to-end (failedState, fed by real damage saturation). Root cause of the freezes: MFD page-mode gating -- the dev composite shows ALL pages at once, so off-page dials legitimately stopped; under BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio untouched). The SEH gauge guard now names its kills; repaint-heal resets the incremental arc after panel repaints; [panel]/[arc] probes added. COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole Player__StatusMessage queue; wired the binary's one producer -- the kill branch, victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS counted via the observed-death tally (each node scores every pilot from locally observed events, the same model as the KILLS credit) and the -2/-1 engine seed clamped for display. DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480, true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering, BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar (first second-chassis live outing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
afefaeece5 |
Mech: THE 0xBD3 UNTANGLE -- real SubsystemMessageManager in [0x10d], the mapper's one true home is roster slot 0 (task #7)
Binary census [T1]: mech+0x434 has exactly 2 writers (ctor init + factory case 0xBD3) and ONE reader (@0x4b984b in MechWeapon::SendDamageMessage @004b9728) -- pure message-manager semantics, nothing mapper-like. The factory case now builds the real SubsystemMessageManager (ctor @0049bca4, 0x130, ConsolidateAndSendDamage Performance); the misbuilt MechControlsMapper squatter is evicted. Every mapper consumer re-pointed to MappingMapper() (roster slot 0 = the binary's **(mech+0x128); SetMappingSubsystem @0049fe40 touches ONLY slot 0 -- its [0x10d] mirror removed). Non-viewpoint mechs get a slot-0 demand LATCH (base mapper, [T3] accommodation -- the binary leaves slot 0 NULL off-viewpoint but never drives/animates those paths; ours does). Verified: solo drive+gait clean; MP replicant gait through the latch (full run lifecycle); 3x kill/respawn cycle with type-6 records on the observer. Remaining (task #7 tail): the messmgr's consolidated TakeDamageStream send + weapon-path routing (@004b9728 real body). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
63206d7f19 |
Model record: the authoritative 200-byte overlay -- struct skew fixed, floor-25 era retired (task #4)
Two size errors (animationPrefix char[12] vs 4 bytes; a FICTITIOUS 64-byte skeletonName block -- the record has no skl name) skewed every member after word 0x10 by +2/+15 words and pushed the fieldXX block past the record end. Full writer map recovered (FUN_004a2da8 + Mover FUN_004238bc + Reticle FUN_00435ac8 chains) + runtime reader map (ctor pass @0x4a24d1-0x4a2968, capstone-verified). Struct rewritten as the exact overlay (static_assert- locked); all Pass-2 consumers renamed to real fields; retired: ctor + replicant + master-gait floor-25 hacks, the throttle guard, the fictitious classID stamp. Verified: accel=30 superStop=10 throttleAdj=1 (was ~1/OOB/0), deadbands byte-identical via named fields, full walk->run lifecycle clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a652ddcdbc |
MP: authentic update-record deadbands streamed from the model record (task #3)
Binary ctor @0x4a26a7-0x4a26ef: mech+0x768/76c/770 <- raw record words 0x26/0x27/0x28 (UpdatePositionDiffrence / UpdateTurnVelocityDiffrence / UpdateTurnDegreeDiffrence x pi/180 [const @0x4a2d44]). Read at RAW record offsets -- the Mech__ModelResource struct is layout-skewed (field98 gave -1.5 where the record holds 0.3; audit filed as task #4, also explains the old forwardCycleRate "floor 25" hack). Sender triggers now use the authored constants: pos-deadband type 0, quat-Y delta + yaw-rate delta + stopped-turn edge type 4 (binary @0x4aad35/@0x4aac2b/@0x4aac6c). Verified 2-node: madcat streams 0.3/0.045/3deg, type-4 spam gone, walking replicant clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c9f0c2a7f1 |
MP: the Mech-level update records @0x4a0c2c/@0x4a1232 -- all 9 types byte-exact (task #1)
- Writer transcribed from the recovered disasm; reader rewritten from part_012.c with every Wword absorber promoted to named engine/port members (updateOrigin/updateVelocity/projectedOrigin/projectedVelocity/nextUpdate/ lastUpdate + bodyTargetSpeed/latches/alarms). Wire sizes verified live: 0x14/0x20/0x2c/0x78. - movementMode UNIFIED with Simulation::simulationState (binary mech+0x40 = StateIndicator@0x2c currentState) -- death/limbo/airborne now replicate in every record header. Three mislabels of the same binary fns retired: SetInstanceFlags + RequestActionFlags -> Mech::ForceUpdate (updateModel |= mask, 0xfe03 disabled filter); IsNetworkCopy -> IsDisabled. - Senders wired byte-exact: gait transitions Force(8), knockdown Force(1|0x20), death Force(1|0x40), Reset Force(0x1f) + binary zero-set, perf-loop deadbands (speed type 2, orientation type 4, heat type 7). - 2-node verified: types 2/3/4 flow while driving; kill -> type-6 record (simState=9) -> the OBSERVER's replicant runs the wreck sink loop with no double death transition; respawn 0x1f burst snaps + un-wrecks the peer; walking replicant un-regressed (run 12, cycle tracking); solo clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
946323b696 |
Gyro: damage fan-out FUN_004b2980 -- the cockpit hit-BOUNCE is live (task #56)
Re-disassembled the unexported gap 0x4b2980-0x4b2d8b byte-exact (capstone over
section_dump.txt) and reconstructed Gyroscope::ApplyDamageResponse:
- zero-damage + Collision(type 0) no-op; hit direction = Damage::damageForce
or a RANDOM horizontal fallback when ~zero (per-component sign roll then
value roll -- binary-legal until senders fill damageForce)
- direction rotated by the yaw-only torso-twist frame (placeRot=(0,twist,0)
-> euler->matrix, the FUN_00408744 raw row-dot reproduced verbatim)
- per-type scaling amount / damageMultiplier[type] * damageResponse[type]
.{trans,pitchRoll,yaw,vibration}; Explosive alone multiplies burstCount;
each clamped at 1.3f
- four kicks: impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
vibrationDirection@0x390) and verticalImpulse(yaw, dir)
Layout corrections (byte-verified): gyro+0x390 is ONE Vector3D vibration axis
init (0,1,0) -- was mis-split as scalars animationOffset/Scale/Phase; mech
+0x5c4 is a FLOAT rumble-period countdown (gyroRumbleTimer) -- was mis-typed
int clipLoadGuard.
Call sites wired (binary-gated):
- take-damage hub: mech.cpp TakeDamageMessageHandler, FIRST action (@0x4a0264
in hub FUN_004a0230) -- an invalid-zone hit still bounces
- crushable-icon crunch: mech4.cpp, torque 0.4 along the bounce delta-v + up
impulse 0.2 (@4aa81e/@4aa86c)
- firing recoil: projweap.cpp FireWeapon, damage>3 -> impulse (0,0.6,-1.5)
x damage/16 (@4bc136-4bc19c) via the new Mech::GetGyroSubsystem accessor
DEFERRED (recorded in KB with the byte recipe): the alternate-gait engage
jolt + 0.4s rumble (@4aa158-4aa365) -- its gates are [T3]-flagged and it
mutates the #49/#50-stabilized gait machine.
Verified live: [gyro-dmg] fires per hit with correct type scaling; eyePosition
shows the damped bounce (ramp to ~0.015u, oscillating Y, decay to zero); kill
chain un-regressed; no NaN.
Also recorded in KB: the OFFSET-COCKPIT verification -- Thor's authored
jointeye tranx=+1.13 matches its canopy X-center exactly AND our in-game eye
reproduces it to the hundredth (the placement model is correct per-mech); and
the playable roster is 8 models (strider/raptor/firstrtr/blkjack have assets
but no model resource -- BT_FORCE_MODEL with those names = NULL-resource AV,
cdb-verified, not a code bug).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
cb85517ede |
Gyro: byte-exact re-enable -- ctor map, integrators, jointeye writers (task #56)
The Gyroscope subsystem is LIVE again (was a stub since the NaN revert).
Reconstructed byte-exact from @004b3778 / @004b2ec0 / @004b30ec / @004b33e0 /
@004b34ec + the byte-verified dispatch in the unexported Mech performance
FUN_004a9b5c (calls @0x4aaf74/0x4aaf83):
- ctor field map corrected: springConstant@0x1E8 / dampingConstant@0x1F4 were
mislabelled (the old 'eyeOrientation = r->springConstant' poison line), the
0x254-0x2B3 block (externalPitchPtr, work matrix, placement) was missing so
everything after was mis-offset, and clamps/accumulators were 0xCD fill --
every field now initialised per the binary; 33 offsets static_assert-locked,
sizeof == 0x3D0 exact
- integrators: state-minus-target displacements, componentwise damping
OVERWRITE (FUN_004086d0 is a component multiply, not a cross), position step
without dt, clamp to [posSpring, negSpring]; IntegrateBody's X/Z-crossed
force + integration terms
- writers: WriteMechJoint drives ONE node ('jointeye', type 5): TRANSLATION =
eyePosition spring, ROTATION = bodyOrientation spring; WriteEyeJoint is a
multiplicative sway attenuator on 'jointlocal' (post-anim only)
- dispatch moved out of GyroscopeSimulation into the Mech performance tail
(GyroFrameJointWrite bridge, mech4.cpp) with the binary gates; gyro<->torso
pitch link wired (gyro+0x258 = &torso currentTwist) via complete-type-TU
bridges, retiring the SubProxy::linkTarget landmine
- Mech ctor: deathAnimationLatched/legResetLatch were never initialised --
0xCDCDCDCD gated the writers off silently
Runtime-verified: joints resolve (both BallTranslation), Performance installs,
WriteMechJoint finite from frame 0 (no NaN), Madcat cockpit un-regressed,
combat targeting healthy. Empirical: spring targets are SYMMETRIC -> eye
equilibrium (0,0,0), clamps +/-0.1-0.15u -- the gyro is the hit-BOUNCE
mechanism, not a steady eye offset (hypothesis disproven, recorded in KB).
Pending (task #56 tail): the damage->gyro fan-out FUN_004b2980 (unexported
gap) so hits actually kick the springs; the mech+0x3F0 overspeed sway model;
the torso-pitch EyepointRotation writer (FUN_004b66b4) + glance-look states.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
b6fe686b22 |
Shadow: authentic terrain tilt + decal draw order (feet layer correctly)
User report: the ground shadow painted OVER the mech's feet. Root cause: the
flat quad + big depth-bias (-0.004 ~= 2-4 world units) workaround for slope
burial -- a bias big enough to beat the TERRAIN also beat the FEET in the
depth test, and the shadow drew in the late alpha-blend pass (after the mech),
so z-arbitration was the only layering control.
Fix, four co-dependent parts (user-verified live: feet on top, opaque,
survives inclines/declines):
- TILT (mech.cpp UpdateShadowJoint): apply the SKL's own contract for
jointshadow ("apply terrain angle to pitch and roll") -- quad up aligned to
the surface normal via an orthonormal basis fed to the ENGINE's own
LinearMatrix->EulerAngles conversion. Hand-derived Euler signs are exactly
how the prior tilt attempts "dug into the hillside"; the engine conversion
is convention-proof. ~35 deg cliff-guard cap.
- SAMPLER (mech4.cpp): surface gradient from the collision probes PLUS
BTVisualGroundLift per probe -- the quad hugs the VISIBLE terrain, whose
lift varies across a slope; world->local rotation by the TRUE yaw from
localToWorld, not the drift-prone gDriveHeading mirror (the task-#48 bug
class). BT_SHADOW_LOG traces the normals.
- DRAW ORDER (new PASS_SHADOW, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw
between the STATIC terrain and the DYNAMIC opaque bodies -- the classic
decal order. The mech, drawn after, z-passes over the shadow: the bias
can never paint the shadow over the feet, structurally.
- BIAS pairing (L4D3D.cpp): tilt on (default) -> decal epsilon -0.0008;
BT_SHADOW_TILT=0 flat fallback -> the old -0.004. BT_SHADOW_BIAS overrides.
KB (locomotion.md): the four-part arrangement recorded as co-dependent, with
the gait-desync-symptom diagnostic hint retained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
54b6663b4d |
MP task #47 SOLVED: cross-pod damage + kill (validate the master mech)
Full cross-pod KILL now works: instance A (host 2) shoots B's mech (host 3) dead over the network; B runs the death transition on its own screen. Verified on a 2-node one-box run (BT_MP_FORCE_DMG on A): B's mech takes ~36 remote hits of 12 and is DESTROYED, with wreck swap + explosion. ROOT CAUSE (one bug): B's own MASTER mech was INVALID. Entity::Receive(Event*) does `if(!IsValid()) event->Defer()` -- so a cross-pod-delivered TakeDamage that arrived, resolved to B's real mech, and Posted, was DEFERRED forever; the handler never ran. The reconstructed Mech::Make force-set ValidFlag only for replicants (the P6 bring-up hack for the partial MakeReady handshake); a locally-created master never got validated. FIX (one line): Mech::Make now sets ValidFlag for the master too -- the reconstructed ctor builds the mech synchronously, so it's safe. Nothing else changed: targeting, the virtual victim->Dispatch, the replicant reroute, the wire, receive, and id resolution were ALL already correct. Two prior root causes were BOTH mis-diagnoses from the WRONG messageID: Entity::TakeDamageMessageID is 18, not 21. Every earlier probe filtered msgID==21, never matched a real TakeDamage, and mis-correlated the stray 21 hits' 3:19 / classID-48 against the real 3:22 hit -- inventing (a) a "cross-TU Entity layout divergence" (disproven: offsetof identical in both TUs, sizeof 444, entityID@0x17C) and (b) a "localID corrupted on the wire". Both FALSE; the id is correct end-to-end (3:22 = B's master). - game/reconstructed/mech.cpp: validate master + replicant at Make. - game/reconstructed/mech4.cpp: BT_MP_FORCE_DMG cleaned to a concise gated 2-node cross-pod reproducer (real virtual Dispatch path). - context/multiplayer.md, reconstruction-gotchas.md: corrected root cause + the validity-defers-cross-pod gotcha. Engine diagnostics reverted to clean. Solo un-regressed (the solo enemy was already force-valid; the change only ADDS ValidFlag). btl4.exe builds; full mission loop clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c78662a0f1 |
MP cross-pod damage ROOT-CAUSED: EntityID localID corrupted by hostID on the wire (task #47)
Traced the dispatched-message delivery end to end with BT_MP_NET / BT_MP_FORCE_DMG. Everything works except one wire step: - A's Entity::Dispatch reroutes the replicant's TakeDamage (application->SendMessage(ownerID=3, EntityManager, msg)); POST-Dispatch the message carries entityID=3:22 (host:local) -- the replicant's own ID, matching B's master (GetEntityID()==3:22). VERIFIED sent. - On B the message ARRIVES, GetEntityPointer finds an entity, Posts it, the event drains (ProcessEventTask = ProcessOneEvent(0)), Event::Process runs, Receive finds+calls a handler. VERIFIED the full deliver chain. - BUG: B receives entityID=3:19, NOT 3:22 -- the localID dropped by exactly the hostID (3) between A's send and B's receive. So GetEntityPointer(3:19) returns the WRONG entity (classID 48, not the mech 0xBB9), whose base handler ignores the unaimed hit -> 0 damage. Auto-replicated UPDATE records (msgID 18) arrive with the correct 3:22 and find the mech, so the corruption is specific to the dispatched- message wire path/direction. Next: the host-relative EntityID (de)serialization on the dispatched-msg path (RoutePacket / packet EntityID encoding) -- diff vs the update path which translates correctly. Diagnostics retained (all BT_MP_NET-gated, off in solo -- verified: solo 22 hits, 0 probe noise). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a9c3e96f20 |
MP combat step 1 (target any peer) DONE; step 2 (cross-pod damage) localised (task #46)
STEP 1 -- target any peer mech: DONE + verified. A live-mech registry (BTRegisterMech/BTDeregisterMech from the Mech ctor/dtor) collects every mech -- player, dummy, and peer replicants. The boresight world-pick walks BTGetTargetCandidates (all mechs != shooter, closest ray hit) and the whole fire/damage block retargets from the solo gEnemyMech to the picked hotTarget; missile/projectile validation generalised via BTIsRegisteredMech. Verified one-box: instance A enumerates B's mech as a live ReplicantInstance (20 zones, ownerID=3). Solo un-regressed (pick->damage->kill clean, 28 hits + DESTROYED). STEP 2 -- cross-pod damage: dispatch + engine reroute are CORRECT and invoked with a valid owner, but the dispatched message doesn't reach the master. Entity::Dispatch (ENTITY.cpp:244) reroutes a replicant's msg via application->SendMessage(ownerID,...); BT_MP_FORCE_DMG proves A dispatches at B's replicant with ownerID=3, yet B takes 0 STEP-6 damage. Corrects the KB's [T3] "Dispatch already reroutes": the reroute FIRES; the break is downstream in the transport/delivery of a dispatched entity-message (update records flow fine -- net-rx works). Next MP task = that wire delivery. Diagnostics BT_MP_LOG / BT_MP_FORCE_DMG retained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
0bceb9a638 |
gauges/map: populate the radar entity grid + draw mech contact blips
Root-caused why the radar showed 0 contacts: the gauge renderer's moving/static entity grids (queried by GetMoving/StaticEntitiesWithinBounds) were NEVER populated -- the 1995 game fed them from ExecuteImplementation's InterestingEntity iterator, which the WinTesla port dropped. Fix (engine): add GaugeRenderer::RebuildEntityGrid() -- Clear + repopulate movingEntities from the world's DynamicMaster + DynamicReplicant entities (the vehicles/mechs; AllEntity would flood the grid with ~300 props/effects/terrain). Called from the map's Execute phase 0 (under the gauge's guarded Execute, so a fault disables only the map). Verified: contacts are now found, combat un- regressed (TARGET DESTROYED), heap-clean under BT_HEAPCHECK. Fix (map): DrawMoving now draws a cross blip for mech-class contacts (0xBB9) when the authentic pip raster set is absent (the BT pip table is stubbed in this engine build). KEY PROJECTION FIX: worldToView is a camera (view->world) matrix and Point3D::Multiply applies only rotation+scale (NOT translation), so projecting the absolute entity position left the viewpoint un-subtracted and the blip landed off-screen (verified: blip @(580,-1154)). Project the RELATIVE position (delta = entity - viewpoint, already computed for the range check) instead -> the blip projects viewpoint-relative + heading-rotated correctly (@(164,221)). radarRange default 500->1000 (1km zoom) so contacts within ~500m show on the radar (SetTargetRange, the real player zoom, is still stubbed). BT_MAP_LOG traces the blip coords. STATE: the grid population + viewpoint-relative blip projection are correct and combat-safe. A reliable on-screen contact demo still needs the display-scale vs spawn-distance reconciliation (the BT_SPAWN_ENEMY dummy sits at a varying, large distance -- 377-828m -- beyond the display radius across runs) + the authentic pip symbol/name/video-object infrastructure (still stubbed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
48ed3a8b73 |
gauges: register + wire the map (radar) -- the tactical display renders
MapDisplay (the radar/threat-map gauge, btl4rdr.cpp) was fully reconstructed (ctor + Execute + DrawViewWedge/DrawStatic/DrawMoving/DrawNames) but never registered and its data attributes never published. Wire it up end-to-end: DATA (the attributes the map binds): * Mech table extended to the full 0x15..0x38 (was the 0x21 prefix): add RadarRange@0x2f -> radarRange, RadarLinearPosition@0x30 -> radarLinearPosition (Point3D* -> &localOrigin.linearPosition), RadarAngularPosition@0x31 -> radarAngularPosition (Quaternion* -> &localOrigin.angularPosition), DuckState @0x37 -> duckState; the rest bind the shared attrPad (kept dense so Find can't strcmp a gap). New members set in the Mech ctor; radarRange defaults to 500 (SetTargetRange is still stubbed) so nearby contacts are on-scale. The map's position/angle attributes resolve to Point3D**/Quaternion** (AttributePointer is int Simulation::*, so the ATTRIBUTE_ENTRY reinterpret binds pointer members fine -- verified live: posPtr/anglePtr resolve). * Sensor (named "Avionics") publishes RadarPercent/SelfTest/BadVoltage: define Sensor::AttributePointers[] + build the previously-empty Sensor::AttributeIndex chained to PoweredSubsystem::GetAttributeIndex() (= HeatSink's dense index). WIDGET (the registration glue -- NOT in the assert-anchored decomp, so reconstructed from the config + ctor): * MapDisplay::methodDescription (config "map") + MapDisplay::Make. The offset_position keyword "center"/"bottom" is typed as a STRING and converted in Make, avoiding a ModeManager named-constant registration. Registered in BTL4MethodDescription[] (btl4grnd.cpp now includes btl4rdr.hpp). Three crash fixes exposed once the map ran (each a pre-existing stub the map is the first consumer of): * Sensor radarPercent went hugely negative (RadarBaseline - HeatMasterEnergy(), where the inherited heatEnergy is un-normalized in the not-byte-exact heat-leaf branch) -> the radar reported non-operational. Guard: treat an out-of-[0,1] heat term as no penalty (marked bring-up; real overheat penalty needs the heat normalization). * ResolveOperatorEntity was a stub returning NULL -> EntityPosition(NULL) crashed CalculateBounds. Fixed to the renderer linked-entity + application viewpoint fallback (same as HeadingPointer). * MapName::ExtractFromEntity didn't guard a NULL entity (the binary's Verify(False) iterator-mismatch path compiles out at DEBUG_LEVEL 0) -> EntityPosition(NULL). Guarded. Verified live (BT_DEV_GAUGES): the radar now draws the view wedge (the mech's 180-deg FOV cone) and completes the full wedge/static/moving/names cycle with operating=1, cap=1, scale=500, position/angle resolved, 0 crashes. Combat un- regressed (TARGET DESTROYED after 8 hits), heap-clean under BT_HEAPCHECK. Permanent gated diagnostic: BT_MAP_LOG traces the phase/draw pipeline. Contacts show 0 (the spatial-query entity classification is a follow-up); DuckState now resolves for the duck button too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9e31408ba2 |
gauges: publish Mech attribute table (dense prefix 0x15..0x21 -> LinearSpeed live)
mech.cpp shipped an EMPTY, unchained Mech::AttributeIndex (a default-constructed AttributeIndexSet passed to DefaultData), so Simulation::GetAttributePointer severed the whole parent chain -> EVERY bare Mech gauge binding (LinearSpeed / MaxRunSpeed / DuckState / RadarRange / ...) resolved to NullAttribute. Add the Mech AttributeID enum (full binary set 0x15..0x38, VA 0x50be84) + a Mech::AttributePointers[] table + Mech::GetAttributeIndex() chained to JointedMover::GetAttributeIndex(), and change DefaultData to pass it. DENSE-TABLE HAZARD (systemic): AttributeIndexSet::Build (SIMULATE.cpp:565) sizes the built index to max(id) and Find (SIMULATE.cpp:663) strcmps EVERY slot, and Build does NOT zero gap slots -> an unpublished id between the parent's NextAttributeID and the max published id holds a garbage entryName -> AV on the next name lookup. So the table is a DENSE PREFIX 0x15..0x21 (JointedMover's NextAttributeID through LinearSpeed): the ids the BLH cockpit doesn't bind (collision/eyepoint/reticle/footstep/anim-state) point at one shared read-only attrPad member; CurrentSpeed/MaxRunSpeed bind to the existing gait members legCycleSpeed/reverseStrideLength; LinearSpeed binds a new linearSpeed member populated each frame in PerformAndWatch (|adv|/dt = forward ground speed). Ids 0x22..0x38 stay declared in the enum for later extension (RadarRange/DuckState when the map/duck widgets land) but are NOT published yet. New members appended to Mech's own region (no locked base/gait offset shifts); inited in the ctor. Verified live (BT_DEV_GAUGES + drive): the radar-surface SPEED readout (numericSpeed(LinearSpeed), a base engine primitive already drawing NULL) now shows 225 (~63 u/s in the widget's display units) instead of 0, tracking the mech's motion. Heat surface CurrentTemperature=77 un-regressed. Combat un- regressed (TARGET DESTROYED after 8 hits), 0 attribute-not-found, and Mech construction + attribute-index build is heap-clean under BT_HEAPCHECK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8b36440d05 |
subsystems: un-stub Wave 4 readouts + Wave 6 Myomers (10->15 factory cases)
WAVE 4 (standalone readouts) -- Sensor(0xBC3)/Searchlight(0xBD8)/ ThermalSight(0xBDE)/AmmoBin(0xBCB) un-stubbed via Create<Class>Subsystem bridges + Torso-style de-shim (drop cross-family shadow fields, redirect accessors to the real inherited base, static_assert layout locks). - FIX: Searchlight/ThermalSight ctors gated their Performance on the shadow segmentFlags(=0) so it NEVER installed; switched to owner->simulationFlags. - AmmoBin: retype ammoAlarm HeatAlarm->WatcherGaugeAlarm(0x54) + drop the statusState@0x40 shadow -> exact 0x22C layout. - Guard HeatWatcher::WatchSimulation against the unresolved watchedLink (null-deref exposed once these sims run; faithful fix = resolve the link). - Heat-leaf branch (Sensor) is not byte-exact -> overflow-lock only. WAVE 6 (Myomers 0xBC6, mover-coupled) -- structural un-stub, gated BT_MYOMERS (default on; =0 -> Actuator stub). Wired INERT: MyomersSimulation early-returns (advanced-damage gate stubbed) + no-op mover feed, so the live JointedMover is untouched and the gait cannot regress. De-shim drops the owner*/segmentFlags shims to fit the exact-0x358 alloc. Authentic mover/heat coupling deferred (needs messmgr 0xBD3 + reconciling the mover feed with the gait cutover). Verified: BLH tick 20->27, Mad Cat 24, combat DESTROYED un-regressed, locomotion un-regressed, 0 crashes, 0 heap detections under BT_HEAPCHECK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7b7d465e5e |
Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.
Layout:
engine/ MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
models) + image codec; the minimal rp/ headers the audio HAL needs
game/ reconstructed BT logic + surviving-original BT source + fwd shims
+ WinMain launcher
content/ full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
docs/ format specs + reconstruction ledgers
reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
tools/ MP console emulator + map/resource scanners
One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|