Brings Cyd's experience-level / simulation-mode research onto master (the KB-decode portion of glass-cockpit 4e01e83; the bundled BT410 source manifest is left for the full glass-cockpit merge). - context/experience-levels.md (NEW): the egg per-pilot 'experience' field (novice/standard/veteran/expert) is the pod's SIMULATION-FIDELITY tier, decoded end-to-end. FUN_004c0bc8 reads btMission->experienceLevel(+0xe4) and fans it into the +0x25c/+0x260/+0x26c/+0x270/+0x274 flag block: +0x25c 'sim live' (novice lockout: jams/searchlight/powersub), +0x260 the HEAT-MODEL master switch (veteran+expert; FUN_004ad7d4), +0x274 raw level (FUN_004ac9c8 = ==0 novice predicate; the valve/advanced-cockpit lockout). 4.0->4.10 drift: the viewscreen hunting-aid gate is GONE in 4.10 (HUD reads none of these flags), and movement heat is veteran+expert not expert-only. - btplayer.cpp/.hpp: CORRECTED comments/labels -- the +0x25c..+0x274 block is seeded from btMission experienceLevel/advancedDamageOn, NOT the scenario role's returnFromDeath; the 'roleClassIndex/showKills' names are scoring-era mislabels. Code logic UNCHANGED (comment-only): the ctor still reads scenarioRole (STAND-IN, defaults 2 = veteran) pending the wiring task. - Corrections swept into gauges-hud.md (ROOKIE->novice lockout), open-questions.md (player+0x260/0x274 semantics now PINNED), pod-hardware.md, subsystems.md, decomp-reference.md; CLAUDE.md router row. No code-logic change -> no rebuild needed (comment + markdown only). The runtime wiring (seed from BTMission::ExperienceLevel()) remains TODO. Co-Authored-By: Cyd <cyd@falloutshelterarcade.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
407 lines
15 KiB
C++
407 lines
15 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
|
|
|
|
//
|
|
// EXPERIENCE-LEVEL game-mode flags (2026-07-18 [T1] — the old "per-role
|
|
// display toggles / returnFromDeath" reading was WRONG). The ctor
|
|
// @004c0bc8 seeds this block from btMission(+0x1f8)->experienceLevel
|
|
// (+0xe4, the egg's per-pilot "experience" novice/standard/veteran/
|
|
// expert) and ->advancedDamageOn(+0xf0) — NOT from the scenario role.
|
|
// The member NAMES below date from the scoring work and do not match
|
|
// the semantics; kept until the wiring task renames them. Binary
|
|
// mapping (nov/std/vet/exp) + consumers: context/experience-levels.md.
|
|
//
|
|
Logical
|
|
showDamageReceived; // @0x25c 0/1/1/1 "sim live" (novice lockout: jams, searchlight, powersub @4b0efc)
|
|
Logical
|
|
showKills; // @0x260 0/0/1/1 HEAT-MODEL master switch (FUN_004ad7d4)
|
|
Logical
|
|
showDamageInflicted;// @0x264 = mission advancedDamageOn (copy 1; NOT part of the level switch)
|
|
Logical
|
|
showScore; // @0x270 0/1/1/1 consumer not located
|
|
Scalar
|
|
roleReturnDelay; // @0x268 = mission advancedDamageOn (copy 2; int in the binary)
|
|
Scalar
|
|
roleReturnDelay2; // @0x26c 0/1/1/0 consumer not located (supercharge-clamp hypothesis [T4])
|
|
int
|
|
roleClassIndex; // @0x274 raw experience level 0..3 (FUN_004ac9c8 = ==0 novice predicate)
|
|
friend int BTPlayerRoleLocksAdvanced(void *); // the FUN_004ac9c8 bridge (task #12)
|
|
friend void BTPlayerCountObservedDeath(void *); // observed-death tally (MP DEATHS fix)
|
|
|
|
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
|