//===========================================================================//
// File:	btl4vid.cpp                                                         //
// Project: BattleTech       Brick: Video Renderer Manager                    //
// Contents: BTL4VideoRenderer -- the BattleTech L4 out-the-window 3D WORLD   //
//           renderer.  Builds the main-view scene each time an interesting   //
//           entity becomes visible: the player mech (jointed-mover segment   //
//           hierarchy + per-subsystem weapon/effect renderables + targeting  //
//           reticle), terrain, and other movers.                            //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
// 02/13/95  CPB  Initial coding.                                            //
//---------------------------------------------------------------------------//
// Copyright (C) 1994-1996, Virtual World Entertainment, Inc.                //
//                      PROPRIETARY and CONFIDENTIAL                         //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (BTL4OPT.EXE).  Behaviour follows the
// Ghidra pseudo-C in the bt_l4 cluster (recovered/all/part_014.c, addresses
// @004cc40c..@004d2bbc).  Class/member/method names come from the embedded
// assert path "d:\tesla\bt\bt_l4\BTL4VID.CPP", the embedded class-name string
// "BTL4VideoRenderer::Material name ... could not be found" (@0051d6f8), and
// the direct Red Planet analogue RP_L4/RPL4VID.cpp.  Each method cites its
// originating @ADDR.
//
// The recovered code targets the 1996 (pre-DPL) renderable API: joint
// renderables parent on a dpl_DCS*, geometry is loaded as a video object, and
// the tree is built against an opaque Scene root.  Engine-object accesses have
// been translated to the surviving public accessors (EntitySegment::GetParent /
// GetParentIndex / GetBaseOffset / GetVideoObjectName, Joint::GetJointType /
// GetHinge / GetEulerAngles / GetTranslation, JointedMover::segmentTable /
// segmentCount / GetJointSubsystem / GetSegment, Subsystem::GetSegmentIndex).
// The BT-specific renderables (declared in btl4vid.hpp) keep the recovered
// construction signatures.
//

#include <bt.hpp>
#if !defined(EMITTER_HPP)
#	include <emitter.hpp>	// Emitter/PPC (the reticle's per-weapon pip wiring)
#endif
#pragma hdrstop

#if !defined(BTL4VID_HPP)
#	include <btl4vid.hpp>
#endif
#if !defined(MECH_HPP)
#	include <mech.hpp>			// Mech / JointedMover segment table + subsystems
#endif
#if !defined(MECHWEAP_HPP)
#	include <mechweap.hpp>		// MechWeapon::GetClassDerivations (reticle pip)
#endif
#if !defined(NOTATION_HPP)
#	include <notation.hpp>
#endif
#if !defined(NAMELIST_HPP)
#	include <namelist.hpp>
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif
#if !defined(TERRAIN_HPP)
#	include <terrain.hpp>		// Terrain::GetClassDerivations (world-pick target, task #41)
#endif

#include <string.h>
#include <math.h>
#include <time.h>				// clock() -- the threat-trail ages (task #37)

// WORLD-PICK TARGET (task #41): a live Terrain entity mech4 cites in
// mech+0x388 when the boresight pick lands on the ground (the binary's target
// slot holds non-mech world entities).  Captured in MakeEntityRenderables.
Entity *gBTTerrainEntity = 0;


//
// Material-name substitution placeholders.  Mirrors RPL4VideoRenderer's
// color_parameter/badge_parameter, plus BT's patch/serno.
//
static const char * const colorParameter = "%color%";	// @0051d188
static const char * const badgeParameter = "%badge%";	// @0051d18c
static const char * const patchParameter = "%patch%";	// @0051d190
static const char * const sernoParameter = "%serno%";	// @0051d194

//
// Radial spacing between adjacent weapon pips along the reticle (_DAT_004cdce8).
//
static const float PIP_SPACING = 0.01f;					// _DAT_004cdce8

//
// One-character serial number stamped into %serno% material names; advances
// '0'..'9' then 'A' each mech loaded.  (DAT @0051d1b5.)
//
static char gSerno = '0';

//
// BattleTech entity / subsystem ClassIDs touched by the dispatch switches that
// were not recoverable from the surviving headers (the rest -- MechClassID,
// BTPlayerClassID, ReservoirClassID, EmitterClassID, PPCClassID -- resolve via
// the BT registration headers).  Values from CLASSMAP.md / the recovered enum.
//
enum
{
	MechMarkerClassID	= 0xBBA,	// timestamp / beacon marker
	MechWeaponClassID	= 0xBCD,	// projectile-weapon tracer
	SearchLightClassID	= 0xBD8		// searchlight subsystem
};

extern NameList
	*materialSubstitutionList;								// DAT_004f1aac

extern Entity
	*Entity_Being_Created;									// DAT_004f1aa8


//
//#############################################################################
// MakeEntityRenderables
//#############################################################################
//
// @004d0774
//
// The ClassID dispatch (analogue of RPL4VideoRenderer::MakeEntityRenderables).
//
void
	BTL4VideoRenderer::MakeEntityRenderables(
		Entity					*entity,
		ResourceDescription	*model_resource,
		ViewFrom					view_type)
{
	Entity_Being_Created = entity;							// DAT_004f1aa8

	HierarchicalDrawComponent *mech_root = NULL;

	switch (entity->GetClassID())						// entity[0x04]
	{
		case MechClassID:								// 0xBB9
		{
			//
			// Fog for the mech's main view, then load the colour/badge/patch
			// material substitutions, build the whole mech, and tear the
			// substitution list back down.
			//
			SetFogStyle(updateFogSetting);					// FUN_0045d3cc(this,0x68)
			SetupMaterialSubstitutionList(entity);			// FUN_004d0cc0
			mech_root = MakeMechRenderables(				// FUN_004cef28
				entity, model_resource, view_type);
			TearDownMaterialSubstitutionList();				// FUN_004d11e8
			// NB: the RootRenderable built by MakeMechRenderables registers
			// itself with the renderer (AddRenderable) and hooks to the entity's
			// localToWorld in its ctor -- no explicit AddDynamicRenderable here
			// (unlike the 1996 VideoComponent path).
			(void)mech_root;
			break;
		}

		case MechMarkerClassID:							// 0xBBA  (timestamp/marker beacon)
		{
			d3d_OBJECT *marker = LoadObject("tmst_c");		// FUN_00498448
			BTRootRenderable *root =						// FUN_00453578, alloc 100
				new BTRootRenderable(
					entity, VideoRenderable::Dynamic, marker,
					GetScene(), 1, 0);
			// watcher that keeps the marker oriented (FUN_00458c58, alloc 0x120)
			new BTMarkerWatcherRenderable(
				entity, 0, GetMainView() /* this[0x2cc] */, root->GetDCS());
			break;
		}

		case BTPlayerClassID:							// 0xBDA
		{
			StateIndicator *sim_state =
				(StateIndicator *)entity->GetAttributePointer(1 /* SimulationState */);
			if ((entity->GetInstance() & 0xC) == 4)		// ReplicantInstance
			{
				//
				// Third-party view: drop-zone translocation effect.
				//
				Point3D *drop_zone =
					(Point3D *)entity->GetAttributePointer("DropZoneLocation");	// @0051d73a
				if (sim_state && drop_zone)
				{
					new BTTranslocationRenderable(			// FUN_00458d2c, alloc 0x40
						entity, VideoRenderable::Watcher, GetMainView(),
						sim_state, drop_zone, 1);
				}
			}
			else if (sim_state)
			{
				//
				// Our own POV start/end (mission fade in/out) using the fog
				// colour + near/far planes stored on the renderer.
				//
				new BTPOVStartEndRenderable(				// FUN_00454394, alloc 0x50
					entity, VideoRenderable::Watcher, GetMainView(),
					dplMainZone, dplDeathZone, sim_state,
					fogRed, fogGreen, fogBlue, fogNear, fogFar,
					3 /* MissionStartingState */, 4 /* MissionEndingState */);
			}
			break;
		}

		default:
		{
			//
			// Unknown / non-mech entities (terrain, cavern world geometry, props,
			// landmarks, doorframes, eyecandy, ...) route to the DPL per-entity
			// builder -- exactly as RP's default does (RPL4VID.cpp:1436).  That
			// builder loads each entity's video object(s) (.bgf via
			// d3d_OBJECT::LoadObject -> LoadObjectBGF) and hangs them on a
			// Root/Static/DCS-instance renderable, which is how the cavern world
			// gets onto the screen.  (Previously this deferred to the no-op
			// VideoRenderer grandparent -> world drew nothing.)  The uninitialised
			// `this_instance` in the CulturalIcon/Landmark arm has been fixed in
			// L4VIDEO.cpp.
			//
			// WORLD-PICK TARGET support (task #41): the boresight pick can hit
			// TERRAIN (the binary's target slot holds non-mech world entities --
			// HudSimulation part_013.c:5620 explicitly handles a target without
			// damage zones).  Capture a live Terrain entity for mech4's ground
			// pick to cite in mech+0x388 (the pick POINT carries the geometry;
			// which specific instance matters only to damage, which never routes
			// to terrain).
			{
				extern Entity *gBTTerrainEntity;
				if (gBTTerrainEntity == 0
					&& entity->IsDerivedFrom(*Terrain::GetClassDerivations()))
					gBTTerrainEntity = entity;
			}
			DPLRenderer::MakeEntityRenderables(
				entity, model_resource, view_type);
			break;
		}
	}

	Entity_Being_Created = NULL;
}


