BT410 5.3.115: the damage hub consolidates -- MESSMGR's three runtime bodies, a dead 1995 feature recorded, and two more donor drifts caught

SubsystemMessageManager reconstructed from the raw decomp with the wire
format pinned by the engine's own parser (TakeDamageStreamMessageHandler:
count, then type/amount/subsystemID per record -- the chain KEY carries the
damage amount, which is why AddDamageMessage keys its plugs by it).
AddDamageMessage @0049b6d8: first hit of the frame wins the common record.
ConsolidateAndSendDamage @0049b784: one DynamicMessage(0x13, 0x34) at the
hit entity, the firing weapon's explosion resource bundled per record
(binary reads roster[id]+0x3e4 blind; [T1] guarded here), DeletePlugs
teardown, commons reset. CreateWeaponExplosions @0049baa0 under its
AUTHENTIC surviving-header signature (the donor's 'SendQueuedExplosions'
rename was drift): per queued resource an Explosion::MakeMessage --
MakeMessageID 3, ExplosionClassID 0x31 (our VDATA enum ordinal matches the
binary exactly), flags 0x100, identity-rotation Origin at the impact point
-- posted to the Registry at CreationEventPriority, staggered 0.1s apart.

The 1995 archaeology: the binary computes a terrain-hit flag from a
Derivation at 0x4e6f54 (unresolved BSS class) and passes it to a parameter
THE SHIPPED BODY NEVER READS; terrainHitExplosionID is resourced by the CSS
and consumed nowhere. A declared, streamed, abandoned feature -- shipped
anyway. We pass False [T3]; behavior is byte-identical.

Ctor corrections vs our stub: weaponExplosions is UNIQUE-keyed (binary
chain ctor flag 1), the Performance registers, resource fields copy from
the stream. MechWeapon grows a public ExplosionResourceIDOf() accessor.
The 0x160-byte engine helper at +0xE0 stays staged [T3]; the CSS stays in
the authoring-family batch. No feeders call AddDamageMessage yet -- the
consolidator idles empty per frame, soak-verified live (no faults, wheel
turning).

