diff --git a/restoration/source410/BT/MECHWEAP.HPP b/restoration/source410/BT/MECHWEAP.HPP index 14c291bc..09d744b0 100644 --- a/restoration/source410/BT/MECHWEAP.HPP +++ b/restoration/source410/BT/MECHWEAP.HPP @@ -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 // diff --git a/restoration/source410/BT/MESSMGR.CPP b/restoration/source410/BT/MESSMGR.CPP index c1d854d1..cbb3acb3 100644 --- a/restoration/source410/BT/MESSMGR.CPP +++ b/restoration/source410/BT/MESSMGR.CPP @@ -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 -#pragma hdrstop - -#if !defined(MESSMGR_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#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 +#pragma hdrstop + +#if !defined(MESSMGR_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(MECHWEAP_HPP) +# include // ExplosionResourceIDOf (weapon @0x3e4) +#endif + +#if !defined(EXPLODE_HPP) +# include // Explosion::MakeMessage +#endif + +#if !defined(APP_HPP) +# include // application->Post / GetRegistry / priorities +#endif + +#if !defined(REGISTRY_HPP) +# include // 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 + 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(). 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 + 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; +} diff --git a/restoration/source410/BT/MESSMGR.NOTES.md b/restoration/source410/BT/MESSMGR.NOTES.md new file mode 100644 index 00000000..531f3012 --- /dev/null +++ b/restoration/source410/BT/MESSMGR.NOTES.md @@ -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()` 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.