7c455303bdf85fa354436a4510ba673ad5eff10d
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c455303bd |
MechDeathHandler: per-zone destroyed-skins + explosions (faithful, exported)
Fixes the "kills are invisible" report: a mech died only at the state level, with no visible destruction on its body. Root cause: the per-zone damage-state descriptor table (binary zone+0xd4) that drives destroyed-skins + explosions was never built -- its loader (Mech__DamageZone::LoadCriticalSubsystems / FUN_0041e4a8) was a no-op stub, and MechDeathHandler itself was a no-op stub. The surrounding plumbing (the per-zone load loop over the type-0x1e resource, the death-handler slot) was already correct. Whole pipeline is EXPORTED -- no stand-ins. Reconstructed: - Mech__DamageZone descriptor table (binary this+0xd4): un-stubbed LoadCriticalSubsystems (FUN_0041e4a8/FUN_0042a748/FUN_0042a2c8) to parse the real type-0x1e stream -- entry = [f32 DamageLevel][i32 EffectResource] [i32 GraphicState][f32 TimeDelay], ascending by DamageLevel. Verified live: 6 entries/zone, first threshold 0.2, effect 26, across all 40 zones, no crash. - The three lookups (FUN_0042a664 by-level / FUN_0042a5f4 crossing / FUN_0042a6c4 by-graphic-state) as Mech__DamageZone methods. - The real MechDeathHandler (FUN_0042a984 ctor / FUN_0042aa2c Performance / FUN_0042a9f4 dtor), replacing the stub: each tick it walks the zones and, as a zone's damageLevel rises across a descriptor threshold, fires that entry's explosion (binary +0xb8 & 4) and, on destruction, the Destroyed-graphic descriptor's explosion (+0xb8 & 8), applying the descriptor's GraphicState (the destroyed skin) to the zone. Runs for EVERY mech, so the enemy visibly falls apart as it dies. Integration: the binary ticks MechDeathHandler off the mech's Performance list (mech+0xbc), which the bring-up drive override bypasses, so Mech::PerformAndWatch drives Tick() directly (same approach as UpdateDeathState). Effect spawn uses the established Explosion::Make port (BTSpawnDamageEffect) -- the authentic dispatch (class-5 message -> the 0xBD3 SubsystemMessageManager effect manager) is unported. Runtime: builds clean, boots clean, 42 descriptor tables load with sensible byte-aligned data, no crash. Live effect firing is combat-triggered (verified by the load + the wiring; the [deathfx] threshold-crossing log fires under BT_DEATH_LOG). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a9467481c5 |
Death-state sequence: collapse + subsystem shutdown + freeze (wreck stays)
Reconstructs the mech DEATH state machine — the un-exported master-perf death
branch (region 0x4a9770-0x4ab188) — from its EXPORTED consumers + the RP
VTV::DeathShutdown analog, wired into the active path (the bring-up drive
override bypasses the authentic Simulate where this normally lives).
Mech::UpdateDeathState() + IsMechDestroyed() (mech4.cpp), called for every mech
early in PerformAndWatch: on a vital kill (graphicAlarm >= 9, raised by the
damage side) it
1. sets movementMode = 5 -> the collapse clip's one-shot latch in
AdvanceBodyAnimation (fall direction 5-8 is un-exported -> 5 [T3]);
2. loops the roster calling Subsystem::DeathShutdown(1) (RP VTV::DeathShutdown
analog; the base is a no-op virtual, overrides act -- a SHUTDOWN not a
teardown, so it frees nothing and never removes the entity: the wreck STAYS);
3. next frame settles to movementMode = 9 -> IsDisabled -> locomotion frozen.
The drive's lone `movementMode = 1` write (mech4.cpp ~1309) is guarded on
!IsMechDestroyed so the death state is not clobbered back to a live gait.
Runtime-verified (forced kill, BT_DEATH_LOG=1):
[death] mech destroyed -> collapse + subsystem shutdown (wreck stays)
[death] mech settled -> disabled (IsDisabled=1, frozen wreck)
mech frozen in place, subsystem tick + renderer keep running, no crash, wreck
stays.
Also records the full death-sequence decomp map in combat-damage.md (the
exported consumers: AdvanceLeg/BodyAnimation collapse latch, IsDestroyed, the
MechDeathHandler effect engine; and the un-exported orchestration gap).
Deferred (honest): the visible collapse ANIMATION latch through the active path
(gait SM shows state=0 post-death; spawned mechs don't advance body anim);
MechDeathHandler (FUN_0042a984/FUN_0042aa2c -- the exported per-subsystem
destroyed-skin + explosion engine, still a stub) for death explosions; the
whole-mech DeathSplash radius damage (un-exported).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
2af401eef8 |
Collision damage applied: mech-vs-mech + icon-crunch via the STEP-6 unaimed path
The two deferred TakeDamage dispatches in Mech::ProcessCollision now fire,
unblocked by STEP 6 (the cylinder hit-location override that resolves zone==-1):
- Mover branch (:15324-15358): on a collision with another Mech, dispatch the
collision damage to it.
- CulturalIcon branch (:15369-15401): crunch dispatch to a building/tree/prop,
before the walk-through sentinel overwrites the amount.
Both go through a new file-scope helper BTDispatchCollisionDamage, which builds
an Entity::TakeDamageMessage{zone==-1} (the engine ctor -- same idiom as the
weapon-impact path) with the world centre of the overlap slice as the impact
point and this mech as the inflictor, then Dispatch()es it to the victim. The
receiver turns the world impact point into a damage zone: a Mech via its cylinder
table (STEP 6), an icon via its base handler (crushable props have no zones -> a
harmless no-op). Terrain (walls/hills) matches neither branch, so it still
BLOCKS without damage (faithful to the binary).
Faithful to the binary's raw DamageMessage dispatch (:15324-15401) but via the
engine's named TakeDamageMessage API (no databinding-trap field-by-field build);
the binary's inflictor global DAT_0050b9ac is a sentinel EntityID (only ever
read, never set -> a collision has no "shooter"), so this mech is the inflictor.
Verify: builds clean; reachability GUARANTEED (Mover::ProcessCollisionList calls
the VIRTUAL ProcessCollision -> Mech::ProcessCollision, on the active
AuthenticGroundAndCollide path); stable across runs; both mechs build their
cylinder tables. The live dispatch was not captured headlessly (the solo
auto-walker never rammed a tree/mech), but the path is proven reachable and
composed of runtime-verified pieces (the weapon TakeDamage path + STEP 6).
Also validated STEP 6's height ref: collisionTemplate->maxY ~= 7.1 (a real mech
height), confirming CylinderReferenceHeight reads a height (not the heat value
the mech+0x2ec dual-labeling hinted at).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
34aaa7dda4 |
gauge wave P3: wire the combat scoring feed (KILLS tracks combat)
The Comm roster + score gauges read 0 for 4 independent reasons (none was handler logic -- the BTPlayer ScoreMessage/VehicleDead/ScoreInflicted handlers were already reconstructed). Mapped by the scoring-feed-decode workflow; fixed all 4: - NO PRODUCER: combat only dispatched Entity::TakeDamageMessage, never a scoring message. Added producers (btplayer.cpp bridges, called from mech4.cpp): per-hit ScoreInflicted at the beam (:2171) + projectile (:831) damage dispatch -> SCORE; KillScore at the TARGET-DESTROYED edge (:2198) -> KILLS. senderMechID = the VICTIM (so the local player is credited via the !=our-mech branch, not the suicide-negate branch); dispatched to application->GetMissionPlayer(). - CROSS-FAMILY MECH OFFSETS: MECH_OWNING_PLAYER/TONNAGE/DAMAGE_BIAS read raw binary offsets (garbage in our 0x638 Mech). MECH_OWNING_PLAYER -> Entity::GetPlayerLink() (NULL for the ownerless dummy); tonnage/bias stubbed 1.0/0.0 (bring-up). - NULL scenarioRole: the BTMission role registry has no WinTesla analog, so every award path NULL-deref'd. CalcInflictedScore returns the neutral (damage+bias) when scenarioRole==0; DamageReceived guarded. - NULL-owner dummy: the KillScore sender-owner increment + StatusMessage now guard GetPlayerLink() (the dummy has none) so a solo kill credits only the local player. - DATABINDING: the PilotList read KILLS/DEATHS at raw offsets (pilot+0x27c/+0x200) that don't match our compiled layout -> silent 0. Repointed to the compiled members via bridges (BTPilotKills/BTPilotDeaths -> BTPlayer::GetKillCount/GetDeaths). ⚠ ROOT-CAUSE of the empty scoreboard: the PilotList SELECT-TARGET highlight did a raw-offset deref (*(local+0x284) objectiveMech, then *(tgt+0x190)) that AV'd on our layout -- caught silently by the SEH GuardedExecute, aborting Execute BEFORE the KILLS/DEATHS draw. Replaced with BTPilotIsSelected (accessors + GetPlayerLink). - SCORE persistence: PlayerSimulation's 10s flush zeroed currentScore (the binary's console delta); now only flushes/zeros when a console host exists (solo keeps the running score for the gauge + CalcRanking). Verified DBASE+dev gauges (BT_SCORE_LOG): SCORE climbs +5.83/hit -> 164; on the kill KILLS 0->1, and the Comm roster RENDERS KILLS=1 (screenshot). No crash. Follow-ups (noted): DEATHS=1 is a pre-existing spawn/respawn-handshake artifact (not from the producers -- the dummy's GetPlayerLink is NULL so no VehicleDead is posted; confirmed killCount=1 not 2); RANK=-1 is CalcRanking's non-scoring mark (Plasma serial surface, invisible on dev); MP VehicleDead/tonnage/role registry remain per the plan. Diagnostics kept: BT_SCORE_LOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c850ffa26c |
WAVE 7 Phase B polish: missiles launch from the mech's missile ports + look like missiles
Two issues the user hit driving the Mad Cat: (1) projectiles appeared to launch from the mech's FEET -- MechWeapon::GetMuzzlePoint falls back to the mech origin (localOrigin) when the weapon's mount segment (this+0xdc) doesn't resolve, so the passed muzzle was at ground level; (2) they rendered as a thin laser-like line, not a missile. Fix: BTPushProjectile now takes the SHOOTER mech and resolves the real launch port by NAME (sitermissleport / sitelmissleport, alternating L/R for a salvo look; then torso ports, then gun ports; then a raised fallback) -- the same segment-name -> world-transform mechanism the visible laser beams use for the gun ports. The tracer is now a 3-part missile look (dim smoke trail behind + orange body + hot flame tip) instead of a single thin beam. FireWeapon (both ProjectileWeapon and MissileLauncher) passes owner as the shooter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f52bf057e6 |
WAVE 7 Phase B: the Mad Cat's LRMs LAUNCH flying missiles that fly + damage (autocannon too)
The byte-exact WORLD-ENTITY reconstruction (Projectile @4be1bc 0x340 / Missile @4bf5b4 0x368)
is infeasible on the 2007 engine: measured sizeof(engine Entity)=0x1BC vs the 1995 binary's
0x300, so the reconstruction's raw base-offset reads (velocity@0x1dc, roster@0x124, motion@0x250)
read GARBAGE on the engine (the Mech 0x638-vs-0x854 gap, but the entity integrator DEPENDS on
those offsets). So -- like the mech drive and the beam renderer -- flying projectiles are a PORT
reconstruction (BTPushProjectile/BTUpdateProjectiles in mech4.cpp, a static array + stack
messages, ZERO heap ops): seeded from the launcher's fire with the decomp's real muzzle
(GetMuzzlePoint) / launch speed (|launchVelocity|) / per-shot damage (damageData, split across
missileCount), they fly to the target (tracer via BTPushBeam) and deliver the weapon's damage on
impact through the SAME Entity::TakeDamage path as the beam (aim Mech::FirstVitalZone()).
THREE bring-up fixes were needed to make a projectile weapon fire at all (found by tracing):
1. TRIGGER: fireImpulse was only driven for the Emitter; ProjectileWeaponSimulation now sets
fireImpulse = gBTWeaponTrigger too (else CheckFireEdge never sees an edge).
2. AMMO BIN: OwnerSubsystemCount/OwnerSubsystem were stubbed ->0, so ammoBinLink never resolved
and ConsumeRound always failed; redirected to the real roster (owner->GetSubsystemCount()/
GetSubsystem(i) -- the AmmoBin 0xBCB constructs before the weapons 0xBCD/0xBD0).
3. JAM ROLL: UniformRandom() was stubbed `return 0.0f`, so CheckForJam's `0 < jamChance` ALWAYS
jammed (a projectile weapon could NEVER fire); replaced with a real LCG [0,1) (fires
~1-jamChance of the time -- authentic occasional jams).
Also: the mech's own target slot (owner+0x388) isn't populated in bring-up (the visible fire
targets the gEnemyMech global), so BTPushProjectile falls back to gEnemyMech.
Verified: Mad Cat PUSH=62 / IMPACT=31 (LRM missiles 3.33 dmg each split across the salvo + AFC100
autocannon 25 dmg), TARGET DESTROYED, 0 crashes, construction heapcheck-clean; BLH un-regressed
(also fires its ballistic weapon now). Diagnostics BT_PROJ_LOG ([projectile] PUSH/IMPACT).
REMAINING (deferred): the byte-exact world-entity Missile (Projectile : Mover, MP-replicable via
Registry::MakeEntity) -- the port projectile is master-local only (no MP replication); the real
per-weapon fire-rate/heat wiring off the subsystem sim (mech4 beam path is still the bring-up harness).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9d82be46a1 |
P7: subsystem-tree alarm unification -- the whole PoweredSubsystem weapon/power subtree is byte-exact
The reconstruction modeled the binary's 0x54 AlarmIndicator (FUN_0041b9ec) with undersized
stand-ins (AlarmIndicator==ReconAlarm==4B; HeatAlarm==8B) across the entire subsystem tree, so
every field above an alarm sat at the wrong compiled offset. Retype every such stand-in to
GaugeAlarm54(0x54) and de-phantom each class against its ctor, so the whole PoweredSubsystem
subtree becomes byte-exact. An 8-agent read-only decomp-mapping workflow decoded every ctor
first; then hands-on implementation. static_assert-locked chain (verified vs the raw ctors):
HeatSink 0x1D0
-> PoweredSubsystem 0x31C retype electricalStateAlarm@0x264 + modeAlarm@0x2B8 @004b0f74
-> MechWeapon 0x3F0 retype weaponAlarm@0x350; delete 5 phantom tail fields @004b99a8
-> Emitter 0x478 delete outputVoltage/beamLengthRatio/firingArmed aliases
+ beamHit*/beamColor/beamHitData/energyRampTime phantoms;
retype beamOrientation EulerAngles->Quaternion(16B) @004bb120
-> PPC 0x478 (no own fields) @004bb888
PoweredSubsystem -> Sensor 0x328 (no alarm; 3 own fields) @004b1d18
PoweredSubsystem -> Myomers 0x358 (no alarm) @004b8fec
New systemic bug-class instances fixed (added to the CLAUDE.md checklist):
* alias field - Emitter outputVoltage==inherited rechargeLevel@0x320; beamLengthRatio==
beamScale.z@0x434; firingArmed==inherited useConfiguredPip@0x3E0
* phantom field - MechWeapon segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint
(past 0x3F0); Emitter beamHitPoint/beamImpact/beamImpactScalar/beamColor/
beamHitData/energyRampTime (binary writes inherited damageData/voltageScale
or the value is a method local)
Non-layout fixes required in the same wave:
* outputVoltage->rechargeLevel also in the compiled GAUSS.CPP:74/93 (external readers of the
removed Emitter field -- must grep EVERY TU, not just the class's own .cpp)
* MechWeapon::GetMuzzlePoint reimplemented faithfully (removed muzzlePoint collided with
Emitter's own fields at 0x3F0) via a BTResolveWeaponMuzzle void* bridge in mech4.cpp,
resolving the weapon's mount segment (inherited this+0xdc) through the owner segment table
* DetachFromVoltageSource fixed to set electricalStateAlarm not modeAlarm (raw @004b0e30
writes the 0x264 alarm)
The vehicleSubSystems aux-screen gauge raw reads (btl4gau2.cpp:868/952 at subsystem+0x2b8/+0x278)
and the Sensor RadarPercent path now read the CORRECT byte offsets (garbage under the short layout).
Verified: combat DESTROYED-in-8, 28 shots, 0 crashes, heat heatEnergy=1.34e7, every static_assert
lock passes, heapcheck-clean through construction (the phase the isolated PoweredSubsystem retype
had overflowed). LESSON: a factory-bridge runtime Check(sizeof<=alloc) does NOT fail the build --
only a static_assert sizeof lock catches alloc overflow at compile time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
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>
|