Six TUs born in one wave (4-agent re-hosting workflow from the BT411 donors + inline integration): BTL4GAUG (16 widget primitives + 5 connections, the TU anchor @004c3f6c), BTL4GAU2 (13 composite panel clusters), BTL4GAU3 (PilotList/PlayerStatus/SectorDisplay/MessageBoard/mapping group), BTL4RDR (the MapDisplay radar vs the authentic surviving header), BTL4GRND (the real renderer: L4LampManager + L4Warehouse + BuildConfigurationFile with the 19-entry BT registry chained onto the engine table). btl4.lib grows 7 -> 11 members; all 50 game TUs compile; link clean. VERIFIED LIVE: with L4GAUGE=640x480x16 the interpreter parses the full authentic L4GAUGE.CFG, Bhk1Init runs, ColorMapperArmor binds the live mech's damage zones by name (cross-chassis names take the authentic inert path), and the complete mutual missile fight runs with the cockpit stack updating from real combat -- 16/16 rounds, 138 zone hits, zero faults. Gauges-off regressions untouched. Engine bring-up deviation (documented): ParseAttribute normalizes unpublished attribute names to a shared writable zero cell -- the 1995 GaugeConnectionDirectOf ctor hard-derefs its source (first boot crashed there); widgets on unpublished names draw static zeros and light up automatically as the attribute waves publish the 1995 CFG spellings the bindings already carry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1162 lines
35 KiB
C++
1162 lines
35 KiB
C++
//===========================================================================//
|
|
// File: btl4rdr.cpp //
|
|
// Project: BattleTech Brick: Gauge Renderer Manager //
|
|
// Contents: MapDisplay -- the cockpit radar / threat-map gauge. Draws a //
|
|
// top-down (X/Z) map centred on the operator mech, plots blips for //
|
|
// nearby static and moving entities, the player's view wedge, and //
|
|
// floating name labels for tracked mechs. //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// 09/13/95 CPB Initial coding. //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1994, Virtual World Entertainment, Inc. All rights reserved //
|
|
// PROPRIETARY and CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED (source410 re-host) from the shipped binary (BTL4OPT.EXE)
|
|
// against the SURVIVING header CODE\BT\BT_L4\BTL4RDR.HPP, whose member byte-
|
|
// offsets match the decompiled object exactly:
|
|
//
|
|
// nameArray[16] @0x090 backgroundColor @0x358
|
|
// offsetPosition @0x2D0 staticColor @0x35C
|
|
// currentScale @0x2D4 boxColor @0x360
|
|
// currentPositionPointer @0x2D8 halfWidth @0x364
|
|
// currentAngularPointer @0x2DC halfHeight @0x368
|
|
// currentCapabilitiesRatio@0x2E0 operating @0x36C
|
|
// LODIndex @0x2E4 rockAndRoll @0x370
|
|
// pixelsPerMeter @0x2E8 flashState @0x374
|
|
// metersPerPixel @0x2EC staticEntityList @0x378
|
|
// maximumRange @0x2F0 movingEntityList @0x394
|
|
// maximumDistanceSquared @0x2F4 previousDrawing @0x3B0
|
|
// xMin..zMax @0x2F8.. halfViewWidthInUnits@0x3C0
|
|
// viewingPosition @0x310 shadowTable @0x3C4
|
|
// viewHorizontalRotation @0x31C worldToView @0x324
|
|
// viewHalfHorizontalWidth @0x320
|
|
//
|
|
// The TU is CONFIRMED by the embedded assert path "d:\tesla\bt\bt_l4\
|
|
// BTL4RDR.CPP" (CalculateBounds line 0x224, DrawNames line 0x408).
|
|
//
|
|
// RE-HOST DELTAS vs the WinTesla-port donor (see GAUGE-BLOCK.NOTES.md,
|
|
// btl4rdr section):
|
|
// * the radar entity lists are fed by the AUTHENTIC interesting-entity
|
|
// notifications (BTL4GaugeRenderer::NotifyOfNewInterestingEntity ->
|
|
// GaugeRenderer movingEntities/staticEntities) -- the port-only
|
|
// RebuildEntityGrid call and the three visible-radius cull blocks are
|
|
// retired, as is the port's fallback mech cross-blip branch;
|
|
// * KEPT (correctness-load-bearing): the rotation-only-assignment-then-
|
|
// translation-row-LAST compose, the true affine Invert, and the
|
|
// YawPitchRoll (yaw-first) heading decode in DrawDisplay;
|
|
// * the torso-twist view-wedge feed reads Mech::CurrentTorsoTwist() through
|
|
// a file-private connection (the reconstructed Torso::currentTwist is
|
|
// protected -- no raw 1995 byte offsets);
|
|
// * the "pip" symbol system is the engine's L4GaugeImage, cached in
|
|
// L4Warehouse::gaugeImageBin (resource type 0x12), used directly.
|
|
//
|
|
|
|
#include <btl4.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(BTL4RDR_HPP)
|
|
# include <btl4rdr.hpp>
|
|
#endif
|
|
|
|
#if !defined(APP_HPP)
|
|
# include <app.hpp>
|
|
#endif
|
|
|
|
#if !defined(MISSION_HPP)
|
|
# include <mission.hpp>
|
|
#endif
|
|
|
|
#if !defined(PLAYER_HPP)
|
|
# include <player.hpp>
|
|
#endif
|
|
|
|
#if !defined(MECH_HPP)
|
|
# include <mech.hpp>
|
|
#endif
|
|
|
|
#if !defined(L4WRHOUS_HPP)
|
|
# include <l4wrhous.hpp>
|
|
#endif
|
|
|
|
//
|
|
// Tuning constants recovered from the CODE constant pool.
|
|
//
|
|
static const Scalar CalcBoundsEpsilon = 0.0f; // _DAT_004c2284
|
|
static const Scalar MetersPerPixelNum = 1.0f; // _DAT_004c2288
|
|
static const Scalar OperatingEpsilon = 0.0f; // _DAT_004c2174
|
|
static const Scalar HeadingHalf = 0.5f; // _DAT_004c2480 (half-angle)
|
|
static const Scalar ViewWedgeRadius = 10000.0f; // _DAT_004c25ac
|
|
static const Scalar MapVerticalSpan = 32768.0f; // 0x47000000 / 0xc7000000
|
|
|
|
//
|
|
// FUN_004c2f88 -- the fixed blip / name colour index used throughout the map.
|
|
// (Returns the literal 7 in the shipped binary.)
|
|
//
|
|
static int
|
|
MapBlipColor()
|
|
{
|
|
return 7;
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// File-local helpers
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//
|
|
// Entity placement helpers -- the entity keeps its world placement in
|
|
// localOrigin (linearPosition @+0x100 + angularPosition). These give the
|
|
// radar the Point3D position and AffineMatrix transform the draw loops use.
|
|
//
|
|
static inline Point3D &
|
|
EntityPosition(Entity *entity)
|
|
{
|
|
return entity->localOrigin.linearPosition;
|
|
}
|
|
|
|
static inline void
|
|
EntityTransform(AffineMatrix *out, Entity *entity)
|
|
{
|
|
*out = entity->localOrigin; // entity+0xD0
|
|
}
|
|
|
|
//
|
|
// FUN_00417ab4 -- the gauge renderer's "operator entity" (the mech whose
|
|
// cockpit this gauge belongs to): the renderer's linked-entity socket
|
|
// (renderer+0x40), armed by the viewpoint-bind broadcast before the per-model
|
|
// gauges are configured (BTL4APP.CPP:451 ConfigureForModel). Renderer::
|
|
// GetLinkedEntity has been public "for gauges" since 05/25/95.
|
|
//
|
|
static Entity *
|
|
ResolveOperatorEntity(GaugeRenderer *renderer)
|
|
{
|
|
return (renderer != NULL) ? renderer->GetLinkedEntity() : NULL;
|
|
}
|
|
|
|
//
|
|
// renderer+0x4C -- GaugeRenderer::warehousePointer: the L4Warehouse whose
|
|
// gaugeImageBin (+0x58) holds the radar "pip" vector shapes (L4GaugeImage,
|
|
// resource type 0x12; BTL4.RES ships 110 of them -- every mech / vehicle /
|
|
// building / tree).
|
|
//
|
|
static L4Warehouse *
|
|
GetGaugeWarehouse(GaugeRenderer *renderer)
|
|
{
|
|
return (renderer != NULL) ? (L4Warehouse *)renderer->warehousePointer : NULL;
|
|
}
|
|
|
|
//
|
|
// FUN_004688ce(warehouse+0x58, entity+0x1BC, 0) -- the refcounting bin Get,
|
|
// exactly as the binary's per-frame draw loops call it (one vector shape per
|
|
// model; the mission-end warehouse Purge reclaims the references).
|
|
//
|
|
static L4GaugeImage *
|
|
LookUpGaugeImage(
|
|
L4Warehouse *warehouse,
|
|
ResourceDescription::ResourceID resource_ID
|
|
)
|
|
{
|
|
return warehouse->gaugeImageBin.Get(resource_ID, False);
|
|
}
|
|
|
|
//
|
|
// The "global name-bitmap cache": the current Mission's SMALL name-bitmap
|
|
// chain (prebuilt 64x16 egg rasters keyed 1-based by the egg 'bitmapindex').
|
|
//
|
|
static BitMap *
|
|
LookUpNameBitmap(int name_ID)
|
|
{
|
|
Mission
|
|
*mission = (application != NULL)
|
|
? application->GetCurrentMission()
|
|
: NULL;
|
|
|
|
if (mission == NULL || name_ID <= 0)
|
|
{
|
|
return NULL;
|
|
}
|
|
return mission->GetSmallNameBitmap(name_ID);
|
|
}
|
|
|
|
//
|
|
// entity+0x21C -- Landmark::landmarkID (the static loop's label key into the
|
|
// LARGE name chain). DORMANT in shipped content: no runtime writer exists
|
|
// (the id was baked by the 1995 map tool) and no [landmarks] section exists
|
|
// in any egg -- kept faithful-but-INERT (no landmark ever labels).
|
|
//
|
|
static int
|
|
GetNameID(Entity * /*entity*/)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
//
|
|
// FUN_0041a1a4 vs 0x4e70c4 -- IsDerivedFrom(Landmark). Dormant with
|
|
// GetNameID above (no landmark content ships) -- INERT.
|
|
//
|
|
static Logical
|
|
IsLabelledEntity(Entity * /*entity*/)
|
|
{
|
|
return False;
|
|
}
|
|
|
|
//
|
|
// The operator's current target -- the owning BTPlayer's objectiveMech
|
|
// (binary: *(*(owner+0x190)+0x284)). INERT FEED: BTPlayer::objectiveMech
|
|
// exists in the reconstruction (btplayer.hpp) but is protected with no
|
|
// accessor reconstructed AND no writer yet (btplayer.cpp only NULLs it in the
|
|
// constructor), so the target always resolves NULL here -- blips and labels
|
|
// draw normally, the hot-box / flash highlight simply never engages. Wire
|
|
// through a BTPlayer accessor once the targeting wave reconstructs the
|
|
// objectiveMech writer.
|
|
//
|
|
static Entity *
|
|
GetTarget(Entity * /*owner*/)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// MapTwistConnection (file-private)
|
|
//
|
|
// The binary wired a direct-of-Radian gauge connection onto the mech's
|
|
// "Torso" sub-object horizontal rotation (Torso::currentTwist, torso+0x1D8;
|
|
// roster walk FUN_0041f98c) so the view wedge follows the torso twist.
|
|
// RE-HOST (databinding rule): currentTwist is protected on the reconstructed
|
|
// Torso, so this connection reads the documented accessor
|
|
// Mech::CurrentTorsoTwist() (mech.hpp -- NULL-safe: 0 with no torso
|
|
// subsystem, matching the binary's no-torso -> heading-0 behaviour) and
|
|
// copies it into MapDisplay::viewHorizontalRotation every frame, per the
|
|
// engine GaugeConnection contract (GAUGE.HPP:151).
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
class MapTwistConnection:
|
|
public GaugeConnection
|
|
{
|
|
public:
|
|
MapTwistConnection(
|
|
int parameter_ID,
|
|
Radian *data_destination,
|
|
Mech *twist_source
|
|
);
|
|
|
|
protected:
|
|
void
|
|
Update();
|
|
|
|
Mech
|
|
*twistSource;
|
|
Radian
|
|
*dataDestination;
|
|
};
|
|
|
|
MapTwistConnection::MapTwistConnection(
|
|
int parameter_ID,
|
|
Radian *data_destination,
|
|
Mech *twist_source
|
|
):
|
|
GaugeConnection(parameter_ID)
|
|
{
|
|
Check_Pointer(this);
|
|
Check_Pointer(data_destination);
|
|
Check_Pointer(twist_source);
|
|
|
|
dataDestination = data_destination;
|
|
twistSource = twist_source;
|
|
|
|
*dataDestination = twistSource->CurrentTorsoTwist();
|
|
Check(this);
|
|
}
|
|
|
|
void
|
|
MapTwistConnection::Update()
|
|
{
|
|
Check(this);
|
|
Check_Pointer(dataDestination);
|
|
Check_Pointer(twistSource);
|
|
|
|
*dataDestination = twistSource->CurrentTorsoTwist();
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// MapName
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//
|
|
// @004c19fc -- snapshot a tracked entity into one name-array slot: project
|
|
// its world position into the (already scaled) view space, look up its name
|
|
// bitmap from the mission's name cache, and flag whether it is the hot-boxed
|
|
// target.
|
|
//
|
|
void
|
|
MapName::ExtractFromEntity(
|
|
Entity *entity,
|
|
AffineMatrix &worldToView,
|
|
Entity *exclude_entity,
|
|
Entity *hotbox_entity
|
|
)
|
|
{
|
|
//
|
|
// The operator's own mech (exclude_entity) draws no name. A NULL entity
|
|
// (the binary's Verify(False) count/iterator-mismatch path, which
|
|
// compiles out at DEBUG_LEVEL 0) would crash EntityPosition(NULL) below
|
|
// -- guard it.
|
|
//
|
|
if (entity == NULL || entity == exclude_entity)
|
|
{
|
|
nameBitmap = NULL; // param_1[6] = 0
|
|
return;
|
|
}
|
|
|
|
Player
|
|
*owning_player = entity->GetOwningPlayer(); // *(entity+0x190)
|
|
|
|
if (owning_player == NULL)
|
|
{
|
|
nameBitmap = NULL; // unowned (dummy/effect): no label
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// player+0x1E0 playerBitmapIndex -> the Mission's SMALL name-raster
|
|
// chain (prebuilt 64x16 egg bitmaps, loaded 1-based by the mission's
|
|
// name-bitmap loader).
|
|
//
|
|
nameBitmap = LookUpNameBitmap(owning_player->playerBitmapIndex);
|
|
}
|
|
|
|
//
|
|
// Project the entity origin into screen space and stamp the label.
|
|
//
|
|
center.Multiply(EntityPosition(entity), worldToView); // FUN_00408b98
|
|
color = MapBlipColor(); // 7
|
|
hotBoxed = (Logical)(entity == hotbox_entity);
|
|
|
|
//
|
|
// The decompiled object always parks the name's pointer direction at
|
|
// (-1, 0, 0) -- labels are screen-aligned, not entity-aligned.
|
|
//
|
|
direction.x = -1.0f;
|
|
direction.y = 0.0f;
|
|
direction.z = 0.0f;
|
|
}
|
|
|
|
//
|
|
// @004c1ab0 -- blit one extracted name bitmap at its projected screen point,
|
|
// in either its own colour or (when hot-boxed) the supplied highlight colour.
|
|
//
|
|
void
|
|
MapName::Draw(
|
|
GraphicsView *graphics_view,
|
|
int hotbox_color
|
|
)
|
|
{
|
|
if (nameBitmap == NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
graphics_view->MoveToAbsolute(Round(center.x), Round(center.z)); // vtbl+0x24
|
|
|
|
if (!hotBoxed)
|
|
{
|
|
graphics_view->SetColor(color); // vtbl+0x18
|
|
}
|
|
else
|
|
{
|
|
graphics_view->SetColor(hotbox_color);
|
|
}
|
|
|
|
graphics_view->DrawBitMap( // vtbl+0x54
|
|
0, nameBitmap, 0, 0,
|
|
nameBitmap->Data.Size.x - 1,
|
|
nameBitmap->Data.Size.y - 1
|
|
);
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// MapDisplay
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//#############################################################################
|
|
// Method Description + Make -- the config "map" primitive registration glue.
|
|
//
|
|
// NOT in the assert-anchored decomp (the ctor @004c1c24 has no exported
|
|
// caller and the "map" string was not captured), so this is reconstructed
|
|
// from the config invocation (L4GAUGE.CFG:4923) + the ctor signature. The
|
|
// offset_position keyword "center"/"bottom" is typed as a STRING and
|
|
// converted in Make.
|
|
//
|
|
MethodDescription
|
|
MapDisplay::methodDescription =
|
|
{
|
|
"map",
|
|
MapDisplay::Make,
|
|
{
|
|
// map( rate, mode, (w,h), offsetPos, viewDeg, bg,static,hotbox,
|
|
// maxRange, scaleAttr, positionAttr, angleAttr, capabilitiesAttr )
|
|
{ ParameterDescription::typeRate, NULL }, // rate ID
|
|
{ ParameterDescription::typeModeMask, NULL }, // mode mask
|
|
{ ParameterDescription::typeVector, NULL }, // (width,height)
|
|
{ ParameterDescription::typeString, NULL }, // offset position ("center"/"bottom")
|
|
{ ParameterDescription::typeInteger, NULL }, // view width (degrees)
|
|
{ ParameterDescription::typeColor, NULL }, // background colour
|
|
{ ParameterDescription::typeColor, NULL }, // static/default colour
|
|
{ ParameterDescription::typeColor, NULL }, // hotbox colour
|
|
{ ParameterDescription::typeScalar, NULL }, // maximum range (metres)
|
|
{ ParameterDescription::typeAttribute, NULL }, // scale (RadarRange)
|
|
{ ParameterDescription::typeAttribute, NULL }, // position (RadarLinearPosition, Point3D*)
|
|
{ ParameterDescription::typeAttribute, NULL }, // angle (RadarAngularPosition, Quaternion*)
|
|
{ ParameterDescription::typeAttribute, NULL }, // capabilities (Avionics/RadarPercent)
|
|
PARAMETER_DESCRIPTION_END
|
|
}
|
|
};
|
|
|
|
Logical
|
|
MapDisplay::Make(
|
|
int display_port_index,
|
|
Vector2DOf<int> position,
|
|
Entity *entity,
|
|
GaugeRenderer *gauge_renderer
|
|
)
|
|
{
|
|
ParameterDescription
|
|
*parameterList = methodDescription.parameterList;
|
|
|
|
# if DEBUG_LEVEL > 0
|
|
parameterList[0].CheckIt(ParameterDescription::typeRate); // rate ID
|
|
parameterList[1].CheckIt(ParameterDescription::typeModeMask); // mode mask
|
|
parameterList[2].CheckIt(ParameterDescription::typeVector); // size
|
|
parameterList[3].CheckIt(ParameterDescription::typeString); // offset pos
|
|
parameterList[4].CheckIt(ParameterDescription::typeInteger); // view width
|
|
parameterList[5].CheckIt(ParameterDescription::typeColor); // background
|
|
parameterList[6].CheckIt(ParameterDescription::typeColor); // static
|
|
parameterList[7].CheckIt(ParameterDescription::typeColor); // hotbox
|
|
parameterList[8].CheckIt(ParameterDescription::typeScalar); // max range
|
|
parameterList[9].CheckIt(ParameterDescription::typeAttribute); // scale
|
|
parameterList[10].CheckIt(ParameterDescription::typeAttribute); // position
|
|
parameterList[11].CheckIt(ParameterDescription::typeAttribute); // angle
|
|
parameterList[12].CheckIt(ParameterDescription::typeAttribute); // capabilities
|
|
# endif
|
|
|
|
Check(gauge_renderer);
|
|
|
|
OffsetPosition
|
|
offset =
|
|
(strcmp(parameterList[3].data.string, "bottom") == 0)
|
|
? bottom
|
|
: center;
|
|
|
|
# if DEBUG_LEVEL > 0
|
|
MapDisplay
|
|
*map_display =
|
|
# endif
|
|
new MapDisplay(
|
|
parameterList[0].data.rate,
|
|
parameterList[1].data.modeMask,
|
|
(L4GaugeRenderer *) gauge_renderer,
|
|
0, // owner_ID
|
|
display_port_index,
|
|
position.x, position.y, // left, bottom
|
|
position.x + parameterList[2].data.vector.x, // right
|
|
position.y + parameterList[2].data.vector.y, // top
|
|
offset,
|
|
parameterList[4].data.integer, // view width (degrees)
|
|
parameterList[5].data.color, // background colour
|
|
parameterList[6].data.color, // static colour
|
|
parameterList[7].data.color, // hotbox colour
|
|
parameterList[8].data.scalar, // maximum range
|
|
entity,
|
|
(Scalar *) parameterList[9].data.attributePointer, // scale (RadarRange)
|
|
(Point3D **) parameterList[10].data.attributePointer, // position (RadarLinearPosition)
|
|
(Quaternion **) parameterList[11].data.attributePointer, // angle (RadarAngularPosition)
|
|
(Scalar *) parameterList[12].data.attributePointer // capabilities (Avionics/RadarPercent)
|
|
|
|
// HACK!!!! The attribute casts are VERY dangerous! (The native
|
|
// idiom -- see NumericDisplayScalar::Make, L4GAUGE.CPP.)
|
|
);
|
|
Register_Object(map_display);
|
|
|
|
return True;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Construction / Destruction
|
|
//
|
|
// @004c1c24 -- vtable 0051422c, base name "MapDisplay". nameArray, the two
|
|
// entity lists and previousDrawing are member objects (the decompiler's
|
|
// explicit Construct_Array / ctor thunks are the compiler-generated member
|
|
// construction). Sizes the graphics port from (left,bottom,right,top),
|
|
// allocates the radar shadow table, and wires the four data-source gauge
|
|
// connections (scale, position, angle, capabilities) plus an optional
|
|
// torso-twist feed.
|
|
//
|
|
MapDisplay::MapDisplay(
|
|
GaugeRate rate,
|
|
ModeMask mode_mask,
|
|
L4GaugeRenderer *renderer,
|
|
unsigned int owner_ID,
|
|
int graphics_port_number,
|
|
int left,
|
|
int bottom,
|
|
int right,
|
|
int top,
|
|
OffsetPosition offset_position,
|
|
int view_width_in_degrees,
|
|
int bg_color,
|
|
int static_color,
|
|
int hotbox_color,
|
|
Scalar maximum_range,
|
|
Entity *entity,
|
|
Scalar *scale_value_pointer,
|
|
Point3D **position_pointer,
|
|
Quaternion **angle_pointer,
|
|
Scalar *capabilities_ratio,
|
|
Logical allow_rock_and_roll
|
|
):
|
|
GraphicGauge( // FUN_00444818
|
|
rate, mode_mask, renderer, owner_ID, graphics_port_number, "MapDisplay"
|
|
)
|
|
{
|
|
//
|
|
// Size the graphics port and centre the origin.
|
|
//
|
|
localView.SetPositionWithinPort(left, bottom, right, top); // vtbl+0x08
|
|
if (offset_position == MapDisplay::bottom)
|
|
{
|
|
localView.SetOrigin((right - left) >> 1, 0); // vtbl+0x10
|
|
}
|
|
else
|
|
{
|
|
localView.SetOrigin((right - left) >> 1, (top - bottom) >> 1);
|
|
}
|
|
|
|
backgroundColor = bg_color; // @0x358
|
|
staticColor = static_color; // @0x35C
|
|
boxColor = hotbox_color; // @0x360
|
|
maximumRange = maximum_range; // @0x2F0
|
|
rockAndRoll = allow_rock_and_roll; // @0x370
|
|
|
|
halfWidth = (right - left) >> 1; // @0x364
|
|
halfHeight = (top - bottom) >> 1; // @0x368
|
|
offsetPosition = offset_position; // @0x2D0
|
|
|
|
//
|
|
// The radar shadow table: the binary allocates halfViewWidthInUnits*8
|
|
// bytes = one ShadowRecord per half-degree column across the full view
|
|
// width. Never populated in the shipped build (BuildRadarShadow below
|
|
// compiled empty) -- allocated for fidelity.
|
|
//
|
|
halfViewWidthInUnits = view_width_in_degrees >> 1; // @0x3C0
|
|
shadowTable = new ShadowRecord[halfViewWidthInUnits * 2]; // @0x3C4
|
|
|
|
viewHorizontalRotation = 0.0f; // @0x31C
|
|
viewHalfHorizontalWidth = 0.3926991f; // @0x320 = PI/8
|
|
flashState = False; // @0x374
|
|
|
|
//
|
|
// Data-source connections: each is a small heap GaugeConnection wired so
|
|
// that the gauge framework copies the external value into a member field
|
|
// every frame. scale -> currentScale, position -> currentPositionPointer,
|
|
// angle -> currentAngularPointer, capabilities -> currentCapabilitiesRatio.
|
|
//
|
|
AddConnection( // (*this+0x34)
|
|
new GaugeConnectionDirectOf<Scalar>(
|
|
0, ¤tScale, scale_value_pointer)); // FUN_00474855
|
|
AddConnection(
|
|
new GaugeConnectionDirectOf<Point3D*>(
|
|
0, ¤tPositionPointer, position_pointer)); // FUN_004c2a3e
|
|
AddConnection(
|
|
new GaugeConnectionDirectOf<Quaternion*>(
|
|
0, ¤tAngularPointer, angle_pointer)); // FUN_004c2bc0
|
|
AddConnection(
|
|
new GaugeConnectionDirectOf<Scalar>(
|
|
0, ¤tCapabilitiesRatio, capabilities_ratio)); // FUN_00474855
|
|
|
|
//
|
|
// If the host entity is a mech (MechClassID 0xBB9), feed its live torso
|
|
// twist into the map's view heading so the view wedge follows the torso.
|
|
// Binary: "Torso" roster walk (FUN_0041f98c) + direct-of-Radian
|
|
// connection onto torso+0x1D8 (FUN_004c2d42); re-host: the accessor-based
|
|
// MapTwistConnection above (see its comment banner).
|
|
//
|
|
if (entity->GetClassID() == (Entity::ClassID)MechClassID)
|
|
{
|
|
AddConnection(
|
|
new MapTwistConnection(
|
|
0, &viewHorizontalRotation, Cast_Object(Mech *, entity)));
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004c1ea8 -- free the shadow table. previousDrawing, the two entity lists
|
|
// and the name array are member objects and are destroyed automatically
|
|
// after this body runs.
|
|
//
|
|
MapDisplay::~MapDisplay()
|
|
{
|
|
delete[] shadowTable; // FUN_004022E8(this[0xF1])
|
|
// base ~GraphicGauge -- FUN_00444870
|
|
}
|
|
|
|
//
|
|
// @004c1f30
|
|
//
|
|
Logical
|
|
MapDisplay::TestInstance() const
|
|
{
|
|
return GraphicGauge::TestInstance(); // FUN_004448ac
|
|
}
|
|
|
|
//
|
|
// @004c1f40 -- emit "ThreatIndicator:" then chain to the base ShowInstance
|
|
// with a deeper (tab) indent.
|
|
//
|
|
void
|
|
MapDisplay::ShowInstance(char *indent)
|
|
{
|
|
cout << indent << "ThreatIndicator:"; // FUN_004dbb24 x2
|
|
|
|
char
|
|
deeper[80];
|
|
strcpy(deeper, indent);
|
|
strcat(deeper, "\t"); // FUN_004d49b8
|
|
|
|
GraphicGauge::ShowInstance(deeper); // FUN_004448bc
|
|
}
|
|
|
|
//
|
|
// @004c1fc0 -- reset the per-frame phase counter when the gauge is shown.
|
|
//
|
|
void
|
|
MapDisplay::BecameActive()
|
|
{
|
|
operatingPhase = 0; // @0x354
|
|
}
|
|
|
|
//
|
|
// @004c1fd0 -- the per-frame work, spread across four ticks (phases 0..3) so
|
|
// the cost is amortised:
|
|
// 0 decide whether the radar is powered, compute range, set up bounds
|
|
// 1 spatial-query static (terrain/structure) entities into the list
|
|
// 2 spatial-query moving (mech/vehicle) entities into the list
|
|
// 3 erase the previous frame and redraw the map
|
|
//
|
|
// The renderer's movingEntities / staticEntities records queried in phases
|
|
// 1/2 are populated by the AUTHENTIC interesting-entity notifications
|
|
// (BTL4GaugeRenderer::NotifyOfNewInterestingEntity, btl4grnd.cpp -- Mover ->
|
|
// movingEntities, Terrain -> staticEntities, gated on the entity owning a
|
|
// type-0x12 gauge-image resource). Note the authentic grid quirks the range
|
|
// cull below compensates for: GaugeEntityList::CopyEntitiesWithinBounds's
|
|
// per-entity box test is commented out in GAUGMAP.CPP (grid-cell granular
|
|
// only), so this gauge's maximumDistanceSquared test in DrawStatic/DrawMoving
|
|
// is the REAL range filter.
|
|
//
|
|
void
|
|
MapDisplay::Execute()
|
|
{
|
|
int
|
|
phase = operatingPhase++; // @0x354
|
|
|
|
if (phase == 0)
|
|
{
|
|
operating = (Logical)(OperatingEpsilon < currentCapabilitiesRatio);
|
|
if (operating)
|
|
{
|
|
maximumDistanceSquared = currentCapabilitiesRatio * maximumRange;
|
|
maximumDistanceSquared = maximumDistanceSquared * maximumDistanceSquared;
|
|
CalculateBounds(); // FUN_004c2178
|
|
}
|
|
}
|
|
else if (phase == 1)
|
|
{
|
|
staticEntityList.Clear(); // FUN_004435ac(this+0x378)
|
|
if (operating)
|
|
{
|
|
renderer->GetStaticEntitiesWithinBounds( // staticEntities @+0xB0
|
|
&staticEntityList,
|
|
xMin, yMin, zMin, xMax, yMax, zMax);
|
|
}
|
|
}
|
|
else if (phase == 2)
|
|
{
|
|
movingEntityList.Clear(); // FUN_004435ac(this+0x394)
|
|
if (operating)
|
|
{
|
|
renderer->GetMovingEntitiesWithinBounds( // movingEntities @+0x94
|
|
&movingEntityList,
|
|
xMin, yMin, zMin, xMax, yMax, zMax);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (phase == 3)
|
|
{
|
|
EraseDisplay(); // FUN_004c2294
|
|
if (operating)
|
|
{
|
|
DrawDisplay(); // FUN_004c22c4
|
|
}
|
|
|
|
//
|
|
// Re-host verification trace (worksheet open question: does the
|
|
// authentic NotifyOf*InterestingEntity feed populate the radar
|
|
// lists once RebuildEntityGrid is retired?). Logs only when the
|
|
// gathered counts change.
|
|
//
|
|
if (getenv("BT_MECH_LOG") != NULL)
|
|
{
|
|
static int previousStaticCount = -1;
|
|
static int previousMovingCount = -1;
|
|
|
|
int static_count = staticEntityList.NumberOfItems();
|
|
int moving_count = movingEntityList.NumberOfItems();
|
|
if (static_count != previousStaticCount
|
|
|| moving_count != previousMovingCount)
|
|
{
|
|
previousStaticCount = static_count;
|
|
previousMovingCount = moving_count;
|
|
DEBUG_STREAM << "MapDisplay: operating=" << (int)operating
|
|
<< " static=" << static_count
|
|
<< " moving=" << moving_count << endl << flush;
|
|
}
|
|
}
|
|
}
|
|
operatingPhase = 0;
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004c2178 -- CONFIRMED by the embedded assert path
|
|
// "d:\tesla\bt\bt_l4\BTL4RDR.CPP", line 0x224.
|
|
//
|
|
// Derive the metres<->pixels scale from the current zoom (currentScale) and
|
|
// build the world-space axis-aligned box that the spatial queries will use.
|
|
// The map is top-down, so the X and Z bounds hug the viewing position while
|
|
// the Y (vertical) bounds are opened wide (+/-32768).
|
|
//
|
|
void
|
|
MapDisplay::CalculateBounds()
|
|
{
|
|
if (currentScale <= CalcBoundsEpsilon)
|
|
{
|
|
//
|
|
// "MapDisplay::CalculateBounds detected a bad scale"
|
|
// ("d:\tesla\bt\bt_l4\BTL4RDR.CPP", line 0x224)
|
|
//
|
|
Verify(False); // FUN_0040385c
|
|
currentScale = 1.0f; // 0x3f800000
|
|
}
|
|
|
|
pixelsPerMeter = (Scalar)(halfWidth * 2) / currentScale;
|
|
metersPerPixel = MetersPerPixelNum / pixelsPerMeter; // 1.0 / ppm
|
|
LODIndex = metersPerPixel;
|
|
|
|
Scalar
|
|
halfWorldWidth = (Scalar)halfWidth / pixelsPerMeter;
|
|
Scalar
|
|
halfWorldHeight = (Scalar)halfHeight / pixelsPerMeter;
|
|
|
|
Entity
|
|
*owner = ResolveOperatorEntity(renderer); // FUN_00417ab4(renderer+0x40)
|
|
Check_Pointer(owner);
|
|
viewingPosition = EntityPosition(owner); // FUN_00408440(this+0x310)
|
|
|
|
xMin = viewingPosition.x - halfWorldWidth;
|
|
xMax = viewingPosition.x + halfWorldWidth;
|
|
yMin = -MapVerticalSpan; // 0xc7000000
|
|
yMax = MapVerticalSpan; // 0x47000000
|
|
zMin = viewingPosition.z - halfWorldHeight;
|
|
zMax = viewingPosition.z + halfWorldHeight;
|
|
}
|
|
|
|
//
|
|
// @004c2294 -- erase last frame's footprint and forget it.
|
|
//
|
|
void
|
|
MapDisplay::EraseDisplay()
|
|
{
|
|
previousDrawing.Draw(&localView, backgroundColor); // FUN_0044a650
|
|
previousDrawing.Clear(); // FUN_0044a630
|
|
}
|
|
|
|
//
|
|
// @004c22c4 -- compose the world->view matrix (translate to the viewing
|
|
// position, optionally rotate by heading, then scale to pixels), then paint
|
|
// the wedge, the static and moving blips and the names, recording the
|
|
// touched region for next frame's erase.
|
|
//
|
|
void
|
|
MapDisplay::DrawDisplay()
|
|
{
|
|
AffineMatrix
|
|
view;
|
|
view.BuildIdentity(); // FUN_0040aadc (the decomp shows the
|
|
view.BuildIdentity(); // identity built twice -- faithful)
|
|
|
|
//
|
|
// Default the centre to the operator mech if no explicit feed is wired.
|
|
//
|
|
Entity
|
|
*owner = ResolveOperatorEntity(renderer);
|
|
if (currentPositionPointer == NULL && owner != NULL)
|
|
{
|
|
currentPositionPointer = &EntityPosition(owner); // owner+0x100
|
|
}
|
|
|
|
if (currentPositionPointer != NULL)
|
|
{
|
|
Point3D
|
|
center = *currentPositionPointer; // FUN_00408440
|
|
|
|
//
|
|
// TRANSCRIPTION FIX (the invisible-pip bug): FUN_0040adec writes ONLY
|
|
// the 3x3 rotation elements ([0..2],[4..6],[8..10]) -- it never
|
|
// touches the translation row. The binary therefore ends with
|
|
// view = [R(q) | center]: rotation SET, centre PRESERVED. A full
|
|
// compose (view *= yaw) would also rotate the translation
|
|
// (view = [R | center x R]) -- the subsequent Invert then maps
|
|
// center x R to the origin instead of the viewer, so every blip lands
|
|
// hundreds of pixels off-scope. Rotation first (rotation-only
|
|
// assignment), translation row LAST.
|
|
//
|
|
if (currentAngularPointer != NULL)
|
|
{
|
|
if (!rockAndRoll)
|
|
{
|
|
//
|
|
// Top-down: keep heading only. Extract the yaw, build a
|
|
// yaw-only quaternion, SET the rotation block from it.
|
|
//
|
|
// FIX (kept on re-host): YawPitchRoll (yaw-first), not
|
|
// EulerAngles -- the EulerAngles(Quaternion) decomposition is
|
|
// ambiguous for a yawing mech: as yaw sweeps past +/-pi the
|
|
// quaternion double-cover flips it onto a pitch=roll=pi
|
|
// branch, so euler.yaw REVERSES (the radar spins back).
|
|
// YawPitchRoll applies yaw FIRST -> a clean continuous 360
|
|
// degree heading (ROTATION.HPP:162, operator= at :190).
|
|
//
|
|
YawPitchRoll
|
|
euler;
|
|
euler = *currentAngularPointer;
|
|
Scalar
|
|
heading = (Scalar)euler.yaw * HeadingHalf; // * 0.5f half-angle
|
|
|
|
SinCosPair
|
|
sc;
|
|
sc = Radian(heading); // FUN_00408328
|
|
Quaternion
|
|
yaw(0.0f, sc.sine, 0.0f, sc.cosine); // FUN_00409948
|
|
view = yaw; // rotation-only (FUN_0040adec)
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// "Rock and roll": use the full orientation.
|
|
//
|
|
view = *currentAngularPointer; // rotation-only build
|
|
}
|
|
}
|
|
view.SetFromAxis(W_Axis, center); // FUN_0040b0ac -- translation LAST
|
|
}
|
|
|
|
//
|
|
// TRANSCRIPTION FIX (the invisible-pip bug): FUN_0040b244 is the full
|
|
// affine INVERSE (cofactor expansion + determinant divide), not a copy.
|
|
// `view` above is the OPERATOR's local->world pose (translate to the mech
|
|
// + yaw); the radar needs its inverse -- world->scope, centre subtracted,
|
|
// heading unrotated. Read as a copy, every pip/name transforms to
|
|
// (world + centre) x ppm = thousands of pixels off-scope: the scope draws
|
|
// empty at all times.
|
|
//
|
|
worldToView.Invert(view); // FUN_0040b244(this+0x324)
|
|
|
|
//
|
|
// Bake the metres->pixels scale into the matrix.
|
|
//
|
|
Vector3D
|
|
scale;
|
|
scale.x = scale.y = scale.z = pixelsPerMeter;
|
|
worldToView.Multiply(worldToView, scale); // FUN_0040b374
|
|
|
|
BuildRadarShadow(worldToView); // FUN_004c228c
|
|
|
|
flashState = (Logical)!flashState; // @0x374 ^= 1
|
|
|
|
localView.AttachRecorder(&previousDrawing); // vtbl+0x64
|
|
DrawViewWedge(); // FUN_004c2484
|
|
DrawStatic(worldToView); // FUN_004c25b4
|
|
DrawMoving(worldToView); // FUN_004c2788
|
|
DrawNames(worldToView); // FUN_004c28c4
|
|
localView.DetachRecorder(); // vtbl+0x68
|
|
}
|
|
|
|
//
|
|
// @004c228c -- BuildRadarShadow. In the shipped build this compiled to an
|
|
// empty body (the shadow-cast pass is disabled), so the allocated shadowTable
|
|
// is never populated. Reconstructed as a no-op to match the binary.
|
|
// (BEST-EFFORT: the intended algorithm -- ray-marching the shadowTable per
|
|
// half-degree column -- is not present in the captured object.)
|
|
//
|
|
void
|
|
MapDisplay::BuildRadarShadow(AffineMatrix & /*worldToView*/)
|
|
{
|
|
}
|
|
|
|
//
|
|
// @004c2484 -- draw the two edges of the player's field-of-view wedge as
|
|
// lines from the map centre out to a far (clipped) radius.
|
|
//
|
|
void
|
|
MapDisplay::DrawViewWedge()
|
|
{
|
|
int
|
|
i;
|
|
|
|
Radian
|
|
leftEdge = viewHorizontalRotation - viewHalfHorizontalWidth;
|
|
Radian
|
|
rightEdge = viewHorizontalRotation + viewHalfHorizontalWidth;
|
|
|
|
localView.SetColor(staticColor); // vtbl+0x18
|
|
|
|
Radian
|
|
edges[2];
|
|
edges[0] = leftEdge;
|
|
edges[1] = rightEdge;
|
|
|
|
for (i = 0; i < 2; ++i)
|
|
{
|
|
Scalar
|
|
angle = -edges[i];
|
|
SinCosPair
|
|
sc;
|
|
sc = Radian(angle); // FUN_00408328
|
|
|
|
Scalar
|
|
x = sc.sine * ViewWedgeRadius; // * 10000.0f
|
|
Scalar
|
|
z = sc.cosine * ViewWedgeRadius;
|
|
|
|
localView.MoveToAbsolute(0, 0); // vtbl+0x24 (centre)
|
|
localView.DrawLineToAbsolute(Round(x), Round(z)); // vtbl+0x30
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004c25b4 -- plot every in-range static entity as a blip, plus a name
|
|
// bitmap for the special (labelled) ones.
|
|
//
|
|
void
|
|
MapDisplay::DrawStatic(AffineMatrix &worldToView)
|
|
{
|
|
ChainIteratorOf<Entity*>
|
|
iter(staticEntityList.GetInstanceList()); // FUN_0042ac1c(this+0x384)
|
|
iter.First();
|
|
|
|
L4Warehouse
|
|
*pips = GetGaugeWarehouse(renderer); // *(renderer+0x4C)
|
|
|
|
for (;;)
|
|
{
|
|
Entity
|
|
*entity = iter.ReadAndNext(); // vtbl+0x28
|
|
if (entity == NULL)
|
|
{
|
|
break;
|
|
}
|
|
|
|
//
|
|
// The REAL range filter (the grid query is cell-granular only --
|
|
// see the Execute banner).
|
|
//
|
|
Vector3D
|
|
delta;
|
|
delta.Subtract(EntityPosition(entity), viewingPosition); // FUN_00408644
|
|
if (delta.x*delta.x + delta.y*delta.y + delta.z*delta.z
|
|
> maximumDistanceSquared)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
L4GaugeImage
|
|
*pip = (pips != NULL)
|
|
? LookUpGaugeImage(pips, entity->GetResourceID()) // FUN_004688ce
|
|
: NULL;
|
|
if (pip != NULL)
|
|
{
|
|
AffineMatrix
|
|
blipXform;
|
|
AffineMatrix
|
|
entityXform;
|
|
EntityTransform(&entityXform, entity);
|
|
blipXform.Multiply(entityXform, worldToView); // FUN_0040b104
|
|
pip->Draw( // FUN_0046f0c0
|
|
LODIndex, metersPerPixel, &localView,
|
|
staticColor, blipXform, 0);
|
|
}
|
|
|
|
//
|
|
// Labelled static entities (e.g. nav beacons) also blit a name
|
|
// bitmap. (Dormant: no landmark content ships -- see the inert
|
|
// helpers above.)
|
|
//
|
|
if (IsLabelledEntity(entity)) // FUN_0041a1a4 (Landmark 0x4e70c4)
|
|
{
|
|
int
|
|
nameID = GetNameID(entity); // entity+0x21C
|
|
BitMap
|
|
*bitmap = LookUpNameBitmap(nameID);
|
|
if (bitmap != NULL)
|
|
{
|
|
Point3D
|
|
screen;
|
|
screen.Multiply(EntityPosition(entity), worldToView); // FUN_00408b98
|
|
localView.MoveToAbsolute(Round(screen.x), Round(screen.z));
|
|
localView.SetColor(MapBlipColor()); // 7
|
|
localView.DrawBitMap(0, bitmap, 0, 0,
|
|
bitmap->Data.Size.x - 1, bitmap->Data.Size.y - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004c2788 -- plot every in-range moving entity (mechs, vehicles) as a
|
|
// blip, skipping the operator's own mech and boxing the current target.
|
|
//
|
|
void
|
|
MapDisplay::DrawMoving(AffineMatrix &worldToView)
|
|
{
|
|
ChainIteratorOf<Entity*>
|
|
iter(movingEntityList.GetInstanceList()); // FUN_0042ac1c(this+0x3A0)
|
|
iter.First();
|
|
|
|
L4Warehouse
|
|
*pips = GetGaugeWarehouse(renderer); // *(renderer+0x4C)
|
|
Entity
|
|
*owner = ResolveOperatorEntity(renderer);
|
|
Entity
|
|
*target = GetTarget(owner); // objectiveMech (inert feed)
|
|
|
|
for (;;)
|
|
{
|
|
Entity
|
|
*entity = iter.ReadAndNext();
|
|
if (entity == NULL)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (entity == owner)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector3D
|
|
delta;
|
|
delta.Subtract(EntityPosition(entity), viewingPosition); // FUN_00408644
|
|
if (delta.x*delta.x + delta.y*delta.y + delta.z*delta.z
|
|
> maximumDistanceSquared)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
L4GaugeImage
|
|
*pip = (pips != NULL)
|
|
? LookUpGaugeImage(pips, entity->GetResourceID())
|
|
: NULL;
|
|
if (pip != NULL)
|
|
{
|
|
AffineMatrix
|
|
blipXform;
|
|
AffineMatrix
|
|
entityXform;
|
|
EntityTransform(&entityXform, entity);
|
|
blipXform.Multiply(entityXform, worldToView); // FUN_0040b104
|
|
|
|
int
|
|
boxColour = (entity == target) ? boxColor : 0; // highlight target
|
|
pip->Draw( // FUN_0046f0c0
|
|
LODIndex, metersPerPixel, &localView,
|
|
MapBlipColor() /*7*/, blipXform, boxColour);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004c28c4 -- CONFIRMED by the embedded assert path
|
|
// "d:\tesla\bt\bt_l4\BTL4RDR.CPP", line 0x408.
|
|
//
|
|
// Snapshot up to maximumNames moving entities into the name array, then draw
|
|
// their labels. The hot-box colour flashes (flashState) on the target.
|
|
//
|
|
void
|
|
MapDisplay::DrawNames(AffineMatrix &worldToView)
|
|
{
|
|
int
|
|
i;
|
|
|
|
Entity
|
|
*owner = ResolveOperatorEntity(renderer);
|
|
Entity
|
|
*target = GetTarget(owner); // objectiveMech (inert feed)
|
|
|
|
ChainIteratorOf<Entity*>
|
|
iter(movingEntityList.GetInstanceList()); // FUN_0042ac1c(this+0x3A0)
|
|
iter.First();
|
|
|
|
int
|
|
count = iter.GetSize(); // vtbl+0x14
|
|
if (count < 1)
|
|
{
|
|
return;
|
|
}
|
|
if (count > maximumNames)
|
|
{
|
|
count = maximumNames;
|
|
}
|
|
|
|
//
|
|
// First pass: extract.
|
|
//
|
|
for (i = 0; i < count; ++i)
|
|
{
|
|
Entity
|
|
*entity = iter.ReadAndNext(); // vtbl+0x28
|
|
if (entity == NULL)
|
|
{
|
|
//
|
|
// "MapDisplay::DrawNames -- GetSize() / iterator mismatch"
|
|
// ("d:\tesla\bt\bt_l4\BTL4RDR.CPP", line 0x408)
|
|
//
|
|
Verify(False); // FUN_0040385c
|
|
}
|
|
nameArray[i].ExtractFromEntity(entity, worldToView, owner, target); // FUN_004c19fc
|
|
}
|
|
|
|
//
|
|
// Second pass: draw. The target's label flashes in boxColor.
|
|
//
|
|
int
|
|
hotboxColor = flashState ? boxColor : 0;
|
|
for (i = 0; i < count; ++i)
|
|
{
|
|
nameArray[i].Draw(&localView, hotboxColor); // FUN_004c1ab0
|
|
}
|
|
}
|