Files
BT411/game/reconstructed/messmgr.cpp
T
arcattackandClaude Fable 5 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>
2026-07-11 14:16:27 -05:00

503 lines
20 KiB
C++

//===========================================================================//
// File: messmgr.cpp //
// Project: BattleTech //
// Contents: Consolidates Messages from Subsystems and Graphics Generation //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// 09/30/95 JM Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
// pseudo-C cluster @0049b6d8..@0049c3e3 (recovered/all/part_012.c) and the
// surviving MESSMGR.HPP. Each non-trivial method cites its @ADDR.
//
// The SubsystemMessageManager is the per-mech message hub: it accumulates
// damage / weapon-explosion notifications from the other subsystems during a
// frame (AddDamageMessage), then, when its Performance fires
// (ConsolidateAndSendDamage), it folds them into the common-damage record,
// re-broadcasts TakeDamage messages to the affected entities, and emits the
// queued explosion graphics to the renderer.
//
// Object layout (this+offset), recovered from the ctor/dtor:
// this[0x07] @0x1C activePerformance (Simulation base; set to
// ConsolidateAndSendDamage)
// this[0x34] @0xD0 owner Mech* (Subsystem base)
// this[0x38] @0xE0 message/graphics helper object (0x160 bytes, created
// in ctor; engine-internal,
// NOT declared in MESSMGR.HPP)
// this[0x39] @0xE4 commonDamageInformation.entityHit
// this[0x3A] @0xE8 commonDamageInformation.damageZoneIndex
// this[0x3B] @0xEC commonDamageInformation.impactPoint (Point3D)
// this[0x3E] @0xF8 rendererCompensateTime
// this[0x3F] @0xFC weaponExplosions (VChainOf, link vtable @0050b938)
// this[0x45] @0x114 damageInformation (VChainOf, link vtable @0050b91c)
// this[0x4B] @0x12C terrainHitExplosionID
//
// Helper-function name mapping (engine internals referenced by the decomp):
// FUN_0041c52c Subsystem base constructor
// FUN_0041c648 Subsystem destructor
// FUN_0041de1c message-helper-object constructor (0x160-byte object @0xE0)
// FUN_00402298 operator new(size) FUN_004022d0 operator delete
// FUN_00415e90 Plug constructor FUN_00415ed8 Plug destructor
// FUN_00408440 Point3D::operator= / copy FUN_004084fc Point3D::AlmostEqual
// FUN_00419e18 <message>::ctor(classID,size)
// FUN_0041a1a4 IsDerivedFrom(classDerivations) / name compare
// FUN_00420ea4 Point3D copy into message field
// FUN_004336f0 build renderer command record
// FUN_0041acbc GraphicsPipe::Submit(...)
// FUN_00424716 PlugOf<ResourceID>::ctor
// VChain helpers: 0049bfde/0049c123 (chain ctors), 0049c001/0049c146
// (chain dtors), 0049c02d/0049c05c/0049c172/0049c1a3
// (key comparators), 0049c37e/0049c3ae (link allocators).
//
// Constants:
// message ClassID 0x13 (damage/notify message, vtable @0050b984, size 0x34)
// explosion ClassID 0x5C, sub-type 3 (queued weapon explosion record)
// 0.1f (DAT @0049b784 frame) = per-explosion render time step
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MESSMGR_HPP)
# include <messmgr.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
//
// `Origin3D` (DAT_004e0f80) is the zero point. The engine point is
// `Point3D::Identity` (== 0,0,0); Point3D has no `AlmostEqual`, so compare the
// components directly. (Used to detect "no impact point recorded yet".)
//
static Logical
PointIsOrigin(const Point3D &p)
{
return p.x == 0.0f && p.y == 0.0f && p.z == 0.0f;
}
//#############################################################################
// Shared Data Support
//
// SubsystemMessageManager derives DIRECTLY from Subsystem, which exposes the
// parent derivation via the function `Subsystem::GetClassDerivations()`
// (returns a Derivation*), not a `ClassDerivations` object -- so no '&' here.
Derivation
SubsystemMessageManager::ClassDerivations( // static @0050b6cc
Subsystem::GetClassDerivations(),
"SubsystemMessageManager"
);
Receiver::MessageHandlerSet
SubsystemMessageManager::MessageHandlers;
SubsystemMessageManager::AttributeIndexSet
SubsystemMessageManager::AttributeIndex;
// Real Simulation__SharedData ctor: (Derivation*, MessageHandlerSet&,
// AttributeIndexSet&, int stateCount). StateCount resolves to
// Simulation::StateCount. The default activePerformance (@0050b6fc ->
// 0049b784 ConsolidateAndSendDamage) is installed in the ctor body instead.
SubsystemMessageManager::SharedData
SubsystemMessageManager::DefaultData(
&SubsystemMessageManager::ClassDerivations,
SubsystemMessageManager::MessageHandlers,
SubsystemMessageManager::AttributeIndex,
SubsystemMessageManager::StateCount
);
//#############################################################################
// Construction / Destruction
//#############################################################################
//
// @0049bca4 -- chains to the Subsystem base ctor, installs the vtable
// (@0050b954), constructs both message chains in place, allocates the
// message-helper object, installs the default Performance, and copies the two
// streamed resource fields.
//
SubsystemMessageManager::SubsystemMessageManager(
Mech *owner,
int subsystem_ID,
SubsystemResource *sub_res,
SharedData &shared_data
):
Subsystem(owner, subsystem_ID, sub_res, shared_data), // FUN_0041c52c
weaponExplosions(NULL, True), // @0xFC unique-keyed by explosion id
damageInformation(NULL, False) // @0x114 keyed by damage amount
{
Check(owner);
Check_Pointer(sub_res);
//
// In-place construction of the two VChains (the ctor calls the chain
// header constructors directly: weaponExplosions @0049bfde, with one
// pre-allocated link; damageInformation @0049c123).
//
// weaponExplosions : VChainOf<ResourceIDPlug*, ResourceID> @0xFC
// damageInformation : VChainOf<DamageInformation*, Scalar> @0x114
//
// Engine-internal message/graphics helper (0x160-byte object) parked at
// +0xE0. Created via FUN_0041de1c(obj, this, 0); released in the dtor by
// virtual call (op==3). Not a MESSMGR.HPP member -- best-effort.
// this[0x38] = new <messageHelper>(this);
// Default Performance := ConsolidateAndSendDamage (this[0x07..0x09] copied
// from the constants @0050b6fc/0050b700/0050b704).
SetPerformance(&SubsystemMessageManager::ConsolidateAndSendDamage);
terrainHitExplosionID = sub_res->terrainHitExplosionID; // this[0x4B] = sub_res+0x30
rendererCompensateTime = sub_res->rendererCompensateTime; // this[0x3E] = sub_res+0x34
commonDamageInformation.entityHit = 0; // this[0x39]
commonDamageInformation.damageZoneIndex = -1; // this[0x3A]
commonDamageInformation.impactPoint = Point3D::Identity; // this[0x3B] = DAT_004e0f80
Check_Fpu();
}
//
// @0049bd7c (vtable slot 0) -- if the message-helper object exists, release it
// (virtual op 3); destroy both message chains; chain to ~Subsystem.
//
SubsystemMessageManager::~SubsystemMessageManager()
{
Check(this);
// release this[0x38] message-helper object (virtual destroy, op==3)
// damageInformation.~VChainOf(); // @0049c146
// weaponExplosions.~VChainOf(); // @0049c001
Check_Fpu();
}
//
// @0049be10 -- SubsystemMessageManager::CreateStreamedSubsystem.
// Reached from the mech3.cpp CreateSubsystemStream factory when the component
// type string == "SubsystemMessageManager". Stamps ClassID 0xBD3 / model
// record size 0x38, then reads the two manager-specific resource fields.
//
Logical
SubsystemMessageManager::CreateStreamedSubsystem(
ResourceFile *resource_file,
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories
)
{
if (
!Subsystem::CreateStreamedSubsystem(
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories
)
)
{
return False;
}
subsystem_resource->classID = (RegisteredClass::ClassID)0xBD3; // resource+0x20
subsystem_resource->subsystemModelSize = 0x38; // resource+0x24
//
// "TerrainHitExplosion" -- a resource name that must resolve to a model in
// resource_file; the resolved ResourceID is stored at resource+0x30.
//
const char *terrainHitName;
if (!subsystem_file->GetEntry(subsystem_name, "TerrainHitExplosion", &terrainHitName))
{
std::cerr << subsystem_name << " missing TerrainHitExplosion!\n";
return False;
}
ResourceDescription *rd = // FUN_00406ff8
resource_file->FindResourceDescription(
terrainHitName,
ResourceDescription::GameModelResourceType // recovered type literal 1
);
if (rd == 0)
{
std::cerr << subsystem_name << " cannot find " << terrainHitName
<< " in resource file!\n";
return False;
}
subsystem_resource->terrainHitExplosionID = rd->resourceID; // resource+0x30
//
// "RendererCompensateTime" -- a Scalar stored at resource+0x34.
//
if (!subsystem_file->GetEntry(subsystem_name, "RendererCompensateTime",
&subsystem_resource->rendererCompensateTime)) // resource+0x34
{
std::cerr << subsystem_name << " missing RendererCompensateTime\n";
return False;
}
return True;
}
//
// TestInstance -- emitted inline by the shipped build (no standalone body in
// the cluster). Standard subsystem form.
//
Logical
SubsystemMessageManager::TestInstance() const
{
Check(this);
return IsDerivedFrom(ClassDerivations);
}
//#############################################################################
// AddDamageMessage @0049b6d8
//#############################################################################
//
// Called by other subsystems / the damage path when an entity takes damage.
// Records the first hit's common information (entity, zone, impact point) and
// queues a per-message DamageInformation Plug into the damageInformation
// chain, keyed by the message's timestamp (Scalar at message+0x30).
//
void
SubsystemMessageManager::AddDamageMessage(
Entity *damaged_entity,
Entity__TakeDamageMessage *message
)
{
// First damaged entity of the frame wins the common record.
if (commonDamageInformation.entityHit == 0)
{
commonDamageInformation.entityHit = damaged_entity;
}
// First non-origin impact point of the frame wins. The recovered struct
// fields live inside the embedded Damage record (Entity__TakeDamageMessage::
// damageData): impactPoint @message+0x4C, damageType @+0x2C, amount @+0x30.
if (PointIsOrigin(commonDamageInformation.impactPoint)) // FUN_004084fc
{
commonDamageInformation.impactPoint = message->damageData.impactPoint; // message+0x4C
}
// First valid damage-zone index of the frame wins.
if (commonDamageInformation.damageZoneIndex == -1)
{
commonDamageInformation.damageZoneIndex = message->damageZone; // message+0x24
}
//
// Queue a DamageInformation plug (size 0x14: Plug + damageType + subsystemID),
// inserted into the damageInformation chain keyed by message+0x30 (the damage
// amount -- the Scalar immediately after damageType inside the Damage record).
//
DamageInformation *info = new DamageInformation; // FUN_00402298(0x14)
if (info != 0)
{
// Plug base init (FUN_00415e90 / vtable @0050b9a8)
info->damageType = message->damageData.damageType; // puVar2[3] = message+0x2C
info->subsystemID = message->inflictingSubsystemID; // puVar2[4] = message+0x5C
}
damageInformation.AddValue(info, message->damageData.damageAmount); // chain+0x114, key = message+0x30
}
//#############################################################################
// ConsolidateAndSendDamage @0049b784 (Performance)
//#############################################################################
//
// The per-frame Performance (installed as activePerformance by the ctor).
// Walks the queued DamageInformation chain, re-broadcasting a consolidated
// damage/notify message (ClassID 0x13, vtable @0050b984) for the common
// entity, queues a terrain-hit explosion resource into weaponExplosions when
// the hit zone has no explosion yet, flushes the explosion graphics to the
// renderer (SendQueuedExplosions @0049baa0), then clears the common record and
// empties the damage chain.
//
void
SubsystemMessageManager::ConsolidateAndSendDamage(Scalar /*time_slice*/)
{
Mech *owner = GetEntity(); // this[0x34] @0xD0
VChainIteratorOf<DamageInformation*, Scalar>
damageWalk(&damageInformation); // FUN_0049c28b over this+0x114
// VChainIteratorOf has no IsDone(); GetCurrent() returns NULL past the end.
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;
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
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
{
ResourceIDPlug *plug = new ResourceIDPlug(explosionID); // FUN_00402298(0x10) / FUN_00424716
weaponExplosions.AddValue(plug, explosionID); // chain+0xFC, slot 8
}
}
// Render queued weapon explosions and tear down the message.
SendQueuedExplosions(); // @0049baa0
}
// 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).
}
//#############################################################################
// SendQueuedExplosions @0049baa0 (file-local helper)
//#############################################################################
//
// Drains the weaponExplosions chain, emitting one renderer command per queued
// explosion (ClassID 0x5C, sub-type 3) at an increasing time step (0.1f per
// explosion) so multiple impacts in one frame are staggered. Submitted via
// the global graphics pipe (DAT_004efc94+0x60).
//
// NOTE: not declared in MESSMGR.HPP -- the header's protected
// CreateWeaponExplosions(terrain_hit, entity_hit, explode_position) is the
// per-explosion *queueing* helper (inlined into ConsolidateAndSendDamage
// above); this routine is the per-frame *flush* of the accumulated queue.
// Name is best-effort.
//
void
SubsystemMessageManager::SendQueuedExplosions()
{
Check(this);
VChainIteratorOf<ResourceIDPlug*, ResourceDescription::ResourceID>
explosionWalk(&weaponExplosions); // FUN_0049c314 over this+0xFC
// Base time was SystemClock Now() (FUN_00414b60); derive it from the last
// performance time so no global clock accessor is required here.
Scalar baseTime =
(Scalar)lastPerformance.ticks / (Scalar)SystemClock::GetTicksPerSecond();
Scalar step = 0.0f;
const Scalar stepIncrement = 0.1f; // local_c
// VChainIteratorOf has no IsDone(); GetCurrent() returns NULL past the end.
for (explosionWalk.First(); explosionWalk.GetCurrent() != 0; explosionWalk.Next())
{
ResourceDescription::ResourceID id = explosionWalk.GetValue(); // link+0x1C (the key)
//
// Each queued explosion is fired at an increasing time step (0.1s each)
// so multiple impacts in one frame stagger.
//
step += stepIncrement;
Scalar fireTime = baseTime + step;
SubmitExplosion(id, fireTime); // CROSS-FAMILY renderer submission
}
// chain emptied as the iterator is destroyed (FUN_0049c352).
}
//#############################################################################
// CROSS-FAMILY compile shims
//#############################################################################
//
// 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.
//
ResourceDescription::ResourceID
SubsystemMessageManager::ResolveExplosionID(Subsystem *subsystem)
{
Check_Pointer(subsystem);
// TODO(cross-family): return subsystem->GetExplosionID();
return terrainHitExplosionID;
}
//
// 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".
//
void
SubsystemMessageManager::SubmitExplosion(
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.
}
//#############################################################################
// DeathReset / ResetToInitialState
//#############################################################################
//
// vtable slot 10 @0049c3de is an empty body (the shipped build folds the
// trivial ResetToInitialState / DeathReset(Logical){} override to a bare
// "return"). Declared inline in MESSMGR.HPP (DeathReset); reproduced here for
// completeness.
//
// void SubsystemMessageManager::DeathReset(Logical) {} // inline, see header
//#############################################################################
// DamageInformation::~DamageInformation @0049c3e3
//#############################################################################
//
// The nested DamageInformation : Plug has a single virtual (its destructor,
// vtable @0050b9a8). Chains to ~Plug (FUN_00415ed8); the Plug has no owned
// resources beyond the base.
//
// SubsystemMessageManager::DamageInformation::~DamageInformation() {} // @0049c3e3
//###########################################################################
// CreateMessageManagerSubsystem -- factory bridge (task #7)
//
// mech.cpp's streamed-subsystem factory (case 0xBD3) calls this in the
// house complete-type-TU pattern (cf. CreateEmitterSubsystem et al).
//
Subsystem *CreateMessageManagerSubsystem(Mech *owner, int id, void *seg)
{
return new (Memory::Allocate(0x130)) SubsystemMessageManager(
owner, id,
(SubsystemMessageManager::SubsystemResource *)seg,
SubsystemMessageManager::DefaultData);
}