gauge wave P2b: build the Comm KILLS/DEATHS pilot roster (PilotList)

PilotList (keyword "pilotList") was PROSE-ONLY, so the Comm surface showed only
the baked btcomm.pcx labels.  Reconstructed all 7 functions from part_014.c:
3156-3434 (Make/ctor/dtor/BecameActive/TestInstance/Execute/DrawMechIcon), with
the 8x(x,y,layoutMode) layout table DAT_0051af88 PE-parsed exact from BTL4OPT.EXE.
Draws ONE roster slot/frame (round-robin) from the viewpoint mech's cockpit-mapper
pilot roster.

- Roster FEED: new BTResolveRosterPilot(slot) bridge in mechmppr.cpp (a complete-Mech
  TU) resolves the viewpoint mech's ControlsMapper (subsystemArray[0]) -> GetPilot;
  btl4gau3.cpp reads the returned pilot at raw BTPlayer offsets.  The mapper's
  FillPilotArray already fills the roster (pilotArray[0]=GetMissionPlayer,
  [1..]=FindGroup("Players")).
- KILLS = killCount@0x27c (real; ScoreMessageHandler increments it in combat).
- DEATHS: the binary reads pad_0x280 which has NO writer anywhere (a shipped dead
  field -> perpetual 0).  FIX per "if it doesn't work, fix it": read the real
  deaths counter Player::deathCount@0x200 (VehicleDeadMessageHandler increments it)
  so DEATHS is meaningful in MP.
- DrawMechIcon: App+0xC8 name-bitmap cache is unwired (same deferral PlayerStatus
  uses) -> LookupPlayerNameBitmap returns NULL -> the tinted name box (never an AV).

