Files
TeslaRel410/restoration/source410/BT_L4/BTL4GAU3.CPP
T
CydandClaude Fable 5 fc0cafe867 BT410 5.3.39: the Comm page goes exact -- the own row and its name raster
Two things, both read off the shipped framebuffer.

SLOT 0 IS THE OWN ROW.  Its layout entry is the odd one out -- {180,225} at
layout mode 0, KILLS/DEATHS at +0x95/+0xe8 instead of the strip boxes'
+0x48 -- and the shipped Comm page fills it with the LOCAL pilot while the
seven surrounding boxes stay empty in a solo mission.  So slot 0 takes the
mission player directly and only slots 1..7 come from the pod roster.  That
also explains the artifact this session opened with: the ungated walk was
feeding the mission player into the STRIP slots, which is why gating the
roster removed wrong pixels but left the centre row missing.

THE OWN ROW TAKES THE LARGE NAME RASTER.  Mission carries both; we asked for
the small one, so 'Aeolus' drew visibly undersized and low in the box.  The
strip slots keep the small raster -- their boxes are half the height.

  Comm   missing 2099 -> 733 -> 0     extra 0 -> 152 -> 0

The Comm head is now EXACT: 15656 pixels against the shipped binary's 15656.
Four heads exact (Eng1/2/3, Comm), the three MFDs within ~150px, and the
whole cockpit at 98.9% identical / 99% coverage -- from 89.7% / 92% when the
session started.

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

1663 lines
50 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 rows stay 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, Logical large)
{
//
// 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;
}
//
// SMALL vs LARGE (A/B rig, 5.3.39): Mission carries both rasters.
// The Comm page's OWN row is the wide box and takes the large one
// -- the shipped page draws 'Aeolus' there in the tall face, and
// the small raster left it visibly undersized and low in the box.
// The seven strip slots keep the small raster: their boxes are
// half the height.
//
return large
? mission->GetLargeNameBitmap(player_bitmap_index)
: 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,
(currentSlot == 0) ? True : False); // slot 0 == the own row
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)
{
//
// SLOT 0 IS THE OWN ROW. Its layout entry is the odd one out
// ({180,225} at layout mode 0, KILLS/DEATHS at +0x95/+0xe8
// instead of the strip boxes' +0x48), and the shipped Comm page
// fills it with the LOCAL pilot -- name, kills, deaths -- while
// the seven surrounding boxes stay empty in a solo mission. So
// slot 0 takes the mission player directly; only slots 1..7 come
// from the pod roster.
//
pilot = (currentSlot == 0)
? local_player
: ResolveCommRosterPlayer(currentSlot);
if (pilot == NULL && currentSlot > 0)
{
built = True; // past the first empty roster slot -> rest empty
}
}
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.