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 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 14:30:30 -05:00
co-authored by Claude Fable 5
parent afefaeece5
commit 77190c93e5
7 changed files with 180 additions and 58 deletions
+4
View File
@@ -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.
+17 -1
View File
@@ -152,6 +152,7 @@
#include <BOXSOLID.hpp> // BoxedSolid / BoxedSolidCollision / BoxedSolidCollisionList
#include <cultural.hpp> // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations
#include <hostmgr.hpp> // HostManager::GetEntityPointer (band-effect attacker resolve)
#include <messmgr.hpp> // SubsystemMessageManager (task #7 consolidated delivery)
#if !defined(PLAYER_HPP)
# include <player.hpp> // 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())
+9 -4
View File
@@ -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)
+6
View File
@@ -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
+123 -40
View File
@@ -60,6 +60,8 @@
// 0.1f (DAT @0049b784 frame) = per-explosion render time step
//
#include <bt.hpp>
#include <mechweap.hpp> // MechWeapon::GetExplosionResourceID (task #7 explosion bundling)
#include <EXPLODE.hpp> // 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);
}
}