Dropped the bogus x,y from the header ctor (positions come from DAT_0051af88).
/FORCE-safe (all vtable slots real; link log clean, no unresolved PilotList/
BTResolveRosterPilot).  Verified DBASE+dev gauges: the Comm surface now renders the
live local pilot row (KILLS 0 DEATHS 0 + name box; 0/0 authentic in solo -- the
combat scoring feed that moves them is Phase 3), combat un-regressed (DESTROYED),
0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-07 17:24:24 -05:00
co-authored by Claude Opus 4.8
parent d68284ede2
commit 0060a3e1ca
4 changed files with 249 additions and 17 deletions
+217 -15
View File
@@ -176,24 +176,226 @@ Logical
//########################################################################### //###########################################################################
// PilotList @004ca90c Make / @004ca958 ctor // PilotList (config keyword "pilotList" -- the Comm KILLS/DEATHS roster).
// Reconstructed from part_014.c:3156-3434; vtable PTR_FUN_0051be64, sizeof 0x178.
// Draws ONE roster slot per frame (round-robin over 8) from the viewpoint mech's
// cockpit-mapper pilot roster. Was PROSE-ONLY -> the "pilotList" line was
// parse-skipped and the Comm surface showed only the baked btcomm.pcx labels.
//########################################################################### //###########################################################################
// The roster lives on the viewpoint mech's ControlsMapper (subsystemArray[0]);
// the bridge (mechmppr.cpp, a complete-Mech TU) resolves it -- this TU only sees
// the pilot as an opaque pointer read at BTPlayer raw offsets.
extern void *BTResolveRosterPilot(int slot);
// App+0xC8 player-name-bitmap cache is not wired in the port (same deferral
// PlayerStatus uses for its nameImage) -> NULL routes DrawMechIcon to the binary's
// own cache-miss branch (the tinted name box), never an AV.
static BitMap *LookupPlayerNameBitmap(int /*name_id*/) { return NULL; }
MethodDescription
PilotList::methodDescription =
{
"pilotList",
PilotList::Make,
{
{ ParameterDescription::typeRate, NULL }, // rate (C)
{ ParameterDescription::typeModeMask, NULL }, // mode (ModeAlwaysActive)
{ ParameterDescription::typeString, NULL }, // font (bigfont.pcc)
PARAMETER_DESCRIPTION_END
}
};
// //
// @004ca90c -- Make: allocate (0x178) and construct "PilotList" from // @004ca90c -- Make. Allocate 0x178 + placement-construct. position UNUSED
// DAT_0051ab4c..0051abd4. // (each entry carries absolute coords from DAT_0051af88). Always returns True.
// @004ca958 -- ctor (vtable PTR_FUN_0051be64). GraphicGauge base. For each of
// 8 entries, read its (x,y,layoutMode) from DAT_0051af88 (stride 3); compute the
// three NumericDisplay positions from layoutMode (0/1/2) and build them
// (this entry +0x10/+0x14/+0x18). resolvedMech = 0; selected = 0.
// @004cab10 -- dtor: delete the 3 numerics per entry (8 entries), base dtor.
// @004caba4 -- BecameActive: per-entry resolvedMech (+0x08) = -1; currentSlot = 9.
// @004cabd0 -- Execute (one slot per frame). Look up the lance roster
// (App+0x6c mech +0x128 -> FUN_004b0898 slot lookup). For the current slot:
// if empty, erase the three numerics; else compute the player-name bitmap and
// blit it (DrawMechIcon @004cad70 tints by team via FUN_004c2f88), then DrawAt
// the mech (+0x27c) and score (+0x280) numerics. Advance currentSlot (wraps 8).
// helpers DrawMechIcon @004cad70, DrawAmmoCount @004cae20.
// //
Logical
PilotList::Make(
int display_port_index,
Vector2DOf<int> /*position -- unused*/,
Entity * /*entity -- unused*/,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
PilotList *g = (PilotList *)operator new(0x178); // FUN_00402298(0x178)
if (g != NULL)
{
new (g) PilotList(
p[0].data.rate, p[1].data.modeMask,
(L4GaugeRenderer *)gauge_renderer,
display_port_index, // graphics_port_number
p[2].data.string, // font (bigfont.pcc)
"PilotList");
}
return True;
}
//
// @004ca958 -- ctor. GraphicGauge base; build 3 NumericDisplays/entry from the
// PE-parsed 8x(x,y,layoutMode) layout table (DAT_0051af88).
//
PilotList::PilotList(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int graphics_port_number,
const char *font_image,
const char *id_string
):
GraphicGauge(rate, mode_mask, renderer_in, 0, graphics_port_number, id_string)
{
currentSlot = 0;
built = False;
L4Warehouse *wh = (L4Warehouse *)renderer_in->warehousePointer; // renderer+0x4c
// DAT_0051af88 -- 8 x (x, y, layoutMode); PE-parsed exact from BTL4OPT.EXE.
static const int L[8][3] =
{
{180,225,0},{17,443,1},{177,443,1},{337,443,1},
{497,443,1},{17,7,2},{177,7,2},{337,7,2}
};
for (int i = 0; i < 8; ++i)
{
int x = L[i][0], y = L[i][1], m = L[i][2];
int nx, ny, mx, my;
if (m == 1) { nx = x-8; ny = y-0x50; mx = x+0x48; my = y-0x50; }
else if (m == 2) { nx = x-8; ny = y+0x44; mx = x+0x48; my = y+0x44; }
else { nx = x+0x95; ny = y-0x0d; mx = x+0xe8; my = y-0x0d; } // m==0
entry[i].x = x; entry[i].y = y;
entry[i].resolvedMech = 0; entry[i].selected = 0;
entry[i].nameDisplay = new NumericDisplay(wh, nx, ny, font_image, 2,
NumericDisplay::signedBlankedZerosFormat, 0, 0xff); // KILLS
entry[i].mechDisplay = new NumericDisplay(wh, mx, my, font_image, 2,
NumericDisplay::signedBlankedZerosFormat, 0, 0xff); // DEATHS
entry[i].scoreDisplay = new NumericDisplay(wh, x, y, font_image, 1,
NumericDisplay::signedBlankedZerosFormat, 0, 0xff); // erase-only
}
}
//
// @004cab10 -- dtor. Free the 3 numerics per entry; the base chain runs implicitly.
//
PilotList::~PilotList()
{
for (int i = 0; i < 8; ++i)
{
delete entry[i].nameDisplay; entry[i].nameDisplay = NULL;
delete entry[i].mechDisplay; entry[i].mechDisplay = NULL;
delete entry[i].scoreDisplay; entry[i].scoreDisplay = NULL;
}
}
//
// @004caba4 -- BecameActive. Invalidate every entry so the first cycle repaints.
//
void
PilotList::BecameActive()
{
for (int i = 0; i < 8; ++i)
entry[i].resolvedMech = -1;
currentSlot = 9; // forces the wrap-reset on the first Execute
}
//
// @004cab8c -- TestInstance (out-of-line forward to the base; body required so
// the declared method is not /FORCE-stubbed).
//
Logical
PilotList::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
// @004cad70 -- DrawMechIcon. Blit the pilot's name bitmap (or, on a cache miss --
// always, in the port -- a tinted 0x80 x 0x20 box), tinted by the selected flag.
//
void
PilotList::DrawMechIcon(void *pilot, int selected)
{
int nameId = *(int *)((char *)pilot + 0x1e0); // pilot name/id key
BitMap *bmp = LookupPlayerNameBitmap(nameId);
int boxColor = selected ? 0xff : 0;
int blitColor = selected ? 0 : 0xff;
if (bmp == NULL)
{
localView.SetColor(boxColor); // vtbl+0x18
localView.DrawFilledRectangleToRelative(0x80, 0x20); // vtbl+0x4c
}
else
{
localView.SetColor(blitColor);
localView.DrawBitMapOpaque(boxColor, 0, bmp, 0, 0, // vtbl+0x58
bmp->Data.Size.x - 1, bmp->Data.Size.y - 1);
}
}
//
// @004cabd0 -- Execute (one roster slot per frame). Look up the current slot from
// the viewpoint mech's ControlsMapper roster; erase an empty slot, or draw the
// pilot's name icon + KILLS + DEATHS for an occupied one, tinting the row when it
// is the local pilot's current target.
//
void
PilotList::Execute()
{
void *local = BTResolveRosterPilot(0); // slot-0 (local) pilot gates the body
if (local == NULL)
return;
if (currentSlot > 7) { currentSlot = 0; built = False; } // wrap the round-robin
Entry &e = entry[currentSlot];
void *pilot;
if (built == False)
{
pilot = BTResolveRosterPilot(currentSlot);
if (pilot == NULL) built = True; // past the first empty slot -> rest empty this cycle
}
else
{
pilot = NULL;
}
if (pilot == NULL)
{
if (e.resolvedMech != 0) // (-1 after BecameActive) -> erase the box + numerics
{
localView.MoveToAbsolute(e.x, e.y); // vtbl+0x24
localView.SetColor(0); // vtbl+0x18
localView.DrawFilledRectangleToRelative(0x80, 0x20); // vtbl+0x4c
e.nameDisplay->Erase(&localView);
e.mechDisplay->Erase(&localView);
e.scoreDisplay->Erase(&localView);
}
}
else
{
void *tgt = *(void **)((char *)local + 0x284); // local pilot's target (objectiveMech)
int selected = (tgt != NULL) && (pilot == *(void **)((char *)tgt + 0x190)); // target's owningPlayer
if ((int)(size_t)pilot != e.resolvedMech || e.selected != selected)
{
localView.MoveToAbsolute(e.x, e.y); // vtbl+0x24
DrawMechIcon(pilot, selected);
e.selected = selected;
// (byte-faithful: the binary does NOT latch e.resolvedMech here, so the
// icon redraws each occupied frame.)
}
// KILLS = killCount@0x27c (real; ScoreMessageHandler kill branch increments it).
e.nameDisplay->Draw(&localView, (Scalar)(*(int *)((char *)pilot + 0x27c)));
// DEATHS: the binary reads pad_0x280 (NO writer anywhere -> perpetual 0 = a
// shipped dead field). FIX (correctness deviation, per "if it doesn't work,
// fix it"): read the real deaths counter Player::deathCount@0x200
// (VehicleDeadMessageHandler increments it), so DEATHS is meaningful in MP.
e.mechDisplay->Draw(&localView, (Scalar)(*(int *)((char *)pilot + 0x200)));
}
++currentSlot;
}
//########################################################################### //###########################################################################
+5 -2
View File
@@ -175,16 +175,19 @@
public GraphicGauge public GraphicGauge
{ {
public: public:
// gauge wave P2: registered as "pilotList" -- the Comm KILLS/DEATHS roster.
static MethodDescription methodDescription;
static Logical Make(int, Vector2DOf<int>, Entity *, GaugeRenderer *); // @004ca90c static Logical Make(int, Vector2DOf<int>, Entity *, GaugeRenderer *); // @004ca90c
PilotList( // @004ca958 PilotList( // @004ca958 (NO x,y: positions come from DAT_0051af88)
GaugeRate, ModeMask, L4GaugeRenderer *, int, GaugeRate, ModeMask, L4GaugeRenderer *, int,
int x, int y, const char *font_image, const char *font_image,
const char *identification_string); const char *identification_string);
~PilotList(); // @004cab10 ~PilotList(); // @004cab10
Logical TestInstance() const; Logical TestInstance() const;
void BecameActive(); // @004caba4 void BecameActive(); // @004caba4
void Execute(); // @004cabd0 void Execute(); // @004cabd0
protected: protected:
void DrawMechIcon(void *pilot, int selected); // @004cad70
int currentSlot; // @0x90 this[0x24] (round-robin, =9 init) int currentSlot; // @0x90 this[0x24] (round-robin, =9 init)
Logical built; // @0x94 this[0x25] Logical built; // @0x94 this[0x25]
struct Entry { // 8 entries (stride 0x1C) from this[0x26] struct Entry { // 8 entries (stride 0x1C) from this[0x26]
+1
View File
@@ -145,6 +145,7 @@ MethodDescription
&VehicleSubSystems::methodDescription, // "vehicleSubSystems" -- engineering-screen subsystem cluster panels &VehicleSubSystems::methodDescription, // "vehicleSubSystems" -- engineering-screen subsystem cluster panels
&BitMapInverseWipe::methodDescription, // "LeakGauge" -- coolant-leak inverse-wipe (Condenser/CoolantMassLeakRate) &BitMapInverseWipe::methodDescription, // "LeakGauge" -- coolant-leak inverse-wipe (Condenser/CoolantMassLeakRate)
&VertNormalSlider::methodDescription, // "vertNormalSlider" -- condenser valve-setting slider (heat MFD) &VertNormalSlider::methodDescription, // "vertNormalSlider" -- condenser valve-setting slider (heat MFD)
&PilotList::methodDescription, // "pilotList" -- Comm KILLS/DEATHS pilot roster
&BTL4ChainToPrevious &BTL4ChainToPrevious
}; };
+26
View File
@@ -1177,3 +1177,29 @@ void
MechControlsMapper::NotifyOfDisplayModeChange(int /*new_mode*/) MechControlsMapper::NotifyOfDisplayModeChange(int /*new_mode*/)
{ {
} }
//
// gauge wave P2: BTResolveRosterPilot -- the bridge PilotList (btl4gau3.cpp, the
// Comm KILLS/DEATHS roster) uses to read the viewpoint mech's pilot roster. The
// roster lives on the mech's ControlsMapper (subsystemArray[0]); this complete-Mech
// TU resolves it, PilotList reads the returned pilot at raw BTPlayer offsets.
// Faithful to the binary's Execute: **(App+0x6c mech +0x128)[slot] -> GetPilot.
//
void *BTResolveRosterPilot(int slot)
{
if (application == 0)
{
return 0;
}
Mech *mech = (Mech *)application->GetViewpointEntity(); // App+0x6c local mech
if (mech == 0 || mech->GetSubsystemCount() < 1)
{
return 0;
}
MechControlsMapper *mapper = (MechControlsMapper *)mech->GetSubsystem(0); // subsystemArray[0]
if (mapper == 0)
{
return 0;
}
return (void *)mapper->GetPilot(slot); // FUN_004b0898
}