//
//#############################################################################
// MakeMechRenderables
//#############################################################################
//
// @004cef28  (6157 bytes -- the main world-view builder)
//
// Build the renderable tree for one mech and submit it to the scene.  This is
// the BattleTech analogue of RPL4VideoRenderer::MakeJointedMoverRenderables.
//
HierarchicalDrawComponent*
	BTL4VideoRenderer::MakeMechRenderables(
		Entity					*entity,
		ResourceDescription	*model_resource,	// (unused; tree built from segment table)
		ViewFrom					type)
{
	//
	// RECONSTRUCTION NOTE (WinTesla port):
	// The shipped 1996 BattleTech built this tree from a bespoke pre-DPL
	// renderable hierarchy (BTRootRenderable / BTHingeRenderable / ...) that
	// parented on raw dpl_DCS* handles and drove the Division IG board.  That
	// hierarchy was never ported to WinTesla -- the engine here replaced it with
	// the D3D-backed VideoRenderable family (RootRenderable / HingeRenderable /
	// BallJointRenderable / BallTranslateJointRenderable / DPLStaticChildRenderable
	// / DPLEyeRenderable, see MUNGA_L4/L4VIDRND).  Those renderables self-register
	// with the renderer, build their own DCS, and parent on the PARENT RENDERABLE
	// (a HierarchicalDrawComponent*), not a dpl_DCS*.  So this body is rebuilt the
	// RP way (mirrors RPL4VideoRenderer::MakeJointedMoverRenderables, the
	// segment-table variant) using the engine renderables, which is what actually
	// gets mech geometry onto the screen.  The BT-specific 2D reticle + weapon/
	// effect renderables (BTReticleRenderable, beams, tracers, searchlight) still
	// depend on the unported dpl2d_ layer and are deferred -- TODO(bring-up).
	//
	JointedMover *jointed_mover = (JointedMover *)entity;

	//
	//~~~~~~~~~~~~~~~~~~~~~~~
	// Inside or Outside view: pick skeleton variant + intersect mode/mask.
	//~~~~~~~~~~~~~~~~~~~~~~~
	//
	bool							inDeathZone;
	dpl_ISECT_MODE					intersect_mode;		// stub type (empty); kept for parity
	uint32							intersect_mask;
	EntitySegment::SkeletonType		skeletonType;

	//
	// DEBUG(bring-up): external chase camera.  The player POV mech is normally
	// built with the INSIDE skeleton -- the camera sits AT the cockpit eyepoint
	// with no world geometry ahead, so the frame is black.  To make the mech
	// BODY visible, treat the player's own mech as an OUTSIDE build (which loads
	// the full body geometry) and, after the renderable tree is built, install a
	// fixed external camera a few mech-heights in FRONT looking back at the mech.
	// Other (already-outside) mechs keep their normal no-camera body build.
	// TODO(bring-up): replace with a real spectator/chase view-mode toggle wired
	// through BTL4Application; see RECONCILE.md.
	//
	const bool buildDebugChaseCamera = (type == insideEntity);
	if (buildDebugChaseCamera)
		type = outsideEntity;

	if (type == insideEntity)
	{
		inDeathZone		= true;
		intersect_mask	= 0;
		skeletonType	= EntitySegment::SkeletonType_A;	// 4
	}
	else
	{
		inDeathZone		= false;
		intersect_mask	= INTERSECT_ALL;					// 0xffffffff
		skeletonType	= EntitySegment::SkeletonType_N;	// 0
	}

	//
	// Root renderable for this entity.  Its ctor calls AddRenderable(this) and
	// binds to entity->localToWorld, so the whole tree is driven from the entity
	// position every frame.
	//
	RootRenderable *this_root =
		new RootRenderable(
			entity, VideoRenderable::Dynamic, NULL,
			inDeathZone, intersect_mode, intersect_mask);

	//
	// Start (or reset) this mech's RemakeEntity bookkeeping: record the skeleton
	// variant now; the per-segment renderables + initial graphic states are filled
	// in as the tree is built below (see RemakeEntityRenderables).
	//
	MechRenderTree &render_tree = mMechRenderTrees[entity];
	render_tree = MechRenderTree();
	render_tree.skeletonType   = (int)skeletonType;
	render_tree.viewSkeleton   = (int)skeletonType;
	render_tree.rootRenderable = this_root;
	render_tree.wrecked        = 0;
	if (getenv("BT_DEATH_LOG"))
		DEBUG_STREAM << "[BTrender] tracking mech tree for entity " << (void*)entity
					 << " classID=" << entity->GetClassID() << " ("
					 << mMechRenderTrees.size() << " tracked)\n" << std::flush;

	//
	// Per-segment renderable array (the parent for each segment's children).
	//
	int segment_count = jointed_mover->segmentCount;			// [0x318]
	HierarchicalDrawComponent **dcs_array =
		new HierarchicalDrawComponent*[segment_count];
	for (int i = 0; i < segment_count; ++i)
		dcs_array[i] = NULL;

	// bring-up diagnostics (counts geometry actually loaded vs. requested)
	int dbg_obj_requested = 0, dbg_obj_loaded = 0, dbg_eye = 0;
	DEBUG_STREAM << "[BTrender] MakeMechRenderables: " << segment_count
				 << " segments, view=" << (int)type << "\n" << std::flush;

	JointSubsystem *joint_subsystem = jointed_mover->GetJointSubsystem();	// [0x31c]

	//
	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// Walk the EntitySegment table.  For each segment: build its offset matrix,
	// find its parent renderable, choose its joint renderable, load and hang its
	// geometry.  Site segments (eyepoint, gun ports) are handled specially.
	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	//
	EntitySegment::SegmentTableIterator segment_iterator(jointed_mover->segmentTable /* [0x300] */);
	EntitySegment *segment;

	while ((segment = segment_iterator.ReadAndNext()) != NULL)		// vtbl+0x28
	{
		LinearMatrix offset_matrix;
		offset_matrix = segment->GetBaseOffset();					// [0x74]

		//
		// Parent renderable: root if the segment has no parent, else the
		// renderable already built for its parent segment.
		//
		HierarchicalDrawComponent *parent_DCS;
		if (!segment->GetParent() /* [0xc4] */)
		{
			parent_DCS = this_root;
		}
		else
		{
			parent_DCS = dcs_array[segment->GetParentIndex() /* [0xc8] */];
		}

		//
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// Site segment?  The eyepoint site builds the camera (DPLEyeRenderable)
		// for the inside view; other sites carry no body geometry here (their
		// subsystem effects are deferred -- TODO(bring-up)).
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		//
		if (segment->IsSiteSegment() /* [0x10] */ != 0)
		{
			// The authentic COCKPIT EYEPOINT.  Built for the true inside view
			// AND for the player's chase build (buildDebugChaseCamera), so the
			// V-key toggle can switch to it -- the pod's only view was this
			// eyepoint; the chase camera is the port's usability addition.
			if ((type == insideEntity || buildDebugChaseCamera) &&
				strcmp((const char *)segment->GetName() /* [0x11c] */, "siteeyepoint") == 0)	// @0051d290
			{
				EulerAngles *eye_rot =
					(EulerAngles *)entity->GetAttributePointer("EyepointRotation");	// @0051d29d
				//
				// Mount the eye at the eyepoint's REST position but with a clean
				// UPRIGHT forward basis, parented on the tree ROOT: riding the
				// live joint chain fed the camera the site's authored tilt + the
				// torso pose (a permanently canted horizon).  The authentic pitch
				// /yaw came from the gyro-driven eye-joint chain (deferred); until
				// that lands, the cockpit view = eyepoint position + mech yaw.
				//
				AffineMatrix restToEntity = segment->GetSegmentToEntity();
				Point3D eyePos;
				eyePos = restToEntity;					// rest translation (W row)
				LinearMatrix eyeBasis(True);
				eyeBasis(0,0) = -1.0f; eyeBasis(0,1) = 0.0f; eyeBasis(0,2) =  0.0f;	// X row
				eyeBasis(1,0) =  0.0f; eyeBasis(1,1) = 1.0f; eyeBasis(1,2) =  0.0f;	// Y row (up)
				eyeBasis(2,0) =  0.0f; eyeBasis(2,1) = 0.0f; eyeBasis(2,2) = -1.0f;	// Z row (look = mech forward)
				eyeBasis(3,0) = (Scalar)eyePos.x;
				eyeBasis(3,1) = (Scalar)eyePos.y;
				eyeBasis(3,2) = (Scalar)eyePos.z;
				mEyeCockpit = new DPLEyeRenderable(
					entity, eyeBasis, this_root, eye_rot);
				if (type == insideEntity)			// true inside build: it IS the camera
					mCamera = mEyeCockpit;
				dbg_eye = 1;
			}
			continue;
		}

		//
		// Load this segment's geometry (skeleton-variant .bgf), if any.
		//
		d3d_OBJECT *this_object = NULL;
		// Select the segment's model VARIANT by its damage zone's graphic state.
		// The engine keys video-object names by {skeleton, damage_graphic_state}
		// (SEGMENT.h:172): a Destroyed zone (GetGraphicState()==1) returns the
		// destroyed/damaged model, so a wrecked segment visibly comes apart.  The
		// recon previously passed ONLY the skeleton type, leaving the state at its
		// default 0 (Exists) -> always the intact model = no visible damage.
		Enumeration seg_gstate = 0;						// ExistsGraphicState
		{
			int zone_index = segment->GetPrimaryDamageZone();			// SEGMENT.h:107 (a zone INDEX)
			if (zone_index >= 0 && zone_index < entity->damageZoneCount
				&& entity->damageZones[zone_index] != 0)
				seg_gstate = entity->damageZones[zone_index]->GetGraphicState();	// DAMAGE.h:196
		}
		CString *object_name = segment->GetVideoObjectName(skeletonType, seg_gstate);	// FUN_00424084
		if (object_name != NULL)
		{
			char filename[44];
			strcpy(filename, (const char *)*object_name);
			int len = (int)strlen(filename);
			if (len >= 4)
				filename[len - 4] = '\0';					// strip ".bgf"
			strcat(filename, ".bgf");						// d3d_OBJECT::LoadObject wants the extension
			this_object = d3d_OBJECT::LoadObject(GetDevice(), filename);
			++dbg_obj_requested;
			if (this_object != NULL) ++dbg_obj_loaded;
			else DEBUG_STREAM << "[BTrender]   no mesh for '" << filename
							  << "' (expects VIDEO\\*.x)\n" << std::flush;
			// SHADOW PROXY (task #20): the binary's shadow is the flat *_tshd.bgf
			// silhouette posed by jointshadow/jointtshadow (model-record
			// ShadowJointName @0xB4, part_012.c:10285).  Tag it so d3d_OBJECT
			// draws it translucent in the blend pass instead of opaque black;
			// alphaTest=true routes it there (HierarchicalDrawComponent::Execute,
			// L4VIDRND.cpp:149, schedules the pass per-drawOp on alphaTest).
			if (this_object != NULL && strstr(filename, "tshd") != NULL)
			{
				this_object->SetIsShadow(1);
				for (int op = 0; op < this_object->GetDrawOpCount(); ++op)
					this_object->GetDrawOp(op)->alphaTest = true;
			}
		}

		//
		// Determine joint type: static (-1 -> Static) or look up the joint in
		// the JointSubsystem's joint table.
		//
		int segment_slot = segment->GetIndex() /* [0xcc] */;
		Joint *this_joint = NULL;
		Joint::JointType joint_type;
		if (segment->GetJointIndex() /* [0xc0] */ == -1)
		{
			joint_type = Joint::StaticJointType;				// 3
		}
		else
		{
			this_joint = joint_subsystem->GetJoint(segment->GetJointIndex());	// FUN_0041d3b3
			joint_type = this_joint->GetJointType() /* [0x10] */;
		}

		//
		// Build the appropriate engine joint renderable, recording it in the
		// per-segment array so children can parent to it.
		//
		HierarchicalDrawComponent *child;
		switch (joint_type)
		{
			case Joint::BallJointType:						// 4
			{
				child = new BallJointRenderable(
					entity, VideoRenderable::Dynamic, this_object,
					inDeathZone, intersect_mode, intersect_mask,
					parent_DCS, &offset_matrix, &this_joint->GetEulerAngles());
				break;
			}
			case Joint::BallTranslationJointType:			// 5
			{
				child = new BallTranslateJointRenderable(
					entity, VideoRenderable::Dynamic, this_object,
					inDeathZone, intersect_mode, intersect_mask,
					parent_DCS, &offset_matrix,
					&this_joint->GetEulerAngles(), &this_joint->GetTranslation());
				break;
			}
			case Joint::StaticJointType:					// 3
			{
				child = new DPLStaticChildRenderable(
					entity, inDeathZone, this_object,
					intersect_mode, intersect_mask, offset_matrix, parent_DCS);
				break;
			}
			default:										// 0,1,2 HingeX/Y/Z
			{
				child = new HingeRenderable(
					entity, VideoRenderable::Dynamic, this_object,
					inDeathZone, intersect_mode, intersect_mask,
					parent_DCS, &offset_matrix, &this_joint->GetHinge() /* [0xc] */);
				break;
			}
		}
		dcs_array[segment_slot] = child;

		// Record this segment's renderable + the graphic state it was built with,
		// so a later damage-state change can swap its mesh in place (RemakeEntity).
		render_tree.segRenderable[segment_slot] = child;
		render_tree.segGState[segment_slot]     = (int)seg_gstate;
	}

	delete [] dcs_array;

	DEBUG_STREAM << "[BTrender] mech tree built: meshes " << dbg_obj_loaded
				 << "/" << dbg_obj_requested << " loaded, eye=" << dbg_eye << "\n" << std::flush;

	//
	// If this mech DIED before its tree was built (a fast kill during mission
	// creation), apply the remembered wreck swap now.
	//
	{
		extern int BTTakePendingWreck(Entity *entity);
		if (BTTakePendingWreck(entity))
			SwapToWreck(entity);
	}

	//
	// The TARGETING RETICLE (the main-view HUD): built for the player's mech,
	// per the 1996 inside-view path (@part_014.c:5127-5158 constructs the
	// 0x358 BTReticleRenderable, then :5390-5436 registers one pip per weapon
	// from its TargetWithinRange / WeaponRange / PipPosition / PipColor /
	// PipExtendedRange / SimulationState attributes).  Drawn by BTDrawReticle
	// in the cockpit view only.
	//
	if (buildDebugChaseCamera)
	{
		extern BTReticleRenderable *BTBuildReticle(Entity *mech);
		BTBuildReticle(entity);
	}

	// DEV: BT_START_INSIDE=1 begins in the cockpit view (also exercises the
	// inside-skeleton swap headlessly).
	if (buildDebugChaseCamera && getenv("BT_START_INSIDE"))
		SetViewInside(1);

	//
	// TODO(bring-up): inside-view targeting reticle (BTReticleRenderable +
	// AddWeapon pips) and the per-subsystem weapon/effect renderables (PPC/
	// emitter beams, projectile tracers, coolant, searchlight) are NOT built
	// here yet -- they depend on the dpl2d_ 2D display-list layer (stubbed) and
	// the BT effect renderables (stubbed).  Adding them is the HUD / weapons
	// render bring-up step; they are not required to get the mech body drawn.
	//

	//
	// DEBUG(bring-up): install the fixed external chase camera for the player's
	// own mech.  The eye renderable parents on this_root, so when the render
	// tree executes (RootRenderable::Execute pushes entity->localToWorld onto the
	// matrix stack) the camera's offset is composed with the mech's world matrix
	// -- i.e. the camera tracks the mech.  DPLEyeRenderable looks down its local
	// +Z axis (row 2) with local +Y as up (row 1) from its translation (row 3);
	// D3DXMatrixLookAtRH re-derives the basis from pos/at/up.
	//
	// Mech-local frame (from the .skl): +Y up, the mech FACES -Z (gun ports /
	// eyepoint are at -Z), and the Avatar is ~10-12 units tall (hip at y~5.3,
	// eyepoint ~y9).  Place the camera in FRONT (-Z) and above, looking back
	// toward the mech centre.
	//
	if (buildDebugChaseCamera)
	{
		// CHASE view (task #15 usability): the mech faces -Z, so the original
		// debug placement (camera at -Z, "in front, looking back at its face")
		// made W walk the mech TOWARD the viewer -- hopelessly disorienting to
		// drive.  Default is now BEHIND (+Z) and above, looking forward over the
		// shoulder: press forward, the mech walks away from you; turns read
		// correctly.  env BT_CAM=face restores the old face-on animation view.
		const char *camMode = getenv("BT_CAM");
		const bool faceView = (camMode != 0 && camMode[0] == 'f');
		float camPx = 0.0f,  camPy = faceView ? 9.0f : 11.0f;
		float camPz = faceView ? -28.0f : 28.0f;				// -Z front / +Z behind
		const float tgtX  = 0.0f,  tgtY  = 6.0f;
		const float tgtZ  = faceView ? 0.0f : -6.0f;				// chase: look ahead of the mech
		// DEBUG(bring-up): BT_CAM_Y / BT_CAM_Z override the fixed chase-camera offset --
		// raising it clears mound-shoulder OCCLUSION, but NOT genuine geometry clipping
		// where the mech is stopped on a steep slope and the terrain rises through its
		// legs (that is a collision-vs-visual issue, not a camera one).
		if (const char *cy = getenv("BT_CAM_Y")) camPy = (float)atof(cy);
		if (const char *cz = getenv("BT_CAM_Z")) camPz = (float)atof(cz);

		// look direction (local +Z of the camera) = normalize(target - pos)
		float zx = tgtX - camPx, zy = tgtY - camPy, zz = tgtZ - camPz;
		float zl = (float)sqrt(zx*zx + zy*zy + zz*zz);
		if (zl < 1e-6f) zl = 1.0f;
		zx /= zl; zy /= zl; zz /= zl;

		// world up
		const float ux = 0.0f, uy = 1.0f, uz = 0.0f;

		// right (local +X) = up x forward
		float xx = uy*zz - uz*zy, xy = uz*zx - ux*zz, xz = ux*zy - uy*zx;
		float xl = (float)sqrt(xx*xx + xy*xy + xz*xz);
		if (xl < 1e-6f) xl = 1.0f;
		xx /= xl; xy /= xl; xz /= xl;

		// recomputed up (local +Y) = forward x right
		float yx = zy*xz - zz*xy, yy = zz*xx - zx*xz, yz = zx*xy - zy*xx;

		LinearMatrix debugOffset;	// identity
		debugOffset(0,0) = xx;    debugOffset(0,1) = xy;    debugOffset(0,2) = xz;	// X row
		debugOffset(1,0) = yx;    debugOffset(1,1) = yy;    debugOffset(1,2) = yz;	// Y row (up)
		debugOffset(2,0) = zx;    debugOffset(2,1) = zy;    debugOffset(2,2) = zz;	// Z row (look)
		debugOffset(3,0) = camPx; debugOffset(3,1) = camPy; debugOffset(3,2) = camPz;	// W row (pos)

		mEyeChase = new DPLEyeRenderable(entity, debugOffset, this_root, NULL);
		// Respect the pilot's CHOSEN view across renderable rebuilds: this build
		// used to stomp mCamera back to chase on every remake (damage swaps,
		// start-inside), silently flipping the active eye out from under the V
		// toggle (and the aim-ray camera feed with it).
		mCamera = (mViewInside && mEyeCockpit != 0) ? mEyeCockpit : mEyeChase;
		DEBUG_STREAM << "[BTrender] external debug chase camera installed at ("
					 << camPx << "," << camPy << "," << camPz << ") looking at ("
					 << tgtX << "," << tgtY << "," << tgtZ << ") -- V toggles the cockpit eyepoint"
					 << (mEyeCockpit ? "" : " (COCKPIT EYE MISSING)") << "\n" << std::flush;
	}

	return this_root;
}


