Files
BT412/game/reconstructed/btl4vid.cpp
T
arcattackandClaude Opus 4.8 a35f321ba1 Warp: exact POVTranslocateRenderable replica (collapse/throb/reveal + world mask)
Stop approximating -- replicate POVTranslocateRenderable::Execute() behaviorally,
byte-for-byte on the constants and transitions (engine T0, L4VIDRND.cpp:1763-2076),
per the user's "replicate the effect exactly".  The five prior piecemeal attempts
each fixed one facet and broke another because they left out the WHOLE mechanism.

Full state machine (btl4vid.cpp), driven by two decoupled events (NOT the
SimulationState trigger -- that dial also drives camera/POV+targeting, the f053535
regression; SetIsDead is a separate pure-render flag, verified safe):
  - LOCAL DEATH -> InitialCollapse: scale (pct_left*100)+1 over 1.3s (world visible),
    then SetIsDead(true) + WaitForReincarnate.  (btplayer VehicleDeadMessageHandler,
    deathCount==-1, local-guarded.)
  - WaitForReincarnate: world BLACK (mask), scale 1, Lissajous throb
    (cos(t*3.33), sin(t*2.5)) x2.0 -- the "dances around playfully".
  - LOCAL RESPAWN -> ExpandReveal: SetIsDead(false) + scale (pct_used*150)+1 over
    1.0s -- blasts open, world revealed ("shoots off into the distance").  (btplayer
    DropZoneReplyMessageHandler, local-guarded.)

Draw EXACTLY as the engine (isDeathDraw -> drawAsSky): PASS_SKY, OPAQUE,
CULLMODE=CW, z-test ON (L4VIDEO.cpp:7526/7568-7570/7693).  This kills the "glitchy
funnel" -- opaque = one write/pixel (no translucent double-blend), CW = one winding
of the double-sided mesh (no coincident z-fight), z-on = the expand shell is
occluded/revealed by the returning world.  Placement = Scale * inverse(view) with
NO recenter (the mesh is authored off-origin so the eye sits inside, authentic);
dropped the wrong mCullCenter recenter and the Z-off overlay hack.  Colour is the
"sky" ramp (per prior commit); no geometry spin (myRotateY is dead) -- swirl is the
texture scroll.  SetIsDead reached via a BTSetWorldDead bridge (L4VIDRND.cpp).

STUCK-BLACK SAFETY (the one new risk of decoupling mask-on from mask-off): a 12s
WaitForReincarnate render-loop timeout un-masks if no respawn arrives, plus
BTWarpForceUnmask on the mission-ending + no-DropZones abort paths.  Smoke-verified
2-node: collapse->wait(masked=1)->expand(masked=0), ends masked=0, no crash.

