master
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f4b2a7f87f |
Audio: fix POISONED calibration rate (raw-offset garbage) + calibrated audio clock (task #50)
Two real bugs found + fixed chasing the footstep volume: 1. BTL4Application::MakeAudioRenderer read sample_rate from a raw 1995-layout offset (divisionParameters+0x10) -- the databinding trap: it yielded -1.6e14 (measured), poisoning Renderer::calibrationRate for the ENTIRE audio system. Every AudioTime conversion (sequence event scheduling, compression curve durations, Seconds_To_Frames) was garbage. Sanity-clamp to the authored 1000 frames/sec default (BT_AUDIO_LOG logs the value). 2. AudioHead::Execute fed audioFrameCount raw OS ticks; now converts ticks -> calibrated frames via SystemClock::GetTicksPerSecond (1000ms fallback when the static isn't measured yet), so AudioTime consumers see the rate they were calibrated for. With rate=1000 + ms ticks the units now align end-to-end. Footstep status: chain verified through pulse -> matcher -> Start -> renderer; the volume mixer's two inputs receive only value-0 AudioControlSequence events even with correct timing -- the authored event VALUES need decoding next (sequence tempo/divisions ctor dump; possibly Verify()-stripped tempo garbage or the events are resets and the true volume rides another control id). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
99757a947d |
Audio: footstep chain traced to the END -- gait VINDICATED; audio clock 18x unit mismatch found (task #50)
MAJOR RESULT -- THE GAIT IS CORRECT, the enum was mislabeled: LoadLocomotionClips (binary-verified slot map) proves namedClip==&animationClips[5]: clips 6/7 = wwr/wwl (WALK cycle), 12/13 = rrr/rrl (RUN cycle -- what autodrive uses at full throttle), 10/11 = wrr/wrl, 14/15 = rwr/rwl, 16-21 = reverse set. The mech2.cpp "MechAnimationState" enum (0x3c-stride name table) does NOT match the runtime clip ids -- it is the mislabel (12/13 are NOT StandToReverse). The authored AudioStateTriggers on AnimationState (statecfg dump: states 1,2,5,8,9,10,11,14,15,16,17,20,21,22,23,26,27 -> Start) align EXACTLY with the TRANSITION clips under the true map; the CYCLING strides (6/7, 12/13) have no state triggers because the FootStep pulse attribute serves them -- our FootStep reconstruction is the authentic design. DO NOT renumber the gait. Footstep chain, fully mapped (all empirtical): pulse -> AudioMatchOf(match=1) -> Start on the FootFall DirectPatchSource -> DROPPED at volume 0. Volume path: an AudioControlMixer (2 inputs, never fed) <- an AudioControlSplitter <- AudioControlSequence timeline events. The sequences DO fire -- but only value-0 events (the tick-0 initializers); the later (real-volume) events never land. NEW ROOT-CAUSE CANDIDATE [T3]: audio clock unit mismatch. The renderer is constructed with DefaultRendererRate=30 (calibrationRate: Seconds_To_Frames = s*30) but AudioHead::Execute sets audioFrameCount = Now().ticks which advances ~550/s (measured via the [audioclock] probe) -- sequence event scheduling runs ~18x off the authored timing. Fix candidate: calibrate the audio renderer to the actual Now().ticks rate (or drive audioFrameCount at the calibrated 30/s). Also corrected en route: FUN_004b9550/95b8 are MechWeapon ConfigureMappables/ ChooseButton (mapper EnterConfiguration +0x38), NOT Myomers::ConnectToMover; +0x31c there is fireImpulse. Myomers::speedEffect has NO binary writer found beyond the ctor 1.0f (its 'mover coupling' stand-in is spurious). Diagnostics added: [statecfg] AudioStateTrigger ctor dump, [split] splitter forwards, [seqev] sequence timeline events, [audioclock] frame-rate probe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c4be8d3a07 |
Audio: footstep volume chain FULLY mapped -- blocked on gait-state id mismatch (task #50)
Chain (every hop verified live): FootStep pulse (SetBodyAnimation, decay in PerformAndWatch) -> AudioMatchOf match=1 -> Start(2.0) on the FootFall DirectPatchSource... which the renderer DROPS because its volume is 0: an AudioControlMixer (outCtl=3/Volume + a second outCtl=5 mixer) feeds the source, and BOTH mixer inputs are 0 -- nothing ever sends them. Eliminated as senders (all traced): AudioScaleOf scales (29 total, none target the mixer), AudioControlMultiplier (one instance, other target), Random/ AudioSampleAndHold (RNG verified WORKING -- varied samples; RANDOM.cpp is in the build, the self-init ctor runs). HYPOTHESIS [T4, next session]: the mixer inputs are fed by AudioStateTriggers on AnimationState keyed to the AUTHORED gait-clip ids (walk 2/3, run 8/9 = per-gait footstep volume). Our runtime body SM walks in states 12<->13 -- which the recovered MechAnimationState enum names RightStandToReverse/ LeftStandToReverse. If the authored audio expects 2/3 for forward walk, the runtime clip NUMBERING in the reconstructed gait SM is offset from the authentic ids -- a locomotion-layer mismatch with implications beyond audio (the audio config is an independent witness to the true clip numbering). Verify via the AudioStateTrigger configs on the AnimationState watcher (trigcfg dump exists) and cross-check the SetBodyAnimation clip table. New diagnostics: [snh] SampleAndHold sends, [mix] mixer forwards with input index + sum, [scalecfg] authored scale bindings/boundaries, [volset]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1d019e8109 |
Audio: footstep pulse VERIFIED end-to-end to the matcher; volume chain traced (task #50)
Footstep chain progress (each step empirically verified): - The FootStep watcher is an AudioMatchOf<Logical> (AudioLogicalTrigger extends AudioMatchOf, NOT AudioTriggerOf): authored config match=1 -> Start(2.0) on a DirectPatchSource. [trigcfg]/[matchcfg] ctor dumps added. - The pulse was pinned at 1: the decay lived in IntegrateMotion, which the current gait path NEVER CALLS. Moved to Mech::PerformAndWatch (provably per-frame) with a time-based 150ms window sized to the ~10Hz watcher poll. The matcher now fires per stride (14/run, was 1). - Every footstep Start is DROPPED at the renderer: the source's volumeScale is 0 (five VolumeAudioHandler sends of exactly 0 = inert/one-shot primes). The volume arrives through the authored control chain (AudioControlMultiplier inputs ctl 100+; one 0 input pins the product). Chain scales identified on this entity: LocalVelocity (live), LocalAcceleration (live), Myomers. SpeedEffect (=1.0 healthy; one authored scale maps [0,1]->[1,0] INVERTED), UnstablePercentage (INERT PAD -- always 0!), HeatSink.CurrentTemperature. PRIME SUSPECT: an UnstablePercentage-fed multiplier input primes 0 once and never updates (the pad never changes), pinning footstep volume at 0. - SF2 numbering fact recorded: 38 presets in AUDIO1 / 5 in AUDIO2 have wPreset != file index (gap at preset 77); my table keys by wPreset. The 184x ProgramButton01 (bank1 patch83) storm at ~7/s needs an identity check against index-numbering (it may be the wrong sample for that patch id). Diagnostics added (BT_AUDIO_SPATIAL/BT_ATTRBIND_LOG): [trigcfg]/[matchcfg] ctor dumps, [matchfire] with component ptr+class, [volset] VolumeAudioHandler, [mult0] multiplier zero-products, scale sends with authored boundaries, [fswatch] dedicated footstep watcher tracer (g_btFootStepAddr), SetupPatch ENTRY with bank/patch/state, Simulation::DebugAudioWatcherCount. NEXT: back UnstablePercentage with a real (live) member -- it is the mech's stability 0..1 (the stabilityAlarm/gyro family) -- or confirm via a one-run chain dump which multiplier input pins the FS source; then the footstep transient should clear the 0.3 drop gate at walking speed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ba3040ea09 |
Audio: footstep-chain diagnostics -- every link verified except the final trigger fire (task #50)
Instrumentation (all BT_AUDIO_LOG/BT_AUDIO_SPATIAL-gated): mech watcher-poll probe (delayed flag + socket count via new Simulation::DebugAudioWatcherCount), per-sim audio-socket size in ExecuteWatchers, trigger notifications (val>0), watcher poll-rate/change probes, footstep pulse address trace. VERIFIED working: FootStep binds to the real member (pulse addr == bound ptr, same run); pulses fire per stride (40/run); the watcher registers on the mech (audioWatchers=20 after audio-object creation); the mech's poll runs (delayed=0, simFlags=0x1110); Logical == int (STYLE.H:132) matches the member type. The idle<->moving engine crossfade (AudioMotionScale on LocalVelocity through the restored watcher poll) is CONFIRMED audible in play. REMAINING: the FootFall transient still doesn't reach SetupPatch -- next probe is the trigger's streamed threshold/inverse config and the speed-scaled transient volume at the renderer drop gate (LowAudioVolumeThreshold=0.3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
744ef8cc16 |
Audio: restore the dropped ExecuteWatchers poll -- polled audio watchers were ALL dead (task #50)
User: "when I start moving all the sounds fade away, no footsteps." Diagnosis chain (all empirical, BT_AUDIO_SPATIAL/BT_ATTRBIND_LOG traces): - The audio head DOES track the mech (listener-relative positioning verified while driving) -- not a spatial bug. - The idle sounds correctly STOP on leaving the standing state; the MOVING sounds never started because every POLLED audio watcher was dead: the reconstructed Mech::PerformAndWatch replaced the engine performance but dropped the ExecuteWatchers() step from the engine tail (Simulation:: PerformAndWatch = Perform -> ExecuteWatchers -> WriteSimulationUpdate). Only PUSHED StateIndicator watchers (SetState->Execute) ever fired -- which is exactly why state sounds worked but footsteps/motion-scaled audio didn't. FIX: poll ExecuteWatchers() (AreWatchersDelayed-gated) before the update write. Immediately unlocks the polled family: MissileLoaded01/LaserLoaded01 ready dings, ProgramButton01, motion scales (PlayNote 15 -> 30 per run). - FootStep (0x1e) backed REAL: was the inert attrPad (an AudioLogicalTrigger polls it -- threshold-crossing pulse per foot plant). New footStep member, pulsed by SetBodyAnimation on entering any locomotion clip 1..0x17 (runtime stride alternation MEASURED as body states 12<->13 -- the enum-name numbering does not match runtime clip ids), decayed by IntegrateMotion (~1/3 s). - Footstep volume is speed-scaled (AudioMotionScale on LocalVelocity): at standstill the transient start computes volume 0 and the renderer drops it (LowAudioVolumeThreshold) -- so footfalls are audible at real walking speed. Diagnostics kept (BT_AUDIO_SPATIAL): spatial dist/vol per source, CLIPPED/DROP/ START at the request gate, vol=0 factor breakdown, scale->0 sends, trigger notifications, watcher poll-rate probe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5ba541ed56 |
Audio: subsystem STATE sounds -- GaugeAlarm54 IS a StateIndicator; register Generator/Condenser/Reservoir state (task #50)
The audio subsystem binds AudioStateWatchers (class 28) to per-subsystem state attrs (GeneratorState/CondenserState/ReservoirState) BY NAME. Recovered the watcher TYPE per attribute via a MakeObjectImplementation ClassID trace. Root cause of the subsystem-state crash: the binary's 0x54-byte subsystem alarm (FUN_0041b9ec, reconstructed as GaugeAlarm54) IS a StateIndicator -- its "three sub-indicators" @0x18/0x2c/0x40 are the audio/video/gauge watcher SChains, and level@0x14 is currentState. The reconstruction had modeled that tail as an opaque `_tail`, so the sockets were never constructed -> AddAudioWatcher AV'd on 0xCDCDCDCD. - GaugeAlarm54: give it the three REAL, constructed SChainOf<Component*> sockets + AddAudioWatcher/Video/Gauge; SetLevel now fires the watchers on a level CHANGE (empty sockets = no-op, so the ~all other alarms are unaffected). levelB (weapon LevelCountB "reset source") is left untouched -- weapons unchanged. sizeof stays 0x54 (static_assert holds -> SChainOf<Component*> is 0x14, layout exact). - Register the state attrs -> the subsystem's own alarm (own AttributePointers[] chained to the parent): Generator.GeneratorState->stateAlarm, Condenser. CondenserState->condenserAlarm, Reservoir.ReservoirState->reservoirAlarm. Those alarms are already SetLevel'd by the sim, so the state audio fires on transition. - AudioStateWatcher guard now validates the +0x18 SOCKET (what AddAudioWatcher touches), not the +0 vtable -- GaugeAlarm54 is non-polymorphic at +0 (raw header) but has a real socket at +0x18. audiostate skips 85 -> 10 (only AmmoBin AmmoState/SimulationState left). The Logical/Scalar/Enum subsystem attrs (ReportLeak/GeneratorOn/ConfigureActivePress/ MotionState/SpeedOfTorsoHorizontal/TargetRangeExponent) stay inert (read 0, silent, non-crashing) -- they need backing members in size-locked layouts (a polish wave). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7b1e469ffa |
Audio: in-game TRIGGERING works -- real StateIndicator attrs + audio-watcher databinding (task #50)
Enabling audio unlocked L4AudioRenderer::CreateEntityAudioObjects, which binds AudioStateWatchers to entity/subsystem state attrs BY NAME (attribute-pointer databinding) and calls AddAudioWatcher on them. The reconstruction pointed the Mech's state attrs at the scalar read-pad `attrPad` (fine for gauges that READ a value, fatal for a state watcher that calls a METHOD) -> AV in SChainOf::Add on 0xCDCDCDCD. Root-caused via cdb + a BT_ATTRBIND_LOG trace in AttributeWatcher. Mech-level state audio now REAL + driven (verified: walking fires SetupPatch + PlayNote/alSourcePlay, patches 78/80/116): - AnimationState (0x1f) / ReplicantAnimationState (0x20): real StateIndicators (0x21 states covering MechAnimationState 0..0x20), driven from SetBodyAnimation so footstep/gait/transition sounds fire on animation change. - CollisionState (0x16): real 4-state StateIndicator driven from ProcessCollision via collisionTemporaryState (the deferred @4aa741 per-frame zero + the part_012.c:15406-15413 contact tail, InitialHit accumulation; the 1-vs-2 Slide split awaits the 0x240/0x244 floats still stubbed by fieldAt()). - @0x44c CORRECTED: "ammoState (0/1 leaking/2 dry)" was a mislabel -> it is collisionTemporaryState (never read as ammo). Bring-up guards [T3, temporary, self-clearing] so audio-on is STABLE while the subsystem state models are still stubs (Generators/Condensers/Myomers/Torso/ Avionics/Reservoir/ControlsMapper -- ~15 subsystems, ~40 attrs: GeneratorState, CondenserState, ReportLeak, ...): a NULL subsystem attr redirects to an inert pad (was a fatal Fail); an AudioStateWatcher on an unconstructed StateIndicator (null/ 0xCDCDCDCD) skips instead of AV. Both pass automatically once the subsystem is reconstructed. Those subsystem sounds stay silent until then (their audio wave). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
de8f6d02c1 |
MP: world structures (garages/walls) TARGETABLE -- boresight ray-tests the static collision tree (task #50)
The boresight's non-mech pick only sampled the VISUAL heightfield (BTGroundRayHit), which on arena1 is a single flat 'sky'-named ground mesh (and class-42 BuildTables likewise holds only 'sky') -- so shots passed THROUGH the garages/walls and only mechs moved the HUD range axis (user-reported regression: "buildings used to move the range axis; now only mechs do, and you can only fire at a peer"). Real fix: the boresight now ALSO ray-tests the ZONE'S STATIC COLLISION SOLID TREE -- the same geometry that already blocks the mech's walk. Authentic engine mechanism: Mover::FindBoxedSolidHitBy already tests the static world via zone->GetCollisionRoot()->FindBoundingBoxHitBy(line). Factored its "static world" tail into Mover::FindStaticSolidHitBy(Line*) (static solids only, no movers), wrapped by Mech::WorldStructurePick(start,dir,range,&hit) (builds the world-space Line, reads the entry point back via line->FindEnd since HitByBounded clips line->length=enter). Boresight pick order is now: closest MECH (PickRayHit, damage zones + lock) -> closest STRUCTURE (WorldStructurePick; occludes a mech BEHIND it; designates the gBTTerrainEntity sentinel + entry point, so the range caret reads the structure distance and NO lock ring draws, mech4.cpp:4529) -> flat ground (BTGroundRayHit) -> sky (fire-at-nothing). Also un-skips arena1's misnamed-'sky' flat ground so the ground tier works (btvisgnd geometry-aware skip + [mapent]/[rendent] census). Verified headless: a BT_WSWEEP horizontal ray-fan on arena1 tracks position (3/24 hits near the boundary -> 17-21/24 inside the interior garage cluster -> 3/24 past it = DISCRETE interior solids, not an enclosing box), no crash/assert/AV. Interactive aim (BTGetAimRay) can't run headless (no window -> noRay), so the sweep is the headless proof; interactive aim is user-verified. Note: FindBoundingBoxUnder (the ground/containedByNode BoundingBoxTree) is DOWNWARD-only (gravity/ground-snap: *height = FindDistanceBelowBounded), useless for a horizontal boresight; the static SOLID tree's FindBoundingBoxHitBy is the only ray-vs-world query. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a8eb8a427f |
MP: FIX peer spin hang/divergence -- exact quaternion integration + frequent orientation (task #50)
Answers 'how did the original handle this?' from the decomp (subagent hunt):
the 1995 binary's replicant reckoner (FUN_004ab1c8 -> FUN_00409f58, part_000.c:9359)
integrates heading EXACTLY: build a unit axis-angle rotation quaternion from
angularVelocity*dt ({axis*sin(t/2), cos(t/2)}) and Hamilton-multiply it onto the
heading (FUN_00409d9c) -- exact for any timestep, stays on the unit sphere. It
further carries the full orientation quaternion in the FREQUENT pose record
(FUN_0040a938, 7-float pose), so the dead-reckon gap stays tiny.
Our reconstruction diverged two ways, both fixed:
1. ReconQuatIntegrate (mechrecon.hpp) -- the reconstruction of FUN_00409f58 -- was
STUBBED as , a crude small-angle VECTOR add. Restored to
the real exact axis-angle composition. (A 'no stand-ins' violation: the comment
even wrongly claimed Quaternion::Add == FUN_00409f58.)
2. The engine Mover reckoner (MOVER.cpp AcceleratedDeadReckoner/LinearDeadReckoner)
also did the vector Add on projectedOrigin.angularPosition -> over a long peer
record gap it diverged to ~180deg then snapped (the reported spin HANG/hesitation).
Routed both through a new ExactAngularProject() helper (same exact math).
3. Orientation only rode the sparse type-4 resync; during a PURE spin the linear
dense-send never fires (not translating), so the gap ballooned (~1.6s) and the now-
exact projection sat far ahead -> the slerp jumped. Added an ANGULAR dense-send
(resync every frame while |yawRate|>0.1), mirroring the original's frequent-
orientation model -> gap stays tiny -> smooth.
Verified live-autonomous (BT_AUTODRIVE+BT_FORCE_TURN + BT_RENDHDG render-rate probe):
the ~180deg divergence + multi-radian snaps are GONE (rendered maxStep 0.05-0.10 rad,
no jumps). User confirms: no frame hang/hesitation. MOVER.cpp change is strictly
more correct (exact==crude for the small per-frame master case; only large-gap peer
extrapolation changes), so walking is unaffected.
KNOWN REMAINING (separate, smaller): the turn-STEP leg animation (trn clip, mech2.cpp
advance_normally) runs at a FIXED idleStrideScale cadence that does not scale with the
rotation rate, so the legs lag/skip vs the (now-correct) body rotation. Next.
Co-Authored-By: Claude Opus 4.8 (1M context) <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> |
||
|
|
fd055281a8 |
Combat: THE AUTHENTIC DAMAGE ECONOMY -- authored per-weapon amounts through the real fire chain (task #8)
Three root causes, all fixed: 1. kDamageScale was 1.0 -- _DAT_004bafbc is an x87 float80 = 1e-7, cancelling the ctor's x1e7: damagePortion = authored DamageAmount x (charge/seekV)^2 (closed form at fire time; madcat AC=25/LRM=50/ERLL=6, bhk1 PPC=12/SRM=35). The observed "0.25" was the degenerate EC=1 fallback, never authored data. 2. CheckFireEdge NaN latch: TriggerState carries ControlsButton INTS; the release value (-65) is a negative NaN. The binary's x87 unordered compare read it as "released"; IEEE-correct float compares latched the edge detector shut after the first release -- the reason the emitter discharge chain NEVER fired in-game. Fixed with bit-pattern sign compares. 3. The weapon-side submission LIVE: Emitter::FireWeapon fills damageData (amount + damageForce=target-muzzle [the gyro directional-bounce feed] + impact) -> MechWeapon::SendDamageMessage (@004b9728 real body) -> messmgr consolidation with per-weapon records + explosion bundling. The mech4 bring-up damage block + flat kShotDamage retired to diag hooks. Bonus: LODReuseHysteresis 0.82 -> 0.33 (double misread). Zone model verified byte-exact (no change). Solo end-to-end: 5-record volleys (2 PPC + 3 ERML with authored amounts), per-weapon zone granularity, explosions, kill. Heat stays bring-up scale pending the heat-calibration audit [T3]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
04a2049ce2 |
MP task #47: DISPROVE the Entity-layout root cause; correct the record
The prior commit ( |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
fb8c1d6ace |
gauges: colorMapperMultiArmor (worst-of-8-zones) + fix the interpreterTable overflow [widget recon 4]
Reconstructs colorMapperMultiArmor -- tints one schematic colour (a whole torso section) by the WORST of up to 8 damage zones: - MultiArmorConnection (@004c346c/@004c34f4): scans 8 zones via entity->damageZones[idx], keeps the max damageLevel, count==0 ? 100 : Round(worst*100). - ColorMapperMultiArmor::Make/ctor (@004c3c48/@004c3d60): resolves 8 named zones to indices via Entity::GetDamageZoneIndex (13-param methodDescription). ALSO fixes a latent HEAP CORRUPTION that registering this widget exposed (and that would have blocked every further widget): GaugeInterpreter's bytecode buffer is a fixed interpreterTableSize=46864 (GAUGREND.h) tuned for RP's config, and its Insert() bounds guard is a Verify() that compiles out at DEBUG_LEVEL 0. As each BT gauge widget is registered, more of BT's larger L4GAUGE.CFG resolves into bytecode (vs being parse-skipped); colorMapperMultiArmor's 13 params x hundreds of calls pushed the total past 46864 -> silent overflow past the char[] -> heap smash detected later in mission bitmap loading (not the gauge code). Fix: interpreterTableSize -> 262144 (sized for BT's full config). Verified: parses, builds, all zones resolve, no /FORCE unresolved, combat un-regressed (TARGET DESTROYED, 0 crashes), stable. Details: docs/GAUGE_COMPOSITE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a256813395 |
gauges: secondary/radar MFD composites live on a dev box (Milestone B)
The pod's secondary/radar cockpit surface now renders as an inset in the 800x600 dev window under BT_DEV_GAUGES: radar/tactical grid, SPEED/HEADING/ MAGNETIC/PROP dials, and the color-coded ARMOR DAMAGE mech schematic -- real gauge content (nzSec=27247), composited from the shared CPU pixelBuffer. Composite pass (engine): SVGA16::DrawDevInset palette-expands the secondary plane into a MANAGED texture on the MAIN device and draws an XYZRHW inset quad; BTDrawGaugeInset() reaches the gauge renderer's 'sec' port and is called from DPLRenderer::ExecuteImplementation as the last draw before EndScene. Three reconstruction bugs fixed to get real content: 1. BTL4Application::MakeGaugeRenderer signature mismatch (REAL fix): the reconstructed no-arg override only HID the 2007-engine's widened 3-arg virtual MakeGaugeRenderer(int*,int*,int*), so the engine built a base L4GaugeRenderer whose ctor never parsed gauge/l4gauge.cfg -> empty symbol table -> "undefined label 'bhk1Init'" -> no ports/gauges. Matched the 3-arg signature so it truly overrides (args ignored; BT is fullscreen). Same bug class as the BTL4GaugeRenderer(false,NULL,NULL,NULL) ctor fix. 2. Parse hung on undefined primitives (gated dev accommodation): the BT gauge primitive table (BTL4MethodDescription) is still a stub, so an unknown primitive -> ReportParsingError -> Fail() -> a MODAL dialog freezing the parse. Under BT_DEV_GAUGES, skip the unknown primitive's params so labels register and the base "configure" primitive builds the ports. 3. Gauge widgets AV on NULL data bindings (gated): NumericDisplayScalar's NULL value_pointer -> GaugeConnectionDirectOf ctor deref (bind NULL->static zero); RankAndScore::Execute derefs unreconstructed game state (Gauge::GuardedExecute SEH wrapper -> Disable(True) on first fault). All guards gated on BT_DEV_GAUGES: default DEV un-regressed (TARGET DESTROYED, 0 crashes) and the pod path is byte-unchanged (strict Fail, no guards). Remaining (docs/GAUGE_COMPOSITE.md): the real gauge WIDGET reconstruction (BTL4MethodDescription method table + gauge->game-state data bindings), then Step 2 (RP<->BT MFD port-name reconcile) + Step 3 (2-window layout). 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>
|