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>
462 lines
14 KiB
C++
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;
|
|
}
|