//===========================================================================// // File: btl4gau3.cpp // // Project: BattleTech Brick: Gauge Renderer Manager // // Contents: Cockpit instrument library, part 3 -- multiplayer / tactical HUD // // displays (see btl4gau3.hpp). // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // 02/22/96 CPB Initial coding. // //---------------------------------------------------------------------------// // Copyright (C) 1996, Virtual World Entertainment, Inc. All rights reserved // // PROPRIETARY AND CONFIDENTIAL // //===========================================================================// // // RECONSTRUCTED from the shipped binary (BTL4OPT.EXE). Behaviour follows the // Ghidra pseudo-C in recovered/all/part_014.c (@004c9bd0..@004cbea0). Class / // member names from the CODE identification strings; per-method @ADDR cited. // This TU links between btl4gau2.obj and btl4grnd.obj. // // Engine-internal helper map (consistent with btl4gau2.cpp): // FUN_00444818 GraphicGauge ctor FUN_00444870 GraphicGauge dtor // FUN_004700bc NumericDisplay ctor FUN_0047018c ::dtor // FUN_00470430 ::DrawAt FUN_00470758 ::Erase FUN_004703f4 ::Reset // FUN_00442aec/00442c12 BitMapCache AddRef/Release // FUN_00442d2b/00442e51 PixMapCache AddRef/Release // FUN_004700ac InternName FUN_004022e8 FreeName // FUN_00403ad0 NamedNode::Find(dictionary, name) ("Players") // FUN_00421414 ChainOf::Iterator ctor FUN_00421452 ::dtor // FUN_0042076c Subsystem::FindDamageZone -> index // FUN_004c3b98 ColorMapperArmor ctor (btl4gaug) // FUN_004c2f88 -> 7 (selection/blip colour) FUN_004c2f94 entity colour // FUN_0044fc10 score/rank lookup FUN_004b0898 lance-slot mech lookup // FUN_0049fb54 / 0x50bdb4 damage-mapping-group derivation // FUN_00449c88 Pixmap ctor FUN_00449d0c Pixmap dtor // FUN_00407064 Warehouse::FindResource (App+0x68) FUN_004032dc MemoryStream // FUN_004dcd94 Round FUN_004dbb24 DebugStream< #pragma hdrstop #if !defined(BTL4GAU3_HPP) # include #endif #if !defined(APP_HPP) # include #endif // (SectorDisplay's + PlayerStatus's DAT_ placeholder pools removed -- their rewired // Make()s read methodDescription.parameterList[] like the other registered widgets.) //########################################################################### // PlayerStatusMappingGroup @004c9bd0 //########################################################################### // // @004c9bd0 -- build the 28 ColorMapperArmor zone gauges. For each name in the // damage-zone name table (PTR_s_dz_door_0051a240, 28 entries) build a transient // damage-zone descriptor, resolve its index on the mech (FindDamageZone // @0042076c), allocate (0x70) a ColorMapperArmor and construct it (@004c3b98) // wired to that zone (incrementing the next base colour index each iteration). // @004c9ca8 -- destroy: delete all 28, optionally free the array. // @004c9cf4 -- Execute: run each element's Execute (vtbl+0xc). // @004c9d18 -- SetColor: recolour all 28 (FUN_004c3c38 = ColorMapper SetColor). // //########################################################################### // SectorDisplay @004c9d44 Make / @004c9e10 ctor //########################################################################### // @0x51a2b0 -- registered so the interpreter builds the CFG line // sectorDisplay( K, ModeAlwaysActive, helv15.pcc, 0, 3 ) (L4GAUGE.CFG:5146; the // port index + offset=(125,579) arrive via Make's args, not config tokens). MethodDescription SectorDisplay::methodDescription = { "sectorDisplay", SectorDisplay::Make, { { ParameterDescription::typeRate, NULL }, // p[0] rate [row0 type=1] { ParameterDescription::typeModeMask, NULL }, // p[1] modeMask [row1 type=2] { ParameterDescription::typeString, NULL }, // p[2] image [row2 type=9] { ParameterDescription::typeColor, NULL }, // p[3] color [row3 type=4] { ParameterDescription::typeColor, NULL }, // p[4] okColor [row4 type=4] PARAMETER_DESCRIPTION_END } }; // // @004c9d44 -- Make. Alloc 0xc4 + construct; then peek the grid image and warn // "SectorDisplay: Missing image " if absent (binary-faithful: the base ctor // self-registers the gauge regardless of the return value). // Logical SectorDisplay::Make( int display_port_index, Vector2DOf position, Entity * /*entity -- unused*/, GaugeRenderer *gauge_renderer ) { ParameterDescription *p = methodDescription.parameterList; SectorDisplay *gauge = (SectorDisplay *)operator new(0xc4); // FUN_00402298(0xc4) if (gauge != NULL) new (gauge) SectorDisplay( p[0].data.rate, p[1].data.modeMask, (L4GaugeRenderer *)gauge_renderer, display_port_index, // graphics_port_number (FIX: was 0) position.x, position.y, // SetOrigin (FIX: was 0,0) p[2].data.string, // grid/font image (helv15.pcc) p[3].data.color, // numericColor p[4].data.color, // gridColor / numeric okColor "SectorDisplay"); BitMap *grid = gauge_renderer->warehousePointer->bitMapBin.GetIfAlreadyExists( p[2].data.string); // FUN_00442aec (peek, no AddRef) if (grid == NULL) DebugStream << "SectorDisplay: Missing image " << p[2].data.string << "\n"; if (getenv("BT_SECTOR_LOG")) DEBUG_STREAM << "[sector] Make port=" << display_port_index << " pos=(" << position.x << "," << position.y << ") image=" << p[2].data.string << " gridCached=" << (int)(grid != NULL) << "\n" << std::flush; return (grid != NULL); } // // @004c9e10 -- ctor (vtable PTR_FUN_0051beec). GraphicGauge base (owner 0); copy // the grid-image name; from the cached bitmap compute cellWidth=imageW/14, // gridLeft=cellWidth*12, gridRight=cellWidth*13-1, gridHeight=imageH; SetOrigin; // build two 3-digit NumericDisplays (@0 and @cellWidth*4). // SectorDisplay::SectorDisplay( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int x, int y, const char *image, int color, int ok_color, const char *identification_string) : GraphicGauge(rate, mode_mask, renderer_in, 0, // FUN_00444818 (owner 0) graphics_port_number, identification_string) { // nameCopy(image) -- Execute reads gridImage per-frame, so it must outlive the // (transient) config parameterList string; store a durable copy (FUN_004700ac). gridImage = NULL; if (image != NULL) { gridImage = new char[strlen(image) + 1]; strcpy(gridImage, image); } numericColor = color; // @0xA8 gridColor = ok_color; // @0xAC subject = NULL; // @0x90 sectorBaseB = 500; // @0xB4 (0x1f4) sectorBaseA = 500; // @0xB0 dirty = 1; // @0xB8 (binary leaves uninit; BecameActive sets it) L4Warehouse *wh = (L4Warehouse *)renderer_in->warehousePointer; // renderer+0x4c BitMap *img = wh->bitMapBin.GetIfAlreadyExists(gridImage); // FUN_00442aec (peek) if (img == NULL) { cellWidth = gridLeft = gridRight = gridHeight = 0; } else { cellWidth = img->Data.Size.x / 14; // img+0xc width gridLeft = cellWidth * 12; gridRight = cellWidth * 12 + cellWidth - 1; gridHeight = img->Data.Size.y; // img+0x10 height } localView.SetOrigin(x, y); // this+0x48 vtbl+0x10 // NumericDisplay(warehouse, x, y, image, fieldWidth=3, format=unsignedFormat(0), color, okColor) numericA = new NumericDisplay(wh, 0, 0, image, 3, NumericDisplay::unsignedFormat, color, ok_color); numericB = new NumericDisplay(wh, cellWidth * 4, 0, image, 3, NumericDisplay::unsignedFormat, color, ok_color); } // // @004c9f94 -- dtor. Free the copied name + both numerics; the base // GraphicGauge::~GraphicGauge runs implicitly (do NOT call it). // SectorDisplay::~SectorDisplay() { delete[] gridImage; gridImage = NULL; delete numericA; numericA = NULL; // FUN_0047018c delete numericB; numericB = NULL; } // @004ca020 -- TestInstance: out-of-line forward to the base (so it isn't /FORCE-stubbed). Logical SectorDisplay::TestInstance() const { return GraphicGauge::TestInstance(); } // @004ca068 -- LinkToEntity (slot 9): cache the renderer's linked entity as the subject. void SectorDisplay::LinkToEntity(Entity *entity) { subject = entity; } // @004ca038 -- BecameActive (slot 3): mark dirty + reset both numerics. NON-inactivating // (does NOT chain the base) -> satisfies the container-Execute rule. void SectorDisplay::BecameActive() { dirty = 1; numericB->ForceUpdate(); // FUN_004703f4 (B then A, binary order) numericA->ForceUpdate(); } // @004ca07c -- Execute (slot 16): draw the two sector numerics from the linked mech's // world X/Z (Round via the codebase +0.5 idiom == FUN_004dcd94), then on the first // active frame blit the grid-cell background. void SectorDisplay::Execute() { Entity *s = subject; // this+0x90 // PORT accommodation (marked): the LinkToEntity broadcast may be unwired on // WinTesla -> fall back to the viewpoint mech (same as btl4rdr ResolveOperatorEntity). // The binary gates purely on `subject != 0` (@0x4ca08e). if (s == NULL && application != NULL) s = (Entity *)application->GetViewpointEntity(); if (s == NULL) return; const Point3D &pos = s->localOrigin.linearPosition; // s+0x100 (x@+0x100, z@+0x108) Scalar za = -pos.z * 0.01f; // @0x4ca194 = 0.01 (100-unit sectors) Scalar xb = pos.x * 0.01f; int vA = (int)(za + (za < 0.0f ? -0.5f : 0.5f)) + sectorBaseA; // Round(-Z*0.01)+500 int vB = (int)(xb + (xb < 0.0f ? -0.5f : 0.5f)) + sectorBaseB; // Round( X*0.01)+500 numericA->Draw(&localView, (Scalar)vA); // FUN_00470430 numericB->Draw(&localView, (Scalar)vB); if (getenv("BT_SECTOR_LOG")) { static int s_n = 0; if ((s_n++ % 120) == 0) DEBUG_STREAM << "[sector] Execute -Z=" << (-pos.z) << " X=" << pos.x << " -> sectorA=" << vA << " sectorB=" << vB << "\n" << std::flush; } if (dirty) { // this+0xB8 dirty = 0; BitMap *grid = ((L4Warehouse *)renderer->warehousePointer) ->bitMapBin.GetIfAlreadyExists(gridImage); // FUN_00442aec if (grid != NULL) { localView.MoveToAbsolute(cellWidth * 3, 0); // vtbl+0x24 localView.SetColor(gridColor); // vtbl+0x18 (this+0xAC) localView.DrawBitMap(0, grid, gridLeft, 0, gridRight, gridHeight); // vtbl+0x54 } } } //########################################################################### // TeamStatusDisplay @004ca208 //########################################################################### // // @004ca208 -- ctor (vtable PTR_FUN_0051bea8). GraphicGauge base. rowSpacing = // param_10; originY = param_8 - 0x20; flags = param_12. Reads the font bitmap // width to size the highlight (cellW = w/14; highlightWidth = cellW*8 + 0x86, // plus an extra column when flags&4). Builds 8 rows, each with a number // NumericDisplay (this[0x3B+i]) and -- when flags&4 -- a kill NumericDisplay // (this[0x4C+i]); rows step down by rowSpacing. // @004ca39c -- dtor: delete the 8 numeric pairs, base dtor. // @004ca424 -- SetEnable: this[0x24] = enable. // @004ca438 -- BecameActive: selectedRow = 999; per-row prev = -1, prevScore = // -0.001f (0xba83126f); per-row prevRank = -1. // @004ca478 -- Execute. Resolve the local player's index/team // (App+0x6c mech -> +0x190 -> +0x1cc index, +0x204 team via FUN_0044fc10). // Read the "Players" node (FindByName @00403ad0), slot each player into [0..7] // by entity+0x1cc (skipping spectators, +0x29 bit 0x40). For each row: if the // occupant changed, blit the player's interned name bitmap (App+0xc8 palette // cache lookup) or clear, tinting friendly/self with FUN_004c2f88 (=7). Then // update each kill numeric from entity+0x1c8. When flags&4, also track each // player's rank (entity+0x204 - rankBase via FUN_0044fc10). Finally redraw the // selection underline on the local player's row. // //########################################################################### // 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); // gauge scoring wave: read a roster pilot's scoreboard via its compiled BTPlayer // members (btplayer.cpp), not raw offsets (which don't match our layout). extern int BTPilotKills(void *pilot); extern int BTPilotDeaths(void *pilot); extern int BTPilotIsSelected(void *pilot, void *local_player); // SELECT-TARGET highlight (safe) // 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 + 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 { static int s_pl = 0; if (getenv("BT_SCORE_LOG") && (s_pl++ % 120) == 0) DEBUG_STREAM << "[score] PilotList::Execute local=" << local << "\n" << std::flush; } 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 { // SELECT-TARGET highlight -- via the safe bridge (accessors), NOT raw offsets: // the old `*(local+0x284)` objectiveMech + deref `*(tgt+0x190)` read garbage in // our compiled layout and AV'd inside the SEH-guarded Execute, silently aborting // it before the KILLS/DEATHS draw below (= the empty scoreboard). int selected = BTPilotIsSelected(pilot, local); 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 / DEATHS -- gauge scoring wave (Step 1, the databinding fix): read the // pilot's COMPILED named members via the BTPlayer bridge, NOT raw binary offsets. // The earlier pilot+0x27c / +0x200 reads assumed the 1995 layout; our compiled // BTPlayer/Player layout differs (engine base != binary), so those offsets read a // stale/garbage slot -> a silent 0 even after the handlers increment the members. // The bridge reads the SAME members the ScoreMessageHandler/VehicleDead handlers // write (killCount / Player::deathCount). (DEATHS uses the real deathCount, not // the binary's dead pad_0x280.) e.nameDisplay->Draw(&localView, (Scalar)BTPilotKills(pilot)); // KILLS (killCount) e.mechDisplay->Draw(&localView, (Scalar)BTPilotDeaths(pilot)); // DEATHS (Player::deathCount) } ++currentSlot; } //########################################################################### // PlayerStatus @004cae90 Make / @004cb1a8 ctor //########################################################################### // // @004cae90 -- Make. DAT_0051b07c is the player number; must be 1..8 or // "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 -- unused by PlayerStatus*/, GaugeRenderer *gauge_renderer ) { 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"; return False; } PlayerStatus *gauge = (PlayerStatus *)operator new(200); if (gauge != NULL) { new (gauge) PlayerStatus( 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 // the port origin; builds the score NumericDisplay (this[0x2E], helv font, // color/okColor). // @004cb28c -- dtor: delete the score numeric; free the mutant pixmap // (FUN_00449d0c); base dtor. // @004cb310 -- BecameActive: reset score numeric; run mappingGroup BecameActive // (FUN_004c9cf4); dirty flags = 1 / -1. // @004cb358 -- Execute. Find player N in the "Players" node (@00403ad0, // "Players" 0x51bd97, indexed lookup vtbl+0x34). When the player changes and // has a mech (+0x1fc): build a recoloured name pixmap (CreateMutantPixelmap8 // @004cafac) and a fresh PlayerStatusMappingGroup (@004c9bd0, adpal.pcc/ // adpal2.pcc) tinting the mech outline; cache the interned name bitmap. Each // frame: DrawAt the score (+0x1c8); update the armour mapping group when the // damage-mapping derivation (FUN_0049fb54 / 0x50bdb4) changes; on dirty, blit // the name bitmap then the recoloured mech outline; finally pick the frame // colour (aliveColor/deadColor by subsys+0x1c4) and draw the status box. // //########################################################################### // CreateMutantPixelmap8 @004cafac //########################################################################### // // @004cafac -- per-player pixmap recolour. Find the source pixmap resource // (Warehouse @00407064 by id mech+0x1bc); on failure warn "CreateMutantPixelmap8: // couldn't find ...". Open it as a MemoryStream, read the embedded image name // (ReadStreamString @004caf50, 0x50 chars), look it up in the PixMapCache. Copy // the source 8-bit pixels into a (possibly reused) Pixmap, biasing every palette // index >= 0x20 by (palette_base - 0x20) so each player's mech draws in their // own colour ramp. Returns the recoloured Pixmap. // //########################################################################### // MessageBoard @004cb678 Make / @004cb704 ctor //########################################################################### // // Resolve the tracked mech's current HUD status message (msg strip cell + sender // name bitmap). DEFERRED: StatusMessagePool (btstubs.cpp:62) is a NULL stub + // BTPlayer+0x1dc is never populated, so this returns False today -> the board stays // empty. Lives in btplayer.cpp (a complete-BTPlayer TU) to dodge the databinding // trap; MUST have a real (stub) definition or the /FORCE link AVs the call. extern Logical BTResolveMessageBoard(Entity *tracked_mech, int *messageId, BitMap **nameBitmap); // Overflow lock only (like SectorDisplay/Sensor: the 2007 GraphicGauge base is not // byte-identical to 1995, so exact-offset asserts would fail; every read here is via // named members). Binary offsets: trackedMech@0x90 stripImage@0x94 color@0x98 // previousNameId@0x9C previousMessageId@0xA0, sizeof 0xA4 == Make alloc. struct MessageBoardLayoutCheck { static_assert(sizeof(MessageBoard) <= 0xA4, "MessageBoard must fit its 0xA4 Make alloc"); }; // CFG shape (L4GAUGE.CFG:4913, port=sec, offset=(113,607)): // messageBoard( E, ModeAlwaysActive, btsmsgs.pcx, 0 ) MethodDescription MessageBoard::methodDescription = { "messageBoard", MessageBoard::Make, { { ParameterDescription::typeRate, NULL }, // p[0] rate (E) { ParameterDescription::typeModeMask, NULL }, // p[1] mode (ModeAlwaysActive) { ParameterDescription::typeString, NULL }, // p[2] strip bitmap (btsmsgs.pcx) { ParameterDescription::typeColor, NULL }, // p[3] draw/erase color (0) PARAMETER_DESCRIPTION_END } }; // // @004cb678 -- Make. Alloc 0xa4 + construct; presence-check the strip bitmap // (binary returns whether it exists). The `entity` param is ignored by the binary. // Logical MessageBoard::Make( int display_port_index, Vector2DOf position, Entity * /*entity -- ignored by FUN_004cb678*/, GaugeRenderer *gauge_renderer ) { ParameterDescription *p = methodDescription.parameterList; MessageBoard *gauge = (MessageBoard *)operator new(0xa4); // FUN_00402298(0xa4) if (gauge != NULL) new (gauge) MessageBoard( p[0].data.rate, p[1].data.modeMask, (L4GaugeRenderer *)gauge_renderer, display_port_index, // graphics_port_number position.x, position.y, // offset=(113,607) -> SetOrigin p[2].data.string, // strip image (btsmsgs.pcx) p[3].data.color, // color (0) "MessageBoard"); L4Warehouse *wh = (L4Warehouse *)gauge_renderer->warehousePointer; // renderer+0x4c BitMap *strip = wh->bitMapBin.Get(p[2].data.string); // FUN_00442aec if (strip == NULL) return False; wh->bitMapBin.Release(p[2].data.string); // FUN_00442c12 return True; } // // @004cb704 -- ctor (vtable PTR_FUN_0051bddc). GraphicGauge base (owner 0); copy + // hold the strip bitmap; SetOrigin; color = param. trackedMech starts NULL. // MessageBoard::MessageBoard( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int x, int y, const char *strip_image, int color_in, const char *identification_string) : GraphicGauge(rate, mode_mask, renderer_in, 0, // FUN_00444818 (owner 0) graphics_port_number, identification_string) { stripImage = new char[strlen(strip_image) + 1]; // nameCopy (FUN_004700ac) strcpy(stripImage, strip_image); L4Warehouse *wh = (L4Warehouse *)renderer_in->warehousePointer; wh->bitMapBin.Get(stripImage); // FUN_00442aec (hold for life) localView.SetOrigin(x, y); // this+0x48 vtbl+0x10 color = color_in; // this[0x26] trackedMech = NULL; // this[0x24] (:3864) // previousNameId / previousMessageId set by BecameActive (byte-faithful). } // // @004cb788 -- dtor. Release + free the strip; base dtor runs implicitly. // MessageBoard::~MessageBoard() { L4Warehouse *wh = (L4Warehouse *)renderer->warehousePointer; wh->bitMapBin.Release(stripImage); // FUN_00442c12 delete[] stripImage; stripImage = NULL; // FUN_004022e8 } // @004cb7e4 -- TestInstance: out-of-line forward (so it isn't /FORCE-stubbed). Logical MessageBoard::TestInstance() const { return GraphicGauge::TestInstance(); } // @004cb818 -- SetSource: bind the source mech. (No recovered caller -> deferred.) void MessageBoard::SetSource(Entity *mech) { trackedMech = mech; } // @004cb7fc -- BecameActive: reset the change-sentinels. NON-inactivating. void MessageBoard::BecameActive() { previousNameId = -1; // this[0x27] @0x9c (:3916) previousMessageId = -2; // this[0x28] @0xa0 (:3917) } // // @004cb82c -- Execute. Draw the message strip cell + sender name when they change. // FAITHFUL: the board only draws while a source mech is bound; the binder (SetSource) // has no recovered caller AND the per-player status feed (StatusMessagePool) is a NULL // stub, so trackedMech stays NULL and Execute is a safe no-op == the DEFERRED empty board. // void MessageBoard::Execute() { if (trackedMech == NULL) // binary gate :3949 return; // Message record via the safe bridge (NOT raw *(mech+0x190)/+0x1dc offsets). int messageId = -1; BitMap *nameBitmap = NULL; BTResolveMessageBoard(trackedMech, &messageId, &nameBitmap); // --- message strip cell (:3960-3981) --- if (messageId != previousMessageId) { previousMessageId = messageId; localView.MoveToAbsolute(0, 0); // vtbl+0x24 if (messageId == -1) { localView.SetColor(color); // vtbl+0x18 localView.DrawFilledRectangleToRelative(0x80, 0x20); // vtbl+0x4c } else { int cellX = (messageId & 3) << 7; // :3969 int cellY = (messageId >> 2) << 5; // :3970 localView.SetColor(7); // FUN_004c2f88()==7 L4Warehouse *wh = (L4Warehouse *)renderer->warehousePointer; BitMap *strip = wh->bitMapBin.Get(stripImage); // FUN_00442aec if (strip != NULL) localView.DrawBitMapOpaque(color, 0, strip, // vtbl+0x58 cellX, cellY, cellX + 0x7f, cellY + 0x1f); wh->bitMapBin.Release(stripImage); // FUN_00442c12 } } // --- sender name cell (:3983-4009). STRUCTURAL SIMPLIFICATION (marked): the binary // tracks the previous name ENTITY pointer at 0x9c and erases it via +0x1e0->App+0xc8; // here previousNameId is a bool (0/1). Functionally identical while DEFERRED (name // always NULL); RESTORE the entity-pointer tracking + erase branch when the feed goes live. int nameState = (nameBitmap != NULL) ? 1 : 0; if (nameState != previousNameId) { localView.MoveToAbsolute(0x80, 0); // vtbl+0x24 (name cell x=128) if (nameBitmap == NULL) { localView.SetColor(color); // vtbl+0x18 localView.DrawFilledRectangleToRelative(0x80, 0x20); // vtbl+0x4c } else { localView.SetColor(7); localView.DrawBitMapOpaque(color, 0, nameBitmap, 0, 0, // vtbl+0x58 nameBitmap->Data.Size.x - 1, nameBitmap->Data.Size.y - 1); } previousNameId = nameState; // :4009 } } // === btl4grnd.cpp begins at @004cbea0 (BTL4GaugeRenderer ctor) -- not this TU.