Files
TeslaRel410/restoration/source410/BT_L4/BTL4GAU3.CPP
T
CydandClaude Fable 5 740ef2b0a7 BT410 Phase 5.3.32: the per-head A/B -- three engineering heads now exact
The gauge framebuffer is PLANE-PACKED: L4GAUGE.CFG gives each port a bit
mask, not a rectangle, so all six heads share the same x/y and are separated
only by bit -- the packing the VDB splits into the pod's physical screens.
Comparing RGB was therefore measuring nothing useful; the "magenta artifact"
just meant the wrong heads were lit at that pixel.

New instruments (emulator/render-bridge/gauge-ab/, with a README):
  planes.py  per-head scoreboard -- shipped vs ours, missing vs extra, per
             port mask, recovering the 16-bit word from DOSBox's RGB565
  BT_VIS_LOG a complete cockpit widget map from the single dispatch every
             gauge widget is built through (MethodDescription::Execute),
             printing name + port + authored position
  ab.sh      stages the fresh build over BTL4REC.EXE and kills any running
             DOSBox before launching -- see below

The fix: the Comm page's pilot roster is the POD roster (the viewpoint mech's
ControlsMapper pilot array, mapper attributes 15/16), not the mission's
player list.  It is zero in a solo mission, so the shipped page draws empty;
ours resolved the local player and painted its icon in colour 0xff.  The gate
belongs in the row source, not PilotList::Execute -- Execute's empty branch
still has to run, because erasing is what the shipped page draws.

  head    shipped    ours   missing   extra          (was)
  Eng1      17981   17981         0       0     (0 / 1030)
  Eng2      17981   17981         0       0
  Eng3      17981   17981         0       0
  Comm      15656   13557      2099       0     (2099 / 1253)
  Heat      21374   20913       566     105     (566 / 990)

Title-band magenta 404 -> 0.  The 2099 Comm pixels we are missing were
missing before the gate, so it is a strict improvement.

Method note, which cost more than the bug: the rig's confs run BTL4REC.EXE
out of the mount while the build writes build410/btl4opt.exe, and nothing
connected the two.  Three measurements ran an hour-old binary, so a change
that "did nothing" three times had never been tested -- and the correct first
hypothesis was discarded on that evidence.  ab.sh now re-stages every launch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 12:04:43 -05:00

1641 lines
49 KiB
C++

