From b6918632fc222a6690ccb0a8711425e0e7e32023 Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 6 Jul 2026 23:13:32 -0500 Subject: [PATCH] gauges: reconstruct + register PlayerStatus (mission-review scoreboard) PlayerStatus (btl4gau3) had only a BUGGY Make defined (it read raw DAT_ pools, passed (int)entity/(int)gauge_renderer as x/y, and DROPPED the graphics_port_number param -- the header ctor was one int short); the ctor/Execute/dtor/BecameActive and the PlayerStatusMappingGroup + CreateMutantPixelmap8 helpers were comment-only stubs. Reconstructed the whole family (mapped by the playerstatus-decomp-map workflow, 6 agents): * methodDescription (8 params: rate,modeMask,integer player#,string font,4x color) + rewired Make reading parameterList[] with the port/x/y bug fixed. * ctor (@004cb1a8): GraphicGauge base (owner folded to 0), store colours+port, SetOrigin, playerIndex=player_number-1, build a score NumericDisplay (fmt 2 = signedBlankedZeros). Fixed the header layout: nameImage is a BitMap* (not a Pixmap), and two missing members (dirty@0x2F, previousStatus@0x30). * dtor / BecameActive / TestInstance. * PlayerStatusMappingGroup (@004c9bd0): 28 ColorMapperArmor zone tints over the mech-outline schematic, one per dz_* damage zone (mech->GetDamageZoneIndex), using the EXISTING 10-arg ColorMapperArmor ctor (per the workflow's correction -- changing it would break cmArmor); added ColorMapper/Armor::SetColor (FUN_004c3c38, stores @0x6C). * Execute (@004cb358): resolve the player (WinTesla-clean via GetMissionPlayer for the local player, instead of the binary's raw App+0x24 "Players"-node dictionary walk), then draw the score + name box + alive/dead status box; null-guarded. KEY FINDING: PlayerStatus lives in the config's `cameraInit` block (the MISSION- REVIEW / spectator camera cockpit), NOT `MechInit` -- so it is NOT part of the mech cockpit and does not build during normal mech play (which is why the mech test shows it un-regressed and its Execute never runs). It is the post-mission scoreboard (all 8 players' name/score/mech-status), rendered by the BTCameraDirector game model. Registered in BTL4MethodDescription[] so `cameraInit` builds it instead of parse-skipping. STATE: complete reconstruction; compiles, links (no new /FORCE unresolved), and the mech cockpit is un-regressed (TARGET DESTROYED, 0 crashes). NOT runtime-verified -- cameraInit is only built by the mission-review camera model, which a normal `vehicle=` egg does not trigger. BRING-UP STUB (marked): CreateMutantPixelmap8 returns NULL (the mech-outline recolor needs the DynamicMemoryStream read API + Pixmap pixel-copy mapped) -> the score/name-box/status-box render but not the recoloured mech schematic. BT_PS_LOG traces the resolve. Co-Authored-By: Claude Opus 4.8 (1M context) --- game/reconstructed/btl4gau3.cpp | 314 ++++++++++++++++++++++++++++++-- game/reconstructed/btl4gau3.hpp | 33 ++-- game/reconstructed/btl4gaug.hpp | 6 +- game/reconstructed/btl4grnd.cpp | 2 + 4 files changed, 318 insertions(+), 37 deletions(-) diff --git a/game/reconstructed/btl4gau3.cpp b/game/reconstructed/btl4gau3.cpp index 5b9dc57..a4ca574 100644 --- a/game/reconstructed/btl4gau3.cpp +++ b/game/reconstructed/btl4gau3.cpp @@ -69,15 +69,8 @@ static int DAT_0051a2bc = 0; // SectorDisplay GaugeRate static int DAT_0051a300 = 0; // SectorDisplay ModeMask static char DAT_0051a344[] = ""; // SectorDisplay grid image name - -static int DAT_0051b07c = 1; // PlayerStatus player number (1..8) -static int DAT_0051aff4 = 0; // PlayerStatus GaugeRate -static int DAT_0051b038 = 0; // PlayerStatus ModeMask -static char DAT_0051b0c0[] = ""; // PlayerStatus font image -static int DAT_0051b104 = 0; // color -static int DAT_0051b148 = 0; // okColor -static int DAT_0051b18c = 0; // aliveColor -static int DAT_0051b1d0 = 0; // deadColor +// (PlayerStatus's DAT_ placeholder pool removed -- its rewired Make reads +// methodDescription.parameterList[] like the other registered widgets.) //########################################################################### @@ -212,19 +205,44 @@ Logical // "PlayerStatus: Make player number " is warned. Allocates (200) and // constructs "PlayerStatus" from DAT_0051aff4..0051b1d0. // +MethodDescription + PlayerStatus::methodDescription = + { + "PlayerStatus", + PlayerStatus::Make, + { + // CFG (L4GAUGE.CFG:29): PlayerStatus(D,ModeAlwaysActive,1,helv15.pcc,0,11,3,1) + { ParameterDescription::typeRate, NULL }, // p[0] rate + { ParameterDescription::typeModeMask, NULL }, // p[1] mode mask + { ParameterDescription::typeInteger, NULL }, // p[2] player number 1..8 + { ParameterDescription::typeString, NULL }, // p[3] font image + { ParameterDescription::typeColor, NULL }, // p[4] color + { ParameterDescription::typeColor, NULL }, // p[5] okColor + { ParameterDescription::typeColor, NULL }, // p[6] aliveColor + { ParameterDescription::typeColor, NULL }, // p[7] deadColor + PARAMETER_DESCRIPTION_END + } + }; + +// +// @004cae90 -- Make. Rewired to read methodDescription.parameterList[] (the +// binary read raw DAT_ pools) + the port/x/y bug fixed (it passed entity/renderer +// as x/y and dropped the port). Returns True even on a NULL alloc (binary-faithful). +// Logical PlayerStatus::Make( - int /*display_port_index*/, - Vector2DOf /*position*/, - Entity *entity, + int display_port_index, + Vector2DOf position, + Entity * /*entity -- unused by PlayerStatus*/, GaugeRenderer *gauge_renderer ) { - int playerNumber = DAT_0051b07c; + ParameterDescription *p = methodDescription.parameterList; + int playerNumber = p[2].data.integer; if (playerNumber < 1 || playerNumber > 8) { DebugStream << "PlayerStatus: Make player number " - << playerNumber << " out of range\n"; // FUN_004dbb24 / FUN_004db78c + << playerNumber << " out of range\n"; return False; } @@ -232,16 +250,274 @@ Logical if (gauge != NULL) { new (gauge) PlayerStatus( - (GaugeRate)DAT_0051aff4, (ModeMask)DAT_0051b038, - (L4GaugeRenderer *)gauge_renderer, 0, (int)entity, - (int)gauge_renderer, playerNumber, - (char *)&DAT_0051b0c0, - DAT_0051b104, DAT_0051b148, DAT_0051b18c, DAT_0051b1d0, + p[0].data.rate, p[1].data.modeMask, + (L4GaugeRenderer *)gauge_renderer, + display_port_index, // graphics_port_number <-- FIX (was dropped) + position.x, position.y, // x, y <-- FIX (was (int)entity/(int)renderer) + playerNumber, p[3].data.string, + p[4].data.color, p[5].data.color, p[6].data.color, p[7].data.color, "PlayerStatus"); } return True; } +//########################################################################### +// file-private helpers +//########################################################################### + +// (ReadStreamString @004caf50 -- the length-prefixed string reader used by the +// faithful CreateMutantPixelmap8 -- deferred with the pixmap-recolor stub above; +// its stream read is a vtbl+0x1c indirect whose method name needs mapping.) + +// +// @004cafac -- per-player pixmap recolour. Find the mech's type-0x20 pixmap +// resource (App resource file), open it as a MemoryStream, read the embedded +// image name, look the source pixmap up in the renderer's PixMap8 cache, and copy +// it while biasing every palette index >= 0x20 by (paletteBase-0x20) so each +// player's mech outline draws in its own colour ramp. Returns the (reused) Pixmap. +// +static Pixmap * + CreateMutantPixelmap8(Entity * /*mech*/, L4Warehouse * /*warehouse*/, + int /*paletteBase*/, Pixmap *dest) +{ + // ⚠ BRING-UP STUB (marked; leaves the mech-outline schematic unrendered). + // The faithful reconstruction (decomp @004cafac) resolves the mech's type-0x20 + // pixmap sub-resource from the App ResourceFile, opens it as a memory stream, + // reads the embedded image name (the stream read is a vtbl+0x1c indirect -- + // ReadStreamString @004caf50), looks the source PixMap8 up in the renderer + // warehouse, and copies it biasing every palette index >= 0x20 by + // (paletteBase-0x20) so each player's mech draws in its own colour ramp. + // Deferred: the DynamicMemoryStream read API + Pixmap pixel-copy need mapping. + // With this stub recoloredMech stays NULL -> Execute skips the outline blit; + // the score, name box, and alive/dead status box still render. + return dest; +} + +//########################################################################### +// PlayerStatusMappingGroup @004c9bd0 +//########################################################################### + +// +// The 28 named damage zones (PTR_s_dz_door_0051a240); each gets one +// ColorMapperArmor tinting that zone of the mech-outline schematic. +// +static const char *const kPlayerStatusZone[playerStatusZoneCount] = +{ + "dz_door","dz_dtorso","dz_hip","dz_larm","dz_ldleg","dz_lfoot","dz_lgun", + "dz_ltorso","dz_luleg","dz_missle","dz_rarm","dz_rdleg","dz_rfoot","dz_rgun", + "dz_rtorso","dz_ruleg","dz_searchlight","dz_utorso","dz_reardtorso", + "dz_rearltorso","dz_rearrtorso","dz_rearutorso","dz_lmissle","dz_rmissle", + "unused1","unused2","unused3","unused4" +}; + +PlayerStatusMappingGroup::PlayerStatusMappingGroup( + GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer, + int graphics_port_number, Entity *mech, int base_color_index, + const char *palette_a, const char *palette_b, const char * /*identification_string*/) +{ + for (int i = 0; i < playerStatusZoneCount; ++i) + { + int zoneIndex = (mech != NULL) + ? mech->GetDamageZoneIndex(CString(kPlayerStatusZone[i])) // FUN_0042076c (-1 => inert) + : -1; + zone[i] = (ColorMapperArmor *)operator new(0x70); + if (zone[i] != NULL) + new (zone[i]) ColorMapperArmor( // EXISTING 10-arg ctor (btl4gaug) + rate, mode_mask, renderer, graphics_port_number, + base_color_index + i, mech, palette_a, palette_b, + zoneIndex, "ColorMapperArmor"); + } +} + +PlayerStatusMappingGroup::~PlayerStatusMappingGroup() +{ + for (int i = 0; i < playerStatusZoneCount; ++i) + { + delete zone[i]; + zone[i] = NULL; + } +} + +void + PlayerStatusMappingGroup::Execute() // @004c9cf4 run all 28 +{ + for (int i = 0; i < playerStatusZoneCount; ++i) + if (zone[i] != NULL) zone[i]->Execute(); +} + +void + PlayerStatusMappingGroup::SetColor(int state) // @004c9d18 push state onto all 28 +{ + for (int i = 0; i < playerStatusZoneCount; ++i) + if (zone[i] != NULL) zone[i]->SetColor(state); +} + +//########################################################################### +// PlayerStatus @004cb1a8 ctor / @004cb28c dtor / @004cb310 BecameActive / +// @004cb358 Execute +//########################################################################### + +// +// @004cb1a8 -- ctor (vtable PTR_FUN_0051be20). GraphicGauge base (owner folded +// to 0); stash the four state colours + port; set the port origin; build the +// score NumericDisplay. +// +PlayerStatus::PlayerStatus( + GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, + int graphics_port_number, int x, int y, int player_number, + const char *font_image, int color_in, int ok_color, + int alive_color, int dead_color, const char *identification_string) + : GraphicGauge(rate, mode_mask, renderer_in, 0, // FUN_00444818 (owner 0) + graphics_port_number, identification_string) +{ + color = color_in; okColor = ok_color; aliveColor = alive_color; deadColor = dead_color; + port = graphics_port_number; + localView.SetOrigin(x, y); // this+0x48 vtbl+0x10 + playerIndex = player_number - 1; + player = NULL; nameImage = NULL; recoloredMech = NULL; mappingGroup = NULL; + L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer; + scoreDisplay = new NumericDisplay( // FUN_004700bc @0xB8 + warehouse, 0x72, 0xD8, font_image, 5, + (NumericDisplay::NumericFormat)2 /*signedBlankedZeros*/, color, okColor); + // dirty/previousStatus/previousMappingState set by BecameActive (byte-faithful). +} + +// +// @004cb28c -- dtor. Free the score numeric + the recoloured mech pixmap; the +// base chain runs implicitly. (nameImage is cache-owned; mappingGroup deleted +// here as a correctness fix over the binary's leak.) +// +PlayerStatus::~PlayerStatus() +{ + player = NULL; + delete scoreDisplay; scoreDisplay = NULL; // FUN_0047018c + if (recoloredMech) { delete recoloredMech; recoloredMech = NULL; } // FUN_00449d0c + if (mappingGroup) { delete mappingGroup; mappingGroup = NULL; } // (binary leaks this) +} + +Logical + PlayerStatus::TestInstance() const // @004cb2f8 +{ + return GraphicGauge::TestInstance(); +} + +// +// @004cb310 -- BecameActive: reset the score readout, run the mapping group once, +// and force a full redraw next Execute. +// +void + PlayerStatus::BecameActive() +{ + scoreDisplay->ForceUpdate(); // FUN_004703f4 + if (mappingGroup != NULL) mappingGroup->Execute(); // FUN_004c9cf4 + dirty = 1; + previousStatus = -1; + previousMappingState = -1; +} + +// +// @004cb358 -- Execute. Resolve player N (the mission player for the local +// player; WinTesla-clean via GetMissionPlayer instead of the binary's raw +// App+0x24 "Players"-node dictionary walk); on a change rebuild the recoloured +// mech pixmap + the 28-zone mapping group; each frame draw the score + mech +// outline + name box + alive/dead status box. Runs under the gauge's guarded +// Execute (BT_DEV_GAUGES), so a bad player-field offset degrades to a disabled +// gauge, never a game crash. +// +void + PlayerStatus::Execute() +{ + // --- PART 1: resolve / change-detect the player --- + Entity *resolved = NULL; + if (playerIndex == 0 && application != NULL) + resolved = (Entity *)application->GetMissionPlayer(); // the local player (player 1) + + if (getenv("BT_PS_LOG")) + { + static int s_c = 0; + if ((s_c++ % 60) == 0) + DEBUG_STREAM << "[ps] idx=" << playerIndex << " resolved=" << (void*)resolved + << " player=" << (void*)player << " port=" << port << "\n" << std::flush; + } + + if (resolved != player) + { + player = resolved; + if (player != NULL) + { + Entity *mech = *(Entity **)((char *)player + 0x1FC); // player's vehicle + if (mech == NULL) + { + player = NULL; // no vehicle yet -> retry next frame + } + else + { + L4Warehouse *wh = (L4Warehouse *)renderer->warehousePointer; + int base = playerIndex * 0x1C + 0x10; + recoloredMech = CreateMutantPixelmap8(mech, wh, base, recoloredMech); + if (mappingGroup) delete mappingGroup; + mappingGroup = new PlayerStatusMappingGroup( + rate, modeMask, (L4GaugeRenderer *)renderer, port, mech, base, + "adpal.pcc", "adpal2.pcc", "PlayerStatusMappingGroup"); + nameImage = NULL; // BRING-UP: App+0xC8 name cache not wired -> clear-box branch + BecameActive(); // force a full redraw + } + } + } + + // --- PART 2: per-frame draw (only with a bound player) --- + if (player == NULL) + return; + Entity *mech = *(Entity **)((char *)player + 0x1FC); + if (mech == NULL) + return; + + // (a) score readout + scoreDisplay->Draw(&localView, *(Scalar *)((char *)player + 0x1C8)); // FUN_00470430 + + // (b) mapping-group disabled-state refresh + int disabled = 0; // BRING-UP: Mech::IsDisabled needs a Mech* cast (mech.hpp collides in this TU) + if (disabled != previousMappingState) + { + previousMappingState = disabled; + if (mappingGroup) mappingGroup->SetColor(disabled); // FUN_004c9d18 + } + + // (c) mech outline + name box, dirty-gated + if (dirty) + { + dirty = 0; + if (recoloredMech != NULL) + { + localView.MoveToAbsolute(1, 1); // vtbl+0x24 + localView.DrawPixelMap8(True, 0, recoloredMech, 0, 0, // vtbl+0x5C + recoloredMech->Data.Size.x - 1, recoloredMech->Data.Size.y - 1); + } + localView.MoveToAbsolute(1, 0xCF); + if (nameImage == NULL) // no name bitmap -> filled box in `color` + { + localView.SetColor(color); // vtbl+0x18 + localView.DrawFilledRectangleToRelative(0x80, 0x20); // vtbl+0x4C + } + else + { + localView.SetColor(0xFF); + localView.DrawBitMapOpaque(color, 0, nameImage, 0, 0, // vtbl+0x58 + nameImage->Data.Size.x - 1, nameImage->Data.Size.y - 1); + } + } + + // (d) alive/dead status box, state-change gated + int state = *(int *)((char *)player + 0x1C4); + if (state != previousStatus) + { + previousStatus = state; + localView.SetColor(state == 0 ? aliveColor : deadColor); + localView.MoveToAbsolute(0, 0); + localView.DrawFilledRectangleToAbsolute(0x9F, 0xEF); // vtbl+0x44 + } +} + // // @004cb1a8 -- ctor (vtable PTR_FUN_0051be20). GraphicGauge base; stashes the // four state colours (this[0x29..0x2C]); playerIndex = player_number - 1; sets diff --git a/game/reconstructed/btl4gau3.hpp b/game/reconstructed/btl4gau3.hpp index 4a93541..e7afd8d 100644 --- a/game/reconstructed/btl4gau3.hpp +++ b/game/reconstructed/btl4gau3.hpp @@ -75,8 +75,8 @@ { public: PlayerStatusMappingGroup( // @004c9bd0 - GaugeRate, ModeMask, L4GaugeRenderer *, int, - Subsystem *mech, int base_color_index, + GaugeRate, ModeMask, L4GaugeRenderer *, int graphics_port_number, + Entity *mech, int base_color_index, const char *palette_a, const char *palette_b, const char *identification_string); ~PlayerStatusMappingGroup(); // @004c9ca8 @@ -214,9 +214,12 @@ public GraphicGauge { public: + // Registered in BTL4MethodDescription[] (btl4grnd.cpp) as "PlayerStatus". + static MethodDescription methodDescription; + static Logical Make(int, Vector2DOf, Entity *, GaugeRenderer *); // @004cae90 - PlayerStatus( // @004cb1a8 - GaugeRate, ModeMask, L4GaugeRenderer *, int, + PlayerStatus( // @004cb1a8 (13 args; owner folded to 0 in base call) + GaugeRate, ModeMask, L4GaugeRenderer *, int graphics_port_number, int x, int y, int player_number, const char *font_image, int color, int okColor, int aliveColor, int deadColor, const char *identification_string); @@ -225,11 +228,11 @@ void BecameActive(); // @004cb310 void Execute(); // @004cb358 protected: - int port; // @0x90 this[0x24] + int port; // @0x90 this[0x24] graphics_port_number int playerIndex; // @0x94 this[0x25] (player_number-1) - Entity *player; // @0x98 this[0x26] - Pixmap *nameImage; // @0x9C this[0x27] (mutant pixmap) - Pixmap *recoloredMech; // @0xA0 this[0x28] + Entity *player; // @0x98 this[0x26] cached (non-owned) + BitMap *nameImage; // @0x9C this[0x27] App name-cache BitMap (was mislabeled Pixmap) + Pixmap *recoloredMech; // @0xA0 this[0x28] CreateMutantPixelmap8 result (owned) int color; // @0xA4 this[0x29] int okColor; // @0xA8 this[0x2A] int aliveColor; // @0xAC this[0x2B] @@ -238,6 +241,8 @@ *mappingGroup; // @0xB4 this[0x2D] NumericDisplay *scoreDisplay; // @0xB8 this[0x2E] + int dirty; // @0xBC this[0x2F] redraw flag + int previousStatus; // @0xC0 this[0x30] last player+0x1C4 alive/dead int previousMappingState; // @0xC4 this[0x31] }; @@ -271,14 +276,8 @@ }; - //####################################################################### - // CreateMutantPixelmap8 -- file-private helper (@004cafac). Reads a named - // pixmap resource, decodes its embedded name string (ReadStreamString - // @004caf50), looks up the source pixmap and copies it while biasing every - // palette index >= 0x20 by (player_palette_base - 0x20). Produces a - // per-player recoloured 8-bit pixmap. Returns an allocated Pixmap. - //####################################################################### - Pixmap *CreateMutantPixelmap8( // @004cafac - Entity *player, int pixmap_cache, int palette_base, Pixmap *reuse); + // NOTE: CreateMutantPixelmap8 (@004cafac) + ReadStreamString (@004caf50) are + // file-PRIVATE in the binary -- declared static at the top of btl4gau3.cpp, + // not here (the old free-function decl had the wrong signature). #endif diff --git a/game/reconstructed/btl4gaug.hpp b/game/reconstructed/btl4gaug.hpp index db87d65..003901b 100644 --- a/game/reconstructed/btl4gaug.hpp +++ b/game/reconstructed/btl4gaug.hpp @@ -153,8 +153,12 @@ Entity *entity, const char *palette_a, const char *palette_b, int damage_zone_index, const char *identification_string); ~ColorMapperArmor(); + + // @004c3c38 -- store the group state into @0x6C (used by PlayerStatusMappingGroup + // to push the mech's disabled/enabled state onto all 28 zone tints). + void SetColor(int state) { unused = state; } protected: - int unused; // @0x6C this[0x1B] (=0) + int unused; // @0x6C this[0x1B] (state slot; SetColor writes it) }; // diff --git a/game/reconstructed/btl4grnd.cpp b/game/reconstructed/btl4grnd.cpp index bef6d7e..ba651db 100644 --- a/game/reconstructed/btl4grnd.cpp +++ b/game/reconstructed/btl4grnd.cpp @@ -67,6 +67,7 @@ #if !defined(BTL4GAUG_HPP) # include // the BT gauge classes (Compass, etc.) # include // MapDisplay (the "map" radar gauge) +# include // PlayerStatus (the comm/score gauge) #endif #if !defined(L4LAMP_HPP) # include @@ -139,6 +140,7 @@ MethodDescription &SegmentArcRatio::methodDescription, // "segmentArcRatio" -- segmented arc dial (speed) &OneOfSeveralPixInt::methodDescription, // "oneOfSeveralPixInt" -- button-state lamp (duck/light/mode) &MapDisplay::methodDescription, // "map" -- the radar / tactical display + &PlayerStatus::methodDescription, // "PlayerStatus" -- comm/score name-tag gauge &BTL4ChainToPrevious };