//===========================================================================//
// 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, &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, ...)

		// 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.
//###########################################################################
//###########################################################################