//===========================================================================//
// 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, @004c9bd0..@004cbea0)
// and re-hosted onto the authentic 1995 engine (MUNGA GAUGE/GAUGREND +
// MUNGA_L4 L4GAUGE). This TU links between btl4gau2.obj and btl4grnd.obj
// (BTL4.MAK order). Per-method binary addresses cited at each definition.
//
// 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
//
// DATABINDING RULE (the gauge scoring wave): every player read below goes
// through the reconstructed Player/BTPlayer NAMED members (PLAYER.HPP /
// BTPLAYER.HPP) -- currentScore, GetDeathCount(), GetPlayerVehicle(),
// playerBitmapIndex, killCount -- never the 1995 binary byte offsets
// (+0x1C4/+0x1C8/+0x1CC/+0x1E0/+0x1FC), which do not match our compiled
// object layout. Feeds with no reconstructed source stay as clearly-marked
// INERT DEFAULTS (the widget draws, the value is static).
//
#include <btl4.hpp>
#pragma hdrstop
#if !defined(MECHMPPR_HPP)
# include <mechmppr.hpp>
#endif
#if !defined(MISSION_HPP)
# include <mission.hpp>
#endif
#if !defined(BTL4GAU3_HPP)
# include <btl4gau3.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(BTPLAYER_HPP)
# include <btplayer.hpp>
#endif
#if !defined(MECHMPPR_HPP)
# include <mechmppr.hpp>
#endif
#if !defined(NTTMGR_HPP)
# include <nttmgr.hpp>
#endif
//
//#############################################################################
// File-private player-roster resolution
//#############################################################################
//
// The binary resolved pilots through the application's "Players" dictionary
// node (FindByName @00403ad0) and, for the Comm roster, through the viewpoint
// mech's ControlsMapper pilot array. Our reconstructed MechControlsMapper
// (MECHMPPR.HPP) does not carry the pilot roster, so the 1995-native
// equivalent is the EntityManager "Players" entity group: every non-camera
// player joins it in Player::Player (MUNGA PLAYER.CPP), and the engine's own
// Player::CalcRanking walks it with exactly this iterator idiom. Group order
// is creation order == the mission's player join order.
//
static Player *
ResolveRosterPlayer(int slot)
{
if (application == NULL)
{
return NULL;
}
EntityGroup
*players = application->GetEntityManager()->FindGroup("Players");
if (players == NULL)
{
return NULL;
}
ChainIteratorOf<Node*>
iterator(players->groupMembers);
Player
*roster_player;
int
index = 0;
while ((roster_player = (Player *) iterator.ReadAndNext()) != NULL)
{
if (index == slot)
{
return roster_player;
}
++index;
}
return NULL;
}
//
//#############################################################################
// COMM-PAGE roster gate -- the cockpit's pilot list is NOT the mission's
// player list. The binary fed it from the viewpoint mech's ControlsMapper
// pilot array (mapper attributes PilotArrayPage / PilotArray, ids 15/16),
// which the pod network authors; a mission with no pod roster leaves it
// zero and the shipped cockpit draws the page EMPTY.
//
// A/B EVIDENCE (5.3.32): with the ungated walk our slot 0 resolved the solo
// mission player and painted its icon in colour 0xff -- 404 magenta pixels
// in the Comm title band where the shipped binary paints the black erase
// rect. Gate the ROW SOURCE here, not PilotList::Execute: Execute's empty
// branch still has to run, because erasing IS what the shipped page draws.
//
// PlayerStatus (the observer/camera scoreboard) keeps the ungated walk -- it
// is a mission-wide standings page, not the pod roster.
//#############################################################################
//
static Player *
ResolveCommRosterPlayer(int slot)
{
if (application == NULL)
{
return NULL;
}
Entity
*viewpoint = application->GetViewpointEntity();
if (viewpoint == NULL || viewpoint->GetSubsystemCount() <= 0)
{
return NULL;
}
Subsystem
*controls = viewpoint->GetSubsystem(0);
if (controls == NULL
|| !controls->IsDerivedFrom(MechControlsMapper::ClassDerivations))
{
return NULL;
}
if (((MechControlsMapper *) controls)->GetPilotArray() == 0)
{
return NULL; // no pod roster authored -> the page stays empty
}
return ResolveRosterPlayer(slot);
}
//
//#############################################################################
// KILLS access (the gauge scoring wave): the KILLS column must read the SAME
// compiled member the score handlers increment -- BTPlayer::killCount
// (BTPLAYER.CPP ScoreMessageHandler / VehicleDead path). The reconstructed
// BTPLAYER.HPP keeps killCount protected with no accessor yet, so this thin
// derived reader stands in for a GetKillCount() accessor: NAMED-member access
// only, no offsets, no data members, never constructed. Retire it the day
// BTPLAYER.HPP grows the accessor.
//#############################################################################
//
class BTPlayerKillReader:
public BTPlayer
{
public:
static int
KillsOf(Player *pilot)
{
return ((BTPlayerKillReader *) pilot)->killCount;
}
private:
BTPlayerKillReader(); // reader cast only -- never constructed
};
//
// SELECT-TARGET highlight -- INERT DEFAULT. The binary tinted the roster row
// of the local pilot's current target (local player -> objectiveMech -> its
// player). BTPlayer::objectiveMech is protected with no accessor in the
// reconstruction, so the row tint stays off (rows draw untinted; the KILLS /
// DEATHS numerics are unaffected).
//
static Logical
RosterPlayerIsSelected(Player * /*pilot*/, Player * /*local_player*/)
{
return False;
}
//
// Player-name-bitmap cache -- INERT DEFAULT (the donor's own bring-up stub,
// kept per the worksheet). The binary looked prebuilt name rasters up in the
// application's name-bitmap cache by the player's name id (@0x1E0 ==
// Player::playerBitmapIndex). Returning NULL routes every caller to the
// binary's own cache-miss branch (the tinted name box), never an AV.
//
static BitMap *
LookupPlayerNameBitmap(int player_bitmap_index)
{
//
// The mission owns the name art: the egg's [smallbitmap] index page
// lists one notation page of hex rows per pilot, and the authentic
// Mission ctor loads them into smallNameBitmapChain (MISSION.CPP:525).
// A player's `bitmapindex` egg entry selects its 1-based slot. A miss
// still routes the caller to the binary's cache-miss branch (the
// tinted name box), never an AV.
//
if (player_bitmap_index <= 0 || application == NULL)
{
return NULL;
}
Mission
*mission = application->GetCurrentMission();
if (mission == NULL)
{
return NULL;
}
return mission->GetSmallNameBitmap(player_bitmap_index);
}
//#############################################################################
//#############################################################################
// 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"
};
//
//#############################################################################
// @004c9bd0 -- build the 28 ColorMapperArmor zone gauges. For each name in
// the damage-zone table resolve its index on the mech (GetDamageZoneIndex,
// binary FUN_0042076c; -1 => inert) and construct a ColorMapperArmor wired to
// that zone, incrementing the base colour index each iteration.
//#############################################################################
//
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*/
)
{
Check_Pointer(this);
int
i;
for (i = 0; i < playerStatusZoneCount; ++i)
{
//
// (BC4.52: CString temporaries are illegal in a ternary -- hoist.)
//
int
zone_index = -1;
if (mech != NULL)
{
zone_index = mech->GetDamageZoneIndex(kPlayerStatusZone[i]);
}
zone[i] = new ColorMapperArmor(
rate,
mode_mask,
renderer,
graphics_port_number,
base_color_index + i,
mech,
palette_a,
palette_b,
zone_index,
"ColorMapperArmor"
);
Register_Object(zone[i]);
}
Check_Fpu();
}
//
//#############################################################################
// @004c9ca8 -- destroy: delete all 28.
//#############################################################################
//
PlayerStatusMappingGroup::~PlayerStatusMappingGroup()
{
Check(this);
int
i;
for (i = 0; i < playerStatusZoneCount; ++i)
{
if (zone[i] != NULL)
{
Unregister_Object(zone[i]);
delete zone[i];
zone[i] = NULL;
}
}
Check_Fpu();
}
Logical
PlayerStatusMappingGroup::TestInstance() const
{
return True;
}
//
//#############################################################################
// @004c9cf4 -- Execute: run each element's Execute.
//#############################################################################
//
void
PlayerStatusMappingGroup::Execute()
{
Check(this);
int
i;
for (i = 0; i < playerStatusZoneCount; ++i)
{
if (zone[i] != NULL)
{
zone[i]->Execute();
}
}
Check_Fpu();
}
//
//#############################################################################
// @004c9d18 -- SetColor: push the state onto all 28.
//#############################################################################
//
void
PlayerStatusMappingGroup::SetColor(int state)
{
Check(this);
int
i;
for (i = 0; i < playerStatusZoneCount; ++i)
{
if (zone[i] != NULL)
{
zone[i]->SetColor(state);
}
}
Check_Fpu();
}
//#############################################################################
//#############################################################################
// 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). Keyword + parameter list VERBATIM (parse-critical).
//
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. Construct, then check 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
*parameterList = methodDescription.parameterList;
Check(gauge_renderer);
SectorDisplay
*gauge = new SectorDisplay(
parameterList[0].data.rate,
parameterList[1].data.modeMask,
(L4GaugeRenderer *) gauge_renderer,
display_port_index, // graphics_port_number (FIX: was 0)
position.x, position.y, // SetOrigin (FIX: was 0,0)
parameterList[2].data.string, // grid/font image (helv15.pcc)
parameterList[3].data.color, // numericColor
parameterList[4].data.color, // gridColor / numeric okColor
"SectorDisplay"
);
Register_Object(gauge);
//--------------------------------------------------------
// Make sure the grid image made it into the warehouse
//--------------------------------------------------------
L4Warehouse
*warehouse = (L4Warehouse *) gauge_renderer->warehousePointer;
Check(warehouse);
if (warehouse->bitMapBin.Get(parameterList[2].data.string) == NULL)
{
DEBUG_STREAM << "SectorDisplay: Missing image "
<< parameterList[2].data.string << "\n" << flush;
Check_Fpu();
return False;
}
warehouse->bitMapBin.Release(parameterList[2].data.string);
Check_Fpu();
return True;
}
//
//#############################################################################
// @004c9e10 -- ctor (vtable 0051beec). GraphicGauge base (owner 0); copy the
// grid-image name; from the 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,
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,
0, // owner folded to 0 (binary base call)
graphics_port_number,
identification_string
)
{
Check_Pointer(this);
//------------------------------------------------------------
// Execute reads gridImage per frame, so it must outlive the
// (transient) config parameterList string -- keep a durable
// copy (native nameCopy, cf. NumericDisplay ctor).
//------------------------------------------------------------
gridImage = nameCopy(image);
numericColor = color; // @0xA8
gridColor = ok_color; // @0xAC
subject = NULL; // @0x90
sectorBaseA = 500; // @0xB0 (0x1f4)
sectorBaseB = 500; // @0xB4
dirty = 1; // @0xB8 (binary leaves uninit; BecameActive sets it)
//------------------------------------------------------------
// Size the grid cells from the image (Get / measure / Release,
// the NumericDisplay ctor idiom -- the art stays warehoused).
//------------------------------------------------------------
L4Warehouse
*warehouse = (L4Warehouse *) renderer->warehousePointer;
Check(warehouse);
BitMap
*grid = warehouse->bitMapBin.Get(gridImage);
if (grid == NULL)
{
cellWidth = gridLeft = gridRight = gridHeight = 0;
}
else
{
cellWidth = grid->Data.Size.x / 14;
gridLeft = cellWidth * 12;
gridRight = cellWidth * 12 + cellWidth - 1;
gridHeight = grid->Data.Size.y;
warehouse->bitMapBin.Release(gridImage);
}
localView.SetOrigin(x, y);
//------------------------------------------------------------
// NumericDisplay(warehouse, x, y, image, digits=3,
// unsignedFormat, color, okColor)
//------------------------------------------------------------
numericA = new NumericDisplay(
warehouse, 0, 0, image, 3,
NumericDisplay::unsignedFormat, color, ok_color);
Register_Object(numericA);
numericB = new NumericDisplay(
warehouse, cellWidth * 4, 0, image, 3,
NumericDisplay::unsignedFormat, color, ok_color);
Register_Object(numericB);
Check_Fpu();
}
//
//#############################################################################
// @004c9f94 -- dtor. Free the copied name + both numerics; the base
// GraphicGauge chain runs implicitly.
//#############################################################################
//
SectorDisplay::~SectorDisplay()
{
Check(this);
Unregister_Pointer(gridImage);
delete[] gridImage;
gridImage = NULL;
Unregister_Object(numericA);
delete numericA;
numericA = NULL;
Unregister_Object(numericB);
delete numericB;
numericB = NULL;
Check_Fpu();
}
// @004ca020 -- TestInstance (out-of-line forward to the base).
Logical
SectorDisplay::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
//#############################################################################
// @004ca068 -- LinkToEntity (slot 9): cache the viewpoint-bind broadcast's
// entity as the subject (APP.CPP MakeViewpointEntity -> GAUGREND.CPP slot-9
// forward arms this on the authentic engine).
//#############################################################################
//
void
SectorDisplay::LinkToEntity(Entity *entity)
{
Check(this);
subject = entity;
}
//
//#############################################################################
// @004ca038 -- BecameActive (slot 3): mark dirty + reset both numerics.
// NON-inactivating (does NOT chain the base).
//#############################################################################
//
void
SectorDisplay::BecameActive()
{
Check(this);
dirty = 1;
numericB->ForceUpdate(); // B then A -- binary order
numericA->ForceUpdate();
}
//
//#############################################################################
// @004ca07c -- Execute. Draw the two sector numerics from the linked mech's
// world X/Z (Round @FUN_004dcd94 -- the native SCALAR.HPP Round), then on the
// first active frame blit the grid-cell background. The binary gates purely
// on subject != 0 (@0x4ca08e).
//#############################################################################
//
void
SectorDisplay::Execute()
{
Check(this);
if (subject == NULL)
{
return;
}
const Point3D
&mech_position = subject->localOrigin.linearPosition;
int
sector_a = Round(-mech_position.z * 0.01f) + sectorBaseA, // 100 m sectors
sector_b = Round( mech_position.x * 0.01f) + sectorBaseB;
numericA->Draw(&localView, (Scalar) sector_a);
numericB->Draw(&localView, (Scalar) sector_b);
if (dirty)
{
dirty = 0;
L4Warehouse
*warehouse = (L4Warehouse *) renderer->warehousePointer;
BitMap
*grid = warehouse->bitMapBin.Get(gridImage);
if (grid != NULL)
{
localView.MoveToAbsolute(cellWidth * 3, 0);
localView.SetColor(gridColor);
localView.DrawBitMap(0, grid, gridLeft, 0, gridRight, gridHeight);
warehouse->bitMapBin.Release(gridImage);
}
}
Check_Fpu();
}
//#############################################################################
//#############################################################################
// TeamStatusDisplay @004ca208
//#############################################################################
//#############################################################################
//
// DEFERRED -- prose-only reconstruction (no recovered bodies, no
// methodDescription, no config keyword registered; nothing constructs it).
// Documented shape, banked for the implementing pass:
//
// @004ca208 -- ctor (vtable 0051bea8). GraphicGauge base. rowSpacing =
// param_10; originY = param y - 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 player-number NumericDisplay and -- when flags&4 -- a kill
// NumericDisplay; 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; read the
// "Players" roster, slot each player into [0..7] by player index (skipping
// spectators); on an occupant change blit the player's name bitmap (or
// clear), tinting friendly/self; update each kill numeric (binary +0x1c8 ==
// the reconstruction's BTPlayer::killCount); when flags&4, also track each
// player's rank; finally redraw the selection underline on the local
// player's row.
//
// (When built, the kill / rank / spectator feeds bind through the same named
// Player/BTPlayer members this TU already uses -- see the DATABINDING RULE
// note at the top of the file.)
//
//#############################################################################
//#############################################################################
// PilotList @004ca90c Make / @004ca958 ctor
//#############################################################################
//#############################################################################
//
// The Comm KILLS/DEATHS roster. Keyword + parameter list VERBATIM
// (parse-critical): pilotList( C, ModeAlwaysActive, bigfont.pcc ).
//
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. position UNUSED (each entry carries absolute coords
// from the DAT_0051af88 layout table). Always returns True (binary).
//#############################################################################
//
Logical
PilotList::Make(
int display_port_index,
Vector2DOf<int> /*position -- unused*/,
Entity * /*entity -- unused*/,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription
*parameterList = methodDescription.parameterList;
Check(gauge_renderer);
PilotList
*gauge = new PilotList(
parameterList[0].data.rate,
parameterList[1].data.modeMask,
(L4GaugeRenderer *) gauge_renderer,
display_port_index, // graphics_port_number
parameterList[2].data.string, // font (bigfont.pcc)
"PilotList"
);
Register_Object(gauge);
Check_Fpu();
return True;
}
//
//#############################################################################
// @004ca958 -- ctor (vtable 0051be64). GraphicGauge base; build 3
// NumericDisplays per entry from the PE-parsed 8x(x, y, layoutMode) table
// (DAT_0051af88 -- exact bytes from BTL4OPT.EXE).
//#############################################################################
//
PilotList::PilotList(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer,
int graphics_port_number,
const char *font_image,
const char *identification_string
):
GraphicGauge(
rate,
mode_mask,
renderer,
0, // owner folded to 0 (binary base call)
graphics_port_number,
identification_string
)
{
Check_Pointer(this);
currentSlot = 0;
built = False;
L4Warehouse
*warehouse = (L4Warehouse *) renderer->warehousePointer;
Check(warehouse);
//------------------------------------------------------------
// DAT_0051af88 -- 8 x (x, y, layoutMode)
//------------------------------------------------------------
static const int
layout[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}
};
int
i;
for (i = 0; i < 8; ++i)
{
int
x = layout[i][0],
y = layout[i][1],
layout_mode = layout[i][2];
int
name_x, name_y, mech_x, mech_y;
if (layout_mode == 1)
{
name_x = x - 8; name_y = y - 0x50;
mech_x = x + 0x48; mech_y = y - 0x50;
}
else if (layout_mode == 2)
{
name_x = x - 8; name_y = y + 0x44;
mech_x = x + 0x48; mech_y = y + 0x44;
}
else // layout_mode == 0
{
name_x = x + 0x95; name_y = y - 0x0d;
mech_x = x + 0xe8; mech_y = y - 0x0d;
}
entry[i].x = x;
entry[i].y = y;
entry[i].resolvedMech = 0;
entry[i].selected = 0;
entry[i].nameDisplay = new NumericDisplay( // KILLS
warehouse, name_x, name_y, font_image, 2,
NumericDisplay::signedBlankedZerosFormat, 0, 0xff);
Register_Object(entry[i].nameDisplay);
entry[i].mechDisplay = new NumericDisplay( // DEATHS
warehouse, mech_x, mech_y, font_image, 2,
NumericDisplay::signedBlankedZerosFormat, 0, 0xff);
Register_Object(entry[i].mechDisplay);
entry[i].scoreDisplay = new NumericDisplay( // erase-only
warehouse, x, y, font_image, 1,
NumericDisplay::signedBlankedZerosFormat, 0, 0xff);
Register_Object(entry[i].scoreDisplay);
}
Check_Fpu();
}
//
//#############################################################################
// @004cab10 -- dtor. Free the 3 numerics per entry; base chain implicit.
//#############################################################################
//
PilotList::~PilotList()
{
Check(this);
int
i;
for (i = 0; i < 8; ++i)
{
Unregister_Object(entry[i].nameDisplay);
delete entry[i].nameDisplay;
entry[i].nameDisplay = NULL;
Unregister_Object(entry[i].mechDisplay);
delete entry[i].mechDisplay;
entry[i].mechDisplay = NULL;
Unregister_Object(entry[i].scoreDisplay);
delete entry[i].scoreDisplay;
entry[i].scoreDisplay = NULL;
}
Check_Fpu();
}
// @004cab8c -- TestInstance (out-of-line forward to the base).
Logical
PilotList::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
//#############################################################################
// @004caba4 -- BecameActive. Invalidate every entry so the first cycle
// repaints; currentSlot = 9 forces the wrap-reset on the first Execute.
//#############################################################################
//
void
PilotList::BecameActive()
{
Check(this);
int
i;
for (i = 0; i < 8; ++i)
{
entry[i].resolvedMech = -1;
}
currentSlot = 9;
}
//
//#############################################################################
// @004cad70 -- DrawMechIcon. Blit the pilot's name bitmap (or, on a cache
// miss -- always, while the name-bitmap cache is the inert default -- a
// tinted 0x80 x 0x20 box), tinted by the selected flag. The binary's name
// id read (@0x1E0) is the reconstruction's Player::playerBitmapIndex.
//#############################################################################
//
void
PilotList::DrawMechIcon(Player *pilot, int selected)
{
Check(this);
Check(pilot);
BitMap
*name_bitmap = LookupPlayerNameBitmap(pilot->playerBitmapIndex);
int
box_color = selected ? 0xff : 0,
blit_color = selected ? 0 : 0xff;
if (name_bitmap == NULL)
{
localView.SetColor(box_color);
localView.DrawFilledRectangleToRelative(0x80, 0x20);
}
else
{
localView.SetColor(blit_color);
localView.DrawBitMapOpaque(box_color, 0, name_bitmap, 0, 0,
name_bitmap->Data.Size.x - 1, name_bitmap->Data.Size.y - 1);
}
}
//
//#############################################################################
// @004cabd0 -- Execute (one roster slot per frame). Look up the current slot
// in the shared player roster; erase an empty slot, or draw the pilot's name
// icon + KILLS + DEATHS for an occupied one.
//
// The KILLS/DEATHS numerics read the COMPILED named members the score
// handlers write (BTPlayer::killCount / Player::deathCount) -- the binary's
// raw pilot+0x27c / +0x200 offsets read a stale/garbage slot on our compiled
// layout (the silent-zero scoreboard bug on record).
//#############################################################################
//
void
PilotList::Execute()
{
Check(this);
//
// The local pilot gates the whole body (binary: roster slot 0 of the
// viewpoint mech's mapper roster; native = the mission player).
//
Player
*local_player = application->GetMissionPlayer();
if (local_player == NULL)
{
return;
}
if (currentSlot > 7) // wrap the round-robin
{
currentSlot = 0;
built = False;
}
Entry
&e = entry[currentSlot];
Player
*pilot;
if (built == False)
{
pilot = ResolveCommRosterPlayer(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
{
localView.MoveToAbsolute(e.x, e.y);
localView.SetColor(0);
localView.DrawFilledRectangleToRelative(0x80, 0x20);
e.nameDisplay->Erase(&localView);
e.mechDisplay->Erase(&localView);
e.scoreDisplay->Erase(&localView);
}
}
else
{
int
selected = RosterPlayerIsSelected(pilot, local_player);
if ((int) pilot != e.resolvedMech || e.selected != selected)
{
localView.MoveToAbsolute(e.x, e.y);
DrawMechIcon(pilot, selected);
e.selected = selected;
// (byte-faithful: the binary does NOT latch e.resolvedMech here,
// so the icon redraws each occupied frame.)
}
e.nameDisplay->Draw(&localView, // KILLS (killCount)
(Scalar) BTPlayerKillReader::KillsOf(pilot));
e.mechDisplay->Draw(&localView, // DEATHS (deathCount)
(Scalar) pilot->GetDeathCount());
}
++currentSlot;
Check_Fpu();
}
//#############################################################################
//#############################################################################
// PlayerStatus @004cae90 Make / @004cb1a8 ctor
//#############################################################################
//#############################################################################
//
// @004cafac -- per-player pixmap recolour. INERT DEFAULT (the donor's own
// bring-up stub, kept per the worksheet). The faithful body resolves the
// mech's type-0x20 pixmap sub-resource from the application resource file
// (warns "CreateMutantPixelmap8: couldn't find ..." on a miss), opens it as a
// memory stream, reads the embedded image name (ReadStreamString @004caf50,
// 0x50 chars -- deferred with this stub), looks the source PixMap8 up in the
// renderer warehouse, and copies it biasing every palette index >= 0x20 by
// (palette_base - 0x20) so each player's mech outline draws in its own colour
// ramp. Deferred: the memory-stream read API + pixel copy are unmapped.
// With this stub recoloredMech stays NULL -> Execute skips the outline blit;
// the score, name box and alive/dead status box still render.
//
static Pixmap *
CreateMutantPixelmap8(
Entity * /*mech*/,
L4Warehouse * /*warehouse*/,
int /*palette_base*/,
Pixmap *destination
)
{
return destination;
}
//
// Keyword + parameter list VERBATIM (parse-critical). CFG (L4GAUGE.CFG:29):
// PlayerStatus( D, ModeAlwaysActive, 1, helv15.pcc, 0, 11, 3, 1 )
//
MethodDescription
PlayerStatus::methodDescription =
{
"PlayerStatus",
PlayerStatus::Make,
{
{ 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. The player number must be 1..8 or "PlayerStatus: Make
// player number <n>" is warned (string 0x51bcd9). Reads the parameterList
// like every registered widget (the binary read raw DAT_ pools) + the
// port/x/y bug fixed on record (the binary-side Make passed entity/renderer
// as x/y and dropped the port).
//#############################################################################
//
Logical
PlayerStatus::Make(
int display_port_index,
Vector2DOf<int> position,
Entity * /*entity -- unused by PlayerStatus*/,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription
*parameterList = methodDescription.parameterList;
Check(gauge_renderer);
int
player_number = parameterList[2].data.integer;
if (player_number < 1 || player_number > 8)
{
DEBUG_STREAM << "PlayerStatus: Make player number "
<< player_number << " out of range\n" << flush;
return False;
}
PlayerStatus
*gauge = new PlayerStatus(
parameterList[0].data.rate,
parameterList[1].data.modeMask,
(L4GaugeRenderer *) gauge_renderer,
display_port_index, // graphics_port_number <-- FIX (was dropped)
position.x, position.y, // x, y <-- FIX (was entity/renderer)
player_number,
parameterList[3].data.string,
parameterList[4].data.color,
parameterList[5].data.color,
parameterList[6].data.color,
parameterList[7].data.color,
"PlayerStatus"
);
Register_Object(gauge);
Check_Fpu();
return True;
}
//
//#############################################################################
// @004cb1a8 -- ctor (vtable 0051be20). GraphicGauge base (owner folded to
// 0); stash the four state colours + port; set the port origin; build the
// score NumericDisplay (5 digits @ (0x72, 0xD8)).
//#############################################################################
//
PlayerStatus::PlayerStatus(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer,
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,
0, // owner folded to 0 (binary base call)
graphics_port_number,
identification_string
)
{
Check_Pointer(this);
color = color_in; // @0xA4
okColor = ok_color; // @0xA8
aliveColor = alive_color; // @0xAC
deadColor = dead_color; // @0xB0
port = graphics_port_number; // @0x90
localView.SetOrigin(x, y);
playerIndex = player_number - 1; // @0x94
player = NULL; // @0x98
nameImage = NULL; // @0x9C
recoloredMech = NULL; // @0xA0
mappingGroup = NULL; // @0xB4
L4Warehouse
*warehouse = (L4Warehouse *) renderer->warehousePointer;
Check(warehouse);
scoreDisplay = new NumericDisplay( // @0xB8
warehouse, 0x72, 0xD8, font_image, 5,
NumericDisplay::signedBlankedZerosFormat, color, okColor);
Register_Object(scoreDisplay);
// dirty/previousStatus/previousMappingState set by BecameActive
// (byte-faithful).
Check_Fpu();
}
//
//#############################################################################
// @004cb28c -- dtor. Free the score numeric + the recoloured mech pixmap;
// base chain implicit. (nameImage is cache-owned; mappingGroup deleted here
// as a correctness fix on record -- the binary leaks it.)
//#############################################################################
//
PlayerStatus::~PlayerStatus()
{
Check(this);
player = NULL;
Unregister_Object(scoreDisplay);
delete scoreDisplay;
scoreDisplay = NULL;
if (recoloredMech != NULL)
{
delete recoloredMech;
recoloredMech = NULL;
}
if (mappingGroup != NULL)
{
delete mappingGroup; // (the binary leaks this)
mappingGroup = NULL;
}
Check_Fpu();
}
// @004cb2f8 -- TestInstance (out-of-line forward to the base).
Logical
PlayerStatus::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
//#############################################################################
// @004cb310 -- BecameActive: reset the score readout, run the mapping group
// once, and force a full redraw next Execute.
//#############################################################################
//
void
PlayerStatus::BecameActive()
{
Check(this);
scoreDisplay->ForceUpdate();
if (mappingGroup != NULL)
{
mappingGroup->Execute(); // @004c9cf4
}
dirty = 1;
previousStatus = -1;
previousMappingState = -1;
}
//
//#############################################################################
// @004cb358 -- Execute. Resolve player N from the shared roster (the binary
// indexed the application's "Players" node; native = the "Players" entity
// group, creation order); 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.
//#############################################################################
//
void
PlayerStatus::Execute()
{
Check(this);
//--- PART 1: resolve / change-detect the player ------------------------
Player
*resolved = ResolveRosterPlayer(playerIndex);
if (resolved != player)
{
player = resolved;
if (player != NULL)
{
Entity
*mech = player->GetPlayerVehicle(); // named accessor (was raw +0x1FC)
if (mech == NULL)
{
player = NULL; // no vehicle yet -> retry next frame
}
else
{
L4Warehouse
*warehouse = (L4Warehouse *) renderer->warehousePointer;
int
palette_base = playerIndex * 0x1C + 0x10;
recoloredMech = CreateMutantPixelmap8(
mech, warehouse, palette_base, recoloredMech);
if (mappingGroup != NULL)
{
delete mappingGroup;
}
mappingGroup = new PlayerStatusMappingGroup(
rate, modeMask, (L4GaugeRenderer *) renderer, port,
mech, palette_base,
"adpal.pcc", "adpal2.pcc",
"PlayerStatusMappingGroup");
// INERT DEFAULT: the application name-bitmap cache is not
// reconstructed -> the filled name-box branch below draws.
nameImage = NULL;
BecameActive(); // force a full redraw
}
}
}
//--- PART 2: per-frame draw (only with a bound player) -----------------
if (player == NULL)
{
return;
}
Entity
*mech = player->GetPlayerVehicle();
if (mech == NULL)
{
return;
}
//
// (a) score readout -- the published Player::currentScore (the same
// member the score handlers write; was raw +0x1C8-as-Scalar).
//
scoreDisplay->Draw(&localView, player->currentScore);
//
// (b) mapping-group disabled-state refresh -- INERT DEFAULT: the binary
// keyed this off the mech's damage-mapping derivation; the recon has
// no bound source in this TU (Mech::IsDisabled needs mech.hpp, which
// collides here), so the group holds the enabled tint.
//
int
disabled = 0;
if (disabled != previousMappingState)
{
previousMappingState = disabled;
if (mappingGroup != NULL)
{
mappingGroup->SetColor(disabled); // @004c9d18
}
}
//
// (c) mech outline + name box, dirty-gated
//
if (dirty)
{
dirty = 0;
if (recoloredMech != NULL)
{
localView.MoveToAbsolute(1, 1);
localView.DrawPixelMap8(True, 0, recoloredMech, 0, 0,
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);
localView.DrawFilledRectangleToRelative(0x80, 0x20);
}
else
{
localView.SetColor(0xFF);
localView.DrawBitMapOpaque(color, 0, nameImage, 0, 0,
nameImage->Data.Size.x - 1, nameImage->Data.Size.y - 1);
}
}
//
// (d) alive/dead status box, state-change gated. INERT DEFAULT: the
// binary keyed this off the player record's status word (@0x1C4); no
// alive/dead member is published on the reconstructed Player/BTPlayer
// yet, so the box paints once in aliveColor and holds.
//
int
state = 0;
if (state != previousStatus)
{
previousStatus = state;
localView.SetColor(state == 0 ? aliveColor : deadColor);
localView.MoveToAbsolute(0, 0);
localView.DrawFilledRectangleToAbsolute(0x9F, 0xEF);
}
Check_Fpu();
}
//#############################################################################
//#############################################################################
// MessageBoard @004cb678 Make / @004cb704 ctor
//#############################################################################
//#############################################################################
//
// Keyword + parameter list VERBATIM (parse-critical). 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. Construct + presence-check the strip bitmap (the 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 the binary*/,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription
*parameterList = methodDescription.parameterList;
Check(gauge_renderer);
MessageBoard
*gauge = new MessageBoard(
parameterList[0].data.rate,
parameterList[1].data.modeMask,
(L4GaugeRenderer *) gauge_renderer,
display_port_index, // graphics_port_number
position.x, position.y, // offset=(113,607) -> SetOrigin
parameterList[2].data.string, // strip image (btsmsgs.pcx)
parameterList[3].data.color, // color (0)
"MessageBoard"
);
Register_Object(gauge);
L4Warehouse
*warehouse = (L4Warehouse *) gauge_renderer->warehousePointer;
Check(warehouse);
if (warehouse->bitMapBin.Get(parameterList[2].data.string) == NULL)
{
Check_Fpu();
return False;
}
warehouse->bitMapBin.Release(parameterList[2].data.string);
Check_Fpu();
return True;
}
//
//#############################################################################
// @004cb704 -- ctor (vtable 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,
int graphics_port_number,
int x, int y,
const char *strip_image,
int color_in,
const char *identification_string
):
GraphicGauge(
rate,
mode_mask,
renderer,
0, // owner folded to 0 (binary base call)
graphics_port_number,
identification_string
)
{
Check_Pointer(this);
stripImage = nameCopy(strip_image);
L4Warehouse
*warehouse = (L4Warehouse *) renderer->warehousePointer;
Check(warehouse);
warehouse->bitMapBin.Get(stripImage); // hold a reference for life
localView.SetOrigin(x, y);
color = color_in; // @0x98
trackedMech = NULL; // @0x90
// previousNameId / previousMessageId set by BecameActive (byte-faithful).
Check_Fpu();
}
//
//#############################################################################
// @004cb788 -- dtor. Release + free the strip; base chain implicit.
//#############################################################################
//
MessageBoard::~MessageBoard()
{
Check(this);
L4Warehouse
*warehouse = (L4Warehouse *) renderer->warehousePointer;
warehouse->bitMapBin.Release(stripImage);
Unregister_Pointer(stripImage);
delete[] stripImage;
stripImage = NULL;
Check_Fpu();
}
// @004cb7e4 -- TestInstance (out-of-line forward to the base).
Logical
MessageBoard::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
//#############################################################################
// @004cb818 -- SetSource: bind the source mech. (No recovered caller in the
// shipped binary -> the board is authentically dormant; see Execute.)
//#############################################################################
//
void
MessageBoard::SetSource(Entity *mech)
{
Check(this);
trackedMech = mech;
}
//
//#############################################################################
// @004cb7fc -- BecameActive: reset the change-sentinels. NON-inactivating.
//#############################################################################
//
void
MessageBoard::BecameActive()
{
Check(this);
previousNameId = -1; // @0x9C
previousMessageId = -2; // @0xA0
}
//
//#############################################################################
// @004cb82c -- Execute. Draw the message strip cell + sender name when they
// change.
//
// FAITHFUL / DEFERRED: the board only draws while a source mech is bound; the
// binder (SetSource) has no recovered caller AND the per-player status feed
// (the pool that fills Player::statusMessagePointer, PLAYER.HPP) is not
// reconstructed, so trackedMech stays NULL and Execute is a safe no-op == the
// authentically empty board. The resolve below is an INERT DEFAULT ("no
// message"); the cell math is byte-verified and goes live the day the feed
// and the SetSource caller are built.
//#############################################################################
//
void
MessageBoard::Execute()
{
Check(this);
if (trackedMech == NULL) // binary gate
{
return;
}
//
// INERT DEFAULT message resolve: the native record is the tracked mech's
// player's statusMessagePointer (messageType -> strip cell id,
// playerInvolved -> sender name bitmap), but its producer pool is
// unbuilt -- report "no message" until it lands.
//
int
messageId = -1;
BitMap
*nameBitmap = NULL;
//--- message strip cell -------------------------------------------------
if (messageId != previousMessageId)
{
previousMessageId = messageId;
localView.MoveToAbsolute(0, 0);
if (messageId == -1)
{
localView.SetColor(color);
localView.DrawFilledRectangleToRelative(0x80, 0x20);
}
else
{
int
cellX = (messageId & 3) << 7, // 0x80-wide cells, 4 across
cellY = (messageId >> 2) << 5; // 0x20-tall rows
localView.SetColor(7); // FUN_004c2f88() == 7
L4Warehouse
*warehouse = (L4Warehouse *) renderer->warehousePointer;
BitMap
*strip = warehouse->bitMapBin.Get(stripImage);
if (strip != NULL)
{
localView.DrawBitMapOpaque(color, 0, strip,
cellX, cellY, cellX + 0x7f, cellY + 0x1f);
warehouse->bitMapBin.Release(stripImage);
}
}
}
//--- sender name cell (x=128). The binary tracks the previous name
// ENTITY pointer @0x9C; the reconstruction tracks the resolved BITMAP
// pointer -- the same change-detection granularity (a new sender = a
// different prebuilt raster), no entity re-deref.
int
nameState = (int) nameBitmap;
if (nameState != previousNameId)
{
localView.MoveToAbsolute(0x80, 0);
if (nameBitmap == NULL)
{
localView.SetColor(color);
localView.DrawFilledRectangleToRelative(0x80, 0x20);
}
else
{
localView.SetColor(7);
localView.DrawBitMapOpaque(color, 0, nameBitmap, 0, 0,
nameBitmap->Data.Size.x - 1, nameBitmap->Data.Size.y - 1);
}
previousNameId = nameState;
}
Check_Fpu();
}
// === btl4grnd.cpp begins at @004cbea0 (BTL4GaugeRenderer ctor) -- not this TU.