Files
BT411/game/reconstructed/btl4rdr.cpp
T
arcattackandClaude Opus 4.8 fb3ed38fa7 map: fix contact-blip projection -- enemy now plots inside the radar FOV wedge
The previous commit found+drew contacts but they landed off-radar.  Debugged with
BT_MAP_LOG entity/blip tracing: the reads were CORRECT all along -- the enemy mech
(cls 0xBB9, own=0) sits at dsq=14400 (exactly 120u) from the player (own=1, dsq=0),
and the grid also carries ~311 cls-0x5E vehicle props per cycle (the map's
scattered vehicles) which the 0xBB9 blip filter correctly skips.

The bug was the PROJECTION: worldToView is a camera (view->world) matrix and
Point3D::Multiply bakes a scale that is wrong for a point transform -- a 120u
contact at ppm=0.366 projected to |167px| instead of |44px| (~3.8x), and with a
spurious rotation.  (The earlier "377-828m" reading was this wrong scale amplifying
the varying player-drift distance, not a distance error.)  Fixed by projecting
delta = (entity - viewpoint) DIRECTLY: rotate by -heading (from the orientation
quaternion's yaw, top-down heading-up) and scale by pixelsPerMeter.  Now |screen|
== |delta|*ppm exactly (blip @(0,44) for the 120u enemy), on-radar inside the view
wedge.  Blip is a cross + box so it reads as a distinct contact marker.

Verified live (BT_DEV_GAUGES): the enemy contact renders as a box marker INSIDE
the FOV wedge (render-confirmed via a 4x radar crop); combat un-regressed (TARGET
DESTROYED after 8 hits), heap-clean under BT_HEAPCHECK.  BT_MAP_LOG traces blip
coords + scale + delta.  Remaining map polish: blip-vs-wedge rotation convention
(the enemy reads ahead/in-wedge, which is correct); the authentic pip symbol/name
infrastructure (still stubbed); SetTargetRange (radar zoom, 1km default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:22:33 -05:00

1128 lines
36 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>
#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)
//###########################################################################
//###########################################################################
//
// A tracked entity's "video object": carries the name-bitmap id used for the
// floating label and the entity's current weapons target. (BT engine concept;
// absent from the WinTesla Entity.)
//
class VideoObject
{
public:
int nameID;
Entity *target;
};
//
// One radar symbol ("pip") + the per-model lookup table that owns them.
//
class Pip
{
public:
void
Draw(
Scalar lod_index,
Scalar meters_per_pixel,
GraphicsView *view,
int color,
AffineMatrix &transform,
int box_color
);
};
class PipTable
{
public:
Pip *
LookUpPip(int model_class_ID, int variant);
};
//
// 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);
//
// Per-entity video object, name-bitmap cache, and the "labelled static entity"
// test (nav beacons etc. carry a name bitmap).
//
static VideoObject *
GetVideoObject(Entity *entity);
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;
}
VideoObject *video_object = GetVideoObject(entity); // *(entity+400)
if (video_object == NULL)
{
nameBitmap = NULL;
}
else
{
//
// Resolve the entity's name id through the global name-bitmap cache.
//
int nameID = video_object->nameID; // *(vobj+0x1E0)
nameBitmap = LookUpNameBitmap(nameID); // cache vtbl+0x0C
}
//
// 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, &currentScale, scale_value_pointer)); // FUN_00474855
AddConnection(
new GaugeConnectionDirectOf<Point3D*>(0, &currentPositionPointer, position_pointer));// FUN_004c2a3e
AddConnection(
new GaugeConnectionDirectOf<Quaternion*>(0, &currentAngularPointer, angle_pointer)); // FUN_004c2bc0
AddConnection(
new GaugeConnectionDirectOf<Scalar>(0, &currentCapabilitiesRatio, 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, ...)
view.SetFromAxis(W_Axis, center); // FUN_0040b0ac(view,3,center)
if (currentAngularPointer != NULL)
{
if (!rockAndRoll)
{
//
// Top-down: keep heading only. Extract the yaw, build a
// yaw-only quaternion and concatenate 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; // FUN_0040adec
}
else
{
//
// "Rock and roll": use the full orientation matrix.
//
AffineMatrix full;
full = *currentAngularPointer; // FUN_00409968
view *= full; // FUN_0040adec
}
}
}
worldToView = 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
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;
}
Pip *pip = (pips != NULL)
? pips->LookUpPip(entity->GetClassID(), 0) // 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;
}
Pip *pip = (pips != NULL)
? pips->LookUpPip(entity->GetClassID(), 0)
: 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);
}
else if (entity->GetClassID() == 0xBB9) // MechClassID -- radar contacts are mechs
{
// BRING-UP: the authentic pip raster set (pips table) is not present
// in this engine build, so draw a simple cross blip at the contact's
// projected radar position (the target flashes in boxColor). Filtered
// to mechs so the ~300 dynamic world entities (effects/props) the grid
// carries don't clutter the display -- the real pip table would key the
// symbol off the model class, which is the proper follow-up.
//
// 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
}
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).
//###########################################################################
//###########################################################################
//
// FUN_0046f0c0 / FUN_004688ce -- radar pip symbol + per-model table.
//
void
Pip::Draw(
Scalar /*lod_index*/,
Scalar /*meters_per_pixel*/,
GraphicsView * /*view*/,
int /*color*/,
AffineMatrix & /*transform*/,
int /*box_color*/
)
{
// BEST-EFFORT: the pip raster set lives in the BT renderer's pip table,
// which is not present in this engine build.
}
Pip *
PipTable::LookUpPip(int /*model_class_ID*/, int /*variant*/)
{
return NULL;
}
//
// 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 -- the BT pip table. Not present on the WinTesla renderer.
//
static PipTable *
GetPipTable(GaugeRenderer * /*renderer*/)
{
return NULL;
}
//
// *(entity+400) -- the entity's video object (name id + current target).
//
static VideoObject *
GetVideoObject(Entity * /*entity*/)
{
return NULL;
}
//
// The global name-bitmap cache (DAT_004efc94 App singleton, +0xC8 graphics).
//
static BitMap *
LookUpNameBitmap(int /*name_ID*/)
{
return NULL;
}
//
// entity+0x21C -- a labelled entity's name id.
//
static int
GetNameID(Entity * /*entity*/)
{
return -1;
}
//
// FUN_0041a1a4 -- IsDerivedFrom(LabelledEntityClass).
//
static Logical
IsLabelledEntity(Entity * /*entity*/)
{
return False;
}
//
// owner->GetVideoObject()->target.
//
static Entity *
GetTarget(Entity *owner)
{
VideoObject *video_object = (owner != NULL) ? GetVideoObject(owner) : NULL;
return (video_object != NULL) ? video_object->target : 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.
//###########################################################################
//###########################################################################