//===========================================================================// // 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 ::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::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 #pragma hdrstop #if !defined(MESSMGR_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(APP_HPP) # include #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 @0xFC // damageInformation : VChainOf @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 (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 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 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); }