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>
332 lines
17 KiB
C++
332 lines
17 KiB
C++
//===========================================================================//
|
|
// File: torso.hpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Torso subsystem -- torso twist (yaw) and elevation (pitch) aim //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// --/--/95 ?? Initial coding. //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, recovered shard
|
|
// part_013.c). NO header survived; declarations are inferred from the
|
|
// decompiled bodies and from the resource field-name string pool parsed by
|
|
// Torso::CreateStreamedSubsystem (@004b6fec) -- the "missing <Name>!"
|
|
// diagnostics are GROUND TRUTH for the resource layout. In-object member names
|
|
// are best-effort (flagged in torso.cpp).
|
|
//
|
|
// Recovered binary facts (see torso.cpp for per-method @ADDR evidence):
|
|
// - Torso IS-A PowerWatcher (powersub.hpp); the ctor @004b6b0c chains to the
|
|
// PowerWatcher base ctor (FUN_004b18a4) passing &Torso::DefaultData
|
|
// (&DAT_00510af8). Full chain:
|
|
// Torso : PowerWatcher : HeatWatcher : MechSubsystem : Subsystem
|
|
// (NOT HeatableSubsystem -- the Watcher branch is separate from the
|
|
// HeatSink/PoweredSubsystem heat leaves; they share only MechSubsystem.)
|
|
// - vtable @0051103c ctor @004b6b0c dtor @004b6fc0
|
|
// - DefaultData @00510af8
|
|
// - RegisteredClass classID 0x0BC5 (resource +0x20), streamed model size
|
|
// 0x158 (resource +0x24).
|
|
//
|
|
// WHAT THE TORSO DOES (from the recovered simulation @004b5cf0 + helpers):
|
|
// It models torso twist (horizontal / yaw) and torso elevation (vertical /
|
|
// pitch) aim. Player turn/elevate commands ramp an angular rate (with a
|
|
// button-acceleration curve from a start value up to a cap), integrate into
|
|
// horizontal/vertical aim angles that are clamped to per-axis software limits
|
|
// (HorizontalLimitLeft/Right, VerticalLimitTop/Bottom), auto-recenter the
|
|
// twist when released, and write the result into two skeleton joints
|
|
// (TorsoHorizontalJoint and its TorsoHorizontalShadowJoint). All angular
|
|
// resource values are stored in radians (deg * PI/180). When the watched
|
|
// power source is dead, not Ready, or in the Failure heat state, the effective
|
|
// rate is zeroed (the torso freezes).
|
|
//
|
|
#if !defined(TORSO_HPP)
|
|
# define TORSO_HPP
|
|
|
|
#if !defined(POWERSUB_HPP)
|
|
# include "powersub.hpp"
|
|
#endif
|
|
|
|
//##################### Forward Class Declarations #######################
|
|
class Mech;
|
|
class Joint; // engine skeleton node (JOINT.h); the twist target
|
|
|
|
//###########################################################################
|
|
//################# Torso Model Resource ##############################
|
|
//###########################################################################
|
|
//
|
|
// Extends the PowerWatcher resource (MinVoltagePercent @+0xF0). Field offsets
|
|
// and names are exactly those read by Torso::CreateStreamedSubsystem @004b6fec.
|
|
// The angular Scalars are stored in radians (the ctor multiplies the parsed
|
|
// degree values by PI/180); they are primed to -1.0f on pass 1 and mandatory.
|
|
//
|
|
struct Torso__SubsystemResource:
|
|
public PowerWatcher::SubsystemResource
|
|
{
|
|
Scalar horizontalRotationPerSecond; // +0xF4 "HorizontalRotationPerSecond"
|
|
Scalar verticalRotationPerSecond; // +0xF8 "VerticalRotationPerSecond"
|
|
Scalar horizontalLimitRight; // +0xFC "HorizontalLimitRight"
|
|
Scalar horizontalLimitLeft; // +0x100 "HorizontalLimitLeft"
|
|
Scalar verticalLimitTop; // +0x104 "VerticalLimitTop"
|
|
Scalar verticalLimitBottom; // +0x108 "VerticalLimitBottom"
|
|
|
|
char torsoHorizontalJoint[32]; // +0x10C "TorsoHorizontalJoint"
|
|
char torsoHorizontalShadowJoint[32]; // +0x12C "TorsoHorizontalShadowJoint"
|
|
|
|
Logical torsoHorizontalEnabled; // +0x14C "TorsoHorizontalEnabled" (default True)
|
|
Scalar buttonAccelerationPerSecond; // +0x150 "ButtonAccelerationPerSecond"
|
|
Scalar buttonAccelerationStartValue; // +0x154 "ButtonAccelerationStartValue"
|
|
}; // sizeof == 0x158 (model size stamp)
|
|
static_assert(offsetof(Torso__SubsystemResource, horizontalRotationPerSecond) == 0xF4, "Torso horizontalRotationPerSecond must be at 0xF4");
|
|
static_assert(offsetof(Torso__SubsystemResource, buttonAccelerationStartValue)== 0x154, "Torso buttonAccelerationStartValue must be at 0x154");
|
|
static_assert(sizeof(Torso__SubsystemResource) == 0x158, "Torso record must be 0x158");
|
|
|
|
//###########################################################################
|
|
//############################# Torso ###################################
|
|
//###########################################################################
|
|
|
|
class Torso:
|
|
public PowerWatcher
|
|
{
|
|
friend struct TorsoLayoutCheck; // compile-time offset locks (torso.cpp STEP 3)
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Shared Data Support (DefaultData @00510af8, vtable @0051103c)
|
|
//
|
|
public:
|
|
static Derivation ClassDerivations;
|
|
static SharedData DefaultData;
|
|
static Receiver::MessageHandlerSet MessageHandlers;
|
|
static AttributeIndexSet AttributeIndex;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// BT segment / base-state / joint compatibility shims.
|
|
//
|
|
// CROSS-FAMILY: these accessors, the segment-flag constants, the joint
|
|
// helpers and the PowerWatcher base-method forwards logically belong on the
|
|
// shared bases (Mech::GetSegmentFlags / Mech::ResolveJoint, HeatSink::
|
|
// HeatStateLevel / HeatModelOff, PoweredSubsystem::ElectricalStateLevel, the
|
|
// skeleton-node writer, the Simulation dirty bit + creation/current time
|
|
// accessors, and PowerWatcher::ResetToInitialState / HandleDeathMessage /
|
|
// UpdateWatch). Until those families expose them they are backed locally so
|
|
// this module compiles; see report "CROSS-FAMILY NEEDS".
|
|
//
|
|
#if !defined(BT_SEGMENT_FLAG_CONSTS)
|
|
# define BT_SEGMENT_FLAG_CONSTS
|
|
enum {
|
|
SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == primary, 4 == damaged copy
|
|
SegmentLiveFlag = 0x01,
|
|
MasterHeatSinkFlag = 0x0100
|
|
};
|
|
#endif
|
|
public:
|
|
// BASE-CHAIN RE-BASE: the 7 shim BACKING FIELDS are removed so the object
|
|
// matches the binary 0x280 layout (currentTwist@0x1D8); these accessors now
|
|
// read the REAL inherited base state:
|
|
// HeatStateLevel <- HeatWatcher::heatAlarm level (this+0x140)
|
|
// ElectricalState <- PowerWatcher::watchdogAlarm level (this+0x198)
|
|
// HeatModelOff <- MechSubsystem::simulationState==Destroyed (this+0x40)
|
|
// Creation/Current <- engine Simulation::lastPerformance (this+0x14/0x10)
|
|
// the net/copy timing is dormant in single-player;
|
|
// equal reads give a 0 window == the shipped master path.
|
|
// SetMovedFlag -> engine Simulation::ForceUpdate() dirty bit (this+0x18)
|
|
// (GetSegmentFlags removed -- unused; the ctor reads owner->simulationFlags.)
|
|
HeatSink::HeatState HeatStateLevel() const { return (HeatSink::HeatState)heatAlarm.GetLevel(); }
|
|
Logical HeatModelOff() const { return simulationState == 1; /* Destroyed */ }
|
|
PoweredSubsystem::ElectricalState ElectricalStateLevel() const{ return (PoweredSubsystem::ElectricalState)watchdogAlarm.GetLevel(); }
|
|
LWord GetCreationTime() const { return (LWord)(long)lastPerformance; }
|
|
LWord GetCurrentTime() const { return (LWord)(long)lastPerformance; }
|
|
// Current horizontal (yaw) torso aim in radians (binary torso+0x1d8); the
|
|
// damage-table PieSlice wheel reads this when RotateWithTorso is set.
|
|
Scalar CurrentTwist() const { return currentTwist; }
|
|
protected:
|
|
// Resolve a named skeleton node to its live Joint* via the owning Mech
|
|
// (the ctor @004b6b0c inlines Mech::ResolveJoint == FUN_00424b60). Defined
|
|
// out-of-line in torso.cpp -- needs the complete Mech type.
|
|
Joint* ResolveJoint(const char *joint_name);
|
|
// Write one twist (yaw) scalar into a resolved skeleton node, preserving its
|
|
// other DOF -- the @004b66b4 inner block (hinge -> SetRotation(Radian);
|
|
// ball -> SetRotation(EulerAngles) with yaw <- twist).
|
|
void PushTwist(Joint *node, Scalar value);
|
|
// Simulation dirty / replicate bit -> real engine Simulation::ForceUpdate().
|
|
void SetMovedFlag() { ForceUpdate(); }
|
|
// Local helper: "is value pinned at this software limit?".
|
|
Logical AtLimit(Scalar value, Scalar limit) const { return fabsf(value - limit) <= 0.0001f; }
|
|
// CROSS-FAMILY delegate shims -- still no-ops (no layout impact; STEP-4 will
|
|
// chain them to the real PowerWatcher/HeatWatcher base once the Torso is live).
|
|
void WatcherResetToInitialState() {}
|
|
Logical WatcherHandleDeathMessage(Message & /*m*/) { return False; }
|
|
void WatcherUpdateWatch() {}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Test Class Support
|
|
//
|
|
public:
|
|
static Logical TestClass(Mech&);
|
|
Logical TestInstance() const;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Command interface (set by MechControlsMapper each frame -- inputs @0x1F8)
|
|
//
|
|
public:
|
|
// Command roles verified against the raw decomp @004b5cf0 (part_013.c
|
|
// 4522-4571): @0x1F8/@0x1FC drive ELEVATION (0x1e4, clamp vertTop/Bottom);
|
|
// @0x200/@0x204 drive TWIST (0x1d8, clamp horizLimitLeft/Right); @0x208
|
|
// recenters twist. (The earlier draft had elevation/twist swapped.)
|
|
enum {
|
|
ElevateUpCommand, // @0x1F8 elevation + (clamp verticalLimitTop)
|
|
ElevateDownCommand, // @0x1FC elevation - (clamp verticalLimitBottom)
|
|
TwistLeftCommand, // @0x200 twist + (clamp horizontalLimitLeft)
|
|
TwistRightCommand, // @0x204 twist - (clamp horizontalLimitRight)
|
|
CenterCommand // @0x208 recenter twist
|
|
};
|
|
|
|
// Analog (RIO/stick) aim demand written by MechControlsMapper::Interpret-
|
|
// Controls (@0x1F0 twist, @0x1F4 elevation); proportional, no button ramp.
|
|
void SetAnalogTwistAxis(Scalar v) { analogTwistAxis = v; }
|
|
void SetAnalogElevationAxis(Scalar v) { analogElevationAxis = v; }
|
|
Logical GetHorizontalEnabled() const { return horizontalEnabled; } // @0x250 (mapper free-aim gate @004afd10)
|
|
|
|
// Reachable horizontal (yaw) half-arc the guns can be brought to bear by
|
|
// twisting the torso: the wider of the two software limits (radians), or 0
|
|
// when horizontal twist is disabled (e.g. the Blackhawk, TorsoHorizontalEnabled=0).
|
|
// This is the AUTHENTIC, data-driven weapon traverse -- weapons carry no arc
|
|
// field; the torso mount is what lets a mech point its guns off dead-ahead.
|
|
Scalar GetHorizontalReach() const
|
|
{
|
|
if (!horizontalEnabled) return 0.0f;
|
|
Scalar l = fabsf(horizontalLimitLeft), r = fabsf(horizontalLimitRight);
|
|
return (l > r) ? l : r;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Simulation Support
|
|
//
|
|
public:
|
|
typedef void
|
|
(Torso::*Performance)(Scalar time_slice);
|
|
|
|
void
|
|
SetPerformance(Performance performance)
|
|
{
|
|
Check(this);
|
|
activePerformance = (Simulation::Performance)performance;
|
|
}
|
|
|
|
void TorsoSimulation(Scalar time_slice); // @004b5cf0 (master, PTR @00510c10)
|
|
void TorsoCopySimulation(Scalar time_slice); // @004b65f8 (damaged copy, PTR @00510c1c)
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Subsystem virtual overrides (slots on vtable @0051103c)
|
|
//
|
|
public:
|
|
void WriteUpdateRecord(UpdateRecord *message, int update_model); // slot 6, @004b6a78
|
|
Logical HandleDeathMessage(Message &message); // slot 9, @004b5be0 -> @004b179c
|
|
void ResetToInitialState(); // slot 10, @004b5bf8
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Construction and Destruction
|
|
//
|
|
public:
|
|
typedef Torso__SubsystemResource SubsystemResource;
|
|
|
|
Torso( // @004b6b0c
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data = DefaultData
|
|
);
|
|
~Torso(); // @004b6fc0
|
|
|
|
static int
|
|
CreateStreamedSubsystem( // @004b6fec
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *subsystem_resource,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes = 1
|
|
);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Internal model helpers
|
|
//
|
|
protected:
|
|
Logical ComputeTargetTwist(); // @004b6510 (returns "still slewing")
|
|
void WriteJoints(Scalar &verticalOut); // @004b66b4
|
|
void UpdateJoints(); // @004b67ec
|
|
Logical Recenter(Scalar time_slice); // @004b6918 (returns "still off-centre")
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Local data. Byte offsets are those observed in the shipped object
|
|
// (this[n] == @0x(4n)); names are best-effort. Angular values are radians.
|
|
// PowerWatcher/HeatWatcher/MechSubsystem base data occupies the low
|
|
// offsets and is NOT redeclared here.
|
|
//
|
|
protected:
|
|
Scalar currentTwist; // @0x1D8 current horizontal (yaw) aim
|
|
Scalar horizontalLimitRight; // @0x1DC resource +0xFC (rad)
|
|
Scalar horizontalLimitLeft; // @0x1E0 resource +0x100 (rad)
|
|
Scalar currentElevation; // @0x1E4 current vertical (pitch) aim
|
|
Scalar twistVelocity; // @0x1E8 d(currentTwist)/dt
|
|
Scalar elevationVelocity; // @0x1EC d(currentElevation)/dt
|
|
Scalar analogTwistAxis; // @0x1F0 RIO/stick twist demand
|
|
Scalar analogElevationAxis; // @0x1F4 RIO/stick elevation demand
|
|
|
|
int elevateUpCommand; // @0x1F8 digital elevation-up (>0 = active)
|
|
int elevateDownCommand; // @0x1FC digital elevation-down
|
|
int twistLeftCommand; // @0x200 digital twist-left
|
|
int twistRightCommand; // @0x204 digital twist-right
|
|
int centerCommand; // @0x208 digital recenter
|
|
int statusFlags; // @0x20C 0/1/2 (limit/active state)
|
|
|
|
Scalar buttonAccelerationPerSecond;// @0x210 resource +0x150
|
|
Scalar buttonAccelerationStart; // @0x214 resource +0x154
|
|
|
|
Scalar targetTwist; // @0x218 ComputeTargetTwist output
|
|
Scalar twistAtUpdate; // @0x21C twist snapshot at last net update
|
|
Scalar verticalLimitTop; // @0x220 resource +0x104 (rad)
|
|
Scalar verticalLimitBottom; // @0x224 resource +0x108 (rad)
|
|
Scalar elevationCenter; // @0x228 derived limit copy
|
|
Scalar elevationHalfBottom; // @0x22C = verticalLimitBottom * 0.5f
|
|
Scalar twistCenterHigh; // @0x230 derived limit copy
|
|
Scalar twistCenterLow; // @0x234 derived limit copy
|
|
|
|
Scalar twistRate; // @0x238 net-synced angular rate
|
|
Scalar baseTwistRate; // @0x23C resource +0xF4 (rad/s)
|
|
Scalar baseElevationRate; // @0x240 resource +0xF8 (rad/s)
|
|
Scalar effectiveTwistRate; // @0x244 baseTwistRate, gated by power/heat
|
|
Scalar effectiveElevationRate; // @0x248 baseElevationRate, gated by power/heat
|
|
|
|
int isDamagedCopy; // @0x24C 0 = live master, 1 = copy
|
|
Logical horizontalEnabled; // @0x250 resource +0x14C
|
|
LWord lastUpdateTime; // @0x254 ms timestamp of last command sample
|
|
|
|
int hitElevTop; // @0x258 elevation-top limit latch (vs verticalLimitTop)
|
|
int hitElevBottom; // @0x25C elevation-bottom limit latch (vs verticalLimitBottom)
|
|
int hitTwistLeft; // @0x260 twist-left limit latch (vs horizontalLimitLeft)
|
|
int hitTwistRight; // @0x264 twist-right limit latch (vs horizontalLimitRight)
|
|
int buttonRampActive; // @0x268
|
|
Scalar buttonRamp; // @0x26C current button-accel multiplier
|
|
int _reserved0x270; // @0x270 binary reserves a 4-byte slot here
|
|
// (cleared by ResetToInitialState @004b5bf8);
|
|
// present so recenterActive lands at 0x274.
|
|
int recenterActive; // @0x274
|
|
|
|
Joint *horizontalJointNode; // @0x278 resolved TorsoHorizontalJoint
|
|
Joint *horizontalShadowJointNode; // @0x27C resolved TorsoHorizontalShadowJoint
|
|
}; // sizeof == 0x280 (own block 0x1D8..0x280); locked by static_asserts below.
|
|
|
|
//
|
|
// BASE-CHAIN RE-BASE: the 7 CROSS-FAMILY shim backing fields that used to sit
|
|
// here (segmentFlags/heatModelOff/heatStateLevel/electricalState/creationTime/
|
|
// currentTime/movedFlag) are DELETED -- they over-sized the object by 28 bytes
|
|
// and duplicated engine-base state. Their accessors now read the real inherited
|
|
// members (HeatWatcher::heatAlarm, PowerWatcher::watchdogAlarm, MechSubsystem::
|
|
// simulationState, engine Simulation::lastPerformance/ForceUpdate). See above.
|
|
//
|
|
|
|
#endif
|