The MadCat torso twists, the view turns with it, and targeting follows. Three reconstruction fronts closed: THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up): - PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog MIRRORS the watched subsystem's electrical level (+0x278), brownout downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong. - The factory watcher-CONNECT pass reconstructed (vtable slot +0x38, @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes): watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the SubProxy::Start() no-op -- every watchdog sat at 0 forever. - MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f = permanent brownout) and PowerWatcher's Derivation chains its REAL base HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the whole Torso/Searchlight/ThermalSight family). - KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not "HeatSink"); the btl4gaug heat-widget gate now tests it via the BTIsHeatWatcher bridge. THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes): - The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in every twist-capable .SKL; the camera + canopy ride the same hinge subtree through HingeRenderable's live matrix-stack compose -- ALREADY WORKING in the port. The crosshair stays screen-centered (center IS the boresight); the twist reads on the tape carets/compass/radar. - The real bug was the port's gBTAimX = tan(twist) slew (the falsified "body-mounted view" model): the camera already carried the twist, so the crosshair counter-slid to hull-forward and the fire ray with it. Deleted; the pick ray inherits the twist from the yawing eye basis. - Two instrumentation traps documented (chase-eye-as-default-camera, BT_FORCE_TORSO clobbering real joints -> the hook now only fills unresolved ones); an over-correcting explicit eye compose was added on those false readings and retired the same day. CONTROLS + REPLICATION: - Q/E spring-center on release (the axis is a twist-RATE demand; the old hold-deflection model drifted forever); X also zeroes the axis and pulses the authentic torso Recenter (@004b6918). M cycles control mode via the real CycleControlMode body. - Torso update-record DIRECTION fixed: engine truth is Write=serialize / Read=apply; @004b6a78 is the READ (was mislabeled Write) and the missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C, twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD -140-degree ghost twist. - Marching-ghost desync: 4 Standing-case guards zero stale reverse cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate). - Kill credit rerouted to the OBSERVED killer (lastInflictingID -> killer's player link) -- kills count, target K/D populates. KB: subsystems.md (watcher chain), multiplayer.md (record direction), combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view re-correction), decomp-reference.md (new addresses + tag fix), open-questions.md (dead capability-roster loops 2-4, snapshot CD read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2975 lines
117 KiB
C++
2975 lines
117 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(MECHDMG_HPP)
|
|
# include <mechdmg.hpp> // Mech__DamageZone::segmentIndex (the @004d097c dispatcher)
|
|
#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]
|
|
|
|
// task #58 diagnostics (BT_SEG_DUMP): the draw-tree topology -- which
|
|
// segment carries which joint, and who parents whom.
|
|
if (getenv("BT_SEG_DUMP"))
|
|
{
|
|
int ji = segment->GetJointIndex();
|
|
DEBUG_STREAM << "[seg] " << segment->GetIndex()
|
|
<< " '" << (const char *)segment->GetName()
|
|
<< "' parent=" << (segment->GetParent()
|
|
? segment->GetParentIndex() : -1)
|
|
<< " joint=" << ji
|
|
<< " jtype=" << (ji == -1 ? -1
|
|
: (int)joint_subsystem->GetJoint(ji)->GetJointType())
|
|
<< " site=" << (int)(segment->IsSiteSegment() != 0)
|
|
<< "\n" << std::flush;
|
|
}
|
|
|
|
//
|
|
// 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
|
|
//
|
|
// AUTHENTIC eye (decomp FUN_004579a8 @part_007.c:9274-9325; caller part_014.c:5525-5566):
|
|
// the eye offset matrix is the siteeyepoint segment's LOCAL rest transform (GetBaseOffset,
|
|
// segment+0x74 -- already in offset_matrix above), and the eye is parented on its PARENT
|
|
// segment's draw component (parent_DCS) -- NOT the root, NOT the full GetSegmentToEntity,
|
|
// NOT an upright hack. World orientation + all live motion (torso twist, gait) come from
|
|
// the parent-chain composition, exactly as the decomp's dpl_AddDCSToDCS hierarchy does;
|
|
// EyepointRotation is combined in Execute as baseOffset * R, and VIEW = inverse(eyeWorld)
|
|
// (FUN_004c22c4 @part_013.c:11788) -- so the look/up axes fall out of the eye's own basis
|
|
// with no per-mech forward-axis assumption.
|
|
//
|
|
mEyeCockpit = new DPLEyeRenderable(
|
|
entity, offset_matrix, parent_DCS, 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;
|
|
|
|
// CAMERA-BASIS CONVENTION (task #56 follow-through): the view is now the
|
|
// authentic VIEW = inverse(eyeWorld) (DPLEyeRenderable::Execute), under
|
|
// which the camera looks along -Z of its own basis -- so the basis rows
|
|
// are right/up/BACK, not right/up/look. This chase matrix was authored
|
|
// for the old LookAt (row2 = look); convert by a 180-degree turn about
|
|
// local Y (negate the X and Z rows -- determinant stays +1).
|
|
LinearMatrix debugOffset; // identity
|
|
debugOffset(0,0) = -xx; debugOffset(0,1) = -xy; debugOffset(0,2) = -xz; // X row (right, flipped)
|
|
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 (BACK = -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);
|
|
if (render_tree.wreckFlames != NULL) render_tree.wreckFlames->SetDrawObj(NULL);
|
|
render_tree.wreckHulk = NULL;
|
|
render_tree.wreckDebris = NULL;
|
|
render_tree.wreckFlames = NULL;
|
|
render_tree.wrecked = 0;
|
|
render_tree.wreckAge = 0.0f;
|
|
render_tree.wreckRevealed = 0;
|
|
render_tree.wreckHulkObj = NULL;
|
|
render_tree.wreckDebrisObj = NULL;
|
|
render_tree.wreckFlamesObj = NULL;
|
|
|
|
//
|
|
// 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);
|
|
}
|
|
|
|
|
|
//
|
|
//#############################################################################
|
|
// StartEntityEffectImplementation @004d097c (coverage-audit reconstruction)
|
|
//#############################################################################
|
|
//
|
|
// The per-zone EFFECT DISPATCHER -- the target of the whole authentic chain:
|
|
// MechDeathHandler's class-5 -> RendererManager::StartEntityEffect ->
|
|
// Renderer::StartEntityEffectMessageHandler [T0: resolves the GameModel +
|
|
// applies the ExplosionResourceTable graphic-state remap] -> this virtual.
|
|
// The binary body (@004d097c, part_014.c:5839):
|
|
// 1. resolve the zone -> its transform (+0x74, row 3 = the world position)
|
|
// and its VIDEO INDEX (+0xc4->+0xc0) = the segment slot;
|
|
// 2. look the entity up in the renderer's per-entity tree (renderer+0x3a4,
|
|
// == our mMechRenderTrees) -> the segment's DCS = the effect SOCKET;
|
|
// 3. SearchList(resource, VideoModelResourceType) -> walk the video-object
|
|
// records; atoi(name) < 1000 WARNS (the authentic gate: only INDIE/psfx
|
|
// ids attach per-zone), >= 1000 starts the effect ON the socket, tagged
|
|
// with the owning player (+0x190 -> +0x1e0) for the stop-all sweep.
|
|
// Port mapping: the socket attachment = BTStartPfxAttached (the emitter rides
|
|
// the segment via BTResolveSegmentWorld each frame); the tag = the entity
|
|
// (BTStopEntityPfx kills by entity on respawn). Layout access is by NAMED
|
|
// members only (zone->segmentIndex; the binary micro-offsets differ on our
|
|
// compiled classes -- the databinding trap).
|
|
//
|
|
void
|
|
BTL4VideoRenderer::StartEntityEffectImplementation(
|
|
Entity *entity,
|
|
DamageZone *damage_zone,
|
|
ResourceDescription::ResourceID resource_ID
|
|
)
|
|
{
|
|
if (entity == NULL || damage_zone == NULL)
|
|
{
|
|
DEBUG_STREAM << "StartEntityEffectImplementation: no entity/zone" << std::endl;
|
|
return;
|
|
}
|
|
|
|
// 1. the zone's segment slot (binary +0xc4->+0xc0; our named member)
|
|
int seg_index = ((Mech__DamageZone *)damage_zone)->EffectSegmentIndex();
|
|
|
|
// 2. the segment's world position + frame (binary: zone transform +0x74)
|
|
extern int BTResolveSegmentWorld(void *entity, int seg_index, float *pos3, float *rows9);
|
|
float pos[3], rows[9];
|
|
if (!BTResolveSegmentWorld(entity, seg_index, pos, rows))
|
|
{
|
|
DEBUG_STREAM << "StartEntityEffectImplementation: entity has no segment table" << std::endl;
|
|
return;
|
|
}
|
|
|
|
// 3. the effect resource's VIDEO MODEL (type 10) -- the effect-number list
|
|
Check(application);
|
|
ResourceDescription *res = application->GetResourceFile()->SearchList(
|
|
resource_ID, ResourceDescription::VideoModelResourceType);
|
|
if (res == NULL)
|
|
{
|
|
DEBUG_STREAM << "StartEntityEffectImplementation: " << (long)resource_ID
|
|
<< " has no video resource" << std::endl;
|
|
return;
|
|
}
|
|
res->Lock();
|
|
// VideoModel payload [T0 L4VIDEO.h L4VideoObject, RES byte-verified]:
|
|
// int32 count + count x 32-byte records {char name[15]; pad; int type;
|
|
// int modes; float blinkPeriod; float pctOn} -- atoi(name) = the effect id.
|
|
const unsigned char *pay = (const unsigned char *)res->resourceAddress;
|
|
int count = *(const int *)pay;
|
|
if (count < 0 || count > 16)
|
|
count = 0; // malformed -- refuse
|
|
const unsigned char *rec = pay + 4;
|
|
for (int i = 0; i < count; ++i, rec += 32)
|
|
{
|
|
char name[16];
|
|
memcpy(name, rec, 15);
|
|
name[15] = 0;
|
|
int fx = atoi(name);
|
|
if (fx < 1000)
|
|
{
|
|
// authentic gate (@004d097c: "< 1000" warns) -- per-zone effects
|
|
// are INDIE/psfx-band only
|
|
DEBUG_STREAM << "StartEntityEffectImplementation: non-INDIE effect "
|
|
<< fx << " in zone effect list" << std::endl;
|
|
continue;
|
|
}
|
|
extern void BTStartPfxAttached(int, void *, int, float, float, float, const float *);
|
|
BTStartPfxAttached(fx - 1000, (void *)entity, seg_index,
|
|
pos[0], pos[1], pos[2], rows);
|
|
if (getenv("BT_DEATH_LOG"))
|
|
DEBUG_STREAM << "[zonefx] entity " << entity->GetEntityID()
|
|
<< " seg " << seg_index << " psfx " << (fx - 1000)
|
|
<< " at(" << pos[0] << "," << pos[1] << "," << pos[2] << ")" << std::endl;
|
|
}
|
|
res->Unlock();
|
|
}
|
|
|
|
//
|
|
// StopAllEntityEffectsImplementation @004d0c14 -- the respawn cleanup: the
|
|
// binary kills every effect tagged (playerIdx<<16 .. |0xffff); the port kills
|
|
// every emitter tagged to the entity.
|
|
//
|
|
void
|
|
BTL4VideoRenderer::StopAllEntityEffectsImplementation(Entity *entity)
|
|
{
|
|
if (entity == NULL)
|
|
{
|
|
DEBUG_STREAM << "StopAllEntityEffectsImplementation: no entity" << std::endl;
|
|
return;
|
|
}
|
|
extern void BTStopEntityPfx(void *owner);
|
|
BTStopEntityPfx((void *)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;
|
|
// EMPTY-PLACEHOLDER guard: some shipped hulks are 153-byte stubs with ZERO
|
|
// geometry (THRDBR.BGF, FLAMESML.BGF) -- they "load" fine and draw nothing,
|
|
// so the missing-file fallback alone never fires. Treat no-vertex hulks
|
|
// as missing.
|
|
if (hulk != NULL && hulk->GetVertCount() == 0)
|
|
{
|
|
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
|
<< "' is an EMPTY placeholder -> gendbr.bgf fallback\n" << std::flush;
|
|
hulk = 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");
|
|
|
|
//
|
|
// The burning-wreck FLAMES (the 1996 case-4 "fires" branch): flamesml +
|
|
// flamebig hung over the pile; flamebig is Y-BILLBOARDED toward the camera
|
|
// (dpl_SetDCSReorientAxes) and the fires DCS falls at -0.01 (slower than
|
|
// the hulk's -0.025, so the flames ride above the sinking pile). In this
|
|
// content build FLAMESML.BGF is an empty placeholder -- only flamebig
|
|
// carries geometry (btfx:firesmoke1_mtl, the scrolling fire-noise card,
|
|
// animated by the authored SCROLL now honoured in the BGF path).
|
|
//
|
|
d3d_OBJECT *flames = d3d_OBJECT::LoadObject(GetDevice(), "flamebig.bgf");
|
|
if (flames != NULL && flames->GetVertCount() == 0)
|
|
flames = NULL;
|
|
|
|
//
|
|
// 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). All pieces start
|
|
// HIDDEN: the 1996 script's InstanceSwitchRenderables reveal them 0.25s
|
|
// after the boom (behind the dnboom flash) -- TickWreck runs the reveal.
|
|
//
|
|
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);
|
|
render_tree.wreckHulk->SetDrawObj(NULL); // hidden until the reveal
|
|
}
|
|
if (debris != NULL)
|
|
{
|
|
render_tree.wreckDebris = new DPLStaticChildRenderable(
|
|
victim, false /* main zone */, debris,
|
|
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
|
render_tree.wreckDebris->SetDrawObj(NULL);
|
|
}
|
|
if (flames != NULL)
|
|
{
|
|
render_tree.wreckFlames = new DPLStaticChildRenderable(
|
|
victim, false /* main zone */, flames,
|
|
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
|
render_tree.wreckFlames->SetDrawObj(NULL);
|
|
}
|
|
}
|
|
render_tree.wrecked = 1;
|
|
render_tree.wreckAge = 0.0f;
|
|
render_tree.wreckRevealed = 0;
|
|
render_tree.wreckHulkObj = hulk;
|
|
render_tree.wreckDebrisObj = debris;
|
|
render_tree.wreckFlamesObj = flames;
|
|
DEBUG_STREAM << "[BTrender] wreck swap: victim -> '"
|
|
<< (hulk_name[0] ? hulk_name : "gendbr.bgf")
|
|
<< (hulk ? "'" : "' (LOAD FAILED -- body hidden only)")
|
|
<< (debris ? " + ldbr debris" : "")
|
|
<< (flames ? " + flamebig flames" : "")
|
|
<< " (reveal in 0.25s)\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;
|
|
|
|
//
|
|
// The 0.25s REVEAL (the 1996 InstanceSwitch delay): the pieces appear
|
|
// behind the dnboom flash, then MakeDCSFall arms and the burial starts.
|
|
//
|
|
const float kRevealDelay = 0.25f; // static_debris/fires_delay
|
|
if (!render_tree.wreckRevealed)
|
|
{
|
|
if (render_tree.wreckAge < kRevealDelay)
|
|
return 1;
|
|
render_tree.wreckRevealed = 1;
|
|
if (render_tree.wreckHulk)
|
|
render_tree.wreckHulk->SetDrawObj(render_tree.wreckHulkObj);
|
|
if (render_tree.wreckDebris)
|
|
render_tree.wreckDebris->SetDrawObj(render_tree.wreckDebrisObj);
|
|
if (render_tree.wreckFlames)
|
|
render_tree.wreckFlames->SetDrawObj(render_tree.wreckFlamesObj);
|
|
}
|
|
|
|
// MakeDCSFall: offsetY = -1/2 g t^2 from the reveal; hulk/debris g=0.025,
|
|
// fires g=0.01 (the flames ride above the sinking pile).
|
|
float t = render_tree.wreckAge - kRevealDelay;
|
|
if (t < 0.0f) t = 0.0f;
|
|
float sink = -0.025f * t * t; // the authored hulk rate
|
|
if (sink < -8.0f)
|
|
{
|
|
// fully buried -> hide + stop ticking (the flames die with the pile:
|
|
// the original removed the whole death entity here)
|
|
if (render_tree.wreckHulk) render_tree.wreckHulk->SetDrawObj(NULL);
|
|
if (render_tree.wreckDebris) render_tree.wreckDebris->SetDrawObj(NULL);
|
|
if (render_tree.wreckFlames) render_tree.wreckFlames->SetDrawObj(NULL);
|
|
render_tree.wreckHulk = NULL;
|
|
render_tree.wreckDebris = NULL;
|
|
render_tree.wreckFlames = 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);
|
|
if (render_tree.wreckFlames)
|
|
{
|
|
render_tree.wreckFlames->SetOffsetTranslation(0.0f, -0.01f * t * t, 0.0f);
|
|
|
|
//
|
|
// Y-BILLBOARD the flame card at the camera (dpl_SetDCSReorientAxes
|
|
// analog). The parent DCS applies the victim's localToWorld (its
|
|
// death yaw), so the camera direction is taken in the victim's LOCAL
|
|
// frame: d_local = R^T * (cam - wreck) using the orthonormal rotation
|
|
// rows of localToWorld.
|
|
//
|
|
float cx, cy, cz;
|
|
d3d_OBJECT::GetCameraPosition(&cx, &cy, &cz);
|
|
float dx = cx - (float)victim->localOrigin.linearPosition.x;
|
|
float dz = cz - (float)victim->localOrigin.linearPosition.z;
|
|
float lx = dx * (float)victim->localToWorld(0, 0) + dz * (float)victim->localToWorld(0, 2);
|
|
float lz = dx * (float)victim->localToWorld(2, 0) + dz * (float)victim->localToWorld(2, 2);
|
|
if (lx * lx + lz * lz > 1e-6f)
|
|
render_tree.wreckFlames->SetOffsetYaw(atan2f(lx, lz));
|
|
}
|
|
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 canopy shell (*_cop -- the frame around the eyepoint) SHOWS by
|
|
// default: with the authentic eye (baseOffset + parent-segment DCS + inverse
|
|
// view) and the loader's single-sided dark-ramp treatment it renders as the
|
|
// dark frame with the world through the openings (task #55, verified vs
|
|
// gameplay footage). BT_HIDE_COCKPIT=1 hides it (diagnostic).
|
|
if (inside && nm != NULL && strstr((const char *)*nm, "_cop") != NULL
|
|
&& getenv("BT_HIDE_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 gWarpSpin = 0.0f; // accumulated throat-axis (Z) spin -- the SPIRAL (decomp FUN_00453dc4)
|
|
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)
|
|
{
|
|
// DIAG (off by default): BT_WARP_SELFTEST=1 forces a steady POV warp
|
|
// (held in WaitForReincarnate, world masked) so the swirl can be frame-captured and
|
|
// compared against the original (capture.png) in a solo game -- no death/respawn
|
|
// needed. Remove once the visual is signed off.
|
|
static int s_selftest = -1;
|
|
if (s_selftest < 0) s_selftest = getenv("BT_WARP_SELFTEST") ? 1 : 0;
|
|
if (s_selftest && gWarpPhase == 0)
|
|
{
|
|
gWarpPhase = 3; gWarpPOV = 1; gWaitClock = 0.0f;
|
|
gWarpX = gWarpY = gWarpZ = 0.0f;
|
|
if (!gWarpMasked) { BTSetWorldDead(1); gWarpMasked = 1; }
|
|
}
|
|
|
|
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 = getenv("BT_WARP_NOSCROLL") ? false : true; // DIAG toggle
|
|
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 (!s_selftest && 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);
|
|
|
|
// THE SPIRAL (the piece I wrongly dropped): a continuous per-frame SPIN about the
|
|
// throat axis (mesh local Z). The 1995 binary's translocate Execute (decomp
|
|
// FUN_00453dc4) accumulates an angle and writes a Z-rotation into the sphere every
|
|
// frame; the WinTesla port STUBBED it out and I followed the stub, calling
|
|
// myRotateY "dead". Spin + the axial V texture-scroll = a HELIX = the smooth
|
|
// spiralling vortex, and the rotation SWEEPS the coarse 12 facet edges so the mesh
|
|
// stops reading as a faceted "sphincter". BT_WARP_SPIN = rad/s (default 4).
|
|
static float s_spinRate = -1.0e9f;
|
|
if (s_spinRate == -1.0e9f)
|
|
{
|
|
const char *sv = getenv("BT_WARP_SPIN");
|
|
s_spinRate = sv ? (float)atof(sv) : 4.0f;
|
|
}
|
|
gWarpSpin += s_spinRate * dt;
|
|
if (gWarpSpin > 6.2831853f) gWarpSpin -= 6.2831853f;
|
|
else if (gWarpSpin < 0.0f) gWarpSpin += 6.2831853f;
|
|
D3DXMATRIX spin;
|
|
D3DXMatrixRotationZ(&spin, gWarpSpin);
|
|
|
|
// tsphere is a 12-facet BICONE tunnel; its long axis (the vortex throat) is the
|
|
// mesh LOCAL Z, which Scale*inverse(view) already points down the view forward --
|
|
// so NO reorientation is needed (default tilt 0). BT_WARP_TILT (deg, pitch about
|
|
// X) is kept only to NUDGE the off-axis convergence toward screen-centre if wanted
|
|
// (the eye sits 8.25 below the throat axis, authentically, so it reads a touch
|
|
// high).
|
|
static float s_tiltDeg = -1.0e9f;
|
|
if (s_tiltDeg == -1.0e9f)
|
|
{
|
|
const char *tv = getenv("BT_WARP_TILT");
|
|
s_tiltDeg = tv ? (float)atof(tv) : 0.0f;
|
|
}
|
|
D3DXMATRIX tilt;
|
|
D3DXMatrixRotationX(&tilt, s_tiltDeg * (float)(3.14159265358979 / 180.0));
|
|
|
|
// EYE-ON-AXIS: bintA is smooth CLOUD noise (no rings of its own); the vortex rings
|
|
// come from that cloud mapped in POLAR (U=angle around the Z throat, V=radius) --
|
|
// which only reads as CONCENTRIC rings when you look straight DOWN the throat axis.
|
|
// The eye is authentically 8.25 below the axis, which skews the rings to diagonal
|
|
// bands (the "sphincter"). BT_WARP_EYE_UP shifts the eye ONTO the axis (mesh local
|
|
// +Y) so the rings go concentric; default 8.25 (the throat-axis offset), =0 for the
|
|
// raw authentic off-axis view.
|
|
static float s_eyeUp = -1.0e9f;
|
|
if (s_eyeUp == -1.0e9f)
|
|
{
|
|
const char *ev = getenv("BT_WARP_EYE_UP");
|
|
s_eyeUp = ev ? (float)atof(ev) : 8.25f;
|
|
}
|
|
D3DXMATRIX eyeUp;
|
|
D3DXMatrixTranslation(&eyeUp, 0.0f, -s_eyeUp, 0.0f);
|
|
|
|
D3DXMATRIX world;
|
|
if (gWarpPOV && view != 0)
|
|
{
|
|
D3DXMATRIX invView;
|
|
D3DXMatrixInverse(&invView, 0, view);
|
|
// throat-axis -> origin (eyeUp), SPIN about Z, scale/throb, tilt, eye->world.
|
|
// eyeUp before spin so the rotation is about the THROAT, not the off-axis eye.
|
|
world = eyeUp * spin * local * tilt * invView;
|
|
}
|
|
else
|
|
{
|
|
D3DXMATRIX anchorM;
|
|
D3DXMatrixTranslation(&anchorM, gWarpX, gWarpY, gWarpZ);
|
|
world = local * anchorM; // peer overlay: external view, keep upright
|
|
}
|
|
gTLocSphere->SetLocalToWorld(world);
|
|
|
|
// The swirl COLOUR: the "sky" ramp already bakes the bintA cloud blue->white
|
|
// (bgfload.cpp un-gate; L4D3D.cpp:480), and we MODULATE that by the material's
|
|
// authored EMISSIVE {0.7,0.5,1} = 0xB380FF (the lavender-blue energy cast) which
|
|
// the LIGHTING-off path would otherwise drop -- that lavender is the original's
|
|
// signature colour. BT_WARP_COLOR=AARRGGBB overrides.
|
|
static DWORD s_warpColor = 0;
|
|
if (s_warpColor == 0)
|
|
{
|
|
const char *wc = getenv("BT_WARP_COLOR");
|
|
s_warpColor = wc ? (DWORD)strtoul(wc, 0, 16) : 0xFFB380FF;
|
|
if (s_warpColor == 0) s_warpColor = 0xFFB380FF;
|
|
}
|
|
|
|
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;
|
|
DWORD sMin, sMag, sMip, sAniso, sFog;
|
|
device->GetRenderState(D3DRS_ZWRITEENABLE, &sZW);
|
|
device->GetRenderState(D3DRS_ALPHABLENDENABLE, &sAB);
|
|
device->GetRenderState(D3DRS_CULLMODE, &sCull);
|
|
device->GetRenderState(D3DRS_LIGHTING, &sLight);
|
|
device->GetRenderState(D3DRS_FOGENABLE, &sFog);
|
|
device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTF);
|
|
device->GetSamplerState(0, D3DSAMP_MINFILTER, &sMin);
|
|
device->GetSamplerState(0, D3DSAMP_MAGFILTER, &sMag);
|
|
device->GetSamplerState(0, D3DSAMP_MIPFILTER, &sMip);
|
|
device->GetSamplerState(0, D3DSAMP_MAXANISOTROPY, &sAniso);
|
|
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
|
|
{ // DIAG: BT_WARP_CULL = none|cw|ccw (default cw). The mesh is double-sided
|
|
// (every tri emitted fwd+rev); if CW doesn't cleanly cull one winding the two
|
|
// coincident surfaces z-fight into radial sparkle "spokes".
|
|
static DWORD s_cull = 0xffffffff;
|
|
if (s_cull == 0xffffffff) {
|
|
const char *cv = getenv("BT_WARP_CULL");
|
|
s_cull = (cv && cv[0]=='n') ? D3DCULL_NONE : (cv && cv[1]=='c') ? D3DCULL_CCW : D3DCULL_CW;
|
|
}
|
|
device->SetRenderState(D3DRS_CULLMODE, s_cull);
|
|
}
|
|
device->SetRenderState(D3DRS_LIGHTING, FALSE);
|
|
device->SetRenderState(D3DRS_FOGENABLE, FALSE); // tsphere_mtl is "IMMUNE 1" (fog-immune) -- world fog was washing the swirl
|
|
device->SetRenderState(D3DRS_TEXTUREFACTOR, s_warpColor);
|
|
// FILTERING: era-authentic ISOTROPIC linear+mip (default). My earlier
|
|
// anisotropic "fix" was the SPOKE driver: on the grazing funnel wall the texel
|
|
// footprint is stretched RADIALLY (down the throat/V), so anisotropy averages
|
|
// ALONG that (smears radially) while keeping azimuthal sharpness -> the soft
|
|
// concentric rings collapse into radial STREAKS/spokes. Plain isotropic linear
|
|
// keeps the rings soft + concentric. BT_WARP_ANISO=N re-enables it (diag only).
|
|
{
|
|
static int s_aniso = -2;
|
|
if (s_aniso == -2) { const char *a = getenv("BT_WARP_ANISO"); s_aniso = a ? atoi(a) : 0; }
|
|
if (s_aniso > 0)
|
|
{
|
|
device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC);
|
|
device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
|
|
device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
|
|
device->SetSamplerState(0, D3DSAMP_MAXANISOTROPY, (DWORD)s_aniso);
|
|
}
|
|
else // force isotropic linear (undo any anisotropy a prior draw left set)
|
|
{
|
|
device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
|
|
device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
|
|
// Trilinear mips. (The grainy radial "spokes" were NOT a mip problem -- they were
|
|
// the texture-scroll precision collapse, fixed in L4D3D::SetTextureScrolling via fmod.
|
|
// BT_WARP_MIP=0 forces base-level-only (diag).)
|
|
{ static int s_mip=-1; if(s_mip<0){const char*mv=getenv("BT_WARP_MIP"); s_mip=(mv&&mv[0]=='0')?0:1;}
|
|
device->SetSamplerState(0, D3DSAMP_MIPFILTER, s_mip?D3DTEXF_LINEAR:D3DTEXF_NONE); }
|
|
}
|
|
}
|
|
// The BANDS are the per-VERTEX Gouraud colour (concentric shade-ramp contours
|
|
// baked from the geometry in bgfload::finish); the bintA texture (lifted to a
|
|
// gentle range) MODULATES churn within them. COLOROP = TEXTURE x DIFFUSE(bands).
|
|
// BT_WARP_TEXMOD=0 shows the bands ALONE (diagnostic: is the geometry shade right).
|
|
device->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1); // vertex colour = diffuse
|
|
{
|
|
static int s_texmod = -1;
|
|
// Ramp is BAKED into the texture now -> default SELECTARG1(TEXTURE) (else branch).
|
|
// BT_WARP_TEXMOD=1 re-enables the TEXTURExDIFFUSE modulate (diag: double-tints).
|
|
if (s_texmod < 0) { const char *t = getenv("BT_WARP_TEXMOD"); s_texmod = (t && t[0] == '1') ? 1 : 0; }
|
|
if (s_texmod)
|
|
{
|
|
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
|
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // bintA cloud churn
|
|
device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); // the band colour
|
|
}
|
|
else
|
|
{
|
|
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
|
|
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // baked lavender ramp -- direct
|
|
}
|
|
}
|
|
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
|
|
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR);
|
|
|
|
gTLocSphere->Draw(PASS_SKY, view, frame_time);
|
|
|
|
device->SetSamplerState(0, D3DSAMP_MINFILTER, sMin);
|
|
device->SetSamplerState(0, D3DSAMP_MAGFILTER, sMag);
|
|
device->SetSamplerState(0, D3DSAMP_MIPFILTER, sMip);
|
|
device->SetSamplerState(0, D3DSAMP_MAXANISOTROPY, sAniso);
|
|
device->SetRenderState(D3DRS_ZWRITEENABLE, sZW);
|
|
device->SetRenderState(D3DRS_ALPHABLENDENABLE, sAB);
|
|
device->SetRenderState(D3DRS_CULLMODE, sCull);
|
|
device->SetRenderState(D3DRS_LIGHTING, sLight);
|
|
device->SetRenderState(D3DRS_FOGENABLE, sFog);
|
|
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);
|
|
}
|
|
|
|
// DIAG (off by default): BT_WARP_SELFSHOT=<prefix> dumps a backbuffer FRAME
|
|
// SEQUENCE (<prefix>_00.png ..) once the warp has settled, so the swirl can be turned
|
|
// into a GIF / compared to capture.png WITHOUT bringing the window to the foreground.
|
|
// Retained as the warp's visual-verification harness (needed for the open peer/collapse work).
|
|
{
|
|
const char *shotPath = getenv("BT_WARP_SELFSHOT");
|
|
if (shotPath && gWarpPhase != 0)
|
|
{
|
|
static int s_shotN = 0;
|
|
++s_shotN;
|
|
// 40 frames, every 3rd, starting at frame 45 (~0.75s in) -> ~2s of the
|
|
// spinning/churning swirl at 60fps.
|
|
if (s_shotN >= 45 && ((s_shotN - 45) % 3 == 0))
|
|
{
|
|
int idx = (s_shotN - 45) / 3;
|
|
if (idx < 40)
|
|
{
|
|
char fn[600];
|
|
_snprintf(fn, sizeof fn, "%s_%02d.png", shotPath, idx);
|
|
IDirect3DSurface9 *bb = 0;
|
|
if (SUCCEEDED(device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &bb)) && bb)
|
|
{
|
|
D3DXSaveSurfaceToFileA(fn, D3DXIFF_PNG, bb, 0, 0);
|
|
bb->Release();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//===========================================================================//
|