Files
TeslaRel410/restoration/source410/BT/MESSMGR.CPP
T
CydandClaude Fable 5 80ac2943a0 BT410 5.3.118: the census closes -- the authoring-side CreateStreamedSubsystem family, and the Fail count reaches ZERO
The last eight stubs plus the two chain layers they actually need
(PoweredSubsystem @004b13ac and ProjectileWeapon @004bc7cc were DECLARED in
our headers all along -- only their definitions were missing).  These are
the notation->stream converters the 1995 authoring pipeline ran; missions
read prebuilt streams and never call them, so the verification standard is
compile + link + the raw decomp's key fingerprints -- and those fingerprints
convicted the donors three more times:

- The heat chain: Condenser, Reservoir AND PoweredSubsystem all chain ONE
  shared parser (@004ae150, reconstructed in full from the decomp -- the
  BT411 donor's body was its own TODO stub with a miscited address).  It
  reads the five thermal scalars and the conduction-graph link (the
  linkedSinkIndex our wire-format work dump-verified from the runtime side,
  with the +2 segment-table bias), and HeatSink adds no keys of its own.
- The Emitter's SeekVoltage LADDER is a notation ENTRY LIST walked in file
  order (curve rungs in sequence, the RecommendedIndex entry by name); the
  donor flattened it to one keyed read, which would author one-rung ladders
  against the runtime's real five-rung ones.  MakeEntryList/GetFirstEntry
  is the engine's own idiom.
- PipColor and MuzzleVelocity parse through the engine's authentic
  Convert_From_Ascii overloads (RGBColor / Vector3D); the donors' sscanf
  substitutes were port drift.  Explosion and alarm models resolve as model
  FAMILIES (ModelListResourceType == the literal 1 in the binary; the donor
  comment calling it GameModelResourceType was wrong-name-right-number).

Entry reads target the SUBSYSTEM notation file -- the decomp's fifth
argument at every site; several donors wrote model_file.  Class IDs stamp
through OUR reader's enum symbols (they parse the real BTL4.RES streams
today, and the 3000-base enum lands exactly on the binary's 0xBBB.. ids;
0x0BCD is the weapon-base stamp every concrete leaf overwrites).

Stubs: NONE.  Every function in restoration/source410 now has a body.
Mission soak clean.  Remaining reconstruction is depth, not holes: the
mech4/mech3 archaeology (IntegrateMotion, the master-perf disasm, the
stream builders that seed the 0xC/0x100 flags), HUD blocks 4-6, and the
live networked/rig verifications queued for when a pod is available.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:22:12 -05:00

462 lines
14 KiB
C++

//===========================================================================//
// 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();
}
//
//#############################################################################
// CreateStreamedSubsystem -- binary @0049be10 (324 bytes). Chains the base
// Subsystem record, stamps classID 0x0BD3 / size 0x38, resolves the
// REQUIRED "TerrainHitExplosion" model family, reads the REQUIRED
// "RendererCompensateTime". (The runtime never consumes either field --
// see the inert terrain-hit note above -- but the authored stream carries
// both, so the converter writes both.)
//#############################################################################
//
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::SubsystemMessageManagerClassID;
subsystem_resource->subsystemModelSize = 0x38;
const char
*terrain_hit_name;
if (
!subsystem_file->GetEntry(subsystem_name, "TerrainHitExplosion",
&terrain_hit_name)
)
{
DEBUG_STREAM << subsystem_name << " missing TerrainHitExplosion!" << endl;
return False;
}
ResourceDescription
*description =
resource_file->FindResourceDescription(
terrain_hit_name,
ResourceDescription::ModelListResourceType);
if (description == NULL)
{
DEBUG_STREAM << subsystem_name << " cannot find " << terrain_hit_name
<< " in resource file!" << endl;
return False;
}
subsystem_resource->terrainHitExplosionID = description->resourceID;
if (
!subsystem_file->GetEntry(subsystem_name, "RendererCompensateTime",
&subsystem_resource->rendererCompensateTime)
)
{
DEBUG_STREAM << subsystem_name << " missing RendererCompensateTime" << endl;
return False;
}
Check_Fpu();
return True;
}