Stubs: 13 across 10 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-04 08:30:06 -05:00
co-authored by Claude Fable 5
parent ea99e8987f
commit 8c777e1e22
3 changed files with 477 additions and 101 deletions
+9
View File
@@ -126,6 +126,15 @@
Logical
TestInstance() const;
//
// The projectile/explosion GameModel this weapon spawns (binary
// @0x3e4) -- read by SubsystemMessageManager's consolidator to
// bundle the firing weapon's explosion with the damage it caused.
//
ResourceDescription::ResourceID
ExplosionResourceIDOf()
{ Check(this); return explosionResourceID; }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
+404 -101
View File
@@ -1,101 +1,404 @@
//===========================================================================//
// File: messmgr.cpp //
// Project: BattleTech Brick: Mech subsystems //
// Contents: SubsystemMessageManager -- the per-mech damage/explosion hub //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MESSMGR_HPP)
# include <messmgr.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
Derivation
SubsystemMessageManager::ClassDerivations(
Subsystem::ClassDerivations,
"SubsystemMessageManager"
);
SubsystemMessageManager::SharedData
SubsystemMessageManager::DefaultData(
SubsystemMessageManager::ClassDerivations,
Subsystem::MessageHandlers,
Subsystem::AttributeIndex,
Subsystem::StateCount
);
SubsystemMessageManager::SubsystemMessageManager(
Mech *owner,
int subsystem_ID,
SubsystemResource *sub_res,
SharedData &shared_data
):
Subsystem(owner, subsystem_ID, sub_res, shared_data),
weaponExplosions(NULL, False),
damageInformation(NULL, False)
{
Check(owner);
Check_Pointer(sub_res);
rendererCompensateTime = 0.0f;
terrainHitExplosionID = ResourceDescription::NullResourceID;
Check_Fpu();
}
SubsystemMessageManager::~SubsystemMessageManager()
{
}
Logical
SubsystemMessageManager::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// The per-frame damage/explosion consolidation. Not yet reconstructed.
//#############################################################################
//
void
SubsystemMessageManager::ConsolidateAndSendDamage(Scalar)
{
Fail("SubsystemMessageManager::ConsolidateAndSendDamage -- messmgr.cpp not yet reconstructed");
}
void
SubsystemMessageManager::CreateWeaponExplosions(Logical, EntityID, Point3D)
{
Fail("SubsystemMessageManager::CreateWeaponExplosions -- messmgr.cpp not yet reconstructed");
}
void
SubsystemMessageManager::AddDamageMessage(Entity *, Entity__TakeDamageMessage *)
{
Fail("SubsystemMessageManager::AddDamageMessage -- messmgr.cpp not yet reconstructed");
}
Logical
SubsystemMessageManager::CreateStreamedSubsystem(
ResourceFile *,
NotationFile *,
const char *,
const char *,
SubsystemResource *,
NotationFile *,
const ResourceDirectories *
)
{
Fail("SubsystemMessageManager::CreateStreamedSubsystem -- messmgr.cpp not yet reconstructed");
return False;
}
//===========================================================================//
// File: messmgr.cpp //
// Project: BattleTech Brick: Mech subsystems //
// Contents: SubsystemMessageManager -- the per-mech damage/explosion hub //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MESSMGR_HPP)
# include <messmgr.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(MECHWEAP_HPP)
# include <mechweap.hpp> // ExplosionResourceIDOf (weapon @0x3e4)
#endif
#if !defined(EXPLODE_HPP)
# include <explode.hpp> // Explosion::MakeMessage
#endif
#if !defined(APP_HPP)
# include <app.hpp> // application->Post / GetRegistry / priorities
#endif
#if !defined(REGISTRY_HPP)
# include <registry.hpp> // complete Registry type for the Post upcast
#endif
Derivation
SubsystemMessageManager::ClassDerivations(
Subsystem::ClassDerivations,
"SubsystemMessageManager"
);
SubsystemMessageManager::SharedData
SubsystemMessageManager::DefaultData(
SubsystemMessageManager::ClassDerivations,
Subsystem::MessageHandlers,
Subsystem::AttributeIndex,
Subsystem::StateCount
);
SubsystemMessageManager::SubsystemMessageManager(
Mech *owner,
int subsystem_ID,
SubsystemResource *sub_res,
SharedData &shared_data
):
Subsystem(owner, subsystem_ID, sub_res, shared_data),
weaponExplosions(NULL, True), // @0049bca4: chain ctor (0, 1) --
// UNIQUE-keyed (one plug per explosion
// resource; the consolidator also
// Find()s before adding)
damageInformation(NULL, False) // (0, 0) -- duplicate keys allowed
{
Check(owner);
Check_Pointer(sub_res);
//
// [T3] The binary ctor also allocates a 0x160-byte engine-internal
// helper object at this+0xE0 (FUN_0041de1c(obj, this, 0); released in
// the dtor by virtual op 3). It is not declared in the surviving
// MESSMGR.HPP and nothing in the reconstructed damage path touches it;
// STAGED until its class is identified.
//
//
// The per-frame Performance (binary @0049bca4 copies the member-pointer
// triple @0050b6fc..0050b704 == &ConsolidateAndSendDamage).
//
SetPerformance(&SubsystemMessageManager::ConsolidateAndSendDamage);
//
// The two streamed resource fields (res+0x30 / res+0x34).
//
terrainHitExplosionID = sub_res->terrainHitExplosionID;
rendererCompensateTime = sub_res->rendererCompensateTime;
//
// The common-damage record starts empty (first hit of a frame fills it).
//
commonDamageInformation.entityHit = NULL;
commonDamageInformation.damageZoneIndex = -1;
commonDamageInformation.impactPoint = Point3D::Identity;
Check_Fpu();
}
SubsystemMessageManager::~SubsystemMessageManager()
{
}
Logical
SubsystemMessageManager::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// AddDamageMessage -- binary @0049b6d8 (172 bytes), verified against the raw
// decomp (part_012.c).
//
// Called by the damage paths while a frame runs: the FIRST hit of the frame
// wins each field of the common record (entity, impact point, zone), and
// every message queues a DamageInformation plug into the damage chain KEYED
// BY ITS DAMAGE AMOUNT (the chain key is the Scalar the consolidator later
// reads back with GetValue()).
//#############################################################################
//
void
SubsystemMessageManager::AddDamageMessage(
Entity *damaged_entity,
Entity__TakeDamageMessage *message
)
{
Check(this);
Check(damaged_entity);
Check_Pointer(message);
if (commonDamageInformation.entityHit == NULL)
{
commonDamageInformation.entityHit = damaged_entity;
}
//
// The binary's origin test is an epsilon compare against the zero point
// (FUN_004084fc(point, origin, 0.0001)).
//
if (Close_Enough(commonDamageInformation.impactPoint, Point3D::Identity, 0.0001f))
{
commonDamageInformation.impactPoint = message->damageData.impactPoint;
}
if (commonDamageInformation.damageZoneIndex == -1)
{
commonDamageInformation.damageZoneIndex = message->damageZone;
}
DamageInformation
*info = new DamageInformation;
Register_Object(info);
info->damageType = message->damageData.damageType;
info->subsystemID = message->inflictingSubsystemID;
damageInformation.AddValue(info, message->damageData.damageAmount);
}
//
//#############################################################################
// ConsolidateAndSendDamage -- binary @0049b784 (796 bytes), the per-frame
// Performance, verified against the raw decomp.
//
// If any damage queued this frame: build ONE TakeDamageStreamMessage for the
// common hit entity (impact point, zone, shooter = the owning mech), append
// {count, [damageType, damageAmount, subsystemID]...} through a dynamic
// message -- the wire format is fixed by the ENGINE'S OWN PARSER
// (Entity::TakeDamageStreamMessageHandler, entity.cpp: count first, then the
// triple per record) -- bundle each firing weapon's explosion resource into
// the explosion queue (dedup'd), Dispatch the stream at the hit entity, and
// flush the queued explosions. Always: purge the damage chain and reset the
// common record for the next frame.
//#############################################################################
//
void
SubsystemMessageManager::ConsolidateAndSendDamage(Scalar)
{
Check(this);
Mech
*owner = GetEntity();
Check(owner);
VChainIteratorOf<DamageInformation*, Scalar>
damageWalk(&damageInformation);
damageWalk.First();
if (damageWalk.GetCurrent() != NULL)
{
//
// The engine parser reads the record count first, so count the
// chain up front.
//
int
entry_count = 0;
for (damageWalk.First(); damageWalk.GetCurrent() != NULL; damageWalk.Next())
{
++entry_count;
}
//
// The consolidated stream (binary: Receiver__DynamicMessage ctor
// FUN_00419e18(TakeDamageStreamMessageID == 0x13, sizeof == 0x34),
// header fields written in place at +0x1c/+0x28/+0x2c).
//
Receiver::DynamicMessage
stream_message(
Entity::TakeDamageStreamMessageID,
sizeof(Entity::TakeDamageStreamMessage)
);
{
Entity::TakeDamageStreamMessage
*header =
(Entity::TakeDamageStreamMessage *)
stream_message.GetMessagePointer();
header->impactPoint = commonDamageInformation.impactPoint;
header->damageZoneIndex = commonDamageInformation.damageZoneIndex;
header->shootingEntity = owner->GetEntityID();
}
stream_message << entry_count;
for (damageWalk.First(); damageWalk.GetCurrent() != NULL; damageWalk.Next())
{
DamageInformation
*info = damageWalk.GetCurrent();
stream_message
<< (int)info->damageType
<< (Scalar)damageWalk.GetValue()
<< (int)info->subsystemID;
//
// Bundle the FIRING weapon's explosion (binary: blind read of
// roster[subsystemID]+0x3e4). [T1] The binary neither range-
// checks the id nor verifies the subsystem is a weapon; damage
// with an unthreaded id (-1) or a non-weapon inflicter would
// read garbage there. Guarded here -- a guarded miss queues
// nothing, which is also what a Null resource does.
//
Subsystem
*firing = NULL;
if (
info->subsystemID >= 0 &&
info->subsystemID < owner->GetSubsystemCount()
)
{
firing = owner->GetSubsystem(info->subsystemID);
}
if (
firing != NULL &&
firing->IsDerivedFrom(MechWeapon::ClassDerivations)
)
{
ResourceDescription::ResourceID
explosion_ID =
((MechWeapon *)firing)->ExplosionResourceIDOf();
if (
explosion_ID != ResourceDescription::NullResourceID &&
weaponExplosions.Find(explosion_ID) == NULL
)
{
ResourceIDPlug
*plug = new ResourceIDPlug(explosion_ID);
Register_Object(plug);
weaponExplosions.AddValue(plug, explosion_ID);
}
}
}
//
// [T3] The binary computes a terrain-hit flag here:
// entityHit->IsDerivedFrom(<Derivation @0x4e6f54>). The class at
// that BSS address is UNRESOLVED (no static-ctor reference survives
// in the decomp parts) -- but the flag is PROVABLY INERT:
// CreateWeaponExplosions (@0049baa0) never reads its first
// argument, and terrainHitExplosionID is never consumed anywhere in
// the shipped cluster. A declared, resourced, and abandoned 1995
// feature; False preserves shipped behavior exactly.
//
Logical
terrain_hit = False;
if (commonDamageInformation.entityHit != NULL)
{
CreateWeaponExplosions(
terrain_hit,
commonDamageInformation.entityHit->GetEntityID(),
commonDamageInformation.impactPoint
);
commonDamageInformation.entityHit->Dispatch(
stream_message.GetMessagePointer());
}
}
//
// Teardown every frame (binary FUN_00417858(iter, 1)): unregister and
// delete the consumed plugs, then reset the common record.
//
damageWalk.First();
damageWalk.DeletePlugs();
commonDamageInformation.entityHit = NULL;
commonDamageInformation.damageZoneIndex = -1;
commonDamageInformation.impactPoint = Point3D::Identity;
Check_Fpu();
}
//
//#############################################################################
// CreateWeaponExplosions -- binary @0049baa0 (516 bytes), verified against
// the raw decomp. The authentic signature is the surviving MESSMGR.HPP's;
// note terrain_hit is never read by the shipped body (see the [T3] note in
// the consolidator).
//
// Flush the queued explosion resources: for each, post an
// Explosion::MakeMessage at the Registry -- the byte-for-byte template from
// the binary: MakeMessageID (3), sizeof 0x5c, ExplosionClassID (0x31 -- the
// VDATA enum ordinal matches the binary exactly), no owner, the queued
// resource, instance flags 0x100, an identity-rotation Origin at the impact
// point, the hit entity, and the owning mech as creator. Deliveries stagger
// 0.1s apart (the first at now+0.1) so several impacts in one frame read as
// separate blasts. Then purge the queue.
//#############################################################################
//
void
SubsystemMessageManager::CreateWeaponExplosions(
Logical /*terrain_hit*/,
EntityID entity_hit,
Point3D explode_position
)
{
Check(this);
Mech
*owner = GetEntity();
Check(owner);
VChainIteratorOf<ResourceIDPlug*, ResourceDescription::ResourceID>
explosionWalk(&weaponExplosions);
Origin
explosion_origin(Origin::Identity);
explosion_origin.linearPosition = explode_position;
Scalar
time_step = 0.0f;
for (
explosionWalk.First();
explosionWalk.GetCurrent() != NULL;
explosionWalk.Next()
)
{
ResourceDescription::ResourceID
explosion_ID = explosionWalk.GetValue();
time_step += 0.1f;
Time
when = Now();
when += time_step;
Explosion::MakeMessage
make_message(
Entity::MakeMessageID,
sizeof(Explosion::MakeMessage),
RegisteredClass::ExplosionClassID,
EntityID(),
explosion_ID,
0x100,
explosion_origin,
entity_hit,
owner->GetEntityID()
);
Check(application);
application->Post(
CreationEventPriority,
application->GetRegistry(),
&make_message,
when
);
}
explosionWalk.First();
explosionWalk.DeletePlugs();
Check_Fpu();
}
Logical
SubsystemMessageManager::CreateStreamedSubsystem(
ResourceFile *,
NotationFile *,
const char *,
const char *,
SubsystemResource *,
NotationFile *,
const ResourceDirectories *
)
{
Fail("SubsystemMessageManager::CreateStreamedSubsystem -- messmgr.cpp not yet reconstructed");
return False;
}
+64
View File
@@ -0,0 +1,64 @@
# MESSMGR.CPP — SubsystemMessageManager reconstruction notes
The per-mech damage/explosion hub: subsystems queue damage during a frame
(`AddDamageMessage`), the Performance (`ConsolidateAndSendDamage`) folds the
queue into ONE `TakeDamageStreamMessage` for the hit entity and flushes the
bundled weapon explosions (`CreateWeaponExplosions`). The surviving
MESSMGR.HPP (CODE/BT/BT) is authentic and untouched.
## Reconstructed 2026-08-04 (5.3.115), decomp-verified (part_012.c)
- **ctor @0049bca4**: `weaponExplosions(NULL, True)` — the binary constructs
the explosion chain UNIQUE-keyed (0049bfde(.., 0, 1)); the damage chain
allows duplicate keys. Installs `ConsolidateAndSendDamage` as the
Performance (member-pointer triple @0050b6fc); copies the two streamed
resource fields (res+0x30/+0x34); zeros the common record.
[T3] The binary also allocates a 0x160-byte engine-internal helper at
this+0xE0 (FUN_0041de1c) — not declared in the surviving header, untouched
by the reconstructed paths, STAGED until identified.
- **AddDamageMessage @0049b6d8**: first hit of the frame wins each common
field (entity / impact point via epsilon-against-origin / zone); every
message queues a `DamageInformation` plug KEYED BY DAMAGE AMOUNT (the
chain key is read back with `GetValue()` at consolidation).
- **ConsolidateAndSendDamage @0049b784**: wire format pinned by the ENGINE'S
OWN PARSER (`Entity::TakeDamageStreamMessageHandler`, RP ENTITY.CPP:855):
a `Receiver::DynamicMessage(TakeDamageStreamMessageID == 0x13, sizeof ==
0x34)` with header {impactPoint@+0x1c, damageZoneIndex@+0x28,
shootingEntity@+0x2c}, then `<< count`, then `<< type << amount <<
subsystemID` per record. Explosion bundling reads the FIRING weapon's
resource (binary: blind `roster[subsystemID]+0x3e4`); [T1] guarded here
(range + IsDerivedFrom(MechWeapon)) — the binary is unguarded and relies
on well-formed feeders. Dispatches the stream directly at the hit entity,
flushes explosions, purges with `DeletePlugs()` (binary FUN_00417858(,1) —
the explicit teardown), resets the commons.
- **CreateWeaponExplosions @0049baa0**: the authentic HEADER signature
(terrain_hit, entity_hit, explode_position) — the BT411 donor's
"SendQueuedExplosions" rename was port drift. Per queued resource:
`Explosion::MakeMessage(MakeMessageID(3), sizeof(0x5c),
RegisteredClass::ExplosionClassID(0x31 — our VDATA enum ordinal matches
the binary byte-for-byte), no owner, resourceID, instanceFlags 0x100,
Origin{impact position, identity rotation}, entity_hit, owner mech)`,
posted at `CreationEventPriority(3)` to `application->GetRegistry()` with
delivery `Now() + 0.1s × n` (impacts in one frame stagger). Then purge.
## The inert terrain-hit flag (recorded, not tuned)
The binary computes `terrain_hit = entityHit->IsDerivedFrom(<Derivation @
0x4e6f54>)` before calling CreateWeaponExplosions. The class at 0x4e6f54 is
UNRESOLVED (BSS object; no static-ctor reference survives in the decomp
parts). It does not matter: @0049baa0 NEVER READS its first argument, and
`terrainHitExplosionID` is never consumed anywhere in the shipped cluster —
a declared, resourced, abandoned 1995 feature. We pass `False` [T3];
shipped behavior is preserved exactly. If the constant is ever resolved
(candidates: a terrain/cultural class), only the dead flag changes.
## Status / staging
- `CreateStreamedSubsystem` @0049be10 remains a Fail stub — the authoring-
side CSS family (8 fns) is batched separately; missions load prebuilt
streams from BTL4.RES and never call it.
- **No feeders yet**: nothing in source410 calls `AddDamageMessage` (the
weapon damage paths still dispatch TakeDamage directly), so the
consolidator runs empty every frame — verified live (5-period soak,
wheel turning, no faults). When the weapon-fire damage path is rebased
onto the manager, this TU is ready for it.