The peer-observer warp stays a world-anchored translucent alpha draw (port
extension; the authentic effect is POV-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:43:54 -05:00

2584 lines
100 KiB
C++

//===========================================================================//
// 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.03f; // _DAT_004cdce8 (a DOUBLE: 0.03 --
// verified from the exe; the 0.01 guess
// overlapped the 0.028-wide pips)
//
// 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;
}
//
//#############################################################################
// RebuildMechRenderables (the HEAL direction of RemakeEntity -- respawn)
//#############################################################################
//
// SwapToWreck hides every body segment and hangs a sinking dbr hulk on the root,
// latching render_tree.wrecked (one-way). On respawn Mech::Reset heals the sim
// state, but the render stays the sunk hulk unless we reverse the swap: drop the
// hulk/debris and restore every segment to its now-intact mesh (the zones are
// healed, so GetGraphicState() == Exists). This is the same in-place mesh swap
// as RemakeEntityRenderables, forced (RemakeEntity early-returns while wrecked).
//
int BTTakePendingWreck(Entity *entity); // defined below (SwapToWreck section)
void
BTL4VideoRenderer::RebuildMechRenderables(Entity *entity)
{
std::map<Entity*, MechRenderTree>::iterator tree_it =
mMechRenderTrees.find(entity);
if (tree_it == mMechRenderTrees.end())
{
BTTakePendingWreck(entity); // clear a queued (never-applied) wreck
return;
}
MechRenderTree &render_tree = tree_it->second;
//
// Drop the wreck hulk + strewn debris (the renderables leak -- the component
// dtor does not cascade, same as the wreck swap -- but hiding them removes
// them from the draw and stops TickWreck from sinking a nulled tree).
//
if (render_tree.wreckHulk != NULL) render_tree.wreckHulk->SetDrawObj(NULL);
if (render_tree.wreckDebris != NULL) render_tree.wreckDebris->SetDrawObj(NULL);
render_tree.wreckHulk = NULL;
render_tree.wreckDebris = NULL;
render_tree.wrecked = 0;
render_tree.wreckAge = 0.0f;
//
// Restore every body segment to its now-intact mesh via the shared view-skeleton
// applier, which ALSO re-asserts the inside-view rules (SkeletonType_A + '_cop'
// canopy suppression). Without this, respawning while in cockpit view rebuilt
// the full OUTSIDE torso around the eyepoint -> the eye ended up inside opaque
// geometry -> black viewport until the next V-toggle. Only the mech the local
// camera views FROM (the player's own) gets the inside treatment; a replicant
// (a peer's mech) is always drawn with its outside skeleton.
//
Entity *viewpoint = (application != 0) ? application->GetViewpointEntity() : 0;
int inside = (entity == viewpoint) ? mViewInside : 0;
int restored = ApplyViewSkeleton(entity, inside);
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[BTrender] respawn: rebuilt intact model ("
<< restored << " segs restored, hulk dropped)\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);
}
// Sim-side bridge for the respawn render un-wreck (Mech::Reset calls this to
// restore the intact model after healing).
void BTRebuildMechModel(Entity *entity)
{
if (entity == NULL || application == NULL)
return;
BTL4VideoRenderer *renderer =
(BTL4VideoRenderer *)application->GetVideoRenderer();
if (renderer != NULL)
renderer->RebuildMechRenderables(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 group (ctor @4546-4560 [T1], color-corrected vs the reference
// screenshot which CONFIRMS the binary): the YELLOW width-2 state applies
// to the CALLED live list (the range BAR + caret translate the Draw
// rebuilds); the caret TRIANGLE itself is GREEN width 1 (@4550-4551 --
// SetLineWidth(1) + SetColor(0,0.75,0) BEFORE the polyline).
dpl2d_PushState(m);
dpl2d_SetLineWidth(m, 2.0f);
dpl2d_SetColor(m, 0.75f, 0.75f, 0.0f); // yellow: the live bar
dpl2d_CallList(m, rangeCaretR);
dpl2d_SetLineWidth(m, 1.0f);
dpl2d_SetColor(m, 0.0f, 0.75f, 0.0f); // green: the caret triangle
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_AddPoint(m, kRetOriginX - kRetTickMajor, kRetOriginY); // close @4558
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)
// (ctor @4565-4587 [T1], color-corrected like the range caret: yellow
// width 2 for the CALLED live twist-deflection list, then GREEN width 1
// @4569-4570 for the bowtie triangles. The binary pops state between the
// over- and under-caret (@4579) -- same green/1 either way.)
{
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); // yellow: the live deflection line
dpl2d_CallList(m, rangeCaretB);
dpl2d_SetLineWidth(m, 1.0f);
dpl2d_SetColor(m, 0.0f, 0.75f, 0.0f); // green: the bowtie carets
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_AddPoint(m, cx, kRetBotY - 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_AddPoint(m, cx, kRetBotY + 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;
}
//
// ApplyViewSkeleton -- select + load each body segment's displayed mesh for the
// given view (inside = SkeletonType_A / outside = the mech's own skeletonType),
// honoring the live damage graphic state, the inside-view '_cop' canopy
// suppression, and the shadow (tshd) flagging. Shared by SetViewInside (the live
// V-toggle) and RebuildMechRenderables (the respawn un-wreck) so the cockpit view
// stays self-consistent ACROSS the death/respawn transition -- the respawn path
// used to restore the full OUTSIDE torso around the cockpit eyepoint (no '_cop'
// suppression, and viewSkeleton left stale), which enclosed the eye in opaque
// geometry -> the "black viewport until you press V" bug. Records viewSkeleton so
// a later rebuild restores the RIGHT skeleton. Returns the count of shown meshes.
//
int
BTL4VideoRenderer::ApplyViewSkeleton(Entity *viewpoint, int inside)
{
std::map<Entity*, MechRenderTree>::iterator tree_it =
mMechRenderTrees.find(viewpoint);
if (tree_it == mMechRenderTrees.end())
return 0;
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;
// Live graphic state from the zone (healed=Exists / damaged / gone) -- read
// fresh so a just-healed respawn shows the intact mesh, not a stale cache.
Enumeration gstate = 0;
int zone_index = segment->GetPrimaryDamageZone();
if (zone_index >= 0 && zone_index < viewpoint->damageZoneCount
&& viewpoint->damageZones[zone_index] != 0)
gstate = viewpoint->damageZones[zone_index]->GetGraphicState();
CString *nm = segment->GetVideoObjectName(
(EntitySegment::SkeletonType)render_tree.viewSkeleton, 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).
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);
if (obj != NULL && strstr(filename, "tshd") != NULL)
{
obj->SetIsShadow(1);
for (int op = 0; op < obj->GetDrawOpCount(); ++op)
obj->GetDrawOp(op)->alphaTest = true;
}
}
r->second->SetDrawObj(obj);
render_tree.segGState[slot] = (int)gstate;
if (obj) ++shown; else ++hidden;
}
DEBUG_STREAM << "[view] skeleton "
<< (inside ? "A (inside)" : "N (outside)") << ": "
<< shown << " segment mesh(es) shown, " << hidden
<< " hidden\n" << std::flush;
return shown;
}
//
// 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.
//
// While WRECKED the body is the sinking hulk, so skip the live mesh swap --
// but still RECORD the chosen skeleton so the respawn rebuild restores the
// right one (RebuildMechRenderables re-applies these same rules on un-wreck).
//
Entity *viewpoint = (application != 0) ? application->GetViewpointEntity() : 0;
std::map<Entity*, MechRenderTree>::iterator tree_it =
mMechRenderTrees.find(viewpoint);
if (tree_it != mMechRenderTrees.end())
{
if (!tree_it->second.wrecked)
ApplyViewSkeleton(viewpoint, inside);
else
tree_it->second.viewSkeleton = inside
? (int)EntitySegment::SkeletonType_A
: tree_it->second.skeletonType;
}
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;
}
//===========================================================================//
// The "blue warp" translocation sphere (task #52)
//
// Reconstructed from the engine's POVTranslocateRenderable (L4VIDRND.cpp:1749):
// a sphere (tsphere.bgf) that COLLAPSES onto the respawn point then EXPANDS to
// reveal the reborn mech, rotating throughout. Loaded by FILENAME (not via the
// RES table) -- which is why every resource-name search missed it. Drawn direct
// from the render loop (BTDrawTranslocationSpheres, beside BTDrawBeams).
//
// SELF-CONTAINED ONE-SHOT: the engine keys this off the player's SimulationState
// dial, but in our reconstruction that dial ALSO drives the camera/POV +
// targeting, so pulsing it for the sphere regressed all of those (inside-view,
// no-fire). Instead the respawn path (btplayer.cpp) calls BTStartWarpEffect at
// the drop-zone origin and the effect plays its own collapse->expand -- touching
// nothing but the render. (The BTTranslocationRenderable objects the entity
// tree still builds for the replicant/POV wiring are now inert.)
//===========================================================================//
namespace {
// The engine #defines VERBATIM (L4VIDRND.cpp:1763-1770): collapse from 100x down
// to 1x over 1.3s, throb at 1x, then expand 1x -> 150x over 1.0s. TRANSLATE_LIMIT
// is the WaitForReincarnate wobble amplitude. Only these five are live (the
// ROTATE_* / TRANSLATE_RATE members are dead -- no geometry rotation).
const float TLOC_COLLAPSE_TIME = 1.3f; // COLLAPSE_TIME
const float TLOC_EXPAND_TIME = 1.0f; // EXPAND_TIME
const float TLOC_TRANSLATE_LIMIT = 2.0f; // wait wobble amplitude
float gWarpCollapseScale = 100.0f; // COLLAPSE_START_SCALE
float gWarpExpandScale = 150.0f; // EXPAND_END_SCALE
d3d_OBJECT *gTLocSphere = 0;
int gTLocSphereTried = 0;
// The single active warp one-shot (one local player per node). Faithful state
// machine: 0 Idle, 1 InitialCollapse, 3 WaitForReincarnate (world masked, wobble),
// 2 ExpandReveal (L4VIDRND.cpp POVTranslocateRenderable states).
int gWarpPhase = 0;
float gWarpT = 0.0f; // phase clock (collapse/expand)
float gWaitClock = 0.0f; // WaitForReincarnate elapsed (wobble + stuck-black failsafe)
float gWarpX = 0.0f, gWarpY = 0.0f, gWarpZ = 0.0f;
int gWarpPOV = 0; // 1 = centre on the local eye (own death/respawn); 0 = world-anchored (peer)
int gWarpMasked = 0; // 1 while we have raised the SetIsDead world mask
}
extern void BTSetWorldDead(int dead); // L4VIDRND.cpp bridge -> l4_application->SetIsDead
// The renderable objects the entity tree builds for the translocation wiring are
// inert now (the warp is the self-contained one-shot below); the ctor/dtor just
// satisfy MakeEntityRenderables.
BTTranslocationRenderable::BTTranslocationRenderable(
Entity *entity, int, dpl_VIEW *,
StateIndicator *effect_trigger, Point3D *drop_zone, int effect_control_state)
: BTRenderableBase(entity),
myWatchedEntity(entity),
myTrigger(effect_trigger),
myDropZone(drop_zone),
myControlState((unsigned)effect_control_state),
mySphereState(TLoc_Idle),
myTimer(0.0f),
myRotateY(0.0f),
mySphereVisible(false)
{
}
BTTranslocationRenderable::~BTTranslocationRenderable()
{
}
static void BTWarpApplyScaleEnv()
{
if (const char *s = getenv("BT_WARP_SCALE"))
{
float v = (float)atof(s);
if (v > 0.0f) { gWarpCollapseScale = v; gWarpExpandScale = v * 1.5f; }
}
}
//
// LOCAL DEATH -> InitialCollapse (engine: trigger becomes == control state, Idle ->
// InitialCollapse, L4VIDRND.cpp:1900-1911). The sphere collapses 100x -> 1x onto
// your own eye over 1.3s (world still visible), then raises the SetIsDead world mask
// and THROBS (WaitForReincarnate) until the respawn kicks the expand. POV only.
//
void BTStartWarpCollapsePOV()
{
BTWarpApplyScaleEnv();
gWarpPhase = 1; // InitialCollapse
gWarpT = 0.0f;
gWaitClock = 0.0f;
gWarpPOV = 1;
gWarpX = gWarpY = gWarpZ = 0.0f;
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] warp COLLAPSE (POV) start\n" << std::flush;
}
//
// LOCAL RESPAWN -> ExpandReveal (engine: trigger becomes != control state,
// WaitForReincarnate -> ExpandReveal + SetIsDead(false), L4VIDRND.cpp:1987-1989).
// Drops the world mask and blasts the sphere 1x -> 150x, revealing the reborn world.
//
void BTStartWarpExpandPOV()
{
BTWarpApplyScaleEnv();
if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } // == SetIsDead(false) :1989
gWarpPhase = 2; // ExpandReveal
gWarpT = 0.0f;
gWarpPOV = 1;
gWarpX = gWarpY = gWarpZ = 0.0f;
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] warp EXPAND (POV) start\n" << std::flush;
}
//
// World-anchored warp (a PORT EXTENSION -- the authentic effect is POV only): an
// OBSERVER seeing a peer respawn over THERE. No world mask (the observer is alive);
// expand-reveal at the peer's world point.
//
void BTStartWarpEffect(float x, float y, float z)
{
BTWarpApplyScaleEnv();
gWarpPhase = 2; // ExpandReveal
gWarpT = 0.0f;
gWarpPOV = 0;
gWarpX = x; gWarpY = y; gWarpZ = z;
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] warp EXPAND (world) at (" << x << "," << y << "," << z << ")\n" << std::flush;
}
//
// FAILSAFE: drop the world mask + end the effect. MUST be called on every path
// where a collapse fired but no respawn/expand can follow (mission end, out of
// lives, dropped DropZoneReply) -- otherwise the SetIsDead world stays BLACK forever.
//
void BTWarpForceUnmask()
{
if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; }
if (gWarpPhase == 3) gWarpPhase = 0;
}
//
// Play the warp one-shot: tsphere.bgf COLLAPSES onto the respawn point (scale
// -> 1 over 1.3s) then EXPANDS to reveal the reborn mech (1 -> max over 1.0s),
// rotating. Alpha pass (beside the beams) so it blends + Z-tests vs the world.
//
void
BTDrawTranslocationSpheres(LPDIRECT3DDEVICE9 device, const D3DXMATRIX *view,
float dt, Time frame_time)
{
if (gWarpPhase == 0)
return;
if (gTLocSphere == 0 && !gTLocSphereTried)
{
gTLocSphereTried = 1;
gTLocSphere = d3d_OBJECT::LoadObject(device, "tsphere.bgf");
if (gTLocSphere != 0)
{
for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op)
{
L4DRAWOP *dop = gTLocSphere->GetDrawOp(op);
// THE SWIRL MOTION. The authentic material scrolls its texture
// (tsphere_scr_tex SPECIAL "SCROLL 0.0 0.0 0.1 0.5"), but the port only
// picks scroll up from a per-texture .met file (L4D3D.cpp:640), which
// tsphere has none of, and the ramp-bake path leaves doScroll=false --
// so our swirl was FROZEN. Set the authored scroll rates here so
// SetTextureScrolling animates it (u -0.1/s, v +0.5/s -> the churning
// swirl that "spins around"), REPEAT wrap so the scroll tiles.
dop->texture.doScroll = true;
dop->texture.scrollUDelta = 0.1f;
dop->texture.scrollVDelta = 0.5f;
dop->texture.wrap_u = L4TEXOP::REPEAT;
dop->texture.wrap_v = L4TEXOP::REPEAT;
// The pass routing (drawAsSky for POV / alphaTest for the peer overlay)
// is set per-frame below, since it differs by mode.
}
}
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] tsphere.bgf load "
<< (gTLocSphere ? "OK" : "FAILED")
<< " ops=" << (gTLocSphere ? gTLocSphere->GetDrawOpCount() : 0)
<< (gTLocSphere ? " center=(" : "")
<< (gTLocSphere ? gTLocSphere->mCullCenter.x : 0.0f) << ","
<< (gTLocSphere ? gTLocSphere->mCullCenter.y : 0.0f) << ","
<< (gTLocSphere ? gTLocSphere->mCullCenter.z : 0.0f) << ") r="
<< (gTLocSphere ? gTLocSphere->GetRadius() : 0.0f)
<< "\n" << std::flush;
}
if (gTLocSphere == 0)
{
gWarpPhase = 0;
return;
}
// ===== The POVTranslocateRenderable::Execute() state machine (L4VIDRND.cpp) =====
float scale = 1.0f;
if (gWarpPhase == 1) // InitialCollapse (:1928): 101 -> 1
{
gWarpT += dt;
float left = 1.0f - (gWarpT / TLOC_COLLAPSE_TIME);
if (left <= 0.0f) // collapse finished (:1935)
{
scale = 1.0f;
gWarpPhase = 3; // -> WaitForReincarnate (:1946)
gWaitClock = 0.0f; // rebaseline for the wobble (:1945)
if (gWarpPOV) { BTSetWorldDead(1); gWarpMasked = 1; } // SetIsDead(true) (:1947)
}
else
scale = left * gWarpCollapseScale + 1.0f; // (pct_left*100)+1
}
else if (gWarpPhase == 3) // WaitForReincarnate (:1978): throb black
{
gWaitClock += dt;
scale = 1.0f;
// FAILSAFE: the respawn kicks Wait->Expand (BTStartWarpExpandPOV). If it never
// arrives (mission end / out of lives / dropped DropZoneReply) the SetIsDead
// world would stay BLACK forever -- so time out and un-mask after 12s.
if (gWaitClock > 12.0f)
{
if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; }
gWarpPhase = 0;
return;
}
}
else // ExpandReveal (:2013): 1 -> 151
{
gWarpT += dt;
float used = gWarpT / TLOC_EXPAND_TIME;
if (used >= 1.0f) // reveal done (:2030)
{
if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } // safety (mask should already be off)
gWarpPhase = 0;
return;
}
scale = used * gWarpExpandScale + 1.0f; // (pct_used*150)+1
}
if (getenv("BT_TLOC_LOG"))
{
static int s_lt = 0;
if ((++s_lt % 15) == 1)
DEBUG_STREAM << "[tloc] warp phase=" << gWarpPhase << " scale=" << scale
<< " pov=" << gWarpPOV << " masked=" << gWarpMasked << "\n" << std::flush;
}
// PLACEMENT (authentic POVTranslocateRenderable, L4VIDRND.cpp:1812 "rotated and
// scaled around the VTV"): the sphere is PURE SCALE -- NO geometry spin. The
// engine's myRotateY is dead code; the swirl is entirely the texture SCROLL.
// - POV (your OWN respawn): centre the sphere ON your eye and orient it to the
// view: world = Scale(s) * inverse(view). In view space that puts the sphere
// at the origin, so you sit at its centre looking out through the swirl -- the
// authentic tunnel. (World-fixing it at the mech's feet was the "blob from my
// own perspective / not aligned to my orientation" the user reported.)
// - Non-POV (observing a PEER respawn): anchor at the peer's world point so the
// swirl plays over there -- pure scale, then translate.
const float s = scale;
// PLACEMENT -- the authentic VTV/eye parenting (L4VIDRND.cpp:2069-2074): the mesh
// LOCAL ORIGIN is the eye, so world = localToWorld * inverse(view) with NO recenter.
// tsphere is authored OFF-origin ON PURPOSE (centre +8.25y) so the eye sits ~0.38r
// low INSIDE the sphere; recentring it to dead-centre (my earlier attempt) was WRONG
// and only worsened the funnel. In WaitForReincarnate the localToWorld is a pure
// Lissajous TRANSLATION (the throb, :1996-2003); otherwise a pure SCALE (no spin --
// myRotateY is dead; the swirl is the texture scroll).
D3DXMATRIX local;
if (gWarpPhase == 3) // WaitForReincarnate throb
D3DXMatrixTranslation(&local,
(float)(cos(gWaitClock * 3.33) * TLOC_TRANSLATE_LIMIT),
(float)(sin(gWaitClock * 2.5) * TLOC_TRANSLATE_LIMIT), 0.0f);
else
D3DXMatrixScaling(&local, s, s, s);
D3DXMATRIX world;
if (gWarpPOV && view != 0)
{
D3DXMATRIX invView;
D3DXMatrixInverse(&invView, 0, view);
world = local * invView; // scale/throb about the eye, then eye->world
}
else
{
D3DXMATRIX anchorM;
D3DXMatrixTranslation(&anchorM, gWarpX, gWarpY, gWarpZ);
world = local * anchorM; // peer overlay: scale about the world anchor
}
gTLocSphere->SetLocalToWorld(world);
// The swirl COLOUR is the material's "sky" ramp remapping the grayscale bintA
// cloud per-texel (un-gated for tsphere_mtl in bgfload.cpp; L4D3D.cpp:480), so the
// bound texture is ALREADY blue-white -- MODULATE by WHITE = the ramp unchanged.
static DWORD s_warpColor = 0;
if (s_warpColor == 0)
{
const char *wc = getenv("BT_WARP_COLOR");
s_warpColor = wc ? (DWORD)strtoul(wc, 0, 16) : 0xFFFFFFFF;
if (s_warpColor == 0) s_warpColor = 0xFFFFFFFF;
}
if (gWarpPOV)
{
// ===== POV: draw as SKY, exactly like the engine (isDeathDraw -> drawAsSky,
// L4VIDRND.cpp:1802-1808). OPAQUE + backface-cull (CW) + z-test ON is what makes
// an inside-viewed dome render CLEAN: no translucent double-blend, no coincident
// double-winding z-fight (that WAS the "glitchy funnel"), and the huge expand
// shell is occluded by the world coming back on = the reveal. The world itself
// is blacked by the SetIsDead mask (raised at collapse-end), so during the throb
// you see ONLY the swirling dome. Sky-pass states: L4VIDEO.cpp:7526 (cull CW),
// 7568-7570 (zwrite on / blend off), 7693 (light off). Leave ZENABLE default. =====
for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op)
{
L4DRAWOP *dop = gTLocSphere->GetDrawOp(op);
dop->drawAsSky = true; dop->alphaTest = false; dop->drawAsDecal = false;
}
DWORD sZW, sAB, sCull, sLight, sTF, sCOp, sCA1, sCA2, sAOp, sAA1;
device->GetRenderState(D3DRS_ZWRITEENABLE, &sZW);
device->GetRenderState(D3DRS_ALPHABLENDENABLE, &sAB);
device->GetRenderState(D3DRS_CULLMODE, &sCull);
device->GetRenderState(D3DRS_LIGHTING, &sLight);
device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTF);
device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp);
device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1);
device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2);
device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp);
device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1);
device->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);
device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); // OPAQUE -> single write/pixel
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // one winding -> no z-fight
device->SetRenderState(D3DRS_LIGHTING, FALSE);
device->SetRenderState(D3DRS_TEXTUREFACTOR, s_warpColor);
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // ramped blue-white swirl
device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR);
gTLocSphere->Draw(PASS_SKY, view, frame_time);
device->SetRenderState(D3DRS_ZWRITEENABLE, sZW);
device->SetRenderState(D3DRS_ALPHABLENDENABLE, sAB);
device->SetRenderState(D3DRS_CULLMODE, sCull);
device->SetRenderState(D3DRS_LIGHTING, sLight);
device->SetRenderState(D3DRS_TEXTUREFACTOR, sTF);
device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp);
device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1);
device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp);
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1);
}
else
{
// ===== Peer overlay (port extension): the observer is ALIVE (no world mask), so
// a sky-drawn sphere would be occluded by the world -> invisible. Draw it in the
// alpha pass, Z-tested + translucent, so the swirl reads at the peer's spot. =====
for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op)
{
L4DRAWOP *dop = gTLocSphere->GetDrawOp(op);
dop->alphaTest = true; dop->drawAsSky = false; dop->drawAsDecal = false;
}
const DWORD peerColor = (s_warpColor == 0xFFFFFFFF) ? 0xB0FFFFFF : s_warpColor; // ~70% alpha
DWORD sSrc, sDst, sLight, sTF, sCOp, sCA1, sCA2, sAOp, sAA1;
device->GetRenderState(D3DRS_SRCBLEND, &sSrc);
device->GetRenderState(D3DRS_DESTBLEND, &sDst);
device->GetRenderState(D3DRS_LIGHTING, &sLight);
device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTF);
device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp);
device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1);
device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2);
device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp);
device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1);
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
device->SetRenderState(D3DRS_LIGHTING, FALSE);
device->SetRenderState(D3DRS_TEXTUREFACTOR, peerColor);
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR);
gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time);
device->SetRenderState(D3DRS_SRCBLEND, sSrc);
device->SetRenderState(D3DRS_DESTBLEND, sDst);
device->SetRenderState(D3DRS_LIGHTING, sLight);
device->SetRenderState(D3DRS_TEXTUREFACTOR, sTF);
device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp);
device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1);
device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp);
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1);
}
}
//===========================================================================//