THE SALVO DAMAGE (#95). Players measured an LRM 15 landing "3ish points". The logs agreed: [projectile] IMPACT damage=3.33333 (Oracle, LRM15) and 3.5 (Rajel, LRM10). Those are right PER MISSILE -- 50/15 and 35/10 -- and the bench shows why the salvo still under-delivers: each launcher pushes N rounds of which exactly ONE carries damage. Two individually-correct changes composed into an N-fold shortfall: * The ctor does what the binary's MissileLauncher ctor does (@0x3ac/@0x3d4): damageData.burstCount = missileCount; damageData.damageAmount /= missileCount; The record holds the PER-MISSILE amount plus the count; the arcade reconstitutes amount x burstCount when it applies the hit. * Task #62 then correctly stopped the port applying the hit once per visual round (that was ~missileCount-x too lethal) by damaging only the lead round -- but handed it the already-divided amount. Our DamageZone::TakeDamage is `damageLevel += amount * scale` and drops burstCount, so the salvo delivered amount/missileCount. Since the port collapses the cluster to one damaging round, multiply the count back in there. Bench: an SRM6 salvo now lands amt=35 (the authored total) taking a zone 0 -> 0.556, where it previously landed 5.83. THE DOUBLE EXPLOSION (#84). Oracle: "missile appear to register hit explosions twice, once where target was and again where the target is." There are two spawn sites: BTSpawnRoundDetonation at the round's own impact point, and the message manager's bundled explosion at the CONSOLIDATED point a frame later. The duplicate was known and thought harmless -- "among a rippled volley it is invisible" -- which held only while a salvo landed N detonations. A projectile now marks its weapon (MarkRoundDetonated) and the consolidation skips queueing a second blast for it; direct-fire weapons never mark, so lasers/AC keep the bundled explosion they rely on. Bench: 4 missile impacts -> 4 SKIPPED, while 11 direct-fire hits still queue normally. SWEPT CONTACT (#84 tail). Contact was a 10-unit sphere sampled only at the END of each step. With the authored thruster live (#84) field rounds arrive at v=955 -- a ~16 unit step at 60fps, larger than the radius -- so samples can straddle the target. (Pre-#84 rounds flew ~100-300 = a 1.7-5 unit step and could never skip it: the velocity fix exposed this, it did not cause it.) Now tests the whole segment travelled and bursts at the point of NEAREST APPROACH, which also stops the detonation being flung past the target at speed. RETRACTION: I posted tunnelling as the leading explanation for the lost salvo. The bench disproves it -- zero fizzles, and the "missing" rounds are the dmg=0 visual rounds of the cluster, which never registered damage by design. The sweep is kept as speed-independent robustness, not as the #95 fix. Bench: scratchpad/night8/salvo.sh (BT_PROJ_LOG + BT_FIRE_LOG). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
635 lines
25 KiB
C++
635 lines
25 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>
|
|
#include <mechweap.hpp> // MechWeapon::GetExplosionResourceID (task #7 explosion bundling)
|
|
#include <EXPLODE.hpp> // Explosion::Make (the established explosion port)
|
|
#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
|
|
|
|
selfDetonatedCount = 0; // port-only (issue #84)
|
|
|
|
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)
|
|
{
|
|
//
|
|
// THE CONSOLIDATED SEND (task #7 tail; this was a "recovered note"
|
|
// stub): build the Entity::TakeDamageStreamMessage (id 0x13, base
|
|
// 0x34 -- ENTITY3.h:189, byte-for-byte the binary's layout), append
|
|
// {count, [damageType, damageAmount, subsystemID]...} through the
|
|
// dynamic message (the binary's msg-vtable slot-8 append,
|
|
// FUN_00419e60), and Dispatch it at the hit entity -- whose T0
|
|
// handler (ENTITY.cpp:817) re-splits it into per-record
|
|
// TakeDamageMessages. A REPLICANT victim's Dispatch reroutes the
|
|
// stream cross-pod exactly as the old direct path did (task #47).
|
|
//
|
|
int entryCount = 0;
|
|
for (damageWalk.First(); damageWalk.GetCurrent() != 0; damageWalk.Next())
|
|
{
|
|
++entryCount;
|
|
}
|
|
|
|
Entity::DynamicMessage streamMessage(
|
|
Entity::TakeDamageStreamMessageID,
|
|
sizeof(Entity::TakeDamageStreamMessage));
|
|
{
|
|
Entity::TakeDamageStreamMessage *header =
|
|
(Entity::TakeDamageStreamMessage *)streamMessage.GetMessagePointer();
|
|
header->impactPoint = commonDamageInformation.impactPoint;
|
|
header->damageZoneIndex = commonDamageInformation.damageZoneIndex;
|
|
header->shootingEntity = owner->GetEntityID();
|
|
}
|
|
streamMessage << entryCount;
|
|
|
|
for (damageWalk.First(); damageWalk.GetCurrent() != 0; damageWalk.Next())
|
|
{
|
|
DamageInformation *info = damageWalk.GetCurrent();
|
|
|
|
streamMessage << (int)info->damageType
|
|
<< (Scalar)damageWalk.GetValue() // the chain KEY = damageAmount
|
|
<< (int)info->subsystemID;
|
|
|
|
//
|
|
// Bundle the FIRING weapon's explosion: the record's subsystemID
|
|
// is the inflicting weapon on the OWNER's roster (ENTITY3.h:143's
|
|
// documented purpose; binary reads roster[id]+0x3E4). Non-weapon
|
|
// / unthreaded (-1) ids resolve Null and queue nothing.
|
|
//
|
|
Subsystem *firingWeapon = (info->subsystemID >= 0
|
|
&& info->subsystemID < owner->GetSubsystemCount())
|
|
? owner->GetSubsystem(info->subsystemID) : 0;
|
|
ResourceDescription::ResourceID explosionID =
|
|
ResolveExplosionID(firingWeapon); // weapon+0x3E4
|
|
if (getenv("BT_FIRE_LOG"))
|
|
DEBUG_STREAM << "[boom-q] subsysID=" << info->subsystemID
|
|
<< " weapon=" << (firingWeapon ? firingWeapon->GetName() : "<none>")
|
|
<< " explosionID=" << explosionID << std::endl;
|
|
//
|
|
// issue #84: skip the bundled explosion for a weapon whose round
|
|
// already detonated itself at its own impact point. Queueing it
|
|
// too puts a second blast at the CONSOLIDATED point a frame later
|
|
// -- the "explosion where the target was, and again where it is".
|
|
//
|
|
if (DidRoundDetonate(info->subsystemID))
|
|
{
|
|
if (getenv("BT_FIRE_LOG"))
|
|
DEBUG_STREAM << "[boom-q] subsysID=" << info->subsystemID
|
|
<< " SKIPPED (round detonated itself)" << std::endl;
|
|
}
|
|
else if (explosionID != ResourceDescription::NullResourceID
|
|
&& !weaponExplosions.Find(explosionID)) // chain+0xFC, slot 0xC
|
|
{
|
|
ResourceIDPlug *plug = new ResourceIDPlug(explosionID);
|
|
weaponExplosions.AddValue(plug, explosionID); // chain+0xFC, slot 8
|
|
}
|
|
}
|
|
|
|
//
|
|
// Deliver the consolidated stream to the hit entity.
|
|
//
|
|
if (commonDamageInformation.entityHit != 0)
|
|
{
|
|
Entity::TakeDamageStreamMessage *finalMessage =
|
|
(Entity::TakeDamageStreamMessage *)streamMessage.GetMessagePointer();
|
|
if (getenv("BT_MSGMGR_LOG"))
|
|
DEBUG_STREAM << "[msgmgr] consolidated " << entryCount
|
|
<< " records -> " << (void *)commonDamageInformation.entityHit
|
|
<< " len=" << (int)finalMessage->messageLength
|
|
<< " zone=" << commonDamageInformation.damageZoneIndex
|
|
<< std::endl;
|
|
commonDamageInformation.entityHit->Dispatch(finalMessage);
|
|
}
|
|
|
|
// Render queued weapon explosions and tear down the message.
|
|
SendQueuedExplosions(); // @0049baa0
|
|
}
|
|
|
|
// PURGE the consumed queue -- the binary's explicit teardown
|
|
// (FUN_00417858(iter, 1)). The old "cleared as the iterator is
|
|
// destroyed" note was WRONG: nothing purges automatically, so stale
|
|
// records re-applied every tick (observed live: counts 1->2->3->4,
|
|
// damage double-counted). The nodes are heap plugs from
|
|
// AddDamageMessage -> delete them here.
|
|
for (damageWalk.First(); damageWalk.GetCurrent() != 0; damageWalk.First())
|
|
{
|
|
DamageInformation *consumed = damageWalk.GetCurrent();
|
|
damageWalk.Remove();
|
|
delete consumed;
|
|
}
|
|
|
|
// 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]
|
|
selfDetonatedCount = 0; // port-only (issue #84): marks are per-flush
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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
|
|
}
|
|
|
|
// PURGE the flushed queue (same explicit-teardown correction as the
|
|
// damage chain above; the plugs are heap nodes).
|
|
for (explosionWalk.First(); explosionWalk.GetCurrent() != 0; explosionWalk.First())
|
|
{
|
|
ResourceIDPlug *flushed = explosionWalk.GetCurrent();
|
|
explosionWalk.Remove();
|
|
delete flushed;
|
|
}
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// ResolveExplosionID -- the real read (task #7 tail)
|
|
//#############################################################################
|
|
//
|
|
// The binary reads the FIRING weapon's explosion resource: roster[subsystemID]
|
|
// + 0x3E4 (the streamed ExplosionModelFile). The old stand-in returned
|
|
// terrainHitExplosionID and framed the record as "the damaged subsystem" --
|
|
// both wrong (records are the OWNER's inflicting weapons). Guarded: the
|
|
// binary's submitters are all weapons; our reachability is wider.
|
|
//
|
|
ResourceDescription::ResourceID
|
|
SubsystemMessageManager::ResolveExplosionID(Subsystem *subsystem)
|
|
{
|
|
if (subsystem != 0
|
|
&& subsystem->IsDerivedFrom(*MechWeapon::GetClassDerivations()))
|
|
{
|
|
return ((MechWeapon *)subsystem)->GetExplosionResourceID(); // +0x3E4
|
|
}
|
|
return ResourceDescription::NullResourceID;
|
|
}
|
|
|
|
//
|
|
// PORT-ONLY (issue #84) -- see the header note. A projectile that spawned its
|
|
// own round detonation marks its weapon here so the consolidation does not queue
|
|
// a SECOND, differently-placed explosion for the same hit.
|
|
//
|
|
void
|
|
SubsystemMessageManager::MarkRoundDetonated(int subsystem_id)
|
|
{
|
|
Check(this);
|
|
if (subsystem_id < 0 || DidRoundDetonate(subsystem_id))
|
|
return;
|
|
if (selfDetonatedCount >= kMaxSelfDetonated)
|
|
return; // full: fall back to the bundled blast
|
|
selfDetonated[selfDetonatedCount++] = subsystem_id;
|
|
}
|
|
|
|
Logical
|
|
SubsystemMessageManager::DidRoundDetonate(int subsystem_id) const
|
|
{
|
|
Check(this);
|
|
for (int i = 0; i < selfDetonatedCount; ++i)
|
|
if (selfDetonated[i] == subsystem_id)
|
|
return True;
|
|
return False;
|
|
}
|
|
|
|
//
|
|
// SubmitExplosion -- the real spawn (task #7 tail). The binary posts a
|
|
// Registry::MakeEntityMessage (id 3, class 0x31 Explosion, flags 0x100) at
|
|
// Now + 0.1s x queue-ordinal (the caller's fire_time) so multi-hit impacts
|
|
// fire sequentially. The port spawns through the established Explosion::Make
|
|
// port at the consolidated impact point. [T3: the 0.1s stagger is computed
|
|
// by the caller but not yet honoured -- timed entity posts need the registry
|
|
// queue; the visual difference on multi-weapon volleys is a subtle ripple.]
|
|
//
|
|
void
|
|
SubsystemMessageManager::SubmitExplosion(
|
|
ResourceDescription::ResourceID explosion_id,
|
|
Scalar /*fire_time*/
|
|
)
|
|
{
|
|
Check(this);
|
|
|
|
Origin o;
|
|
o.linearPosition = commonDamageInformation.impactPoint;
|
|
o.angularPosition = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f));
|
|
|
|
Mech *owner = GetEntity();
|
|
EntityID hitID = (commonDamageInformation.entityHit != 0)
|
|
? commonDamageInformation.entityHit->GetEntityID()
|
|
: owner->GetEntityID();
|
|
|
|
Explosion::MakeMessage m(
|
|
Explosion::MakeMessageID, sizeof(Explosion::MakeMessage),
|
|
(Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null,
|
|
explosion_id, Explosion::DefaultFlags, o,
|
|
hitID, owner->GetEntityID());
|
|
Explosion *e = Explosion::Make(&m);
|
|
if (getenv("BT_FIRE_LOG"))
|
|
DEBUG_STREAM << "[boom] id=" << explosion_id
|
|
<< " at(" << o.linearPosition.x << "," << o.linearPosition.y
|
|
<< "," << o.linearPosition.z << ") made=" << (void *)e << std::endl;
|
|
if (e != 0)
|
|
{
|
|
Register_Object(e);
|
|
}
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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);
|
|
}
|