The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified: COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity per contact and ProcessCollisionList walks EVERY touched solid per frame; with 2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy). Fix: frameEntryWorldVelocity restore per contact (damage always priced at the real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry. Gotcha #16 (engine-facility drift class). MISSILES: peer-visible salvos (the launcher record extension carries a salvo counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone never serialized), the authentic arc (authored MuzzleVelocity vector + the Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78), world-impact bursts (rounds detonate on cave geometry instead of phasing through), contact-only damage (flight-cap expiry = fizzle, no more teleport damage), live re-lead, and ballistic (unguided) shells for autocannons. projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6 corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded. RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244 is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation (never the translation); worldToView now Invert(view) built rotation-first. CulturalIcons sorted out of the moving grid (the phantom red pips were map props), visible-radius culls on all three draw passes, live pip verified at |delta| x ppm px. Gotcha #17 (verify the FUN_ body, not its call shape). WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X lamps, the panel look, the children enable and the ready-lamp gate (which had NEVER executed). Polarity chain corrected end-to-end (failedState, fed by real damage saturation). Root cause of the freezes: MFD page-mode gating -- the dev composite shows ALL pages at once, so off-page dials legitimately stopped; under BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio untouched). The SEH gauge guard now names its kills; repaint-heal resets the incremental arc after panel repaints; [panel]/[arc] probes added. COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole Player__StatusMessage queue; wired the binary's one producer -- the kill branch, victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS counted via the observed-death tally (each node scores every pilot from locally observed events, the same model as the KILLS credit) and the -2/-1 engine seed clamped for display. DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480, true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering, BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar (first second-chassis live outing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1188 lines
41 KiB
C++
1188 lines
41 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 from the shipped binary (BTL4OPT.EXE). Behaviour follows the
|
|
// Ghidra pseudo-C captured in recovered/all/part_013.c; the class/member names
|
|
// are taken verbatim from the SURVIVING header BTL4RDR.HPP, whose member
|
|
// byte-offsets match the decompiled object exactly:
|
|
//
|
|
// offsetPosition @0x2D0 backgroundColor @0x358
|
|
// currentScale @0x2D4 staticColor @0x35C
|
|
// currentPositionPointer @0x2D8 boxColor @0x360
|
|
// currentAngularPointer @0x2DC halfWidth @0x364
|
|
// currentCapabilitiesRatio@0x2E0 halfHeight @0x368
|
|
// LODIndex @0x2E4 operating @0x36C
|
|
// pixelsPerMeter @0x2E8 rockAndRoll @0x370
|
|
// metersPerPixel @0x2EC flashState @0x374
|
|
// maximumRange @0x2F0 staticEntityList @0x378
|
|
// maximumDistanceSquared @0x2F4 movingEntityList @0x394
|
|
// xMin/yMin/zMin @0x2F8.. previousDrawing @0x3B0
|
|
// xMax/yMax/zMax ..0x30C halfViewWidthInUnits@0x3C0
|
|
// viewingPosition @0x310 shadowTable @0x3C4
|
|
// viewHorizontalRotation @0x31C nameArray[16] @0x090 (stride 0x24)
|
|
// viewHalfHorizontalWidth @0x320
|
|
// worldToView @0x324 (base GraphicGauge: renderer@0x1C,
|
|
// embedded GraphicsView@0x48)
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
// PORT NOTE. This module is compiled against the surviving WinTesla (RP411)
|
|
// MUNGA / MUNGA_L4 engine headers. Several facilities the original BattleTech
|
|
// radar leaned on are BT-specific and are NOT present in that engine:
|
|
// * the per-Entity "video object" (a name-bitmap id + current target),
|
|
// * the radar "pip" symbol table,
|
|
// * the gauge-renderer's spatial worlds, operator-entity connection and
|
|
// pip table,
|
|
// * the global name-bitmap cache (the DAT_004efc94 App singleton).
|
|
// They are reproduced as a file-local adaptation layer below so the recovered
|
|
// MapDisplay logic keeps its full structure and compiles; the bodies are
|
|
// best-effort and currently resolve to "unavailable" in this engine build
|
|
// (see the reconstruction CROSS-FAMILY NEEDS). All engine 2D/matrix calls
|
|
// have been retargeted to the real WinTesla API names:
|
|
// GraphicsView::MoveTo->MoveToAbsolute, LineTo->DrawLineToAbsolute,
|
|
// Blit->DrawBitMap, BeginClip->AttachRecorder, EndClip->DetachRecorder,
|
|
// SetExtent->SetPositionWithinPort; GraphicsViewRecord::Erase->Draw,
|
|
// Reset->Clear; AffineMatrix::Identity->BuildIdentity,
|
|
// SetTranslation->SetFromAxis(W_Axis), Concatenate->operator*=,
|
|
// Scale->Multiply(m,v), FromQuaternion->operator=, Transform->point*=m;
|
|
// BitMap width/height -> Data.Size.x/.y; the embedded GraphicsView is the
|
|
// base GraphicGauge::localView.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
// task #17 (radar symbology): the "pip" system is the ENGINE's L4GaugeImage
|
|
// (vector-stroke shapes, resource type 0x12) cached in L4Warehouse::
|
|
// gaugeImageBin; name labels are the Mission's prebuilt egg bitmaps.
|
|
#include <L4GAUIMA.hpp> // L4GaugeImage -- the pip vector shapes (fwd shim)
|
|
#include <L4WRHOUS.hpp> // L4Warehouse::gaugeImageBin -- the pip table
|
|
#include <MISSION.hpp> // Mission::GetSmallNameBitmap -- the name labels
|
|
#include <PLAYER.hpp> // Player::playerBitmapIndex
|
|
#pragma hdrstop
|
|
|
|
#if !defined(BTL4RDR_HPP)
|
|
# include <btl4rdr.hpp>
|
|
#endif
|
|
#if !defined(APP_HPP)
|
|
# include <app.hpp>
|
|
#endif
|
|
|
|
#include <iostream>
|
|
#include <cstring>
|
|
#include <new>
|
|
|
|
//
|
|
// 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;
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// BT radar adaptation layer (file-local -- see PORT NOTE above)
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//
|
|
// task #17 DECODED: the "video object" at *(entity+0x190) IS Entity::
|
|
// owningPlayer (ENTITY.h:589; resourceID is the adjacent +0x1BC) -- the radar
|
|
// reads its playerBitmapIndex (+0x1E0, the egg 'bitmapindex' key) for the
|
|
// name label and BTPlayer::objectiveMech (+0x284) for the target box. The
|
|
// old invented VideoObject class is retired.
|
|
//
|
|
// The "pip" is the ENGINE's L4GaugeImage (L4GAUIMA.h -- FUN_0046f0c0 IS
|
|
// L4GaugeImage::Draw line-for-line), and the "pip table" is L4Warehouse::
|
|
// gaugeImageBin (WarehouseBinOf<L4GaugeImage>, warehouse+0x58) reached via
|
|
// GaugeRenderer::warehousePointer (+0x4C) -- keyed by Entity::resourceID
|
|
// (BTL4.RES ships 110 type-0x12 vector shapes: every mech/vehicle/building/
|
|
// tree). Thin facades keep the recovered draw-loop shape:
|
|
//
|
|
class Pip // == L4GaugeImage
|
|
{
|
|
public:
|
|
void
|
|
Draw(
|
|
Scalar lod_index,
|
|
Scalar meters_per_pixel,
|
|
GraphicsView *view,
|
|
int color,
|
|
AffineMatrix &transform,
|
|
int box_color
|
|
)
|
|
{
|
|
((L4GaugeImage *)this)->Draw(lod_index, meters_per_pixel,
|
|
view, color, transform, box_color);
|
|
}
|
|
};
|
|
|
|
class PipTable // == L4Warehouse
|
|
{
|
|
public:
|
|
Pip *
|
|
LookUpPip(ResourceDescription::ResourceID entity_resource_ID)
|
|
{
|
|
// FUN_004688ce(warehouse+0x58, entity+0x1BC, 0) -- the refcounting
|
|
// Get, exactly as the binary's per-frame loop calls it (the
|
|
// mission-end Purge reclaims; one vector shape per model).
|
|
return (Pip *)((L4Warehouse *)this)->gaugeImageBin.Get(
|
|
entity_resource_ID, False);
|
|
}
|
|
};
|
|
|
|
//
|
|
// Entity placement helpers -- the WinTesla Entity keeps its world placement in
|
|
// localOrigin (linearPosition + angularPosition). These give the radar the
|
|
// Point3D position and AffineMatrix transform the recovered code expects.
|
|
//
|
|
static inline Point3D &
|
|
EntityPosition(Entity *entity)
|
|
{
|
|
return entity->localOrigin.linearPosition;
|
|
}
|
|
|
|
static inline void
|
|
EntityTransform(AffineMatrix *out, Entity *entity)
|
|
{
|
|
*out = entity->localOrigin;
|
|
}
|
|
|
|
//
|
|
// BT-renderer facilities not present on the WinTesla GaugeRenderer.
|
|
//
|
|
static Entity *
|
|
ResolveOperatorEntity(GaugeRenderer *renderer);
|
|
static PipTable *
|
|
GetPipTable(GaugeRenderer *renderer);
|
|
|
|
//
|
|
// Name-label + target resolution (task #17): the label bitmap comes from the
|
|
// owning Player's bitmapindex through the Mission's SMALL prebuilt-raster
|
|
// chain; the target from the owning BTPlayer's objectiveMech (bridge).
|
|
//
|
|
static BitMap *
|
|
LookUpNameBitmap(int name_ID);
|
|
static int
|
|
GetNameID(Entity *entity);
|
|
static Logical
|
|
IsLabelledEntity(Entity *entity);
|
|
|
|
//
|
|
// The owner mech's current target (via its video object).
|
|
//
|
|
static Entity *
|
|
GetTarget(Entity *owner);
|
|
|
|
//
|
|
// Mech sub-object lookup ("Torso") + its horizontal rotation feed.
|
|
//
|
|
static Entity *
|
|
FindSubObject(Entity *entity, const char *name);
|
|
static Radian *
|
|
GetHorizontalRotation(Entity *entity);
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// 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 global font/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
|
|
// Mission::LoadNameBitmaps).
|
|
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 = (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)); // +0x24, FUN_004dcd94
|
|
|
|
if (!hotBoxed)
|
|
{
|
|
graphics_view->SetColor(color); // +0x18
|
|
}
|
|
else
|
|
{
|
|
graphics_view->SetColor(hotbox_color);
|
|
}
|
|
|
|
graphics_view->DrawBitMap( // +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, so it
|
|
// needs no ModeManager named-constant registration.
|
|
//
|
|
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 *p = methodDescription.parameterList;
|
|
|
|
OffsetPosition offset =
|
|
(strcmp(p[3].data.string, "bottom") == 0) ? bottom : center;
|
|
|
|
MapDisplay *gauge = (MapDisplay *)operator new(sizeof(MapDisplay));
|
|
if (gauge != NULL)
|
|
{
|
|
new (gauge) MapDisplay(
|
|
p[0].data.rate, p[1].data.modeMask,
|
|
(L4GaugeRenderer *)gauge_renderer,
|
|
0, // owner_ID
|
|
display_port_index,
|
|
position.x, position.y, // left, bottom
|
|
position.x + p[2].data.vector.x, // right
|
|
position.y + p[2].data.vector.y, // top
|
|
offset,
|
|
p[4].data.integer, // view width (degrees)
|
|
p[5].data.color, p[6].data.color, p[7].data.color, // bg, static, hotbox
|
|
p[8].data.scalar, // maximum range
|
|
entity,
|
|
(Scalar *)p[9].data.attributePointer, // scale (RadarRange)
|
|
(Point3D **)p[10].data.attributePointer, // position (RadarLinearPosition)
|
|
(Quaternion **)p[11].data.attributePointer, // angle (RadarAngularPosition)
|
|
(Scalar *)p[12].data.attributePointer); // capabilities (Avionics/RadarPercent)
|
|
}
|
|
return True;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Construction / Destruction
|
|
//
|
|
// @004c1c24 -- vtable 0051422c, base name "MapDisplay". Builds the 16-entry
|
|
// name array, the static/moving entity lists and the previous-drawing record,
|
|
// 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-rotation 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"
|
|
)
|
|
{
|
|
//
|
|
// nameArray / staticEntityList / movingEntityList / previousDrawing are
|
|
// member objects -- the decompiler's explicit Construct_Array / ctor thunks
|
|
// are the compiler-generated member construction; nothing to do here.
|
|
//
|
|
|
|
//
|
|
// Size the graphics port and centre the origin.
|
|
//
|
|
localView.SetPositionWithinPort(left, bottom, right, top); // (this+0x48 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; // this[0xD6]
|
|
staticColor = static_color; // this[0xD7]
|
|
boxColor = hotbox_color; // this[0xD8]
|
|
maximumRange = maximum_range; // this[0xBC]
|
|
rockAndRoll = allow_rock_and_roll; // this[0xDC]
|
|
|
|
halfWidth = (right - left) >> 1; // this[0xD9]
|
|
halfHeight = (top - bottom) >> 1; // this[0xDA]
|
|
offsetPosition = offset_position; // this[0xB4]
|
|
|
|
halfViewWidthInUnits = view_width_in_degrees >> 1; // this[0xF0]
|
|
shadowTable = (ShadowRecord *)operator new(halfViewWidthInUnits * 8); // this[0xF1]
|
|
|
|
viewHorizontalRotation = 0.0f; // this[199] (0x31C)
|
|
viewHalfHorizontalWidth = 0.3926991f; // this[200] (0x320) = PI/8
|
|
flashState = False; // this[0xDD]
|
|
|
|
//
|
|
// 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 (class id 0xBB9) with a "Torso" sub-object,
|
|
// feed its horizontal rotation into the map's view heading.
|
|
//
|
|
if (entity->GetClassID() == 0xBB9)
|
|
{
|
|
Entity *torso = FindSubObject(entity, "Torso"); // FUN_0041f98c
|
|
if (torso != NULL)
|
|
{
|
|
AddConnection(
|
|
new GaugeConnectionDirectOf<Radian>( // FUN_004c2d42
|
|
0, &viewHorizontalRotation, GetHorizontalRotation(torso) /*+0x1D8*/));
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// @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 (the decompiler's explicit dtor thunks are that automatic
|
|
// member destruction), so only the heap shadowTable is released here.
|
|
//
|
|
MapDisplay::~MapDisplay()
|
|
{
|
|
operator 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)
|
|
{
|
|
std::cout << indent << "ThreatIndicator:"; // FUN_004dbb24 x2
|
|
|
|
char deeper[80];
|
|
strcpy(deeper, indent);
|
|
strcat(deeper, "\t"); // FUN_004d49b8(local_54, "\t")
|
|
|
|
GraphicGauge::ShowInstance(deeper); // FUN_004448bc
|
|
}
|
|
|
|
//
|
|
// @004c1fc0 -- reset the per-frame phase counter when the gauge is shown.
|
|
//
|
|
void
|
|
MapDisplay::BecameActive()
|
|
{
|
|
operatingPhase = 0; // this+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
|
|
//
|
|
void
|
|
MapDisplay::Execute()
|
|
{
|
|
int phase = operatingPhase++; // this+0x354
|
|
const int mlog = getenv("BT_MAP_LOG") ? 1 : 0;
|
|
|
|
if (phase == 0)
|
|
{
|
|
operating = (OperatingEpsilon < currentCapabilitiesRatio); // 0.0f < ratio
|
|
if (mlog) DEBUG_STREAM << "[map] p0 operating=" << (int)operating
|
|
<< " cap=" << currentCapabilitiesRatio << " scale=" << currentScale << "\n" << std::flush;
|
|
if (operating)
|
|
{
|
|
// Repopulate the renderer's entity grids (the port dropped the engine's
|
|
// per-frame InterestingEntity feed, so the within-bounds queries in
|
|
// phases 1/2 found nothing). Done here, under the gauge's guarded
|
|
// Execute, so a fault disables only the map -- not the whole renderer.
|
|
renderer->RebuildEntityGrid();
|
|
maximumDistanceSquared = currentCapabilitiesRatio * maximumRange;
|
|
maximumDistanceSquared = maximumDistanceSquared * maximumDistanceSquared;
|
|
CalculateBounds(); // FUN_004c2178
|
|
if (mlog) DEBUG_STREAM << "[map] p0 bounds ok\n" << std::flush;
|
|
}
|
|
}
|
|
else if (phase == 1)
|
|
{
|
|
staticEntityList.Clear(); // FUN_004435ac(this+0x378)
|
|
if (operating)
|
|
{
|
|
renderer->GetStaticEntitiesWithinBounds( // FUN_00443ba4(renderer+0xB0)
|
|
&staticEntityList,
|
|
xMin, yMin, zMin, xMax, yMax, zMax);
|
|
if (mlog) DEBUG_STREAM << "[map] p1 static gathered\n" << std::flush;
|
|
}
|
|
}
|
|
else if (phase == 2)
|
|
{
|
|
movingEntityList.Clear(); // FUN_004435ac(this+0x394)
|
|
if (operating)
|
|
{
|
|
renderer->GetMovingEntitiesWithinBounds( // FUN_0044366c(renderer+0x94)
|
|
&movingEntityList,
|
|
xMin, yMin, zMin, xMax, yMax, zMax);
|
|
if (mlog) DEBUG_STREAM << "[map] p2 moving gathered\n" << std::flush;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (phase == 3)
|
|
{
|
|
EraseDisplay(); // FUN_004c2294
|
|
if (operating)
|
|
{
|
|
DrawDisplay(); // FUN_004c22c4
|
|
}
|
|
if (getenv("BT_MAP_LOG"))
|
|
{
|
|
static int s_c = 0;
|
|
if ((s_c++ % 15) == 0)
|
|
DEBUG_STREAM << "[map] operating=" << (int)operating
|
|
<< " scale=" << currentScale
|
|
<< " cap=" << currentCapabilitiesRatio
|
|
<< " posPtr=" << (currentPositionPointer ? 1 : 0)
|
|
<< " anglePtr=" << (currentAngularPointer ? 1 : 0)
|
|
<< "\n" << std::flush;
|
|
}
|
|
}
|
|
operatingPhase = 0;
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004c2178 (file=bt/heat... actually bt_l4/btl4rdr.cpp -- 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)
|
|
{
|
|
Verify(False); // FUN_0040385c
|
|
// "MapDisplay::CalculateBounds detected a bad scale"
|
|
// "d:\\tesla\\bt\\bt_l4\\BTL4RDR.CPP", 0x224
|
|
currentScale = 1.0f; // 0x3f800000
|
|
}
|
|
|
|
pixelsPerMeter = (Scalar)(halfWidth * 2) / currentScale;
|
|
metersPerPixel = MetersPerPixelNum / pixelsPerMeter; // 1.0 / pixelsPerMeter
|
|
LODIndex = metersPerPixel;
|
|
|
|
Scalar halfWorldWidth = (Scalar)halfWidth / pixelsPerMeter;
|
|
Scalar halfWorldHeight = (Scalar)halfHeight / pixelsPerMeter;
|
|
|
|
Entity *owner = ResolveOperatorEntity(renderer); // FUN_00417ab4(renderer+0x40)
|
|
viewingPosition = EntityPosition(owner); // FUN_00408440(this+0x310, owner+0x100)
|
|
|
|
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 (twice in decomp)
|
|
view.BuildIdentity();
|
|
|
|
//
|
|
// 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(local_5c, ...)
|
|
|
|
// TRANSCRIPTION FIX #2 (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, center PRESERVED. Our old
|
|
// `view *= yaw` was the engine's full COMPOSE, which also rotated the
|
|
// translation (view = [R | center x R]) -- the subsequent Invert then
|
|
// mapped center x R to the origin instead of the viewer, so every
|
|
// blip landed 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.
|
|
//
|
|
EulerAngles euler;
|
|
euler = *currentAngularPointer; // FUN_0040954c
|
|
Scalar heading = euler.yaw * HeadingHalf; // * 0.5f
|
|
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 semantics)
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// "Rock and roll": use the full orientation.
|
|
//
|
|
view = *currentAngularPointer; // rotation-only build
|
|
}
|
|
}
|
|
view.SetFromAxis(W_Axis, center); // FUN_0040b0ac(view,3,center) -- translation LAST
|
|
}
|
|
|
|
// TRANSCRIPTION FIX (the invisible-pip bug, 2026-07-12): FUN_0040b244 is
|
|
// the full affine INVERSE (cofactor expansion + determinant divide,
|
|
// part_001.c:172), not a copy. `view` above is the OPERATOR's local->
|
|
// world pose (translate to the mech + yaw); the radar needs its inverse
|
|
// -- world->scope, center subtracted, heading unrotated. Read as a copy,
|
|
// every pip/name transformed to (world + center) x ppm = thousands of
|
|
// pixels off-scope: the scope drew empty at all times.
|
|
worldToView.Invert(view); // FUN_0040b244(this+0x324, view)
|
|
|
|
//
|
|
// Bake the metres->pixels scale into the matrix.
|
|
//
|
|
Vector3D scale;
|
|
scale.x = scale.y = scale.z = pixelsPerMeter;
|
|
worldToView.Multiply(worldToView, scale); // FUN_0040b374
|
|
if (getenv("BT_MAP_LOG"))
|
|
{
|
|
static int s_m = 0;
|
|
if ((s_m++ % 30) == 0)
|
|
DEBUG_STREAM << "[map] w2v ppm=" << pixelsPerMeter
|
|
<< " halfW=" << halfWidth << " scale=" << currentScale
|
|
<< " m=[" << worldToView(0,0) << "," << worldToView(0,1) << "," << worldToView(0,2)
|
|
<< " | " << worldToView(1,0) << "," << worldToView(1,1) << "," << worldToView(1,2)
|
|
<< " | " << worldToView(2,0) << "," << worldToView(2,1) << "," << worldToView(2,2)
|
|
<< " | t " << worldToView(3,0) << "," << worldToView(3,1) << "," << worldToView(3,2)
|
|
<< "]\n" << std::flush;
|
|
}
|
|
|
|
BuildRadarShadow(worldToView); // FUN_004c228c
|
|
|
|
flashState = !flashState; // this+0x374 ^= 1
|
|
|
|
localView.AttachRecorder(&previousDrawing); // (this+0x48 vtbl+0x64)
|
|
const int mlog = getenv("BT_MAP_LOG") ? 1 : 0;
|
|
if (mlog) DEBUG_STREAM << "[map] draw: wedge...\n" << std::flush;
|
|
DrawViewWedge(); // FUN_004c2484
|
|
if (mlog) DEBUG_STREAM << "[map] draw: static...\n" << std::flush;
|
|
DrawStatic(worldToView); // FUN_004c25b4
|
|
if (mlog) DEBUG_STREAM << "[map] draw: moving...\n" << std::flush;
|
|
DrawMoving(worldToView); // FUN_004c2788
|
|
if (mlog) DEBUG_STREAM << "[map] draw: names...\n" << std::flush;
|
|
DrawNames(worldToView); // FUN_004c28c4
|
|
if (mlog) DEBUG_STREAM << "[map] draw: done\n" << std::flush;
|
|
localView.DetachRecorder(); // (this+0x48 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()
|
|
{
|
|
Radian leftEdge = viewHorizontalRotation - viewHalfHorizontalWidth;
|
|
Radian rightEdge = viewHorizontalRotation + viewHalfHorizontalWidth;
|
|
|
|
localView.SetColor(staticColor); // (this+0x48 vtbl+0x18)
|
|
|
|
Radian edges[2];
|
|
edges[0] = leftEdge;
|
|
edges[1] = rightEdge;
|
|
|
|
for (int 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(local_68, this+0x384)
|
|
iter.First();
|
|
|
|
PipTable *pips = GetPipTable(renderer); // *(renderer+0x4C)
|
|
|
|
for (;;)
|
|
{
|
|
Entity *entity = iter.ReadAndNext(); // vtbl+0x28
|
|
if (entity == NULL)
|
|
{
|
|
break;
|
|
}
|
|
|
|
Vector3D delta;
|
|
delta.Subtract(EntityPosition(entity), viewingPosition); // FUN_00408644
|
|
if (delta.x*delta.x + delta.y*delta.y + delta.z*delta.z > maximumDistanceSquared)
|
|
{
|
|
continue;
|
|
}
|
|
// VISIBLE-RADIUS cull (port): the capability radius above (RadarPercent
|
|
// x 4000 m) exceeds the SCOPE radius (currentScale/2 at the current
|
|
// zoom); an out-of-scope draw lands beyond the port rect on the shared
|
|
// gauge surface. Skip what the scope can't show.
|
|
if (delta.x*delta.x + delta.z*delta.z > currentScale * currentScale * 0.25f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Pip *pip = (pips != NULL)
|
|
? pips->LookUpPip(entity->GetResourceID()) // FUN_004688ce(pips+0x58,entity+0x1bc,0)
|
|
: NULL;
|
|
if (pip != NULL)
|
|
{
|
|
AffineMatrix blipXform;
|
|
AffineMatrix entityXform;
|
|
EntityTransform(&entityXform, entity);
|
|
blipXform.Multiply(entityXform, worldToView); // FUN_0040b104(local_5c, entity+0xD0, ...)
|
|
pip->Draw( // FUN_0046f0c0
|
|
LODIndex, metersPerPixel, &localView,
|
|
staticColor, blipXform, 0);
|
|
}
|
|
|
|
//
|
|
// Labelled static entities (e.g. nav beacons) also blit a name bitmap.
|
|
//
|
|
if (IsLabelledEntity(entity)) // FUN_0041a1a4 (LabelledEntityClass 0x4e70c4)
|
|
{
|
|
int nameID = GetNameID(entity); // entity+0x21C
|
|
BitMap *bitmap = LookUpNameBitmap(nameID); // (cache+0x68 vtbl+0x0C)
|
|
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(local_1c, this+0x3A0)
|
|
iter.First();
|
|
|
|
PipTable *pips = GetPipTable(renderer); // *(renderer+0x4C)
|
|
Entity *owner = ResolveOperatorEntity(renderer);
|
|
Entity *target = GetTarget(owner); // owner->GetVideoObject()->target
|
|
|
|
const int mlog = getenv("BT_MAP_LOG") ? 1 : 0;
|
|
if (mlog)
|
|
DEBUG_STREAM << "[map] DrawMoving owner=" << (void*)owner
|
|
<< " view=(" << viewingPosition.x << "," << viewingPosition.z << ")\n" << std::flush;
|
|
|
|
for (;;)
|
|
{
|
|
Entity *entity = iter.ReadAndNext();
|
|
if (entity == NULL)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (mlog)
|
|
{
|
|
Point3D ep = EntityPosition(entity);
|
|
Scalar dx = ep.x - viewingPosition.x, dz = ep.z - viewingPosition.z;
|
|
DEBUG_STREAM << "[map] ent=" << (void*)entity
|
|
<< " cls=0x" << std::hex << entity->GetClassID() << std::dec
|
|
<< " own=" << (entity == owner ? 1 : 0)
|
|
<< " pos=(" << ep.x << "," << ep.z << ")"
|
|
<< " dsq=" << (dx*dx + dz*dz)
|
|
<< "\n" << std::flush;
|
|
}
|
|
|
|
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;
|
|
}
|
|
// VISIBLE-RADIUS cull (port; see DrawStatic): don't draw past the scope.
|
|
if (delta.x*delta.x + delta.z*delta.z > currentScale * currentScale * 0.25f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Pip *pip = (pips != NULL)
|
|
? pips->LookUpPip(entity->GetResourceID())
|
|
: NULL;
|
|
if (mlog)
|
|
{
|
|
Point3D sp;
|
|
sp.Multiply(EntityPosition(entity), worldToView);
|
|
DEBUG_STREAM << "[map] pip=" << (void*)pip
|
|
<< " screen=(" << sp.x << "," << sp.y << "," << sp.z << ")"
|
|
<< " lod=" << LODIndex << " mpp=" << metersPerPixel << "\n" << std::flush;
|
|
}
|
|
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);
|
|
}
|
|
else if (entity->GetClassID() == 0xBB9) // MechClassID -- radar contacts are mechs
|
|
{
|
|
// FALLBACK (kept, task #17): a mech whose model has no type-0x12
|
|
// pip resource (all shipped mechs HAVE one -- this covers dev
|
|
// content) still draws the cross blip at the projected position.
|
|
//
|
|
// Project delta = (entity - viewpoint) directly to radar pixels: the
|
|
// worldToView camera matrix bakes a scale that is wrong for a point
|
|
// transform (|blip| came out ~3.8x |delta|*ppm), so compute it here --
|
|
// top-down, heading-up: rotate delta by -heading, then scale by
|
|
// pixelsPerMeter. |screen| == |delta|*ppm, on-radar for in-range mechs.
|
|
Scalar sinH = 0.0f, cosH = 1.0f;
|
|
if (currentAngularPointer != NULL)
|
|
{
|
|
EulerAngles e;
|
|
e = *currentAngularPointer; // Quaternion -> EulerAngles (operator=)
|
|
SinCosPair scH;
|
|
scH = Radian(e.yaw); // FUN_00408328
|
|
sinH = scH.sine;
|
|
cosH = scH.cosine;
|
|
}
|
|
Scalar rx = delta.x * cosH + delta.z * sinH;
|
|
Scalar rz = -delta.x * sinH + delta.z * cosH;
|
|
int sx = Round(rx * pixelsPerMeter);
|
|
int sz = Round(rz * pixelsPerMeter);
|
|
if (getenv("BT_MAP_LOG"))
|
|
DEBUG_STREAM << "[map] blip mech @(" << sx << "," << sz << ")"
|
|
<< " scale=" << currentScale << " ppm=" << pixelsPerMeter
|
|
<< " deltaXZ=(" << delta.x << "," << delta.z << ")\n" << std::flush;
|
|
localView.SetColor((entity == target) ? boxColor : MapBlipColor());
|
|
localView.MoveToAbsolute(sx - 5, sz);
|
|
localView.DrawLineToAbsolute(sx + 5, sz);
|
|
localView.MoveToAbsolute(sx, sz - 5);
|
|
localView.DrawLineToAbsolute(sx, sz + 5);
|
|
// small box around the blip so it reads as a distinct contact marker
|
|
localView.MoveToAbsolute(sx - 4, sz - 4);
|
|
localView.DrawLineToAbsolute(sx + 4, sz - 4);
|
|
localView.DrawLineToAbsolute(sx + 4, sz + 4);
|
|
localView.DrawLineToAbsolute(sx - 4, sz + 4);
|
|
localView.DrawLineToAbsolute(sx - 4, sz - 4);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004c28c4 -- CONFIRMED (assert path "...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)
|
|
{
|
|
Entity *owner = ResolveOperatorEntity(renderer);
|
|
Entity *target = GetTarget(owner); // *(*(owner+400)+0x284)
|
|
|
|
ChainIteratorOf<Entity*> iter(movingEntityList.GetInstanceList()); // FUN_0042ac1c(local_1c, this+0x3A0)
|
|
iter.First();
|
|
|
|
int count = iter.GetSize(); // vtbl+0x14
|
|
if (getenv("BT_MAP_LOG"))
|
|
DEBUG_STREAM << "[map] names count=" << count << "\n" << std::flush;
|
|
if (count < 1)
|
|
{
|
|
return;
|
|
}
|
|
if (count > maximumNames)
|
|
{
|
|
count = maximumNames;
|
|
}
|
|
|
|
//
|
|
// First pass: extract.
|
|
//
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
Entity *entity = iter.ReadAndNext(); // vtbl+0x28
|
|
if (entity == NULL)
|
|
{
|
|
Verify(False); // FUN_0040385c
|
|
// "MapDisplay::DrawNames -- GetSize() / iterator mismatch"
|
|
// "d:\\tesla\\bt\\bt_l4\\BTL4RDR.CPP", 0x408
|
|
}
|
|
else
|
|
{
|
|
// VISIBLE-RADIUS cull (port; see DrawStatic): a label for a
|
|
// contact beyond the scope would blit outside the port rect.
|
|
Vector3D d;
|
|
d.Subtract(EntityPosition(entity), viewingPosition);
|
|
if (d.x*d.x + d.z*d.z > currentScale * currentScale * 0.25f)
|
|
entity = NULL; // extract guards NULL -> no label
|
|
}
|
|
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 (int i = 0; i < count; ++i)
|
|
{
|
|
nameArray[i].Draw(&localView, hotboxColor); // FUN_004c1ab0
|
|
}
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// BT radar adaptation layer -- implementations
|
|
//
|
|
// These reproduce the BT-specific facilities the recovered radar relied on.
|
|
// The WinTesla engine we compile against does not expose them, so they are
|
|
// provided here, best-effort, resolving to "unavailable" until wired to the
|
|
// real BattleTech gauge renderer / video object system (CROSS-FAMILY NEEDS).
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
// (task #17: the Pip::Draw / PipTable::LookUpPip stub bodies that lived here
|
|
// are retired -- both are inline facades over the engine's L4GaugeImage /
|
|
// L4Warehouse::gaugeImageBin now, see the class declarations above.)
|
|
|
|
//
|
|
// FUN_00417ab4 -- the gauge renderer's "operator entity" (the mech whose
|
|
// cockpit this gauge belongs to). The renderer's linked-entity socket may be
|
|
// unwired on WinTesla, so fall back to the application viewpoint entity (the mech
|
|
// we are driving) -- the same fallback HeadingPointer::Execute uses. Returning
|
|
// NULL crashes the map's CalculateBounds/DrawDisplay (EntityPosition(NULL)).
|
|
//
|
|
static Entity *
|
|
ResolveOperatorEntity(GaugeRenderer *renderer)
|
|
{
|
|
Entity *e = renderer ? ((L4GaugeRenderer *)renderer)->GetLinkedEntity() : NULL;
|
|
if (e == NULL && application != NULL)
|
|
e = (Entity *)application->GetViewpointEntity();
|
|
return e;
|
|
}
|
|
|
|
//
|
|
// renderer+0x4C -- GaugeRenderer::warehousePointer: the L4Warehouse whose
|
|
// gaugeImageBin (+0x58) is the pip table (task #17; the "not present on
|
|
// WinTesla" note was wrong -- the facility is engine T0, L4WRHOUS.h).
|
|
//
|
|
static PipTable *
|
|
GetPipTable(GaugeRenderer *renderer)
|
|
{
|
|
return (renderer != NULL) ? (PipTable *)renderer->warehousePointer : NULL;
|
|
}
|
|
|
|
//
|
|
// The "global name-bitmap cache" DECODED (task #17): DAT_004efc94+0xC8 is
|
|
// Application::currentMission, and the cache is the Mission's SMALL
|
|
// name-bitmap VChain (prebuilt 64x16 egg rasters keyed 1-based by the egg
|
|
// 'bitmapindex'). All T0 (MISSION.h / APP.h).
|
|
//
|
|
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 (cultural.h; the static loop's label
|
|
// key into the LARGE 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 [T1].
|
|
//
|
|
static int
|
|
GetNameID(Entity * /*entity*/)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
//
|
|
// FUN_0041a1a4 vs 0x4e70c4 -- IsDerivedFrom("Landmark") (task #17 decode:
|
|
// the "LabelledEntityClass" IS the Landmark class, parent CulturalIcon).
|
|
// Dormant with GetNameID above (no landmark content ships).
|
|
//
|
|
static Logical
|
|
IsLabelledEntity(Entity * /*entity*/)
|
|
{
|
|
return False;
|
|
}
|
|
|
|
//
|
|
// owner->GetVideoObject()->target.
|
|
//
|
|
static Entity *
|
|
GetTarget(Entity *owner)
|
|
{
|
|
// *(*(owner+0x190)+0x284) -- the owning BTPlayer's objectiveMech (the
|
|
// designated target; complete-type bridge, btplayer.cpp).
|
|
extern Entity *BTPlayerObjectiveMechOf(void *player);
|
|
Player *owning_player = (owner != NULL) ? owner->GetOwningPlayer() : NULL;
|
|
return (owning_player != NULL)
|
|
? BTPlayerObjectiveMechOf(owning_player) : NULL;
|
|
}
|
|
|
|
//
|
|
// FUN_0041f98c -- Entity::FindSubObject(name) (locates "Torso").
|
|
//
|
|
static Entity *
|
|
FindSubObject(Entity * /*entity*/, const char * /*name*/)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
//
|
|
// torso+0x1D8 -- the sub-object's horizontal (yaw) rotation feed.
|
|
//
|
|
static Radian *
|
|
GetHorizontalRotation(Entity * /*entity*/)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// Local data-source connection helpers (file-private to btl4rdr.cpp)
|
|
//
|
|
// The MapDisplay constructor wires "direct-of-pointer" gauge connections so
|
|
// that an externally-owned Point3D*, Quaternion* or Radian is copied into the
|
|
// corresponding MapDisplay member every frame. In the surviving engine these
|
|
// are the GaugeConnectionDirectOf<T> template (GAUGE.HPP), so the explicit
|
|
// per-type subclasses the decompiler emitted (vtables 00514220 / 00514214 /
|
|
// 00514208) are just template instantiations -- no separate code needed.
|
|
//###########################################################################
|
|
//###########################################################################
|