//===========================================================================//
// 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<Player*>::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<<char*  FUN_004db78c <<int
//      DAT_004efc94 the Application (App+0x24 dictionary, +0x68 warehouse,
//                   +0xc8 palette cache, +0x6c local mech)
//
// Recovered string evidence:
//      "SectorDisplay: Missing image " 0x51bc90   "Players" 0x51bcba/0x51bd97
//      "PlayerStatus: Make player number " 0x51bcd9
//      "CreateMutantPixelmap8: couldn't ..." 0x51bd0d/0x51bd40/0x51bd6d
//      "PilotList" 0x51bb4a   "PlayerStatus" 0x51bb54   "MessageBoard" 0x51bb61
//      "adpal.pcc" 0x51bd9f  "adpal2.pcc" 0x51bda9  "PlayerStatusMappingGroup" 0x51bdb4
//

// See btl4gaug.cpp: suppress heat.hpp's optional DebugStream so the shared
// mechrecon ReconStream definition is the only one active in this TU.
#define BT_DEBUGSTREAM_DEFINED

#include <bt.hpp>
#pragma hdrstop

#if !defined(BTL4GAU3_HPP)
#	include <btl4gau3.hpp>
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#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 <name>" if absent (binary-faithful: the base ctor
// self-registers the gauge regardless of the return value).
//
Logical
	SectorDisplay::Make(
		int				display_port_index,
		Vector2DOf<int>	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<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
	{
		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 <n>" 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<int>	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<int>	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.
