From 0060a3e1caba2f1029fa6fdcd376c4b19b0f462f Mon Sep 17 00:00:00 2001 From: arcattack Date: Tue, 7 Jul 2026 17:24:24 -0500 Subject: [PATCH] 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) --- game/reconstructed/btl4gau3.cpp | 232 +++++++++++++++++++++++++++++--- game/reconstructed/btl4gau3.hpp | 7 +- game/reconstructed/btl4grnd.cpp | 1 + game/reconstructed/mechmppr.cpp | 26 ++++ 4 files changed, 249 insertions(+), 17 deletions(-) diff --git a/game/reconstructed/btl4gau3.cpp b/game/reconstructed/btl4gau3.cpp index a4ca574..ce18348 100644 --- a/game/reconstructed/btl4gau3.cpp +++ b/game/reconstructed/btl4gau3.cpp @@ -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 -// DAT_0051ab4c..0051abd4. -// @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. +// @004ca90c -- Make. Allocate 0x178 + placement-construct. position UNUSED +// (each entry carries absolute coords from DAT_0051af88). Always returns True. // +Logical + PilotList::Make( + int display_port_index, + Vector2DOf /*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; +} //########################################################################### diff --git a/game/reconstructed/btl4gau3.hpp b/game/reconstructed/btl4gau3.hpp index e7afd8d..1ef3c5c 100644 --- a/game/reconstructed/btl4gau3.hpp +++ b/game/reconstructed/btl4gau3.hpp @@ -175,16 +175,19 @@ public GraphicGauge { public: + // gauge wave P2: registered as "pilotList" -- the Comm KILLS/DEATHS roster. + static MethodDescription methodDescription; static Logical Make(int, Vector2DOf, Entity *, GaugeRenderer *); // @004ca90c - PilotList( // @004ca958 + PilotList( // @004ca958 (NO x,y: positions come from DAT_0051af88) GaugeRate, ModeMask, L4GaugeRenderer *, int, - int x, int y, const char *font_image, + const char *font_image, const char *identification_string); ~PilotList(); // @004cab10 Logical TestInstance() const; void BecameActive(); // @004caba4 void Execute(); // @004cabd0 protected: + void DrawMechIcon(void *pilot, int selected); // @004cad70 int currentSlot; // @0x90 this[0x24] (round-robin, =9 init) Logical built; // @0x94 this[0x25] struct Entry { // 8 entries (stride 0x1C) from this[0x26] diff --git a/game/reconstructed/btl4grnd.cpp b/game/reconstructed/btl4grnd.cpp index fa683e6..b680f1f 100644 --- a/game/reconstructed/btl4grnd.cpp +++ b/game/reconstructed/btl4grnd.cpp @@ -145,6 +145,7 @@ MethodDescription &VehicleSubSystems::methodDescription, // "vehicleSubSystems" -- engineering-screen subsystem cluster panels &BitMapInverseWipe::methodDescription, // "LeakGauge" -- coolant-leak inverse-wipe (Condenser/CoolantMassLeakRate) &VertNormalSlider::methodDescription, // "vertNormalSlider" -- condenser valve-setting slider (heat MFD) + &PilotList::methodDescription, // "pilotList" -- Comm KILLS/DEATHS pilot roster &BTL4ChainToPrevious }; diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 55a28de..30311a3 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -1177,3 +1177,29 @@ void 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 +}