From 77190c93e5d65c68114eb630106e93b2c7f984b1 Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 11 Jul 2026 14:30:30 -0500 Subject: [PATCH] Combat: the AUTHENTIC consolidated damage delivery -- TakeDamageStream through the message manager (task #7 tail) ConsolidateAndSendDamage @0049b784 fully implemented: the beam path submits into AddDamageMessage; each messmgr tick builds ONE Entity:: TakeDamageStreamMessage (id 0x13; wire-verified 0x34+4+Nx12) with the common impact record + appended {type, amount, subsystemID} entries and Dispatches it at the victim -- the T0 handler (ENTITY.cpp:817) re-splits into per-record TakeDamage; replicants reroute cross-pod. Real ResolveExplosionID (firing weapon's +0x3E4, guarded) + SubmitExplosion via the Explosion::Make port; TWO chain-purge bugs fixed (the "iterator dtor clears it" assumption was wrong -- records re-applied every tick, observed 1->2->3->4 double-counting). @004b9728 identity corrected (SendDamageMessage, not DrawWeaponPip). Verified solo: 25 clean single-record consolidations -> kill -> death transition. Residue: weapon-side submission awaits the damage-economy reconciliation (authored 0.25-scale vs kShotDamage=12). Co-Authored-By: Claude Fable 5 --- context/open-questions.md | 32 ++++--- context/subsystems.md | 2 +- game/reconstructed/mech.hpp | 4 + game/reconstructed/mech4.cpp | 18 +++- game/reconstructed/mechweap.cpp | 13 ++- game/reconstructed/mechweap.hpp | 6 ++ game/reconstructed/messmgr.cpp | 163 ++++++++++++++++++++++++-------- 7 files changed, 180 insertions(+), 58 deletions(-) diff --git a/context/open-questions.md b/context/open-questions.md index 9b4c5d1..9ad3de0 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -22,18 +22,26 @@ authentic path scoped. Win10+wrapper pod config. Status: OPEN. ## Deferred subsystems / feeds (authentic path scoped, marked in code) -- **0xBD3 SubsystemMessageManager (WAVE 8)** — a per-mech **damage/explosion consolidation hub** - (`ConsolidateAndSendDamage` Performance @0049b784, `weaponExplosions` queue, `CommonDamageInformation`; - messmgr.hpp). ctor `FUN_0049bca4` (vtable 0x50b954, installs a Performance → ticks). The factory - caches it to **`Mech[0x10d]` = byte 0x434**. What wiring it needs: an untangle — our reconstruction - MISLABELED `Mech[0x10d]` as `controlsMapper` and `mech4`'s live drive reads it as the mapper, so the - real messmgr can't land there until the drive is re-pointed to the true mapper (**roster slot 0**, - installed by `SetMappingSubsystem`/`MakeViewpointEntity`; a `MechRIOMapper`). Payoff = authentic - damage/explosion consolidation (a COMBAT-fidelity improvement) + fixing the mapper/messmgr conflation. - ⚠ **CORRECTION (verified 2026-07):** 0xBD3 does NOT gate the valve/Myomers/MessageBoard — those read a - DIFFERENT object at **`mech+0x190`** (both `FUN_004ac9c8` the valve guard `→+0x274` and `FUN_004ad7d4` - the Myomers gate `→+0x260` deref `owner+0x190`, which ≠ the 0x434 messmgr cache). The earlier - "0xBD3 gates 4 things" note was wrong. +- **✅ 0xBD3 SubsystemMessageManager — UNTANGLED + LIVE (task #7, 2026-07-11) [T1/T2].** + Both halves landed: (1) the factory case builds the REAL messmgr (ctor @0049bca4, 0x130, + static reconstruction messmgr.cpp) cached at `mech+0x434` — the binary-wide census found + exactly ONE reader (@0x4b984b in `MechWeapon::SendDamageMessage` @004b9728, which was ALSO + mislabeled, as "DrawWeaponPip"); every mapper consumer re-pointed to roster slot 0 + (`MappingMapper()`; `SetMappingSubsystem` @0049fe40 touches only slot 0); non-viewpoint mechs + carry a slot-0 demand LATCH ([T3] accommodation for the port's wider drive/gait reachability). + (2) The authentic CONSOLIDATED damage delivery is live: the beam path submits TakeDamage into + `AddDamageMessage` → per-frame `ConsolidateAndSendDamage` (@0049b784) builds ONE + `Entity::TakeDamageStreamMessage` (id 0x13, wire-verified 0x34+4+N×12) dispatched at the + victim, whose T0 handler (ENTITY.cpp:817) re-splits it; replicant victims reroute cross-pod. + Explosion bundling (weapon+0x3E4 per record, unique-queued, Explosion::Make at the impact) + implemented; TWO latent chain-purge bugs fixed (records re-applied every tick). RESIDUE + ([T3], next task): the weapon-side submission with per-weapon `damageData` + + `inflictingSubsystemID` awaits the DAMAGE-ECONOMY reconciliation (authored 0.25-scale + amounts vs the bring-up kShotDamage=12); the 0.1s explosion stagger + the terrain-hit + derivation check are noted in messmgr.cpp. + ⚠ The earlier "0xBD3 gates the valve/Myomers" claim remains WRONG (those read the owning + BTPlayer at mech+0x190 — see the next entry); the old wiring notes are superseded by this + landing. - **✅ `mech+0x190` IDENTIFIED (2026-07): it is the owning `BTPlayer`** (`Mech::GetPlayerLink()`, `MECH_OWNING_PLAYER`; ENTITY.h:430). Set by `FUN_0049f624` (the mech↔player bind: `mech+0x190 = player` AND `player->playerVehicle(+0x1fc) = mech`), which resolves the player from the mission player registry diff --git a/context/subsystems.md b/context/subsystems.md index 0e5d2df..6525538 100644 --- a/context/subsystems.md +++ b/context/subsystems.md @@ -35,7 +35,7 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER `static_assert`-locked (the P7 alarm-unification: HeatSink 0x1D0 → PoweredSubsystem 0x31C → MechWeapon 0x3F0 → Emitter 0x478). [T2] -## Reconstruction waves (state: 19 of 20 factory cases wired to real classes — only 0xBD3 remains, the MechControlsMapper squatter) +## Reconstruction waves (state: ALL 20 factory cases wired to real classes — 0xBD3 = the real SubsystemMessageManager since task #7, 2026-07-11; the mapper lives in roster slot 0) - **WAVE 1** — HeatableSubsystem re-based onto MechSubsystem (de-shadow cascade). - **WAVE 2** — heat family (Condenser/AggregateHeatSink/Reservoir) + HUD + MechTech (un-swap). - **WAVE 3** — power bus (Generator/PoweredSubsystem) + Emitter/PPC fire-path (end-to-end fire; heat diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 1ced0d5..9b13e0c 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -538,6 +538,10 @@ struct ShotDescriptor return (GetSubsystemCount() > 0) ? (MechControlsMapper *)GetSubsystem(0) : 0; } + class SubsystemMessageManager *GetMessageManager() + { + return messageManager; + } Subsystem *GetHudSubsystem() { return hudSubsystem; } // Reachable horizontal firing half-arc (radians) the mech's torso can bring // its guns to bear -- the wider torso twist limit, or 0 for a fixed torso. diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index af416fa..648204b 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -152,6 +152,7 @@ #include // BoxedSolid / BoxedSolidCollision / BoxedSolidCollisionList #include // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations #include // HostManager::GetEntityPointer (band-effect attacker resolve) +#include // SubsystemMessageManager (task #7 consolidated delivery) #if !defined(PLAYER_HPP) # include // Player::VehicleDeadMessage -- the death->respawn notification (task #52) #endif @@ -3183,7 +3184,22 @@ void GetEntityID(), // inflicting = this (shooter) -1, // UNAIMED -> receiver's cylinder resolves dmg); - victim->Dispatch(&take_damage); + // AUTHENTIC DELIVERY (task #7 tail): submit through the + // SubsystemMessageManager -- per-frame consolidation into + // ONE TakeDamageStream (id 0x13) dispatched at the victim, + // whose T0 handler re-splits it (ENTITY.cpp:817). + // Replicant victims reroute cross-pod as before. [T3: + // the inflicting subsystemID is not threaded through this + // bring-up fire block -- the authentic submitter is the + // weapon's own SendDamageMessage @004b9728, pending the + // damage-economy reconciliation (authored 0.25-scale + // amounts vs the bring-up kShotDamage=12); until then the + // messmgr's weapon-explosion bundling stays dormant.] + if (messageManager != 0) + ((SubsystemMessageManager *)messageManager) + ->AddDamageMessage(victim, &take_damage); + else + victim->Dispatch(&take_damage); // SCORE the damage (skip once the victim is dead). if (!((Mech *)victim)->IsMechDestroyed()) diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index 7a38ad9..7298ecc 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -454,10 +454,15 @@ void } // -// @004b9728 -- register the targeting "pip" / fire marker with the owning Mech's -// cockpit HUD manager (entity+0x434). Skipped for non-pip weapons (the -// IsDerivedFrom(0x50bdb4) / segment-index guard). Builds a HUD element record -// {priority 100, kind 0x12, flags} carrying the pip transform and submits it. +// ⚠ IDENTITY CORRECTION (task #7): @004b9728 is MechWeapon::SendDamageMessage +// -- the weapon-fire damage submission into the owning mech's +// SubsystemMessageManager (mech+0x434 -> AddDamageMessage @0049b6d8), NOT a +// HUD pip. The "HUD element record" reading below was the same 0x434 +// misattribution the mapper suffered. The authentic weapon-side submission +// (per-weapon damageData + inflictingSubsystemID) is pending the +// damage-economy reconciliation (authored 0.25-scale amounts vs the +// bring-up kShotDamage=12); the interim submitter is the mech4 fire block +// routing through AddDamageMessage. This body stays dormant meanwhile. // void MechWeapon::DrawWeaponPip(const LinearMatrix &transform) diff --git a/game/reconstructed/mechweap.hpp b/game/reconstructed/mechweap.hpp index 4d3f5f3..1bfe3bb 100644 --- a/game/reconstructed/mechweap.hpp +++ b/game/reconstructed/mechweap.hpp @@ -344,6 +344,12 @@ class CockpitHud; Logical useConfiguredPip; // @0x3E0 = (usesExternalModel == 0) (best-effort) ResourceDescription::ResourceID explosionResourceID; // @0x3E4 resource ExplosionModelFile + public: + // task #7: the SubsystemMessageManager bundles the FIRING weapon's + // explosion per consolidated damage record (ENTITY3.h:143). + ResourceDescription::ResourceID + GetExplosionResourceID() const { return explosionResourceID; } + protected: Scalar recoil; // @0x3E8 reset 0 (Emitter::firingArmed aliases this slot) int segmentPageIndex; // @0x3EC resource segmentIndex (+0x28) -- LAST field, binary ends 0x3F0 diff --git a/game/reconstructed/messmgr.cpp b/game/reconstructed/messmgr.cpp index 96447a7..a1f1e93 100644 --- a/game/reconstructed/messmgr.cpp +++ b/game/reconstructed/messmgr.cpp @@ -60,6 +60,8 @@ // 0.1f (DAT @0049b784 frame) = per-explosion render time step // #include +#include // MechWeapon::GetExplosionResourceID (task #7 explosion bundling) +#include // Explosion::Make (the established explosion port) #pragma hdrstop #if !defined(MESSMGR_HPP) @@ -334,47 +336,99 @@ void damageWalk.First(); if (damageWalk.GetCurrent() != 0) { - // (recovered) the consolidated damage/notify message (ClassID 0x13, - // size 0x34, vtable @0050b984) was built on the stack here and filled - // from commonDamageInformation. The message ctor (FUN_00419e18) and the - // owner target field (+0x184) are renderer/net-side and are not present - // in these headers; the explosion-queueing loop below is what feeds the - // graphics flush, so the message build is left as a recovered note. - // EntityID hitEntity = commonDamageInformation.damageZoneIndex; + // + // THE CONSOLIDATED SEND (task #7 tail; this was a "recovered note" + // stub): build the Entity::TakeDamageStreamMessage (id 0x13, base + // 0x34 -- ENTITY3.h:189, byte-for-byte the binary's layout), append + // {count, [damageType, damageAmount, subsystemID]...} through the + // dynamic message (the binary's msg-vtable slot-8 append, + // FUN_00419e60), and Dispatch it at the hit entity -- whose T0 + // handler (ENTITY.cpp:817) re-splits it into per-record + // TakeDamageMessages. A REPLICANT victim's Dispatch reroutes the + // stream cross-pod exactly as the old direct path did (task #47). + // + int entryCount = 0; + for (damageWalk.First(); damageWalk.GetCurrent() != 0; damageWalk.Next()) + { + ++entryCount; + } + + Entity::DynamicMessage streamMessage( + Entity::TakeDamageStreamMessageID, + sizeof(Entity::TakeDamageStreamMessage)); + { + Entity::TakeDamageStreamMessage *header = + (Entity::TakeDamageStreamMessage *)streamMessage.GetMessagePointer(); + header->impactPoint = commonDamageInformation.impactPoint; + header->damageZoneIndex = commonDamageInformation.damageZoneIndex; + header->shootingEntity = owner->GetEntityID(); + } + streamMessage << entryCount; for (damageWalk.First(); damageWalk.GetCurrent() != 0; damageWalk.Next()) { DamageInformation *info = damageWalk.GetCurrent(); - // resolve the damaged subsystem object from the owner's roster: - // subsystem = owner->subsystems[info->subsystemID]; (mech+0x128) - Subsystem *target = - owner->GetSubsystem(info->subsystemID); // mech+0x128 + id*4 + streamMessage << (int)info->damageType + << (Scalar)damageWalk.GetValue() // the chain KEY = damageAmount + << (int)info->subsystemID; + // + // Bundle the FIRING weapon's explosion: the record's subsystemID + // is the inflicting weapon on the OWNER's roster (ENTITY3.h:143's + // documented purpose; binary reads roster[id]+0x3E4). Non-weapon + // / unthreaded (-1) ids resolve Null and queue nothing. + // + Subsystem *firingWeapon = (info->subsystemID >= 0 + && info->subsystemID < owner->GetSubsystemCount()) + ? owner->GetSubsystem(info->subsystemID) : 0; ResourceDescription::ResourceID explosionID = - ResolveExplosionID(target); // target+0x3E4 - - // - // CreateWeaponExplosions (inlined): if this hit zone has no - // explosion queued yet, append a ResourceIDPlug to the - // weaponExplosions chain so it is rendered this frame. - // - if (!weaponExplosions.Find(explosionID)) // chain+0xFC, slot 0xC + ResolveExplosionID(firingWeapon); // weapon+0x3E4 + if (explosionID != ResourceDescription::NullResourceID + && !weaponExplosions.Find(explosionID)) // chain+0xFC, slot 0xC { - ResourceIDPlug *plug = new ResourceIDPlug(explosionID); // FUN_00402298(0x10) / FUN_00424716 + ResourceIDPlug *plug = new ResourceIDPlug(explosionID); weaponExplosions.AddValue(plug, explosionID); // chain+0xFC, slot 8 } } + // + // Deliver the consolidated stream to the hit entity. + // + if (commonDamageInformation.entityHit != 0) + { + Entity::TakeDamageStreamMessage *finalMessage = + (Entity::TakeDamageStreamMessage *)streamMessage.GetMessagePointer(); + if (getenv("BT_MSGMGR_LOG")) + DEBUG_STREAM << "[msgmgr] consolidated " << entryCount + << " records -> " << (void *)commonDamageInformation.entityHit + << " len=" << (int)finalMessage->messageLength + << " zone=" << commonDamageInformation.damageZoneIndex + << std::endl; + commonDamageInformation.entityHit->Dispatch(finalMessage); + } + // Render queued weapon explosions and tear down the message. SendQueuedExplosions(); // @0049baa0 } + // PURGE the consumed queue -- the binary's explicit teardown + // (FUN_00417858(iter, 1)). The old "cleared as the iterator is + // destroyed" note was WRONG: nothing purges automatically, so stale + // records re-applied every tick (observed live: counts 1->2->3->4, + // damage double-counted). The nodes are heap plugs from + // AddDamageMessage -> delete them here. + for (damageWalk.First(); damageWalk.GetCurrent() != 0; damageWalk.First()) + { + DamageInformation *consumed = damageWalk.GetCurrent(); + damageWalk.Remove(); + delete consumed; + } + // Reset common record and empty the damage chain for the next frame. commonDamageInformation.entityHit = 0; // this[0x39] commonDamageInformation.damageZoneIndex = -1; // this[0x3A] commonDamageInformation.impactPoint = Point3D::Identity; // this[0x3B] - // damageInformation cleared as the iterator is destroyed (FUN_0049c2c9). } @@ -423,45 +477,74 @@ void SubmitExplosion(id, fireTime); // CROSS-FAMILY renderer submission } - // chain emptied as the iterator is destroyed (FUN_0049c352). + // PURGE the flushed queue (same explicit-teardown correction as the + // damage chain above; the plugs are heap nodes). + for (explosionWalk.First(); explosionWalk.GetCurrent() != 0; explosionWalk.First()) + { + ResourceIDPlug *flushed = explosionWalk.GetCurrent(); + explosionWalk.Remove(); + delete flushed; + } } //############################################################################# -// CROSS-FAMILY compile shims +// ResolveExplosionID -- the real read (task #7 tail) //############################################################################# // -// ResolveExplosionID: in the shipped game this read subsystem+0x3E4 via -// Subsystem::GetExplosionID(). The WinTesla Subsystem does not expose it; until -// the base provides the accessor this returns the manager's own -// terrainHitExplosionID as a stand-in (NullResourceID if none). See report. +// The binary reads the FIRING weapon's explosion resource: roster[subsystemID] +// + 0x3E4 (the streamed ExplosionModelFile). The old stand-in returned +// terrainHitExplosionID and framed the record as "the damaged subsystem" -- +// both wrong (records are the OWNER's inflicting weapons). Guarded: the +// binary's submitters are all weapons; our reachability is wider. // ResourceDescription::ResourceID SubsystemMessageManager::ResolveExplosionID(Subsystem *subsystem) { - Check_Pointer(subsystem); - // TODO(cross-family): return subsystem->GetExplosionID(); - return terrainHitExplosionID; + if (subsystem != 0 + && subsystem->IsDerivedFrom(*MechWeapon::GetClassDerivations())) + { + return ((MechWeapon *)subsystem)->GetExplosionResourceID(); // +0x3E4 + } + return ResourceDescription::NullResourceID; } // -// SubmitExplosion: the original built a renderer command (FUN_004336f0) and -// submitted it (with the impact point/orientation from commonDamageInformation -// and rendererCompensateTime) to the global graphics pipe (FUN_0041acbc). The -// WinTesla L4D3D renderer replaced the IG graphics pipe; the submission entry -// point is not present in these headers. Stubbed so the queue/timing logic -// compiles. See report "CROSS-FAMILY NEEDS". +// SubmitExplosion -- the real spawn (task #7 tail). The binary posts a +// Registry::MakeEntityMessage (id 3, class 0x31 Explosion, flags 0x100) at +// Now + 0.1s x queue-ordinal (the caller's fire_time) so multi-hit impacts +// fire sequentially. The port spawns through the established Explosion::Make +// port at the consolidated impact point. [T3: the 0.1s stagger is computed +// by the caller but not yet honoured -- timed entity posts need the registry +// queue; the visual difference on multi-weapon volleys is a subtle ripple.] // void SubsystemMessageManager::SubmitExplosion( - ResourceDescription::ResourceID /*explosion_id*/, + ResourceDescription::ResourceID explosion_id, Scalar /*fire_time*/ ) { Check(this); - // TODO(cross-family): build the explosion render command (ClassID 0x5C, - // sub-type 3) from commonDamageInformation + explosion_id and submit it to - // the L4D3D graphics pipe at fire_time, compensated by rendererCompensateTime. + + Origin o; + o.linearPosition = commonDamageInformation.impactPoint; + o.angularPosition = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f)); + + Mech *owner = GetEntity(); + EntityID hitID = (commonDamageInformation.entityHit != 0) + ? commonDamageInformation.entityHit->GetEntityID() + : owner->GetEntityID(); + + Explosion::MakeMessage m( + Explosion::MakeMessageID, sizeof(Explosion::MakeMessage), + (Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null, + explosion_id, Explosion::DefaultFlags, o, + hitID, owner->GetEntityID()); + Explosion *e = Explosion::Make(&m); + if (e != 0) + { + Register_Object(e); + } }