//
//#############################################################################
// RemakeEntityRenderables  (the render "RemakeEntity" state -- damage swap)
//#############################################################################
//
// A damage zone's graphic state changed (a segment became Destroyed or Gone).
// Walk this mech's segments and, for any whose graphic state now differs from
// what its renderable was built with, re-pick the video-object variant by the
// new graphic state and swap it onto the joint renderable IN PLACE.  Execute()
// re-reads graphicalObject each frame, so the wrecked mesh shows next frame.
// No teardown: the component dtor does not cascade to children (L4VIDRND.cpp:104),
// so a rebuild would leak -- the authentic behaviour is an in-place mesh swap.
//
void
	BTL4VideoRenderer::RemakeEntityRenderables(Entity *entity)
{
	std::map<Entity*, MechRenderTree>::iterator tree_it =
		mMechRenderTrees.find(entity);
	if (tree_it == mMechRenderTrees.end())
	{
		if (getenv("BT_DEATH_LOG"))
			DEBUG_STREAM << "[BTrender] RemakeEntity: no render tree for entity "
						 << (void*)entity << " (" << mMechRenderTrees.size()
						 << " tracked)\n" << std::flush;
		return;						// tree not built yet -- Make will read the state
	}
	MechRenderTree &render_tree = tree_it->second;
	if (render_tree.wrecked)
		return;						// already the dbr hulk -- nothing left to swap

	JointedMover *jointed_mover = (JointedMover *)entity;
	EntitySegment::SkeletonType skeletonType =
		(EntitySegment::SkeletonType)render_tree.viewSkeleton;	// the DISPLAYED set

	EntitySegment::SegmentTableIterator segment_iterator(jointed_mover->segmentTable);
	EntitySegment *segment;
	int swapped = 0, checked = 0, mapped = 0;

	while ((segment = segment_iterator.ReadAndNext()) != NULL)
	{
		if (segment->IsSiteSegment() != 0)
			continue;
		++checked;

		int segment_slot = segment->GetIndex();
		std::map<int, HierarchicalDrawComponent*>::iterator r =
			render_tree.segRenderable.find(segment_slot);
		if (r == render_tree.segRenderable.end() || r->second == NULL)
			continue;
		++mapped;

		//
		// Current graphic state for this segment (from its damage zone).
		//
		Enumeration seg_gstate = 0;						// ExistsGraphicState
		int zone_index = segment->GetPrimaryDamageZone();
		if (zone_index >= 0 && zone_index < entity->damageZoneCount
			&& entity->damageZones[zone_index] != 0)
			seg_gstate = entity->damageZones[zone_index]->GetGraphicState();

		if ((int)seg_gstate == render_tree.segGState[segment_slot])
			continue;									// unchanged -- nothing to swap
		render_tree.segGState[segment_slot] = (int)seg_gstate;

		//
		// Re-pick + load the segment's video-object variant for the new graphic
		// state (same construction as the initial build in MakeMechRenderables).
		//
		CString *object_name = segment->GetVideoObjectName(skeletonType, seg_gstate);
		if (getenv("BT_DEATH_LOG"))
			DEBUG_STREAM << "[BTrender]   seg '" << (const char *)segment->GetName()
						 << "' slot " << segment_slot << " -> gstate " << (int)seg_gstate
						 << " variant=" << (object_name ? (const char *)*object_name : "(none)")
						 << "\n" << std::flush;
		d3d_OBJECT *new_object = NULL;
		if (object_name != NULL)
		{
			char filename[44];
			strcpy(filename, (const char *)*object_name);
			int len = (int)strlen(filename);
			if (len >= 4)
				filename[len - 4] = '\0';				// strip ".bgf"
			strcat(filename, ".bgf");
			new_object = d3d_OBJECT::LoadObject(GetDevice(), filename);
			if (new_object == NULL && getenv("BT_DEATH_LOG"))
				DEBUG_STREAM << "[BTrender]   damaged variant '" << filename
							 << "' FAILED to load (expects VIDEO\\*.x)\n" << std::flush;
			if (new_object != NULL && strstr(filename, "tshd") != NULL)
			{
				new_object->SetIsShadow(1);
				for (int op = 0; op < new_object->GetDrawOpCount(); ++op)
					new_object->GetDrawOp(op)->alphaTest = true;
			}
		}

		//
		// GoneGraphicState (blown off): no mesh -> hide the segment.  Destroyed/
		// Exists: swap to the variant if it loaded; otherwise keep the current
		// mesh (don't blank a segment merely because a damaged .bgf is missing).
		//
		if (new_object != NULL)
			r->second->SetDrawObj(new_object);
		else if ((int)seg_gstate == DamageZone::GoneGraphicState)
			r->second->SetDrawObj(NULL);

		++swapped;
	}

	if (swapped != 0 || getenv("BT_DEATH_LOG"))
		DEBUG_STREAM << "[BTrender] RemakeEntity: " << swapped
					 << " mesh(es) swapped (" << mapped << " body segs mapped of "
					 << checked << " checked)\n" << std::flush;
}


//
//#############################################################################
// BTRemakeMechModel  (sim-side bridge -- see btl4vid.hpp)
//#############################################################################
//
// Reaches the live renderer and refreshes a mech's visible model after its
// damage graphic state changed.  Called from MechDeathHandler (sim TU).  The
// frame loop is single-threaded (sim + render share the main thread; only the
// network RX socket runs on its own thread), so loading geometry here is safe.
//
void BTRemakeMechModel(Entity *entity)
{
	if (entity == NULL || application == NULL)
		return;
	BTL4VideoRenderer *renderer =
		(BTL4VideoRenderer *)application->GetVideoRenderer();
	if (renderer != NULL)
		renderer->RemakeEntityRenderables(entity);
}


