master
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
5b46655b82 |
Audio: ENABLE sound -- real OpenAL + in-tree WAV loader; both backend DLLs were no-op STUBS (task #50)
Root cause of "no sound, ever": the two audio backend DLLs shipped in the repo are fakes. libsndfile-1.dll exports 15 funcs BY ORDINAL ONLY (no names) and sf_open() always returns NULL; OpenAL32.dll (72KB) imports only KERNEL32 -- no dsound/wasapi/winmm -- so it is a pure no-op that returns fake handles (ctx=0x00000001, alGenSources->0) and never touches the hardware. The whole render->device->buffer->source->play chain ran clean and silent. Fixes: - OpenAL32.dll: replace the stub with the real OpenAL Soft 1.25.2 Win32 build (imports AVRT/ole32/WINMM, real WASAPI backend). The exe imports the 25 AL funcs by NAME so it is a drop-in; alGenSources now yields a live source and alSourcePlay reaches AL_PLAYING. - libsndfile: DROPPED entirely. It is replaced by LoadWavPCM() in L4AUDRES -- a tiny RIFF/WAVE fmt+data reader that loads our soundbank WAVs (16-bit PCM) straight into the AL buffer. Removed the .lib/.dll from the link + copy and git-rm'd the stub. (This also kills the "ordinal 50 could not be located in libsndfile-1.dll" load-failure popup: adding an sf_strerror import bound to an ordinal the 2..16-only stub could not satisfy.) - Soundbank: 241 samples cracked from AUDIO1/2.RES (SF2 v1.0) by tools/sf2extract.py into content/AUDIO/*.wav + the allPresets[2][128] table (audiopresets.cpp), replacing the zero-init btstubs stub. All 241 now load (alErr=0). BT_AUDIO_TEST plays buffer0 as proof-of-life; BT_AUDIO_LOG traces the chain. Remaining: in-game triggering (AudioEntities on fire/step/engine/explosion) so PlayNote fires during play -- next audio wave. Co-Authored-By: Claude Opus 4.8 (1M context) <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> |
||
|
|
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> |
||
|
|
247e51e1e1 |
Reset-based respawn: reuse+heal the mech, not create-a-new-one (task #52)
The respawn glitches (2 mechs, on-fire respawn, camera-inside, can't-control, wreck-never-disappears) all traced to one architectural divergence: our respawn SEVERED playerVehicle on death and CREATED a new mech, leaving the old as a permanent wreck and building a duplicate viewpoint whose old render tree was never torn down. The authentic engine (FUN_0049fb74 + RPPlayer) REUSES the same mech entity: on respawn Mech::Reset heals it and moves it in place. Implemented faithfully, adapted to our layout (the 1995 raw offsets map to different 2007 engine fields, so reset the equivalent named members, not the offsets): - Mech::Reset (real, was a reposition-only stub): reposition + kill dead-reckon (projectedOrigin/projectedVelocity/updateVelocity + our relocated gait accumulators) so the replicant stops lerping to the death spot; clear the death latch (movementMode=1, graphicAlarm=0); Heal every damage zone (new Mech__DamageZone::Heal: full structure, intact skin); DeathReset (vtable+0x28) every subsystem; ForceUpdate to broadcast. - btplayer.cpp: VehicleDead no longer severs playerVehicle; the respawn re-post gates on the mech still being dead; DropZoneReply resets the EXISTING mech in place (heal+move) instead of creating a new one, then fires the warp. Warp moved to the shared placement (initial drop-in + respawn). Verified 2-node: mech entity ID stays 3:22 across 3 deaths (reused, not a new 3:32); each Reset logs alive=1, 20 zones healed, 33 subsystems reset; A sees ONE mech (no wreck+new pair). Warp fires each respawn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
63c1c5a460 |
Implement the translocation sphere -- the "blue warp" respawn effect (task #52)
BTTranslocationRenderable was a no-op stub; now reconstructed over the engine's POVTranslocateRenderable: loads tsphere.bgf and runs a collapse-on-arrival / expand-on-reveal sphere (scale 100->1 over 1.3s, then 1->150 over 1.0s, rotating) keyed on the player's SimulationState dial. Drawn direct from the render loop by BTDrawTranslocationSpheres (beside BTDrawBeams, PASS_ALPHABLEND) -- the same accommodation the beams/reticle use. The asset loads by FILENAME (tsphere.bgf), not through the RES table -- which is why every resource-name search missed the effect for three rounds. Trigger wiring: btl4vid.cpp MakeEntityRenderables builds the sphere for the LOCAL player (the authentic wiring builds it only for replicants + a POV fade for self, but peer player-attribute replication isn't wired -- on a replicant SimulationState/ DropZoneLocation read uninitialised). btplayer.cpp pulses SimulationState DropZoneAcquired->VehicleTranslocated at respawn (a 1.4s flip timer stands in for the engine's +1s drop-zone re-post) and writes the respawn origin into the DropZoneLocation attribute. Verified 2-node (BT_TLOC_LOG): on respawn the sphere collapses (~100->5) then expands (->150) at the valid drop-zone origin, deduped to <=2 active, respawn cycle un-regressed, no render errors. Visual appearance (colour/size) still needs a live look; the authentic see-others'-spheres path needs player replication. Co-Authored-By: Claude Opus 4.8 <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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|