Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.
Layout:
engine/ MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
models) + image codec; the minimal rp/ headers the audio HAL needs
game/ reconstructed BT logic + surviving-original BT source + fwd shims
+ WinMain launcher
content/ full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
docs/ format specs + reconstruction ledgers
reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
tools/ MP console emulator + map/resource scanners
One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,878 @@
|
||||
//===========================================================================//
|
||||
// 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>
|
||||
#pragma hdrstop
|
||||
|
||||
#if !defined(BTL4VID_HPP)
|
||||
# include <btl4vid.hpp>
|
||||
#endif
|
||||
#if !defined(MECH_HPP)
|
||||
# include <mech.hpp> // Mech / JointedMover segment table + subsystems
|
||||
#endif
|
||||
#if !defined(MECHWEAP_HPP)
|
||||
# include <mechweap.hpp> // MechWeapon::GetClassDerivations (reticle pip)
|
||||
#endif
|
||||
#if !defined(NOTATION_HPP)
|
||||
# include <notation.hpp>
|
||||
#endif
|
||||
#if !defined(NAMELIST_HPP)
|
||||
# include <namelist.hpp>
|
||||
#endif
|
||||
#if !defined(APP_HPP)
|
||||
# include <app.hpp>
|
||||
#endif
|
||||
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
|
||||
//
|
||||
// Material-name substitution placeholders. Mirrors RPL4VideoRenderer's
|
||||
// color_parameter/badge_parameter, plus BT's patch/serno.
|
||||
//
|
||||
static const char * const colorParameter = "%color%"; // @0051d188
|
||||
static const char * const badgeParameter = "%badge%"; // @0051d18c
|
||||
static const char * const patchParameter = "%patch%"; // @0051d190
|
||||
static const char * const sernoParameter = "%serno%"; // @0051d194
|
||||
|
||||
//
|
||||
// Radial spacing between adjacent weapon pips along the reticle (_DAT_004cdce8).
|
||||
//
|
||||
static const float PIP_SPACING = 0.01f; // _DAT_004cdce8
|
||||
|
||||
//
|
||||
// One-character serial number stamped into %serno% material names; advances
|
||||
// '0'..'9' then 'A' each mech loaded. (DAT @0051d1b5.)
|
||||
//
|
||||
static char gSerno = '0';
|
||||
|
||||
//
|
||||
// BattleTech entity / subsystem ClassIDs touched by the dispatch switches that
|
||||
// were not recoverable from the surviving headers (the rest -- MechClassID,
|
||||
// BTPlayerClassID, ReservoirClassID, EmitterClassID, PPCClassID -- resolve via
|
||||
// the BT registration headers). Values from CLASSMAP.md / the recovered enum.
|
||||
//
|
||||
enum
|
||||
{
|
||||
MechMarkerClassID = 0xBBA, // timestamp / beacon marker
|
||||
MechWeaponClassID = 0xBCD, // projectile-weapon tracer
|
||||
SearchLightClassID = 0xBD8 // searchlight subsystem
|
||||
};
|
||||
|
||||
extern NameList
|
||||
*materialSubstitutionList; // DAT_004f1aac
|
||||
|
||||
extern Entity
|
||||
*Entity_Being_Created; // DAT_004f1aa8
|
||||
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// MakeEntityRenderables
|
||||
//#############################################################################
|
||||
//
|
||||
// @004d0774
|
||||
//
|
||||
// The ClassID dispatch (analogue of RPL4VideoRenderer::MakeEntityRenderables).
|
||||
//
|
||||
void
|
||||
BTL4VideoRenderer::MakeEntityRenderables(
|
||||
Entity *entity,
|
||||
ResourceDescription *model_resource,
|
||||
ViewFrom view_type)
|
||||
{
|
||||
Entity_Being_Created = entity; // DAT_004f1aa8
|
||||
|
||||
HierarchicalDrawComponent *mech_root = NULL;
|
||||
|
||||
switch (entity->GetClassID()) // entity[0x04]
|
||||
{
|
||||
case MechClassID: // 0xBB9
|
||||
{
|
||||
//
|
||||
// Fog for the mech's main view, then load the colour/badge/patch
|
||||
// material substitutions, build the whole mech, and tear the
|
||||
// substitution list back down.
|
||||
//
|
||||
SetFogStyle(updateFogSetting); // FUN_0045d3cc(this,0x68)
|
||||
SetupMaterialSubstitutionList(entity); // FUN_004d0cc0
|
||||
mech_root = MakeMechRenderables( // FUN_004cef28
|
||||
entity, model_resource, view_type);
|
||||
TearDownMaterialSubstitutionList(); // FUN_004d11e8
|
||||
// NB: the RootRenderable built by MakeMechRenderables registers
|
||||
// itself with the renderer (AddRenderable) and hooks to the entity's
|
||||
// localToWorld in its ctor -- no explicit AddDynamicRenderable here
|
||||
// (unlike the 1996 VideoComponent path).
|
||||
(void)mech_root;
|
||||
break;
|
||||
}
|
||||
|
||||
case MechMarkerClassID: // 0xBBA (timestamp/marker beacon)
|
||||
{
|
||||
d3d_OBJECT *marker = LoadObject("tmst_c"); // FUN_00498448
|
||||
BTRootRenderable *root = // FUN_00453578, alloc 100
|
||||
new BTRootRenderable(
|
||||
entity, VideoRenderable::Dynamic, marker,
|
||||
GetScene(), 1, 0);
|
||||
// watcher that keeps the marker oriented (FUN_00458c58, alloc 0x120)
|
||||
new BTMarkerWatcherRenderable(
|
||||
entity, 0, GetMainView() /* this[0x2cc] */, root->GetDCS());
|
||||
break;
|
||||
}
|
||||
|
||||
case BTPlayerClassID: // 0xBDA
|
||||
{
|
||||
StateIndicator *sim_state =
|
||||
(StateIndicator *)entity->GetAttributePointer(1 /* SimulationState */);
|
||||
if ((entity->GetInstance() & 0xC) == 4) // ReplicantInstance
|
||||
{
|
||||
//
|
||||
// Third-party view: drop-zone translocation effect.
|
||||
//
|
||||
Point3D *drop_zone =
|
||||
(Point3D *)entity->GetAttributePointer("DropZoneLocation"); // @0051d73a
|
||||
if (sim_state && drop_zone)
|
||||
{
|
||||
new BTTranslocationRenderable( // FUN_00458d2c, alloc 0x40
|
||||
entity, VideoRenderable::Watcher, GetMainView(),
|
||||
sim_state, drop_zone, 1);
|
||||
}
|
||||
}
|
||||
else if (sim_state)
|
||||
{
|
||||
//
|
||||
// Our own POV start/end (mission fade in/out) using the fog
|
||||
// colour + near/far planes stored on the renderer.
|
||||
//
|
||||
new BTPOVStartEndRenderable( // FUN_00454394, alloc 0x50
|
||||
entity, VideoRenderable::Watcher, GetMainView(),
|
||||
dplMainZone, dplDeathZone, sim_state,
|
||||
fogRed, fogGreen, fogBlue, fogNear, fogFar,
|
||||
3 /* MissionStartingState */, 4 /* MissionEndingState */);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
//
|
||||
// Unknown / non-mech entities (terrain, cavern world geometry, props,
|
||||
// landmarks, doorframes, eyecandy, ...) route to the DPL per-entity
|
||||
// builder -- exactly as RP's default does (RPL4VID.cpp:1436). That
|
||||
// builder loads each entity's video object(s) (.bgf via
|
||||
// d3d_OBJECT::LoadObject -> LoadObjectBGF) and hangs them on a
|
||||
// Root/Static/DCS-instance renderable, which is how the cavern world
|
||||
// gets onto the screen. (Previously this deferred to the no-op
|
||||
// VideoRenderer grandparent -> world drew nothing.) The uninitialised
|
||||
// `this_instance` in the CulturalIcon/Landmark arm has been fixed in
|
||||
// L4VIDEO.cpp.
|
||||
//
|
||||
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);
|
||||
|
||||
//
|
||||
// Per-segment renderable array (the parent for each segment's children).
|
||||
//
|
||||
int segment_count = jointed_mover->segmentCount; // [0x318]
|
||||
HierarchicalDrawComponent **dcs_array =
|
||||
new HierarchicalDrawComponent*[segment_count];
|
||||
for (int i = 0; i < segment_count; ++i)
|
||||
dcs_array[i] = NULL;
|
||||
|
||||
// bring-up diagnostics (counts geometry actually loaded vs. requested)
|
||||
int dbg_obj_requested = 0, dbg_obj_loaded = 0, dbg_eye = 0;
|
||||
DEBUG_STREAM << "[BTrender] MakeMechRenderables: " << segment_count
|
||||
<< " segments, view=" << (int)type << "\n" << std::flush;
|
||||
|
||||
JointSubsystem *joint_subsystem = jointed_mover->GetJointSubsystem(); // [0x31c]
|
||||
|
||||
//
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Walk the EntitySegment table. For each segment: build its offset matrix,
|
||||
// find its parent renderable, choose its joint renderable, load and hang its
|
||||
// geometry. Site segments (eyepoint, gun ports) are handled specially.
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
EntitySegment::SegmentTableIterator segment_iterator(jointed_mover->segmentTable /* [0x300] */);
|
||||
EntitySegment *segment;
|
||||
|
||||
while ((segment = segment_iterator.ReadAndNext()) != NULL) // vtbl+0x28
|
||||
{
|
||||
LinearMatrix offset_matrix;
|
||||
offset_matrix = segment->GetBaseOffset(); // [0x74]
|
||||
|
||||
//
|
||||
// Parent renderable: root if the segment has no parent, else the
|
||||
// renderable already built for its parent segment.
|
||||
//
|
||||
HierarchicalDrawComponent *parent_DCS;
|
||||
if (!segment->GetParent() /* [0xc4] */)
|
||||
{
|
||||
parent_DCS = this_root;
|
||||
}
|
||||
else
|
||||
{
|
||||
parent_DCS = dcs_array[segment->GetParentIndex() /* [0xc8] */];
|
||||
}
|
||||
|
||||
//
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Site segment? The eyepoint site builds the camera (DPLEyeRenderable)
|
||||
// for the inside view; other sites carry no body geometry here (their
|
||||
// subsystem effects are deferred -- TODO(bring-up)).
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
if (segment->IsSiteSegment() /* [0x10] */ != 0)
|
||||
{
|
||||
if (type == insideEntity &&
|
||||
strcmp((const char *)segment->GetName() /* [0x11c] */, "siteeyepoint") == 0) // @0051d290
|
||||
{
|
||||
EulerAngles *eye_rot =
|
||||
(EulerAngles *)entity->GetAttributePointer("EyepointRotation"); // @0051d29d
|
||||
//
|
||||
// DPLEyeRenderable installs itself as the renderer's camera
|
||||
// (mCamera) so the main view tracks the cockpit eyepoint.
|
||||
//
|
||||
mCamera = new DPLEyeRenderable(
|
||||
entity, offset_matrix, parent_DCS, eye_rot);
|
||||
dbg_eye = 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
// Load this segment's geometry (skeleton-variant .bgf), if any.
|
||||
//
|
||||
d3d_OBJECT *this_object = NULL;
|
||||
CString *object_name = segment->GetVideoObjectName(skeletonType); // 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;
|
||||
}
|
||||
|
||||
delete [] dcs_array;
|
||||
|
||||
DEBUG_STREAM << "[BTrender] mech tree built: meshes " << dbg_obj_loaded
|
||||
<< "/" << dbg_obj_requested << " loaded, eye=" << dbg_eye << "\n" << std::flush;
|
||||
|
||||
//
|
||||
// TODO(bring-up): inside-view targeting reticle (BTReticleRenderable +
|
||||
// AddWeapon pips) and the per-subsystem weapon/effect renderables (PPC/
|
||||
// emitter beams, projectile tracers, coolant, searchlight) are NOT built
|
||||
// here yet -- they depend on the dpl2d_ 2D display-list layer (stubbed) and
|
||||
// the BT effect renderables (stubbed). Adding them is the HUD / weapons
|
||||
// render bring-up step; they are not required to get the mech body drawn.
|
||||
//
|
||||
|
||||
//
|
||||
// DEBUG(bring-up): install the fixed external chase camera for the player's
|
||||
// own mech. The eye renderable parents on this_root, so when the render
|
||||
// tree executes (RootRenderable::Execute pushes entity->localToWorld onto the
|
||||
// matrix stack) the camera's offset is composed with the mech's world matrix
|
||||
// -- i.e. the camera tracks the mech. DPLEyeRenderable looks down its local
|
||||
// +Z axis (row 2) with local +Y as up (row 1) from its translation (row 3);
|
||||
// D3DXMatrixLookAtRH re-derives the basis from pos/at/up.
|
||||
//
|
||||
// Mech-local frame (from the .skl): +Y up, the mech FACES -Z (gun ports /
|
||||
// eyepoint are at -Z), and the Avatar is ~10-12 units tall (hip at y~5.3,
|
||||
// eyepoint ~y9). Place the camera in FRONT (-Z) and above, looking back
|
||||
// toward the mech centre.
|
||||
//
|
||||
if (buildDebugChaseCamera)
|
||||
{
|
||||
// CHASE view (task #15 usability): the mech faces -Z, so the original
|
||||
// debug placement (camera at -Z, "in front, looking back at its face")
|
||||
// made W walk the mech TOWARD the viewer -- hopelessly disorienting to
|
||||
// drive. Default is now BEHIND (+Z) and above, looking forward over the
|
||||
// shoulder: press forward, the mech walks away from you; turns read
|
||||
// correctly. env BT_CAM=face restores the old face-on animation view.
|
||||
const char *camMode = getenv("BT_CAM");
|
||||
const bool faceView = (camMode != 0 && camMode[0] == 'f');
|
||||
float camPx = 0.0f, camPy = faceView ? 9.0f : 11.0f;
|
||||
float camPz = faceView ? -28.0f : 28.0f; // -Z front / +Z behind
|
||||
const float tgtX = 0.0f, tgtY = 6.0f;
|
||||
const float tgtZ = faceView ? 0.0f : -6.0f; // chase: look ahead of the mech
|
||||
// DEBUG(bring-up): BT_CAM_Y / BT_CAM_Z override the fixed chase-camera offset --
|
||||
// raising it clears mound-shoulder OCCLUSION, but NOT genuine geometry clipping
|
||||
// where the mech is stopped on a steep slope and the terrain rises through its
|
||||
// legs (that is a collision-vs-visual issue, not a camera one).
|
||||
if (const char *cy = getenv("BT_CAM_Y")) camPy = (float)atof(cy);
|
||||
if (const char *cz = getenv("BT_CAM_Z")) camPz = (float)atof(cz);
|
||||
|
||||
// look direction (local +Z of the camera) = normalize(target - pos)
|
||||
float zx = tgtX - camPx, zy = tgtY - camPy, zz = tgtZ - camPz;
|
||||
float zl = (float)sqrt(zx*zx + zy*zy + zz*zz);
|
||||
if (zl < 1e-6f) zl = 1.0f;
|
||||
zx /= zl; zy /= zl; zz /= zl;
|
||||
|
||||
// world up
|
||||
const float ux = 0.0f, uy = 1.0f, uz = 0.0f;
|
||||
|
||||
// right (local +X) = up x forward
|
||||
float xx = uy*zz - uz*zy, xy = uz*zx - ux*zz, xz = ux*zy - uy*zx;
|
||||
float xl = (float)sqrt(xx*xx + xy*xy + xz*xz);
|
||||
if (xl < 1e-6f) xl = 1.0f;
|
||||
xx /= xl; xy /= xl; xz /= xl;
|
||||
|
||||
// recomputed up (local +Y) = forward x right
|
||||
float yx = zy*xz - zz*xy, yy = zz*xx - zx*xz, yz = zx*xy - zy*xx;
|
||||
|
||||
LinearMatrix debugOffset; // identity
|
||||
debugOffset(0,0) = xx; debugOffset(0,1) = xy; debugOffset(0,2) = xz; // X row
|
||||
debugOffset(1,0) = yx; debugOffset(1,1) = yy; debugOffset(1,2) = yz; // Y row (up)
|
||||
debugOffset(2,0) = zx; debugOffset(2,1) = zy; debugOffset(2,2) = zz; // Z row (look)
|
||||
debugOffset(3,0) = camPx; debugOffset(3,1) = camPy; debugOffset(3,2) = camPz; // W row (pos)
|
||||
|
||||
mCamera = new DPLEyeRenderable(entity, debugOffset, this_root, NULL);
|
||||
DEBUG_STREAM << "[BTrender] external debug chase camera installed at ("
|
||||
<< camPx << "," << camPy << "," << camPz << ") looking at ("
|
||||
<< tgtX << "," << tgtY << "," << tgtZ << ")\n" << std::flush;
|
||||
}
|
||||
|
||||
return this_root;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// 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,
|
||||
int pip_position,
|
||||
StateIndicator *within_range_attr,
|
||||
int extended_range,
|
||||
Scalar pip_red,
|
||||
Scalar pip_green,
|
||||
Scalar pip_blue,
|
||||
StateIndicator *sim_state,
|
||||
int arg9, // constant 2 from caller
|
||||
int arg10, // constant 3 from caller
|
||||
StateIndicator *sim_state2,
|
||||
int arg12, // constant 1 from caller
|
||||
int weapon_mode) // 1 front / 2 rear
|
||||
{
|
||||
if (this->weaponCount /* [0x38] */ >= 10)
|
||||
{
|
||||
Fail("Tried to display too many weapons"); // @0051d24f, line 0x338
|
||||
}
|
||||
|
||||
int n = this->weaponCount;
|
||||
|
||||
//
|
||||
// Record this weapon's control attributes in the parallel arrays.
|
||||
//
|
||||
this->pipColor[n] /* [0x64+n*4] */ = pip_red;
|
||||
this->withinRangeAttr[n] /* [0x8c+n*4] */ = within_range_attr;
|
||||
this->simState[n] /* [0xb4+n*4] */ = sim_state;
|
||||
this->simState2Ptr[n] /* [0x130+n*4] */ = sim_state2;
|
||||
this->simState2Value[n] /* [0xdc+n*4] */ = *(int *)(sim_state2 + 0x14);
|
||||
this->arg12Ptr[n] /* [0x158+n*4] */ = arg12;
|
||||
this->arg12Value[n] /* [0x104+n*4] */ = *(int *)(arg12 + 0x14);
|
||||
this->rangeAttr[n] /* [0x18c+n*4] */ = (Scalar *)within_range_attr; // param_4
|
||||
this->rangeCache[n] /* [0x1b4+n*4] */ = *(Scalar *)within_range_attr;
|
||||
this->weaponMode[n] /* [0x3c+n*4] */ = 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);
|
||||
}
|
||||
|
||||
BTL4VideoRenderer::~BTL4VideoRenderer()
|
||||
{
|
||||
}
|
||||
|
||||
Logical
|
||||
BTL4VideoRenderer::TestInstance() const
|
||||
{
|
||||
return True;
|
||||
}
|
||||
|
||||
//===========================================================================//
|
||||
Reference in New Issue
Block a user