//
//#############################################################################
// SwapToWreck  (ExplosionScripts effect 104, reconstructed)
//#############################################################################
//
// The authentic death chain: the victim's per-mech death ModelList
// ('blhdead'/'lokdead'/... resources 22-25) dispatches effect 104, whose 1996
// script (@0045xxxx, part_008.c:2663 case 4) swaps in the burning WRECK: the
// destroyed hulk mesh + flame meshes with flicker sweeps and a slow settle.
// The 2007 port stubbed the whole script layer.  This reconstruction does the
// core swap: hide every segment mesh and hang the victim's own "<prefix>dbr"
// hulk on the tree root (the 1996 code hardcoded thrdbr.bgf -- a dev shortcut;
// every mech ships its hulk: BLHDBR/MADDBR/LOKDBR/... + GENDBR the generic
// fallback).  Burning comes from the effect layer (the death list also fires
// the 1007 boom + 1001 smoke plume; the wreck re-arms the plume while it
// stands).  Mesh flames (flamesml/flamebig + sweep flicker) are a noted
// follow-up.
//
static std::map<Entity*, int> gBTPendingWrecks;

int BTTakePendingWreck(Entity *entity)
{
	std::map<Entity*, int>::iterator it = gBTPendingWrecks.find(entity);
	if (it == gBTPendingWrecks.end())
		return 0;
	gBTPendingWrecks.erase(it);
	return 1;
}

