STEP 6 COMPLETE: cylinder hit-location LIVE — unaimed hits resolve to zones
The Mech per-impact hit-location resolver (the cylinder damage table) is now functional, wired, and runtime-verified [T2]. Unaimed (zone==-1) hits — the collision-damage path — now resolve an impact point to a damage zone via the authentic height x angle grid + weighted dice roll, instead of dropping. dmgtable.cpp/.hpp was a non-functional skeleton on no-op ReconTable/stream shims; backed it with real std::vector storage and fixed 5 latent runtime bugs: - ReadEntries now consumes the leading cell name-string ([i32 len][len+1]) - PieSlice ctor reads rotateWithTorso into the correct member - SelectSlice direct-indexes (was int lookup on a float-keyed table) - ResolveHit returns the zone (chains SelectSlice -> SelectZone) - real MemoryStream::ReadBytes (was a variadic no-op) mech.cpp ctor: replaced the empty-name StandingAnimation stub with the real load — FindResourceDescription(dzRes->resourceName, type 0x1d) -> stream -> new DamageLookupTable, cached at mech[0x111]; ~Mech deletes it. Mech::TakeDamageMessageHandler override registered (MESSAGE_ENTRY overlays Entity's by ID): on invalidDamageZone, resolve via the table then base-route; aimed reticle hits pass through unchanged. Three named accessors (no databinding-trap raw reads): WorldToLocal (localToWorld.MultiplyByInverse), CylinderReferenceHeight (standingTemplateMaxY == collisionTemplate->maxY == binary mech+0x2ec[+0xc]), TorsoHeading via a BTGetTorsoTwist bridge in torso.cpp (Torso::CurrentTwist == torso+0x1d8; torso.hpp cannot be included into mech.cpp — subsystem-stub collision). Stream format + geometry + roll + handler were all byte-verified against the shipped BTL4.RES type-29 resources (18 tables, exact consumption) and the disassembly (FUN_0049eb54/e678/de14, glue 0x49ed0c, handler @0x4a037a). Runtime: boots clean, "[cyl] table 'bhk1' layers=7" (exact byte-verified layer count, found by name), mech spawns + walks, no asserts/AV/0xCDCDCDCD. Env gate BT_CYL_LOG=1. Unblocks collision-damage application. KB updated (combat-damage.md STEP 6 COMPLETE, open-questions.md marked done). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
33fee712e9
commit
d07ac7dd49
+144
-19
@@ -72,6 +72,13 @@
|
||||
// AUTHENTIC GROUND MODEL ctor half (task #15): complete BoxedSolid type for the
|
||||
// collisionTemplate/collisionVolume extent reads + the template bottom lift.
|
||||
#include <BOXSOLID.hpp>
|
||||
// STEP 6 cylinder hit-location table. dmgtable.hpp pulls in no subsystem
|
||||
// headers (only Plug + mechrecon + <vector>), so it is safe here -- unlike the
|
||||
// real subsystem headers, whose classes collide with mech.cpp's local stubs.
|
||||
// The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp)
|
||||
// for the same reason.
|
||||
#include "dmgtable.hpp"
|
||||
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
|
||||
#if !defined(APP_HPP)
|
||||
# include <app.hpp>
|
||||
#endif
|
||||
@@ -394,14 +401,107 @@ Derivation
|
||||
"Mech"
|
||||
);
|
||||
|
||||
// Chain Mech's handler set to the parent's (Entity's, which Mover/JointedMover use
|
||||
// directly -- they add none). An EMPTY set with no inheritance made Receiver::Receive
|
||||
// find NO handler for TakeDamageMessageID (and every other inherited message), so the
|
||||
// engine base Entity::TakeDamageMessageHandler -- which routes damageZones[zone]->
|
||||
// TakeDamage -- never ran and damage silently dropped. (Mech's OWN TakeDamage override
|
||||
// + cylinder lookup is the later STEP-6 reconstruction; this restores the base routing.)
|
||||
// Mech's OWN handler table, chained to the parent's (Entity's, which Mover/
|
||||
// JointedMover use directly -- they add none). It OVERRIDES Entity's TakeDamage
|
||||
// with the cylinder hit-location resolver (STEP 6): Build() overlays the derived
|
||||
// entry onto the inherited table by message ID (RECEIVER.cpp), so every other
|
||||
// inherited message still routes to its base handler. (Previously this was a
|
||||
// bare copy of Entity's set -- base TakeDamage dropped unaimed/-1 zone hits.)
|
||||
const Receiver::HandlerEntry
|
||||
Mech::MessageHandlerEntries[] =
|
||||
{
|
||||
MESSAGE_ENTRY(Mech, TakeDamage),
|
||||
};
|
||||
|
||||
Receiver::MessageHandlerSet
|
||||
Mech::MessageHandlers(Entity::GetMessageHandlers());
|
||||
Mech::MessageHandlers(
|
||||
ELEMENTS(Mech::MessageHandlerEntries),
|
||||
Mech::MessageHandlerEntries,
|
||||
Entity::GetMessageHandlers());
|
||||
|
||||
//#############################################################################
|
||||
// STEP 6 -- cylinder hit-location support (consumed by dmgtable.cpp)
|
||||
//#############################################################################
|
||||
//
|
||||
// World -> mech-local point transform (binary FUN_00408bf8 reading owner+0xd0).
|
||||
// Uses the inherited Entity::localToWorld (the render/collision object matrix);
|
||||
// MultiplyByInverse gives the world->local change of coordinates.
|
||||
//
|
||||
Point3D
|
||||
Mech::WorldToLocal(const Point3D &world)
|
||||
{
|
||||
Point3D local;
|
||||
local.MultiplyByInverse(world, localToWorld);
|
||||
return local;
|
||||
}
|
||||
|
||||
//
|
||||
// The roster torso (sinkSourceSubsystem @0x438, ClassID 0xBC5) -- parity with
|
||||
// the binary's *(mech+0x438).
|
||||
//
|
||||
void *
|
||||
Mech::TorsoOrientationSource()
|
||||
{
|
||||
return GetTorsoSubsystem();
|
||||
}
|
||||
|
||||
//
|
||||
// Live torso twist (yaw) in radians -- binary torso+0x1d8. Reached via the
|
||||
// torso.cpp bridge (Torso is a complete type there; including torso.hpp here
|
||||
// would collide with mech.cpp's local subsystem stubs). 0 for no/fixed torso.
|
||||
//
|
||||
Scalar
|
||||
Mech::TorsoHeading()
|
||||
{
|
||||
return BTGetTorsoTwist(GetTorsoSubsystem());
|
||||
}
|
||||
|
||||
//
|
||||
// Height reference the cylinder table normalises impact height against -- the
|
||||
// binary's *(mech+0x2ec)+0xc, a stance height written from mech+0x518 (standing)
|
||||
// / +0x51c (ducked). standingTemplateMaxY is that value (collisionTemplate->maxY
|
||||
// captured at ctor); fall back to the live template if the ground model was off
|
||||
// when the ctor ran (standingTemplateMaxY left 0).
|
||||
//
|
||||
Scalar
|
||||
Mech::CylinderReferenceHeight()
|
||||
{
|
||||
if (standingTemplateMaxY > 0.0f)
|
||||
{
|
||||
return standingTemplateMaxY;
|
||||
}
|
||||
BoxedSolid *tmpl = GetCollisionTemplate();
|
||||
return tmpl ? tmpl->maxY : 0.0f; // ResolveHit guards <= 0
|
||||
}
|
||||
|
||||
//
|
||||
// Mech override of Entity::TakeDamageMessageHandler (binary @0x4a037a, the two
|
||||
// call sites into the glue @0x49ed0c). An unaimed hit arrives with
|
||||
// invalidDamageZone set (damageZone < 0); resolve its zone from the cylinder
|
||||
// hit-location table (mech[0x111]) using the impact point, clear the flag, then
|
||||
// hand off to the base handler which routes damageZones[zone]->TakeDamage. Aimed
|
||||
// (reticle) hits carry a valid zone and pass straight through.
|
||||
//
|
||||
void
|
||||
Mech::TakeDamageMessageHandler(TakeDamageMessage *message)
|
||||
{
|
||||
Check(message);
|
||||
DamageLookupTable *table = (DamageLookupTable *)Wword(0x111);
|
||||
if (message->invalidDamageZone && table != 0)
|
||||
{
|
||||
int zone = table->ResolveHit(message->damageData.impactPoint);
|
||||
message->damageZone = zone;
|
||||
message->invalidDamageZone = False;
|
||||
if (BTEnvOn("BT_CYL_LOG", 0))
|
||||
{
|
||||
DEBUG_STREAM << "[cyl] unaimed hit -> zone " << zone
|
||||
<< " (impact " << message->damageData.impactPoint.x << ","
|
||||
<< message->damageData.impactPoint.y << ","
|
||||
<< message->damageData.impactPoint.z << ")\n" << std::flush;
|
||||
}
|
||||
}
|
||||
Entity::TakeDamageMessageHandler(message); // base: damageZones[zone]->TakeDamage
|
||||
}
|
||||
|
||||
//
|
||||
// Mech attribute table. DENSE PREFIX 0x15..0x21 (JointedMover::NextAttributeID
|
||||
@@ -1208,17 +1308,42 @@ Mech::Mech(
|
||||
}
|
||||
|
||||
//
|
||||
// Cylinder damage-zone table (resource type 0x1d, by name) -- the per-impact zone
|
||||
// resolver consumed by Mech::TakeDamageMessageHandler for unaimed/-1 hits. STILL A
|
||||
// STUB here (StandingAnimation placeholder); the real CylinderDamageZoneTable is
|
||||
// STEP 6a of the damage reconstruction. Left as-was so the rest builds; the empty
|
||||
// name makes the lookup a no-op until then.
|
||||
// Cylinder hit-location table (resource type 0x1d = DamageLookupTableStream) --
|
||||
// the per-impact zone resolver consumed by Mech::TakeDamageMessageHandler for
|
||||
// unaimed/-1 hits (STEP 6). Faithful to the ctor @part_012.c:10411-10425: the
|
||||
// table is found by the mech's DamageZoneStream (type-0x14) NAME -- the offline
|
||||
// builder writes BOTH the 0x14 and 0x1d resources under the model name (see
|
||||
// CreateDamageZoneStream ~line 1930), so they share it -- then streamed and
|
||||
// cached at mech[0x111] (byte 0x444). (Was an empty-name StandingAnimation
|
||||
// stub -> 0 rows; the real class is dmgtable.cpp.)
|
||||
//
|
||||
char shadowName[32];
|
||||
shadowName[0] = '\0';
|
||||
int animSeg = ResourceFindByName(shadowName, 0x1d); // FUN_00406ff8 (TODO STEP 6a: cylinder table)
|
||||
MemStreamX animMem(animSeg);
|
||||
Wword(0x111) = (int)new (Memory::Allocate(0x2c)) StandingAnimation(this, &animMem); // FUN_0049ea48
|
||||
Wword(0x111) = 0;
|
||||
ResourceDescription *dzForName =
|
||||
MechFindResource(creation_message->resourceID,
|
||||
ResourceDescription::DamageZoneStreamResourceType); // type 0x14 (for its name)
|
||||
ResourceDescription *cylRes = (dzForName != 0)
|
||||
? application->GetResourceFile()->FindResourceDescription(
|
||||
dzForName->resourceName,
|
||||
ResourceDescription::DamageLookupTableStreamResourceType) // FUN_00406ff8, type 0x1d
|
||||
: 0;
|
||||
if (cylRes != 0)
|
||||
{
|
||||
cylRes->Lock();
|
||||
{
|
||||
DynamicMemoryStream cylStream( // FUN_004032dc, offset 0
|
||||
cylRes->resourceAddress, cylRes->resourceSize, 0);
|
||||
DamageLookupTable *table = new DamageLookupTable(this, &cylStream); // FUN_0049ea48
|
||||
Wword(0x111) = (int)table;
|
||||
DEBUG_STREAM << "[cyl] table '" << dzForName->resourceName
|
||||
<< "' layers=" << table->LayerCount() << "\n" << std::flush;
|
||||
}
|
||||
cylRes->Unlock();
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "[cyl] no DamageLookupTable (type 0x1d) for '"
|
||||
<< (dzForName ? dzForName->resourceName : "?") << "'\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// Bind the three creation-name strings from the MakeMessage.
|
||||
@@ -1276,9 +1401,9 @@ Mech::~Mech()
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
if (Wword(0x111) != 0) // standing animation
|
||||
if (Wword(0x111) != 0) // cylinder hit-location table (STEP 6)
|
||||
{
|
||||
((Releasable *)Wword(0x111))->Release();
|
||||
delete (DamageLookupTable *)Wword(0x111); // frees layers/slices/entries
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user