Files
BT412/game/reconstructed/btplayer.hpp
T
arcattackandClaude Fable 5 d7b900d108 Cockpit: GENERATOR POWER ROUTING -- assign weapons to generators (task #12)
The pilot's second heat-management tool: the PoweredSubsystem message table
@0x50F4EC (ids 4-8) registered and implemented -- SelectGeneratorA-D
(@004b099c..@004b0a74: FindGeneratorByNumber roster walk on generatorNumber
@0x1E0 -> AttachToVoltageSource re-tap with tap accounting -> modeAlarm
Connected) and ToggleGeneratorMode (@004b0abc: Manual -> Auto -> detach+Manual
cycle).  Weapons inherit via the MechWeapon handler chain.  Desktop: F5-F8
assign the selected weapon (BT_CONFIG_SLOT) to Generator A-D, F9 toggles
reconnect mode; BT_GENSEL_TEST scripts a headless verify.

Verified live end-to-end: dispatch -> handler -> re-tap ("PPC_1 ->
GeneratorD (tapped)") -> the charging I^2R physically moved (GenD cold ~90K
baseline -> ~1570K carrying the PPC; GenA relieved), stable over a sustained
autofire soak with thermal-breaker trips.

FOUR defects found and fixed on the way [all T1/T2]:
- THE e17 HEAT EXPLOSION: Generator::SourceLevel misread *(this[0x38]+0x158)
  as linkedSinks->heatEnergy; it is the engine-base DamageZone @0xE0 ->
  damageLevel [0..1] (the same named-member pattern as the bank radiator's
  zone read).  A breaker-restarting generator emitted (1 - 4e8) x 10000
  volts; squared through the customers' I^2R feed, one restart blew the
  whole thermal network to e17.  Authentic: a damaged generator yields
  proportionally less voltage.
- FUN_004ac9c8 is NOT "IsDamaged": raw body = owner -> mech+0x190 player ->
  roleClassIndex(+0x274) == 0 -- the ROOKIE-role lockout for advanced
  cockpit systems.  New bridge BTPlayerRoleLocksAdvanced (NULL player =
  unlocked [T3]; bring-up role 2 = unlocked).  The old stand-in gated the
  handlers off permanently (healthy subsystems have simulationState==1).
- MESSAGE_ENTRY tables must be FUNCTION-LOCAL statics inside the accessor:
  as namespace-scope arrays they are read by other TUs' static-init chains
  before their own initializers run -- Build copies zeros and every id in
  the table is silently dropped (ids 9/10 only worked by TU-order luck;
  both tables relocated; gotcha recorded, reconstruction-gotchas #9).
- The AutoConnect hunt scanned GetSegment() -- the raw @004b0bd0 walks the
  subsystem ROSTER (+0x124/+0x128); EntitySegments were being cast to
  Subsystems (the classic +0x128 gotcha).

Also: the dense handler table's GAP slots (skipped ids) are uninitialized
heap -- the name-based Find strcmp-walks them and AVs; diagnostic probes use
the id-based Find only (the 1995 binary's own tables carry the same holes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:16:48 -05:00

400 lines
14 KiB
C++

//===========================================================================//
// File: btplayer.hpp //
// Project: BattleTech Brick: Player / Scoring //
// Contents: BattleTech player & scoring entity (BT analog of RPPLAYER) //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/96 JM Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (BTL4OPT.EXE) Ghidra pseudo-C
// (the bt/btplayer.cpp cluster @004c012c-@004c0f28) cross-referenced against
// the surviving Red Planet sibling RPPLAYER.cpp/.h (same engine, same Player
// base, same scoring/message-handler/registry idioms) and the surviving BT
// headers BTTEAM.HPP, BTREG.HPP and MUNGA PLAYER.HPP / SCNROLE.HPP.
//
// BTPlayer is the BattleTech player/scoring entity. Where Red Planet's
// RPPlayer scored "score zones", speed bonuses and crusher/runner roles,
// BTPlayer scores mech-vs-mech combat: damage inflicted, damage received,
// kills and friendly-fire, all scaled by the players' relative tonnage and
// by a per-role ScenarioRole modifier set (see SCNROLE.HPP).
//
// Field offsets in comments are the byte offsets observed in the decompiled
// 0x294-byte object (e.g. "@0x278" == this[0x9e]). Names are taken from
// PLAYER.HPP / SCNROLE.HPP where the field is inherited, and inferred from
// usage (and flagged) where BT-specific. See btplayer.cpp for per-method
// @ADDR evidence.
//
#if !defined(BTPLAYER_HPP)
# define BTPLAYER_HPP
#if !defined(PLAYER_HPP)
# include <player.hpp>
#endif
#if !defined(SCNROLE_HPP)
# include <scnrole.hpp>
#endif
#if !defined(CSTR_HPP)
# include <cstr.hpp>
#endif
//##################### Forward Class Declarations #######################
class Mech;
class BTTeam;
class DropZone__ReplyMessage;
//###########################################################################
//##################### BTPlayer::ScoreMessage ########################
//###########################################################################
//
// Extends the MUNGA Player::ScoreMessage (which carries only scoreAward
// @0x1c) with a scoreType selector and the EntityID of the mech that
// generated the points. Parallels RPPlayer__ScoreMessage, but the BT
// score types describe mech combat instead of score zones.
//
// Observed message layout (param_2 in the handlers):
// +0x1c scoreAward (Scalar, base Player::ScoreMessage)
// +0x20 scoreType (int)
// +0x24 damageAmount (Scalar) raw damage / point quantity
// +0x28 pointSenderHi (EntityID word)
// +0x2c pointSenderLo (EntityID word)
// +0x30 auxID (EntityID / host word)
// +0x34 senderMechID (EntityID -- resolved to the inflicting Mech)
//
class BTPlayer__ScoreMessage:
public Player::ScoreMessage
{
public:
//
// Kind of scoring event. Recovered from the branch selector at
// @004c02e4 (this->scoreType, message+0x20) and the dedicated
// inflicted-damage handler at @004c0200.
//
enum ScoreType {
DamageInflictedScore = 0, // to ScoreInflictedMessageHandler
DamageReceivedScore = 1, // I took damage
KillScore = 2 // I destroyed / was destroyed
};
int
scoreType; // +0x20
Scalar
damageAmount; // +0x24
//
// Point-source / auxiliary handles carried alongside the damage record
// (read by ScoreMessageHandler when building the console feed message).
//
int
pointSenderHi; // +0x28
int
pointSenderLo; // +0x2c
int
auxID; // +0x30
EntityID
senderMechID; // +0x34 inflicting mech (point sender)
BTPlayer__ScoreMessage(
Receiver::MessageID message_ID,
size_t length,
int score_type,
Scalar score_award,
Scalar damage_amount,
const EntityID &sender_mech_ID
):
Player::ScoreMessage(message_ID, length, score_award),
scoreType(score_type),
damageAmount(damage_amount),
senderMechID(sender_mech_ID)
{}
};
//###########################################################################
//##################### BTPlayer::MakeMessage #########################
//###########################################################################
//
// Construction message. BTRegistry::MakePlayer (BTREG.CPP) builds this
// from the mission's role name and team name; the ctor (@004c0bc8) copies
// the team name out of it (+0x50) and resolves the role resource (+0x90).
//
class BTPlayer__MakeMessage:
public Player::MakeMessage
{
public:
//
// ⚠ WIRE FORMAT: MakeMessages are sent RAW over the network when the
// entity replicates (InterestManager -> remote Registry::MakeEntity), so
// string payload must be INLINE at the binary's fixed offsets -- teamName
// @ +0x50, roleName @ +0x90 (0x40 bytes each). The earlier draft stored
// const char* POINTERS: fine in one address space, garbage on the
// receiving pod (the first cross-pod BTPlayer replication crashed in
// CString(roleName)).
//
char
teamName[0x40]; // +0x50 team this player belongs to (inline)
char
roleName[0x40]; // +0x90 scenario-role resource name (inline)
BTPlayer__MakeMessage(
Receiver::MessageID message_ID,
size_t length,
Entity::ClassID class_ID,
const EntityID &owner_ID,
ResourceDescription::ResourceID resource_ID,
LWord instance_flags,
const Origin &origin,
int player_bitmap_index,
const char *role_name,
const char *team_name
):
Player::MakeMessage(
message_ID, length, class_ID, owner_ID,
resource_ID, instance_flags, origin, player_bitmap_index
)
{
teamName[0] = 0;
roleName[0] = 0;
if (team_name)
{
strncpy(teamName, team_name, sizeof(teamName) - 1);
teamName[sizeof(teamName) - 1] = 0;
}
if (role_name)
{
strncpy(roleName, role_name, sizeof(roleName) - 1);
roleName[sizeof(roleName) - 1] = 0;
}
}
};
//###########################################################################
//########################### BTPlayer ################################
//###########################################################################
//
// (vtable @00513300, ctor @004c0bc8, Make @004c0ecc, scalar-deleting
// destructor @004c0efc, TestInstance @004c0f28.)
//
class BTPlayer:
public Player
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data support
//
// WinTesla refactored the static derivation/handler/attribute objects
// behind Get* accessors (avoids static-init ordering bugs).
//
public:
static Derivation* GetClassDerivations();
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Scoring / construction message typedefs
//
// (Declared up here so the message-handler signatures below resolve
// ScoreMessage to BTPlayer__ScoreMessage rather than the inherited
// Player::ScoreMessage.)
//
public:
typedef BTPlayer__ScoreMessage ScoreMessage;
typedef BTPlayer__MakeMessage MakeMessage;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Message Support
//
// Handler table (MESSAGE_ENTRY names recovered from the .data string
// block @005130c0): DropZoneReply, VehicleDead, Score, ScoreUpdate,
// MissionStarting, MissionEnding -- plus the dedicated ScoreInflicted
// handler (assert string @00513110). ScoreInflicted is listed here as
// best-effort; its MESSAGE_ENTRY name was not captured in the dump.
//
public:
enum {
ScoreInflictedMessageID = Player::NextMessageID,
ScoreUpdateMessageID,
NextMessageID
};
private:
static const HandlerEntry MessageHandlerEntries[];
protected:
static MessageHandlerSet& GetMessageHandlers();
void
VehicleDeadMessageHandler(VehicleDeadMessage *message); // @004c012c
void
ScoreInflictedMessageHandler(ScoreMessage *message); // @004c0200
void
ScoreUpdateMessageHandler(ScoreMessage *message); // @004c02a8
void
ScoreMessageHandler(ScoreMessage *message); // @004c02e4
//
// @004bffd0 -- the spawn / respawn handshake. When the drop zone
// replies with our spawn location we create (or reset) the player's
// mech there and bind it to us. BT analog of Blocker/RPPlayer
// DropZoneReplyMessageHandler; reconstructed from FUN_004bffd0 (the
// MESSAGE_ENTRY(BTPlayer,DropZoneReply) handler at table @00512fd8).
//
void
DropZoneReplyMessageHandler(DropZone__ReplyMessage *message); // @004bffd0
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Vehicle creation
//
// CreatePlayerVehicle is the virtual the base Player calls to build the
// player's vehicle; BT overrides it (vtable slot +0x40 -> FUN_004bfcac)
// to instantiate a Mech from the mission's game-model resource.
// InitializePlayerLink (FUN_004bfee0) binds the freshly-made mech back to
// this player. Structural model = RPPlayer::CreatePlayerVehicle /
// ::InitializePlayerLink (same engine, same MakeAndLinkViewpointEntity).
//
protected:
virtual void
CreatePlayerVehicle(Origin mech_location); // @004bfcac (vtable +0x40)
void
InitializePlayerLink(); // @004bfee0
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Scoring Support
//
protected:
//
// @004c052c -- the shared "how many points is this damage worth"
// calculation used by both ScoreInflicted and the kill branch of
// ScoreMessageHandler. Applies the role's damageInflictedModifier,
// damageBias and (for same-team hits) friendlyFirePenalty.
//
Scalar
CalcInflictedScore(
const Scalar &damage_amount,
Mech *other_mech,
Scalar bias
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Model Support
//
public:
typedef void
(BTPlayer::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
PlayerSimulation(Scalar time_slice); // @004c083c (console update)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
static BTPlayer*
Make(MakeMessage *creation_message); // @004c0ecc
BTPlayer(
MakeMessage *creation_message,
SharedData &shared_data = DefaultData
); // @004c0bc8
~BTPlayer(); // @004c0efc (deleting dtor)
Logical
TestInstance() const; // @004c0f28
//
// gauge scoring wave: public scoreboard accessors so the cockpit gauges
// (PilotList KILLS/DEATHS) read the SAME compiled members the scoring
// handlers write -- the earlier raw-offset reads (pilot+0x27c / +0x200)
// don't match our compiled layout (engine base != the 1995 binary) and
// silently read 0. currentScore/playerRanking are inherited Player fields.
//
int GetKillCount() const { return killCount; } // @0x27c
int GetDeaths() const { return deathCount; } // @0x200 (Player)
Scalar GetScore() const { return currentScore; } // @0x278 (Player)
int GetRanking() const { return playerRanking; } // (Player)
Mech *GetObjectiveMech() const { return objectiveMech; } // @0x284 (current target)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data
//
// Offsets are byte offsets into the shipped 0x294-byte object. The
// base/derived boundary is approximate; fields known to be inherited
// from Player/Entity (playerVehicle, deathCount, scenarioRole,
// currentScore, ownerID) are noted as such.
//
protected:
// --- inherited from Player / Entity, referenced here ---
// EntityID ownerID; // @0x18c (Entity)
// Mech *playerVehicle; // @0x1fc (Player)
// int deathCount; // @0x200 (Player)
// ScenarioRole *scenarioRole; // @0x208 (Player) -- scoring role
// Scalar currentScore; // @0x278 (Player) -- running score
// --- BT-specific ---
Mission
*btMission; // @0x1f8 cached mission / role registry source
char
teamName[64]; // @0x20c copied from MakeMessage (+0x50)
BTTeam
*teamEntity; // @0x24c resolved from the "Teams" group
Logical
freeForAll; // @0x250 game type == "freeforall" (no team scoring)
Logical
consoleAttached; // @0x254 a console host exists for us
Logical
suppressConsole; // @0x258 skip console notify for this score event
//
// Per-role display toggles, selected from the role's "returnFromDeath"
// class index (scenarioRole resource +0xe4, values 0..3) at @004c0bc8.
//
Logical
showDamageReceived; // @0x25c
Logical
showKills; // @0x260
Logical
showDamageInflicted;// @0x264
Logical
showScore; // @0x270
Scalar
roleReturnDelay; // @0x268 (role resource +0xf0)
Scalar
roleReturnDelay2; // @0x26c
int
roleClassIndex; // @0x274 (role resource +0xe4)
friend int BTPlayerRoleLocksAdvanced(void *); // the FUN_004ac9c8 bridge (task #12)
int
killCount; // @0x27c kills credited to this player
int
pad_0x280; // @0x280
Mech
*objectiveMech; // @0x284 designated target / objective mech
Time
lastPerformance; // @0x288 cached sim clock
Time
lastConsoleUpdate; // @0x28c last time score was sent to console
Logical
deathPending; // @0x290 death notice scheduled / in flight
//
// Name of whoever last killed us (shown on the pilot HUD). Copied from
// the VehicleDead message in the binary; @0x1d0 (CString).
//
CString
killerName; // @0x1d0
};
#endif