void
	BTL4VideoRenderer::SwapToWreck(Entity *victim)
{
	std::map<Entity*, MechRenderTree>::iterator tree_it =
		mMechRenderTrees.find(victim);
	if (tree_it == mMechRenderTrees.end())
	{
		gBTPendingWrecks[victim] = 1;			// died before the tree was built
		return;
	}
	MechRenderTree &render_tree = tree_it->second;
	if (render_tree.wrecked)
		return;

	//
	// The victim's model prefix, from any segment's intact video-object name
	// (e.g. "blh_rfot.bgf" -> "blh" -> "blhdbr.bgf").
	//
	char hulk_name[44];
	hulk_name[0] = '\0';
	{
		JointedMover *jm = (JointedMover *)victim;
		EntitySegment::SegmentTableIterator it(jm->segmentTable);
		EntitySegment *segment;
		while ((segment = it.ReadAndNext()) != NULL)
		{
			if (segment->IsSiteSegment() != 0)
				continue;
			CString *nm = segment->GetVideoObjectName(
				(EntitySegment::SkeletonType)render_tree.skeletonType, 0);
			if (nm != NULL && strlen((const char *)*nm) >= 3)
			{
				strncpy(hulk_name, (const char *)*nm, 3);
				hulk_name[3] = '\0';
				strcat(hulk_name, "dbr.bgf");
				break;
			}
		}
	}
	d3d_OBJECT *hulk = (hulk_name[0] != '\0')
		? d3d_OBJECT::LoadObject(GetDevice(), hulk_name) : NULL;
	if (hulk == NULL)
	{
		DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
					 << "' missing -> gendbr.bgf fallback\n" << std::flush;
		hulk = d3d_OBJECT::LoadObject(GetDevice(), "gendbr.bgf");
	}

	//
	// The strewn-debris field that accompanies the standing hulk (the 1996
	// script pairs them: thrdbr + ldbr, parented together, sinking together).
	//
	d3d_OBJECT *debris = d3d_OBJECT::LoadObject(GetDevice(), "ldbr.bgf");

	//
	// Hide the body; hang the wreck pieces on the tree root (identity offset --
	// the root renderable already pushes the wreck's localToWorld, so they sit
	// at the mech's ground position with its death yaw).
	//
	for (std::map<int, HierarchicalDrawComponent*>::iterator r =
			render_tree.segRenderable.begin();
		 r != render_tree.segRenderable.end(); ++r)
	{
		if (r->second != NULL)
			r->second->SetDrawObj(NULL);
	}
	if (render_tree.rootRenderable != NULL)
	{
		dpl_ISECT_MODE	isect_mode;
		LinearMatrix	identity(True);
		if (hulk != NULL)
			render_tree.wreckHulk = new DPLStaticChildRenderable(
				victim, false /* main zone */, hulk,
				isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
		if (debris != NULL)
			render_tree.wreckDebris = new DPLStaticChildRenderable(
				victim, false /* main zone */, debris,
				isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
	}
	render_tree.wrecked  = 1;
	render_tree.wreckAge = 0.0f;
	DEBUG_STREAM << "[BTrender] wreck swap: victim -> '"
				 << (hulk_name[0] ? hulk_name : "gendbr.bgf")
				 << (hulk ? "'" : "' (LOAD FAILED -- body hidden only)")
				 << (debris ? " + ldbr debris" : "")
				 << "\n" << std::flush;
}


//
//#############################################################################
// TickWreck -- the wreck's quadratic SINK (the 1996 burial)
//#############################################################################
//
// FUN_00456410 (the 1996 sink renderable): offsetY = rate * t^2, hulk rate
// -0.025 (armed 0.25s after the boom by a sweep trigger).  The ~7-unit hulk is
// fully underground ~17s after the kill -- the wreck visual "fades away" by
// burial; the ENTITY (sim/collision) stays, per the wreck-stays rule.  Once
// buried, the pieces are hidden and the sink stops.
//
int
	BTL4VideoRenderer::TickWreck(Entity *victim, float dt)
{
	std::map<Entity*, MechRenderTree>::iterator tree_it =
		mMechRenderTrees.find(victim);
	if (tree_it == mMechRenderTrees.end())
		return 1;										// no tree yet -- not buried
	MechRenderTree &render_tree = tree_it->second;
	if (!render_tree.wrecked)
		return 1;										// not swapped yet
	if (render_tree.wreckHulk == NULL && render_tree.wreckDebris == NULL)
		return 0;										// already buried

	render_tree.wreckAge += dt;
	float sink = -0.025f * render_tree.wreckAge * render_tree.wreckAge;	// the authored rate
	if (sink < -8.0f)
	{
		// fully buried -> hide + stop ticking
		if (render_tree.wreckHulk)   render_tree.wreckHulk->SetDrawObj(NULL);
		if (render_tree.wreckDebris) render_tree.wreckDebris->SetDrawObj(NULL);
		render_tree.wreckHulk   = NULL;
		render_tree.wreckDebris = NULL;
		DEBUG_STREAM << "[BTrender] wreck buried (sink complete)\n" << std::flush;
		return 0;
	}
	if (render_tree.wreckHulk)
		render_tree.wreckHulk->SetOffsetTranslation(0.0f, sink, 0.0f);
	if (render_tree.wreckDebris)
		render_tree.wreckDebris->SetOffsetTranslation(0.0f, sink, 0.0f);
	return 1;
}


//
// Sim-side bridge (UpdateDeathState drives the sink each dead frame).
//
int BTWreckSinkTick(Entity *victim, float dt)
{
	if (victim == NULL || application == NULL)
		return 1;
	BTL4VideoRenderer *renderer =
		(BTL4VideoRenderer *)application->GetVideoRenderer();
	if (renderer == NULL)
		return 1;
	return renderer->TickWreck(victim, dt);
}


//
// Engine-side bridge (the ExplosionClassID dispatch calls this on effect 104).
//
void BTSwapMechToWreck(Entity *victim)
{
	if (victim == NULL || application == NULL)
		return;
	BTL4VideoRenderer *renderer =
		(BTL4VideoRenderer *)application->GetVideoRenderer();
	if (renderer != NULL)
		renderer->SwapToWreck(victim);
}


//
//#############################################################################
// BTReticleRenderable -- ctor (@004cc40c) + Draw (Execute @004cdcf0 is in an
// un-exported gap; the draw dynamics here are [T3], the GLYPHS are [T1])
//#############################################################################
//
// The live reticle instance (the player's; drawn by BTDrawReticle in the
// cockpit view only).
//
static BTReticleRenderable *gBTReticle    = 0;
extern Scalar               gBTHudRangeStorage;		// live target range (defined below)
static int                  gBTHudInside  = 0;		// cockpit view live (set by SetViewInside)

// the dpl2d rasteriser (dpl2d.cpp; device type opaque here)
extern void dpl2d_ExecuteList(dpl2d_DISPLAY *list, struct IDirect3DDevice9 *device);

void BTSetHudTargetRange(Scalar range)	{ gBTHudRangeStorage = range; }
void BTSetHudInside(int inside)			{ gBTHudInside = inside; }

//
// FUN_004cd938 -- the tick-LADDER builder: `count` ticks stepped along an axis
// (dir 0/1 = +x/-x travel with vertical ticks, 2/3 = +y/-y with horizontal
// ticks), a MAJOR tick (halfMajor) every (majorEvery+1)th, minor otherwise.
//
static void
	BTReticleTickLadder(dpl2d_DISPLAY *list, int count, int majorEvery,
		float span, float x0, float y0, int dir, float halfMinor, float halfMajor)
{
	float step = span / (float)(count - 1);
	float tickMinX = 0, tickMinY = 0, tickMajX = 0, tickMajY = 0;
	float stepX = 0, stepY = 0;
	switch (dir)
	{
	case 0: stepX =  step; tickMajY = halfMajor; tickMinY = halfMinor; break;
	case 1: stepX = -step; tickMajY = halfMajor; tickMinY = halfMinor; break;
	case 2: stepY =  step; tickMajX = halfMajor; tickMinX = halfMinor; break;
	case 3: stepY = -step; tickMajX = halfMajor; tickMinX = halfMinor; break;
	}
	float x = x0, y = y0;
	int untilMajor = 0;
	dpl2d_OpenLines(list);
	for (int i = 0; i < count; ++i)
	{
		if (untilMajor == 0)
		{
			dpl2d_AddPoint(list, x + tickMajX, y + tickMajY);
			dpl2d_AddPoint(list, x - tickMajX, y - tickMajY);
			untilMajor = majorEvery;
		}
		else
		{
			dpl2d_AddPoint(list, x + tickMinX, y + tickMinY);
			dpl2d_AddPoint(list, x - tickMinX, y - tickMinY);
			--untilMajor;
		}
		x += stepX; y += stepY;
	}
	dpl2d_CloseLines(list);
}

//
// The authentic calibration constants (the binary ctor's own values).
//
static const float kRetOriginX   = 0.35f;	// [0x7f] right range-ladder x
static const float kRetOriginY   = 0.25f;	// [0x80] ladder bottom y
static const float kRetScaleY    = 0.5f;	// [0x81] ladder span
static const float kRetTickMinor = 0.008f;	// [0x83]
static const float kRetTickMajor = 0.016f;	// [0x82]
static const float kRetBotX      = -0.25f;	// [0x84] bottom heading-ladder x0
static const float kRetBotY      = 0.35f;	// [0x85] heading ladder y
static const float kRetBotSpan   = 0.5f;	// [0x86]
static const float kRetCaret     = 0.02f;	// _DAT_004cd7f4 (caret triangle size) [T3]
static const int   kRetTicksR    = 13;		// [9]  right ladder tick count
static const int   kRetTicksB    = 21;		// [0xb] bottom ladder tick count
static const float kRetMaxRange  = 1200.0f;	// ctor param 11 (0x44960000)

BTReticleRenderable::BTReticleRenderable(Entity *entity, Scalar *range_attr)
	: VideoRenderable(entity, VideoRenderable::Dynamic)
{
	weaponCount = 0;
	originX   = kRetOriginX;   originY = kRetOriginY;
	scaleY    = kRetScaleY;    biasX   = kRetTickMajor;
	maxRange  = kRetMaxRange;  minRange = 0.0f;
	rangeScale= maxRange - minRange;			// [0x8d] -- AddWeapon divides by it
	rangeAttr2= range_attr;
	pipsBuilt  = 0;								// recovered-Execute dynamic state
	lockShown  = 0;
	lockSpinDeg= 0.0f;

	masterList   = dpl2d_NewDisplayList();
	simpleXList  = dpl2d_NewDisplayList();
	aimDotList   = dpl2d_NewDisplayList();
	rangeCaretR  = dpl2d_NewDisplayList();
	rangeCaretB  = dpl2d_NewDisplayList();
	headingList  = dpl2d_NewDisplayList();
	bottomAnchor = dpl2d_NewDisplayList();
	leftArrow    = dpl2d_NewDisplayList();
	rightArrow   = dpl2d_NewDisplayList();
	crossList    = dpl2d_NewDisplayList();
	subB6 = dpl2d_NewDisplayList();
	subB7 = dpl2d_NewDisplayList();
	subB8 = dpl2d_NewDisplayList();
	subB9 = dpl2d_NewDisplayList();
	subBA = dpl2d_NewDisplayList();

	//
	// The MASTER list (@4511-4601, faithfully transcribed).  Colours: green
	// 0.75 for the frame, yellow for the range caret; widths 1/2/3.
	//
	dpl2d_DISPLAY *m = masterList;
	dpl2d_Begin(m, 1);
	dpl2d_SetLineWidth(m, 1.0f);
	dpl2d_FullScreenClip(m);
	dpl2d_SetColor(m, 0.75f, 0.0f, 0.0f);
	dpl2d_CallList(m, crossList);				// [0xa1] target-box slot (empty until lock;
												//   Draw rebuilds it: designator ring at the
												//   target's screen point / off-screen arrows)
	dpl2d_SetColor(m, 0.0f, 0.75f, 0.0f);
	// the AIM GROUP (task #36): [0x9a] is the aim TRANSLATE -- CallList state
	// persists to the caller (the dpl2d inline-include semantic), so the slew
	// translate it carries positions the dot + crosses that follow.  The
	// PushState/PopState pair contains it so the fixed frame (tick ladders,
	// tapes) stays put.  [T3 -- the binary Execute is un-exported; mechanism
	// per the engine ReticleRenderable's position-list pattern, T0.]
	dpl2d_PushState(m);
	dpl2d_CallList(m, aimDotList);				// [0x9a] the aim translate
	dpl2d_OpenPolypoint(m);						// centre dot
	dpl2d_AddPoint(m, 0.0f, 0.0f);
	dpl2d_ClosePolypoint(m);
	dpl2d_OpenLines(m);							// inner cross (gap at centre)
	dpl2d_AddPoint(m,  0.04f, 0.0f);  dpl2d_AddPoint(m,  0.10f, 0.0f);
	dpl2d_AddPoint(m, -0.04f, 0.0f);  dpl2d_AddPoint(m, -0.10f, 0.0f);
	dpl2d_AddPoint(m, 0.0f,  0.04f);  dpl2d_AddPoint(m, 0.0f,  0.10f);
	dpl2d_AddPoint(m, 0.0f, -0.04f);  dpl2d_AddPoint(m, 0.0f, -0.10f);
	dpl2d_CloseLines(m);
	dpl2d_SetLineWidth(m, 3.0f);
	dpl2d_OpenLines(m);							// heavy outer cross
	dpl2d_AddPoint(m,  0.10f, 0.0f);  dpl2d_AddPoint(m,  0.16f, 0.0f);
	dpl2d_AddPoint(m, -0.10f, 0.0f);  dpl2d_AddPoint(m, -0.16f, 0.0f);
	dpl2d_AddPoint(m, 0.0f,  0.10f);  dpl2d_AddPoint(m, 0.0f,  0.16f);
	dpl2d_AddPoint(m, 0.0f, -0.10f);  dpl2d_AddPoint(m, 0.0f, -0.16f);
	dpl2d_CloseLines(m);
	dpl2d_PopState(m);							// contain the aim translate
	dpl2d_SetLineWidth(m, 1.0f);
	// the RIGHT range ladder (13 ticks up the right side, dir 3 = -y travel)
	BTReticleTickLadder(m, kRetTicksR, 1, kRetScaleY,
		kRetOriginX, kRetOriginY, 3, kRetTickMinor, kRetTickMajor);
	// the range CARET (yellow triangle; its called list holds the live
	// translate the Draw rebuilds each frame from the target range)
	dpl2d_PushState(m);
	dpl2d_SetLineWidth(m, 2.0f);
	dpl2d_SetColor(m, 0.75f, 0.75f, 0.0f);
	dpl2d_CallList(m, rangeCaretR);
	dpl2d_OpenPolyline(m);
	dpl2d_AddPoint(m, kRetOriginX - kRetTickMajor,             kRetOriginY);
	dpl2d_AddPoint(m, kRetOriginX - kRetCaret - kRetTickMajor, kRetOriginY + kRetCaret);
	dpl2d_AddPoint(m, kRetOriginX - kRetCaret - kRetTickMajor, kRetOriginY - kRetCaret);
	dpl2d_ClosePolyline(m);
	dpl2d_PopState(m);
	// the BOTTOM heading ladder (21 ticks across, dir 0 = +x travel)
	BTReticleTickLadder(m, kRetTicksB, 1, kRetBotSpan,
		kRetBotX, kRetBotY, 0, kRetTickMinor, kRetTickMajor);
	// the heading carets (over/under triangles at the ladder centre, shifted
	// by the called heading translate)
	{
		float cx = kRetBotX + kRetBotSpan * 0.5f;	// _DAT_004cd7f8 = 0.5 [T1]
		dpl2d_PushState(m);
		dpl2d_SetLineWidth(m, 2.0f);
		dpl2d_SetColor(m, 0.75f, 0.75f, 0.0f);
		dpl2d_CallList(m, rangeCaretB);
		dpl2d_OpenPolyline(m);
		dpl2d_AddPoint(m, cx,             kRetBotY - kRetTickMajor);
		dpl2d_AddPoint(m, cx + kRetCaret, kRetBotY - kRetCaret - kRetTickMajor);
		dpl2d_AddPoint(m, cx - kRetCaret, kRetBotY - kRetCaret - kRetTickMajor);
		dpl2d_ClosePolyline(m);
		dpl2d_OpenPolyline(m);
		dpl2d_AddPoint(m, cx,             kRetBotY + kRetTickMajor);
		dpl2d_AddPoint(m, cx + kRetCaret, kRetBotY + kRetCaret + kRetTickMajor);
		dpl2d_AddPoint(m, cx - kRetCaret, kRetBotY + kRetCaret + kRetTickMajor);
		dpl2d_ClosePolyline(m);
		dpl2d_PopState(m);
	}
	// COMPASS group (Execute @4ce6e0-4ce7e4 [T1]): [0x278] bottomAnchor holds a
	// rotate(CompassHeading rad->deg) + translate to (botX, botY - 3*tickMajor
	// - 0.03) -- the compass rose (circle + north stem, authored at the origin)
	// spins with the mech heading at the bottom-left of the twist tape.  The
	// THREAT trail [0x2e8] draws in the same frame: 0.05-unit direction marks
	// from the compass centre toward recent damage sources (fresh = red, aging
	// = yellow, expired at 6s -- Execute @4ce3e2-4ce6ce [T1]).
	dpl2d_PushState(m);
	dpl2d_CallList(m, bottomAnchor);			// [0x9e] the compass rotate+translate
	dpl2d_Circle(m, 0.0f, 0.0f, 0.03f, 0);		// the rose ring [T3 radius]
	dpl2d_OpenLines(m);
	dpl2d_AddPoint(m, 0.0f, -0.04f);			// the north stem
	dpl2d_AddPoint(m, 0.0f, -0.005f);
	dpl2d_CloseLines(m);
	dpl2d_CallList(m, subBA);					// [0xba] the threat-direction trail
	dpl2d_PopState(m);
	dpl2d_CallList(m, subB6);					// [0xb6] the composed weapon pips
	dpl2d_CallList(m, headingList);				// [0x9d] the LOCK-RING SPIN matrix (4 deg/frame)
	dpl2d_CallList(m, subB7);					// [0xb7] the lock-ring slot (subB9 when locked)
	dpl2d_End(m);
	dpl2d_Compile(m);

	//
	// The green centre-ring sub-lists ([0xb8]/[0xb9]) -- the lock indicator
	// rings the binary Execute swaps in on target state.
	//
	dpl2d_Begin(subB8, 1);
	dpl2d_SetColor(subB8, 0.0f, 0.75f, 0.0f);
	dpl2d_Circle(subB8, 0.0f, 0.0f, 0.12f, 0);
	dpl2d_End(subB8); dpl2d_Compile(subB8);
	dpl2d_Begin(subB9, 1);
	dpl2d_SetColor(subB9, 0.0f, 0.75f, 0.0f);
	dpl2d_Circle(subB9, 0.0f, 0.0f, 0.12f, 0);
	dpl2d_OpenLines(subB9);
	dpl2d_AddPoint(subB9,  0.14f, 0.0f);  dpl2d_AddPoint(subB9,  0.10f, 0.0f);
	dpl2d_AddPoint(subB9, -0.14f, 0.0f);  dpl2d_AddPoint(subB9, -0.10f, 0.0f);
	dpl2d_AddPoint(subB9, 0.0f,  0.14f);  dpl2d_AddPoint(subB9, 0.0f,  0.10f);
	dpl2d_AddPoint(subB9, 0.0f, -0.14f);  dpl2d_AddPoint(subB9, 0.0f, -0.10f);
	dpl2d_CloseLines(subB9);
	dpl2d_End(subB9); dpl2d_Compile(subB9);

	//
	// The off-screen turn ARROWS ([0x9f]/[0xa0]) -- big width-12 chevrons at
	// x = +-1.2..1.5 (screen edges), half-alpha; positioned/enabled by the
	// un-exported Execute -> built but not drawn statically.
	//
	dpl2d_Begin(leftArrow, 1);
	dpl2d_SetLineWidth(leftArrow, 12.0f);
	dpl2d_OpenLines(leftArrow);
	dpl2d_AddPoint(leftArrow, -1.2f, -0.30011f);
	dpl2d_AddPoint(leftArrow, -1.5f, 0.0f);
	dpl2d_AddPoint(leftArrow, -1.5f, 0.0f);
	dpl2d_AddPoint(leftArrow, -1.2f, 0.30011f);
	dpl2d_CloseLines(leftArrow);
	dpl2d_SetLineWidth(leftArrow, 1.0f);
	dpl2d_End(leftArrow); dpl2d_Compile(leftArrow);
	dpl2d_Begin(rightArrow, 1);
	dpl2d_SetLineWidth(rightArrow, 12.0f);
	dpl2d_OpenLines(rightArrow);
	dpl2d_AddPoint(rightArrow, 1.2f, 0.30011f);
	dpl2d_AddPoint(rightArrow, 1.5f, 0.0f);
	dpl2d_AddPoint(rightArrow, 1.5f, 0.0f);
	dpl2d_AddPoint(rightArrow, 1.2f, -0.30011f);
	dpl2d_CloseLines(rightArrow);
	dpl2d_SetLineWidth(rightArrow, 1.0f);
	dpl2d_End(rightArrow); dpl2d_Compile(rightArrow);

	//
	// The SIMPLE X [0x99] (ctor @4689-4705 [T1]): the minimal reticle used
	// when PrimaryHudOn is OFF -- a small green cross (arms +-0.02..0.08)
	// riding the same aim translate.  Draw switches master <-> this on the
	// element-mask bit (the recovered Execute's state-list logic @4cdd9d).
	//
	dpl2d_Begin(simpleXList, 1);
	dpl2d_SetLineWidth(simpleXList, 1.0f);
	dpl2d_FullScreenClip(simpleXList);
	dpl2d_SetColor(simpleXList, 0.0f, 0.75f, 0.0f);
	dpl2d_CallList(simpleXList, aimDotList);	// slews with the crosshair
	dpl2d_OpenLines(simpleXList);
	dpl2d_AddPoint(simpleXList, -0.08f, 0.0f);
	dpl2d_AddPoint(simpleXList, -0.02f, 0.0f);
	dpl2d_AddPoint(simpleXList,  0.02f, 0.0f);
	dpl2d_AddPoint(simpleXList,  0.08f, 0.0f);
	dpl2d_AddPoint(simpleXList, 0.0f, -0.08f);
	dpl2d_AddPoint(simpleXList, 0.0f, -0.02f);
	dpl2d_AddPoint(simpleXList, 0.0f,  0.02f);
	dpl2d_AddPoint(simpleXList, 0.0f,  0.08f);
	dpl2d_CloseLines(simpleXList);
	dpl2d_End(simpleXList);
	dpl2d_Compile(simpleXList);

	// empty placeholders (filled per frame / on lock)
	dpl2d_Begin(crossList, 1);   dpl2d_End(crossList);   dpl2d_Compile(crossList);
	dpl2d_Begin(aimDotList, 1);  dpl2d_End(aimDotList);  dpl2d_Compile(aimDotList);
	dpl2d_Begin(rangeCaretR, 1); dpl2d_End(rangeCaretR); dpl2d_Compile(rangeCaretR);
	dpl2d_Begin(rangeCaretB, 1); dpl2d_End(rangeCaretB); dpl2d_Compile(rangeCaretB);
	dpl2d_Begin(headingList, 1); dpl2d_End(headingList); dpl2d_Compile(headingList);
	dpl2d_Begin(bottomAnchor,1); dpl2d_End(bottomAnchor);dpl2d_Compile(bottomAnchor);
	dpl2d_Begin(subB6, 1);       dpl2d_End(subB6);       dpl2d_Compile(subB6);
	dpl2d_Begin(subB7, 1);       dpl2d_End(subB7);       dpl2d_Compile(subB7);
	dpl2d_Begin(subBA, 1);       dpl2d_End(subBA);       dpl2d_Compile(subBA);
}

BTReticleRenderable::~BTReticleRenderable()
{
	if (gBTReticle == this)
		gBTReticle = 0;
}

//
// Per-frame draw.  Rebuild the live translate lists (the range caret slides
// along its ladder with the target range -- the ctor's translate(0,
// -scaleY * rangeFraction) at @4608-4613), then draw the master, then each
// weapon's pip: the LIT pip (list A) while its within-range flag is up, the
// dark ring (list B) otherwise.  [T3 dynamics / T1 geometry]
//
void
	BTReticleRenderable::Draw(struct IDirect3DDevice9 *device)
{
	// the range caret translate, from the live target range
	Scalar range = (rangeAttr2 != 0) ? *rangeAttr2 : 0.0f;
	if (range < minRange) range = minRange;
	if (range > maxRange) range = maxRange;
	Scalar frac = (range - minRange) / (maxRange - minRange);
	// [0x26c] = the range BAR (a line from the ladder TOP down to the caret
	// height) + the caret translate (Execute @4ceb16-4cebf8: AddPoint(originX,
	// originY-scaleY), AddPoint(originX, originY-scaleY*frac), then the
	// SetMatrix(translate(0, -scaleY*frac)) the caret triangles ride).
	dpl2d_Begin(rangeCaretR, 1);
	dpl2d_OpenLines(rangeCaretR);
	dpl2d_AddPoint(rangeCaretR, originX, originY - scaleY);
	dpl2d_AddPoint(rangeCaretR, originX, originY - scaleY * frac);
	dpl2d_CloseLines(rangeCaretR);
	{
		Scalar six[6] = { 1, 0, 0, 1, 0, -scaleY * frac };
		dpl2d_ConcatMatrix(rangeCaretR, six);
	}
	dpl2d_End(rangeCaretR);
	dpl2d_Compile(rangeCaretR);

	// the AIM translate [0x9a] (Execute @4cde59-4cdedd [T1]: rebuilt on slew
	// move with SetMatrix(translate(reticlePosition))).
	{
		extern float gBTAimX, gBTAimY;
		Scalar t6[6] = { 1, 0, 0, 1, gBTAimX, gBTAimY };
		dpl2d_Begin(aimDotList, 1);
		dpl2d_ConcatMatrix(aimDotList, t6);
		dpl2d_End(aimDotList);
		dpl2d_Compile(aimDotList);
	}

	// the TORSO-TWIST TAPE carets [0x9c] (Execute @4ce7e5-4cea9a [T1]): the
	// bottom 21-tick tape is the TWIST indicator -- a deflection line from the
	// tape centre plus the over/under carets translated by
	//   offset = -/+(span/2) * (RotationOfTorsoHorizontal / twist limit)
	// (attrs 4/5/6: the live twist over the per-side limits; full deflection =
	// the torso hard against its stop).  The fixed-torso BLH reads 0 (centred).
	{
		extern float gBTHudTwist, gBTHudTwistLimit;
		float off = 0.0f;
		if (gBTHudTwistLimit > 1e-4f)
		{
			off = -(kRetBotSpan * 0.5f) * (gBTHudTwist / gBTHudTwistLimit);
			if (off < -kRetBotSpan * 0.5f) off = -kRetBotSpan * 0.5f;
			if (off >  kRetBotSpan * 0.5f) off =  kRetBotSpan * 0.5f;
		}
		const float cx = kRetBotX + kRetBotSpan * 0.5f;
		dpl2d_Begin(rangeCaretB, 1);
		dpl2d_OpenLines(rangeCaretB);
		dpl2d_AddPoint(rangeCaretB, cx,       kRetBotY);
		dpl2d_AddPoint(rangeCaretB, cx + off, kRetBotY);
		dpl2d_CloseLines(rangeCaretB);
		{
			Scalar t6[6] = { 1, 0, 0, 1, off, 0 };
			dpl2d_ConcatMatrix(rangeCaretB, t6);
		}
		dpl2d_End(rangeCaretB);
		dpl2d_Compile(rangeCaretB);
	}

	// the COMPASS rotate [0x9e] (Execute @4ce6e0-4ce7e4 [T1]): the rose spins
	// by CompassHeading (radians; the binary converts x57.2958 for its degree
	// recorder) and sits at (botX, botY - 3*tickMajor - 0.03).
	{
		extern float gBTHudHeading;
		const float c = (float)cos((double)gBTHudHeading);
		const float s = (float)sin((double)gBTHudHeading);
		Scalar r6[6] = { c, s, -s, c,
						 kRetBotX, kRetBotY - 3.0f * kRetTickMajor - 0.03f };
		dpl2d_Begin(bottomAnchor, 1);
		dpl2d_ConcatMatrix(bottomAnchor, r6);
		dpl2d_End(bottomAnchor);
		dpl2d_Compile(bottomAnchor);
	}

	// the THREAT trail [0xba] (Execute @4ce3e2-4ce6ce [T1]): direction marks
	// from the compass centre toward recent damage sources.  Each mark is a
	// 0.05-unit line along the (mech-local x,z) attack direction; FRESH marks
	// (< 2s) draw red, aging ones yellow, expired (> 6s) drop.
	{
		extern int   BTTakeHudThreats(float out_xz[][2], float out_age[], int max_n);
		float txz[16][2]; float tage[16];
		const int n = BTTakeHudThreats(txz, tage, 16);
		dpl2d_Begin(subBA, 1);
		if (n > 0)
		{
			// stale (yellow) first, then fresh (red) -- the binary's color split
			dpl2d_SetColor(subBA, 0.75f, 0.75f, 0.0f);
			for (int pass = 0; pass < 2; ++pass)
			{
				const int wantFresh = (pass == 1);
				if (pass == 1)
					dpl2d_SetColor(subBA, 0.75f, 0.0f, 0.0f);
				for (int i = 0; i < n; ++i)
				{
					const int isFresh = (tage[i] < 2.0f);
					if (isFresh != wantFresh)
						continue;
					dpl2d_OpenLines(subBA);
					dpl2d_AddPoint(subBA, 0.0f, 0.0f);
					dpl2d_AddPoint(subBA, txz[i][0] * 0.05f, txz[i][1] * 0.05f);
					dpl2d_CloseLines(subBA);
				}
			}
			dpl2d_SetColor(subBA, 0.0f, 0.75f, 0.0f);		// restore green
		}
		dpl2d_End(subBA);
		dpl2d_Compile(subBA);
	}

	// the WEAPON PIPS [0xb6] (Execute @4ce2c2-4ce3e1 [T1]): the composed pip
	// list the master calls.  Per weapon: skip unless its GROUP is displayed
	// (weaponMode & elementMask low bits); HIDE it when destroyed (attr 1 ==
	// 1); the LIT pip (A) when the fire cycle is LOADED (attr 0x1c == 2, our
	// source: rechargeLevel >= 1), else the dark ring (B, charging).  Range
	// plays NO part -- the binary never reads TargetWithinRange here.
	{
		extern int gBTHudGroupMask;					// element-mask low bits (0xF = all)
		int dirty = 0;
		for (int i = 0; i < weaponCount; ++i)
		{
			const int destroyed = (simStateAttr[i] != 0 && *simStateAttr[i] == 1);
			const int loaded    = (cycleReady[i] != 0 && *cycleReady[i] >= 0.999f);
			if (destroyed != simStateCache[i] || loaded != alarmCache[i])
			{
				simStateCache[i] = destroyed;
				alarmCache[i]    = loaded;
				dirty = 1;
			}
		}
		static int s_lastMask = -1;
		if (gBTHudGroupMask != s_lastMask) { s_lastMask = gBTHudGroupMask; dirty = 1; }
		if (dirty || !pipsBuilt)
		{
			pipsBuilt = 1;
			dpl2d_Begin(subB6, 1);
			for (int i = 0; i < weaponCount; ++i)
			{
				if ((weaponMode[i] & gBTHudGroupMask) == 0)
					continue;						// group not displayed
				if (simStateCache[i])
					continue;						// destroyed: no pip at all
				dpl2d_CallList(subB6,
					alarmCache[i] ? pipDisplayListA[i] : pipDisplayListB[i]);
			}
			dpl2d_End(subB6);
			dpl2d_Compile(subB6);
		}
	}

	// the LOCK RING [0xb7] + its SPIN [0x9d] (Execute @4cebf9-4cee54 [T1]):
	// while a target is locked the ring+cross (subB9) draws at the reticle
	// frame centre, spinning 4 degrees per frame; unlocked it clears.  (The
	// binary also hangs the PNAMEx.bgf player-name mesh on the 3D marker here
	// -- the 3D marker chain remains deferred.)
	{
		extern int gBTHudLockState;
		const int locked = (gBTHudLockState == 2);	// the Lock attr rule, not just a target
		if (locked != lockShown)
		{
			lockShown = locked;
			dpl2d_Begin(subB7, 1);
			if (locked)
				dpl2d_CallList(subB7, subB9);
			dpl2d_End(subB7);
			dpl2d_Compile(subB7);
		}
		if (locked)
		{
			lockSpinDeg += 4.0f;					// per FRAME, the binary's rate
			if (lockSpinDeg >= 360.0f) lockSpinDeg -= 360.0f;
			const float a = lockSpinDeg * 0.0174532925f;
			const float c = (float)cos((double)a), s = (float)sin((double)a);
			Scalar r6[6] = { c, s, -s, c, 0, 0 };
			dpl2d_Begin(headingList, 1);
			dpl2d_ConcatMatrix(headingList, r6);
			dpl2d_End(headingList);
			dpl2d_Compile(headingList);
		}
	}

	// the TARGET HOTBOX / edge arrows [0xa1] (Execute @4cdf6f-4ce28b [T1]):
	// the box is a RECTANGLE hugging the target's projected extents -- x +-4
	// around the hotbox point, +1 above / -11.5 below it (the authored pod
	// mech envelope) -- switching to the left/right edge arrow when both
	// edges pass +-1.6 or the target is behind.
	{
		extern int   gBTHudLockState;
		extern float gBTHudLockWorld[3];			// the target's hotbox point (top)
		extern int   BTProjectHotBox(const float top[3], float *xl, float *xr,
									 float *yt, float *yb, int *side);
		dpl2d_Begin(crossList, 1);
		if (gBTHudLockState != 0)
		{
			float xl, xr, yt, yb; int side;
			if (BTProjectHotBox(gBTHudLockWorld, &xl, &xr, &yt, &yb, &side))
			{
				dpl2d_OpenPolyline(crossList);		// the closed hotbox rectangle
				dpl2d_AddPoint(crossList, xl, yt);
				dpl2d_AddPoint(crossList, xl, yb);
				dpl2d_AddPoint(crossList, xr, yb);
				dpl2d_AddPoint(crossList, xr, yt);
				dpl2d_ClosePolyline(crossList);
			}
			else
			{
				dpl2d_CallList(crossList, (side < 0) ? leftArrow : rightArrow);
			}
		}
		dpl2d_End(crossList);
		dpl2d_Compile(crossList);
	}

	// the STATE-list switch (Execute @4cdd9d [T1]): PrimaryHudOn (element mask
	// 0x20) selects the full HUD; off = just the simple aim cross.
	{
		extern int gBTHudPrimary;
		dpl2d_ExecuteList(gBTHudPrimary ? masterList : simpleXList, device);
	}
}

//
// The render-loop hook: draw the player's reticle over the finished 3D frame,
// COCKPIT VIEW ONLY (the 1996 build constructed it only for insideEntity).
//
void BTDrawReticle(struct IDirect3DDevice9 *device)
{
	if (gBTReticle != 0 && gBTHudInside)
		gBTReticle->Draw(device);
}

//
// Build the player's reticle + register one pip per weapon (the 1996 wiring:
// @part_014.c:5386-5436).  The binary's gate is IsDerivedFrom(0x511830) --
// MechWeapon::ClassDerivations [T1: the loop hard-aborts on missing
// WeaponRange/PipPosition/... attrs, which only MechWeapons publish] -- so
// EVERY mounted weapon registers a pip: lasers, PPCs AND missile launchers.
// Per weapon it reads WeaponRange / PipPosition / TargetWithinRange /
// PipExtendedRange / PipColor / SimulationState (attrs 1 + 0x1c) / RearFiring;
// mode 1 = front (RearFiring==0), which every BLH weapon is.
//
BTReticleRenderable *BTBuildReticle(Entity *mech)
{
	if (gBTReticle != 0)
		return gBTReticle;

	extern void BTSetHudTargetRange(Scalar range);	// (self; range fed by mech4)
	extern Scalar gBTHudRangeStorage;				// defined below

	gBTReticle = new BTReticleRenderable(mech, &gBTHudRangeStorage);

	Mech *m = (Mech *)mech;
	for (int wi = 0; wi < m->GetSubsystemCount(); ++wi)
	{
		Subsystem *ws = m->GetSubsystem(wi);
		if (ws == 0)
			continue;
		if (!ws->IsDerivedFrom(MechWeapon::ClassDerivations))	// 0x511830
			continue;
		MechWeapon *wp = (MechWeapon *)ws;
		RGBColor pc = wp->PipColor();
		float r = (float)pc.Red, g = (float)pc.Green, b = (float)pc.Blue;
		if (r < 0.0f || g < 0.0f || b < 0.0f) { r = 0.78f; g = 0.08f; b = 0.02f; }
		DEBUG_STREAM << "[hud] pip: classID=" << (int)ws->GetClassID()
			<< " pos=" << wp->PipPosition()
			<< " range=" << wp->WeaponRange()
			<< " ext=" << wp->PipExtendedRange()
			<< " rgb=(" << r << "," << g << "," << b << ")\n" << std::flush;
		gBTReticle->AddWeapon(
			wp->WeaponRange(),
			wp->PipPosition(),
			(int *)wp->WithinRangePtr(),
			wp->PipExtendedRange(),
			r, g, b,
			wp->RechargeLevelPtr(),					// attr 0x1c analog: loaded when >= 1
			2, 3,
			(int *)wp->SimulationStatePtr(),		// attr 1: damage state (1 = destroyed)
			1,
			1 /* Front group (RearFiring==0 on every BLH weapon) */);
	}
	DEBUG_STREAM << "[hud] reticle built: " << gBTReticle->WeaponCount()
				 << " weapon pip(s) registered\n" << std::flush;
	return gBTReticle;
}

// the live target-range storage the reticle's caret binds to
Scalar gBTHudRangeStorage = 0.0f;

//
// THREAT trail store (recovered Execute @4ce3e2-4ce6ce): timestamped attack
// directions pushed on player damage (mech.cpp handler); Draw ages them --
// fresh < 2s (red), expired > 6s (dropped).  World-frame (x,z) directions:
// they draw inside the compass's rotated frame, so the marks stay
// world-referenced on the rose like a true compass bearing.
//
struct BTHudThreat { float x, z; clock_t born; };
static BTHudThreat gBTHudThreats[16];
static int gBTHudThreatCount = 0;

void BTPushHudThreat(float wx, float wz)
{
	float len = sqrtf(wx * wx + wz * wz);
	if (len < 1e-4f)
		return;
	if (gBTHudThreatCount >= 16)					// drop the oldest
	{
		for (int i = 1; i < 16; ++i)
			gBTHudThreats[i - 1] = gBTHudThreats[i];
		gBTHudThreatCount = 15;
	}
	BTHudThreat &t = gBTHudThreats[gBTHudThreatCount++];
	t.x = wx / len;
	t.z = wz / len;
	t.born = clock();
}

int BTTakeHudThreats(float out_xz[][2], float out_age[], int max_n)
{
	const clock_t now = clock();
	int n = 0;
	for (int i = 0; i < gBTHudThreatCount; ++i)
	{
		const float age = (float)(now - gBTHudThreats[i].born) / (float)CLOCKS_PER_SEC;
		if (age > 6.0f)
			continue;								// expired
		gBTHudThreats[n] = gBTHudThreats[i];		// compact in place
		if (n < max_n)
		{
			out_xz[n][0] = gBTHudThreats[n].x;
			out_xz[n][1] = gBTHudThreats[n].z;
			out_age[n]   = age;
		}
		++n;
	}
	gBTHudThreatCount = n;
	return (n < max_n) ? n : max_n;
}

//
//#############################################################################
// BTReticleRenderable::AddWeapon
//#############################################################################
//
// @004cdac0
//
// Append one weapon range/pip marker to the reticle (max 10).  Stores the
// weapon's attribute pointers in parallel arrays indexed by weaponCount, then
// pre-builds the two 2D display lists (the pip glyph + its extended-range arc)
// at the screen position computed from the (clamped) weapon range.
//
void
	BTReticleRenderable::AddWeapon(
		Scalar			weapon_range,			// param_2
		int				pip_position,			// param_3
		int				*within_range_value,	// param_4  TargetWithinRange
		int				extended_range,			// param_5  PipExtendedRange
		Scalar			pip_red,				// param_6..8  PipColor
		Scalar			pip_green,
		Scalar			pip_blue,
		Scalar			*cycle_ready,			// param_9  (port: rechargeLevel)
		int				const2,					// param_10 (2 = loaded)
		int				const3,					// param_11 (3 = charging)
		int				*sim_state_value,		// param_12 weapon attr 1
		int				const1,					// param_13 (1 = destroyed)
		int				weapon_mode)			// param_14 (group bit)
{
	if (this->weaponCount /* [0x38] */ >= 10)
	{
		Fail("Tried to display too many weapons");		// @0051d24f, line 0x338
	}

	int n = this->weaponCount;

	//
	// Record this weapon's control state in the parallel arrays -- the exact
	// store order of @004cdac0 (part_014.c:4827-4837).  The caches hold the
	// pip's DERIVED display flags (loaded / destroyed) for the Execute-style
	// change detection in Draw.
	//
	this->stateConst3[n]		/* [0x64+n*4]  = param_11 */ = const3;
	this->stateConst2[n]		/* [0x8c+n*4]  = param_10 */ = const2;
	this->stateConst1[n]		/* [0xb4+n*4]  = param_13 */ = const1;
	this->cycleReady[n]			/* [0x130+n*4] = param_9  */ = cycle_ready;
	this->alarmCache[n]			/* [0xdc+n*4]  */ = (*cycle_ready >= 0.999f);
	this->simStateAttr[n]		/* [0x158+n*4] = param_12 */ = sim_state_value;
	this->simStateCache[n]		/* [0x104+n*4] */ = (*sim_state_value == const1);
	this->withinRangePtr[n]		/* [0x18c+n*4] = param_4  */ = within_range_value;
	this->withinRangeCache[n]	/* [0x1b4+n*4] = *param_4 */ = *within_range_value;
	this->weaponMode[n]			/* [0x3c+n*4]  = param_14 */ = weapon_mode;

	dpl2d_DISPLAY *pip_list = dpl2d_NewDisplayList();	// FUN_00487f34
	dpl2d_DISPLAY *arc_list = dpl2d_NewDisplayList();
	this->pipDisplayListA[n]	/* [0x2b0+n*4] */ = pip_list;
	this->pipDisplayListB[n]	/* [0x288+n*4] */ = arc_list;

	//
	// Clamp range into [minRange .. maxRange].
	//
	if (weapon_range >= this->minRange /* [0x230] */)
	{
		if (weapon_range > this->maxRange /* [0x22c] */)
			weapon_range = this->maxRange;
	}
	else
	{
		weapon_range = this->minRange;
	}

	//
	// Screen position of this pip from the reticle's calibrated geometry.
	//
	float x = this->originX /* [0x1fc] */ + this->biasX /* [0x208] */ +
			  (float)pip_position * PIP_SPACING /* _DAT_004cdce8 */;
	float y = -this->scaleY /* [0x204] */ *
			  ((weapon_range - this->minRange) / this->rangeScale /* [0x234] */) +
			  this->originY /* [0x200] */;

	//
	// Pip glyph display list: a coloured ring (+ a small filled marker when
	// this is an extended-range / "rear" weapon).
	//
	dpl2d_Begin(pip_list, 1);							// FUN_00487fbc
	dpl2d_SetColor(pip_list, pip_red, pip_green, pip_blue);	// param_6,7,8
	dpl2d_Circle(pip_list, x, y, 0.012f /* 0x3c449ba6 */, 1);
	dpl2d_SetColor(pip_list, 0, 0, 0);
	dpl2d_Circle(pip_list, x, y, 0.014f /* 0x3c656042 */, 0);
	if (extended_range != 0)							// param_5
	{
		dpl2d_SetColor(pip_list, 0.7f, 0.7f, 0.7f);		// 0x3f333333
		dpl2d_PushMatrix(pip_list);
		dpl2d_MoveTo(pip_list, x, y);
		dpl2d_PopMatrix(pip_list);
	}
	dpl2d_End(pip_list);
	dpl2d_Compile(pip_list);

	//
	// Extended-range arc display list (black outline ring).
	//
	dpl2d_Begin(arc_list, 1);
	dpl2d_SetColor(arc_list, 0, 0, 0);
	dpl2d_Circle(arc_list, x, y, 0.014f, 0);
	dpl2d_End(arc_list);
	dpl2d_Compile(arc_list);

	this->weaponCount = n + 1;
}


//
//#############################################################################
// SetupMaterialSubstitutionList
//#############################################################################
//
// @004d0cc0
//
// Read the "vehicletable" resource and build the per-mech material-name
// substitution list, expanding the %color% / %badge% / %patch% / %serno%
// placeholders.  Directly parallels RPL4VideoRenderer::SetupMaterialSubstitution-
// List (which handles %color%/%badge%); BT adds %patch% and a per-load
// incrementing %serno% (serial number, "0".."9","A"...).
//
void
	BTL4VideoRenderer::SetupMaterialSubstitutionList(Entity *entity)
{
	//
	// One-shot cache of the placeholder string lengths.
	//
	static int colorLen = -1, badgeLen, patchLen, sernoLen;		// guards @0051d19c..d1b4
	if (colorLen < 0)
	{
		colorLen = strlen(colorParameter);
		badgeLen = strlen(badgeParameter);
		patchLen = strlen(patchParameter);
		sernoLen = strlen(sernoParameter);
	}

	//
	// Fetch + lock the vehicle table resource, copy it out, and parse it as a
	// NotationFile.
	//
	ResourceDescription *res = application->GetResourceFile()->FindResourceDescription(	// FUN_00406ff8
		"vehicletable" /* @0051d941 */, ResourceDescription::VehicleTableResourceType);
	if (res == NULL)
		return;

	res->Lock();
	long  len  = (long)res->resourceSize;			// [0x40]
	char *copy = new char[len];
	memcpy(copy, res->resourceAddress /* [0x3c] */, len);	// FUN_004d4918
	res->Unlock();

	NotationFile *veh_tbl = new NotationFile();		// FUN_00403e84
	veh_tbl->ReadText(copy, len);					// FUN_00404d00
	delete [] copy;

	//
	// Look up this mech's colour / badge / patch codes from the table, using
	// the egg-supplied names carried on the entity (badge=resourceNameA @0x844,
	// color=resourceNameB @0x848, patch=resourceNameC @0x84c).
	//
	// The recovered code read these directly off the BattleTech mech egg
	// (Mech::vehicleColor / vehicleBadge / vehiclePatch) and Fail()ed on a miss.
	// TODO(bring-up): the reconstructed Mech carries those names as ref-counted
	// creation-name objects (resourceNameA/B/C) whose backing string is the
	// transient MakeMessage buffer -- not safely readable here yet, and the
	// attribute-index path ("VehicleColor"/...) is not wired, so it returns
	// garbage that FindNote then deref-crashed on (btl4vid.cpp:808).  Follow the
	// RP analogue (RPL4VID.cpp:1562) which simply tolerates a missing colour/
	// badge: a NULL egg name or a table miss leaves veh_* == NULL and the
	// placeholder substitution below drops to default materials.  Re-wire the
	// real egg names (a named Mech accessor) once the Mech layout is mapped.
	//
	const char *egg_color = NULL;	// [0x848] resourceNameB (color)
	const char *egg_badge = NULL;	// [0x844] resourceNameA (badge)
	const char *egg_patch = NULL;	// [0x84c] resourceNameC (patch)

	const char *veh_color = NULL, *veh_badge = NULL, *veh_patch = NULL;
	if (egg_color && !veh_tbl->GetEntry("color", egg_color, &veh_color))	// @0051d94e
	{
		DEBUG_STREAM << "   Color value '" << egg_color
					 << "' from egg not found in vehicle table\n";	// @0051d954
		veh_color = NULL;
	}
	if (egg_badge && !veh_tbl->GetEntry("badge", egg_badge, &veh_badge))	// @0051d9b8
	{
		DEBUG_STREAM << "   Badge value '" << egg_badge
					 << "' from egg not found in vehicle table\n";
		veh_badge = NULL;
	}
	if (egg_patch && !veh_tbl->GetEntry("patch", egg_patch, &veh_patch))	// @0051da22
	{
		DEBUG_STREAM << "   Patch value '" << egg_patch
					 << "' from egg not found in vehicle table\n";
		veh_patch = NULL;
	}

	//
	// Generic substitution list, then expand placeholders per entry.
	//
	materialSubstitutionList = veh_tbl->MakeEntryList("substitute");	// @0051da8c, DAT_004f1aac
	for (NameList::Entry *entry = materialSubstitutionList->GetFirstEntry();
		 entry != NULL;
		 entry = entry->GetNextEntry())
	{
		char buffer[80];
		char *dst = buffer;
		const char *src = entry->GetChar();
		*dst = '\0';

		const char *pc;
		while ((pc = strchr(src, '%')) != NULL)			// FUN_004d49f4
		{
			int n = (int)(pc - src);
			const char *resume = src;
			if (n != 0)
			{
				memcpy(dst, src, n);
				dst += n;
				resume = pc;
			}

			if (!strncmp(pc, sernoParameter, sernoLen))
			{
				//
				// %serno% -> the current one-character serial (gSerno, which
				// increments '0'->'9'->'A' each mech loaded).
				//
				if (gSerno /* @0051d1b5 */ != '\0')
					*dst++ = gSerno;
				src = resume + sernoLen;
			}
			else if (!strncmp(pc, colorParameter, colorLen))
			{
				if (veh_color) { strcpy(dst, veh_color); dst += strlen(veh_color); }
				src = resume + colorLen;
			}
			else if (!strncmp(pc, badgeParameter, badgeLen))
			{
				if (veh_badge) { strcpy(dst, veh_badge); dst += strlen(veh_badge); }
				src = resume + badgeLen;
			}
			else if (!strncmp(pc, patchParameter, patchLen))
			{
				if (veh_patch) { strcpy(dst, veh_patch); dst += strlen(veh_patch); }
				src = resume + patchLen;
			}
			else
			{
				*dst++ = *resume;						// stray '%'
				src = resume + 1;
			}
		}
		strcpy(dst, src);								// tail

		//
		// Store the expanded copy back into the list entry.
		//
		char *result = new char[strlen(buffer) + 1];
		strcpy(result, buffer);
		entry->dataReference = result;
	}

	delete veh_tbl;

	//
	// Advance the global serial number ('9' wraps to 'A') and install the
	// per-frame material-name substitution callback.
	//
	if (gSerno == '9')	gSerno = 'A';
	else				gSerno = gSerno + 1;
	dpl_SetMaterialNameCallback(substituteMaterial);	// FUN_0049664c(FUN_00459eb8)
}


//
//#############################################################################
// TearDownMaterialSubstitutionList
//#############################################################################
//
// @004d11e8
//
// Free the expanded substitution strings + the list, and clear the DPL
// material-name callback.
//
void
	BTL4VideoRenderer::TearDownMaterialSubstitutionList()
{
	if (materialSubstitutionList != NULL)
	{
		for (NameList::Entry *entry = materialSubstitutionList->GetFirstEntry();
			 entry != NULL;
			 entry = entry->GetNextEntry())
		{
			char *p = entry->GetChar();
			if (p) { delete [] p; entry->dataReference = NULL; }
		}
		delete materialSubstitutionList;
		materialSubstitutionList = NULL;
	}
	// dpl_SetMaterialNameCallback(NULL);
}

//===========================================================================//
// BTL4VideoRenderer ctor/dtor/TestInstance
//---------------------------------------------------------------------------//
// TODO(bring-up): the shipped BT ctor took the 1995 IG-board calibration tuple
// (rate/complexity/priority/interest/depth) and drove the Division renderer.
// WinTesla replaced that renderer with the D3D DPLRenderer, whose ctor now needs
// (HWND, width, height, fullscreen, interest_type, depth).  The old calibration
// args have no D3D analogue, so for the first link we forward the interest/depth
// and bind the renderer to the active window at the pod main-view size (800x600).
// Real window/size wiring belongs in the BTL4Application video bring-up.
//===========================================================================//
BTL4VideoRenderer::BTL4VideoRenderer(
	RendererRate			/*calibration_rate*/,
	RendererComplexity		/*calibration_complexity*/,
	RendererPriority		/*calibration_priority*/,
	InterestType			interest_type,
	InterestDepth			depth_calibration
)
:
	DPLRenderer(::GetActiveWindow(), 800, 600, false, interest_type, depth_calibration)
{
	Check_Pointer(this);
	mEyeCockpit = 0;
	mEyeChase   = 0;
	mViewInside = 0;
}

//
// The V-key view toggle: switch the live camera between the authentic cockpit
// eyepoint and the port's external chase camera.  A missing eye (e.g. the
// cockpit eye on a mech with no siteeyepoint) leaves the current view.
//
void
	BTL4VideoRenderer::SetViewInside(int inside)
{
	mViewInside = inside;						// persists across renderable rebuilds
	if (inside && mEyeCockpit != 0)
		mCamera = mEyeCockpit;
	else if (!inside && mEyeChase != 0)
		mCamera = mEyeChase;

	//
	// Swap the player's DISPLAYED skeleton with the view: the INSIDE view uses
	// the inside-skeleton mesh set (SkeletonType_A -- most body segments have
	// no inside mesh, so the pilot isn't wrapped in his own torso textures;
	// the authentic pod view worked exactly this way), the chase view restores
	// the full outside set.  Damage graphic states are respected per segment.
	//
	Entity *viewpoint = (application != 0) ? application->GetViewpointEntity() : 0;
	std::map<Entity*, MechRenderTree>::iterator tree_it =
		mMechRenderTrees.find(viewpoint);
	if (tree_it != mMechRenderTrees.end() && !tree_it->second.wrecked)
	{
		MechRenderTree &render_tree = tree_it->second;
		render_tree.viewSkeleton = inside
			? (int)EntitySegment::SkeletonType_A
			: render_tree.skeletonType;

		JointedMover *jm = (JointedMover *)viewpoint;
		EntitySegment::SegmentTableIterator it(jm->segmentTable);
		EntitySegment *segment;
		int shown = 0, hidden = 0;
		while ((segment = it.ReadAndNext()) != NULL)
		{
			if (segment->IsSiteSegment() != 0)
				continue;
			int slot = segment->GetIndex();
			std::map<int, HierarchicalDrawComponent*>::iterator r =
				render_tree.segRenderable.find(slot);
			if (r == render_tree.segRenderable.end() || r->second == NULL)
				continue;
			int gstate = 0;
			std::map<int, int>::iterator g = render_tree.segGState.find(slot);
			if (g != render_tree.segGState.end())
				gstate = g->second;
			CString *nm = segment->GetVideoObjectName(
				(EntitySegment::SkeletonType)render_tree.viewSkeleton,
				(Enumeration)gstate);
			// The cockpit interior shell (blx_cop -- the canopy frame around the
			// eyepoint) currently renders as a black enclosure; keep it hidden
			// until its interior rendering is sorted (BT_INSIDE_COCKPIT=1 shows
			// it for that investigation).
			if (inside && nm != NULL && strstr((const char *)*nm, "_cop") != NULL
				&& !getenv("BT_INSIDE_COCKPIT"))
				nm = NULL;
			d3d_OBJECT *obj = NULL;
			if (nm != NULL)
			{
				char filename[44];
				strcpy(filename, (const char *)*nm);
				int len = (int)strlen(filename);
				if (len >= 4)
					filename[len - 4] = '\0';
				strcat(filename, ".bgf");
				obj = d3d_OBJECT::LoadObject(GetDevice(), filename);
			}
			r->second->SetDrawObj(obj);
			if (obj)
			{
				++shown;
				DEBUG_STREAM << "[view]   shown: seg '"
							 << (const char *)segment->GetName() << "' mesh '"
							 << (nm ? (const char *)*nm : "?") << "'\n" << std::flush;
			}
			else ++hidden;
		}
		DEBUG_STREAM << "[view] skeleton "
					 << (inside ? "A (inside)" : "N (outside)") << ": "
					 << shown << " segment mesh(es) shown, " << hidden
					 << " hidden\n" << std::flush;
	}

	DEBUG_STREAM << "[view] " << (inside ? "COCKPIT eyepoint" : "external chase")
				 << (mCamera == mEyeCockpit ? " (cockpit live)" : " (chase live)")
				 << "\n" << std::flush;

	// the HUD overlay draws in the cockpit view only
	{
		extern void BTSetHudInside(int inside);
		BTSetHudInside(mCamera == mEyeCockpit ? 1 : 0);
	}
}

//
// Sim-side bridge (the mech4 keyboard poll drives it).
//
void BTSetViewInside(int inside)
{
	if (application == NULL)
		return;
	BTL4VideoRenderer *renderer =
		(BTL4VideoRenderer *)application->GetVideoRenderer();
	if (renderer != NULL)
		renderer->SetViewInside(inside);
}

BTL4VideoRenderer::~BTL4VideoRenderer()
{
}

Logical
	BTL4VideoRenderer::TestInstance() const
{
	return True;
}

//===========================================================================//
