VPX_PF_WATCH (fork, cpu/paging.cpp): on a guest page fault at the watched linear address, dump guest registers, the last 64 serial RX deliveries with guest cs:eip at each, the code bytes at the faulting EIP and the stack top. serialnamedpipe's doReceive feeds the ring. Armed in podrun.sh and launch_pod.ps1. The first catch decoded the residual fault completely: CS:EIP 00FF:000066D4 in the DPMI host, EBX = F000CA60 -- an IVT entry read as a dword, segment F000 offset CA60, a BIOS default interrupt handler -- and the faulting access is [EBX+0x3004], whose 0x80000000 segment-base wrap gives exactly cr2 7000FA64. The host probes a word 0x3004 bytes past a real-mode vector value treated as a flat pointer: harmless for its own low-memory handlers, a fault for BIOS F000:xxxx defaults. The serial ring shows a steady 1-byte/1-3ms vRIO stream with nothing special at the fault -- the stream determines which vectors get walked, not the crash itself. The 0x3004 appears nowhere in DPMI32VM.OVL or 32RTM.EXE as an immediate, so the probe now also dumps code bytes at EIP; faulthunt.sh loops runs until the next catch. Shipped baseline streak: 4/4 clean -- consistent with exposure, not yet discriminating. The inside view now loads the COCKPIT skeleton: the fleet-wide X-variant naming convention (MAD->MAX etc., all 64 skeletons present) selects the same 25-joint chain with a single object -- max_cop.bgf, the MAX_COP canopy shell with the PUNCH-texel windows from the capture forensics. The donor names the same mechanism from the decomp side (inside = SkeletonType_A with '_cop' selection). Fallback to the body skeleton when no X file exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
751 lines
25 KiB
C++
751 lines
25 KiB
C++
//===========================================================================//
|
|
// File: btl4vid.cpp //
|
|
// Project: BattleTech //
|
|
// Contents: Implementation details for the BT video renderer //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
|
// All Rights reserved worldwide //
|
|
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
|
//===========================================================================//
|
|
|
|
#include <btl4.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(BTL4VID_HPP)
|
|
# include <btl4vid.hpp>
|
|
#endif
|
|
//
|
|
// Matrix4x4 -- the DCS wire layout (see RecurseSKLFile). l4video.hpp pulls
|
|
// rotation.hpp but not matrix.hpp; L4VIDRND.CPP includes it the same way.
|
|
//
|
|
#if !defined(MATRIX_HPP)
|
|
# include <matrix.hpp>
|
|
#endif
|
|
|
|
//
|
|
// The engine's damage-zone tagging callback reads this while geometry is
|
|
// loading (L4VIDEO.CPP:527). It is a bare global there -- defined at
|
|
// L4VIDEO.CPP:351 with no header declaration -- so declare it here to set
|
|
// it around our own loads, exactly as DPLRenderer does around its.
|
|
//
|
|
extern Entity
|
|
*Entity_Being_Created;
|
|
|
|
BTL4VideoRenderer::BTL4VideoRenderer(
|
|
RendererRate calibration_rate,
|
|
RendererComplexity calibration_complexity,
|
|
RendererPriority calibration_priority,
|
|
InterestType interest_type,
|
|
InterestDepth depth_calibration
|
|
):
|
|
DPLRenderer(
|
|
calibration_rate,
|
|
calibration_complexity,
|
|
calibration_priority,
|
|
interest_type,
|
|
depth_calibration
|
|
)
|
|
{
|
|
}
|
|
|
|
BTL4VideoRenderer::~BTL4VideoRenderer()
|
|
{
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// LoadMissionImplementation -- called by Renderer::LoadMission (the authentic
|
|
// engine, CODE/RP/MUNGA/RENDERER.CPP:263) after it has set
|
|
// LoadingRendererStatus, stamped nextRenderTime and started the renderer with
|
|
// the RendererManager. This is where a GAME renderer builds the mission's
|
|
// scene content on the Division board.
|
|
//
|
|
// BRING-UP NO-OP (phase 1 of the btl4vid ladder). A no-op is a LEGAL body
|
|
// here, not a cheat: the engine's own VideoRenderer::LoadMissionImplementation
|
|
// (CODE/RP/MUNGA/VIDREND.CPP:259) is a bare Tell, and our GaugeRenderer
|
|
// (GAUGREND.CPP:3275) ships the same. The renderer therefore comes up and
|
|
// runs its frame loop with an EMPTY scene, which is exactly what we want to
|
|
// measure before writing any content.
|
|
//
|
|
// THE AUTHENTIC SHAPE, pinned by the surviving sibling header for Red
|
|
// Planet's renderer (CODE/RP/RP_L4/RPL4VID.HPP -- same engine, same board,
|
|
// same year; only the .CPP is missing there too):
|
|
//
|
|
// LoadMissionImplementation walks the mission's entities and calls
|
|
// MakeEntityRenderables(entity, model_resource, view_type) for each,
|
|
// which builds a dpl_DCS hierarchy through ReadSKLFile /
|
|
// RecurseSKLFile (the skeleton notation pages), with a material
|
|
// substitution list set up around it.
|
|
//
|
|
// The BT-specific renderables (BTReticleRenderable, BTTranslocationRenderable,
|
|
// the pending-wrecks map) come from the BT411 donor game/reconstructed/
|
|
// btl4vid.cpp -- 3188 lines -- but NOTE that port restructured this hook and
|
|
// has no method under this name, so the CONTRACT above comes from the 1995
|
|
// engine and only the CONTENT comes from the donor.
|
|
//#############################################################################
|
|
//
|
|
//
|
|
//#############################################################################
|
|
// ReadSKLFile -- build one entity's render skeleton from its .SKL file.
|
|
//
|
|
// The .SKL is a plain NotationFile. Each page is one node of the skeleton:
|
|
//
|
|
// [jointhip]
|
|
// parent=jointlocal
|
|
// Type=hingex <- joint kind (animation; unused at build)
|
|
// Object=mad_hip.bgf <- optional geometry for this node
|
|
// dzone=dz_hip <- damage-zone tags (zero or more)
|
|
// tranx=.. trany=.. tranz=..
|
|
// pitch=.. yaw=.. roll=..
|
|
// joint=jointtorso <- child pages (zero or more)
|
|
//
|
|
// so the whole model is a recursive walk from [ROOT].
|
|
//
|
|
// Files live under video\ -- the engine uses the same bare "video\\" prefix
|
|
// for its .pfx loads (L4VIDEO.CPP:1513).
|
|
//
|
|
// STAGE 1 (this version): the tree, the geometry and the parenting are real;
|
|
// every node is given an IDENTITY matrix. The model therefore collapses onto
|
|
// the entity origin and looks wrong, which is deliberate -- the STRUCTURE is
|
|
// what is being proved here, and it is provable without looking at a pixel:
|
|
// MAD.SKL declares JointCount=25 (+1 root) and DZoneCount=22, and the
|
|
// dpl3-revive capture of a REAL pod decodes as 26 DCS bodies and 22 instance
|
|
// bodies. The counts this walk reports must match.
|
|
//
|
|
// STAGE 2 is the transforms. The dpl_MATRIX convention is NOT yet proven
|
|
// (see RENDER-ROADMAP.NOTES.md) -- a DCS flush body carries 16 float32, and a
|
|
// decoded capture suggests row-major with the translation in the last row,
|
|
// but the decoder's offset is suspect by one word. Rather than guess it and
|
|
// ship a mech that renders confidently in the wrong orientation, this stage
|
|
// leaves identity in place.
|
|
//#############################################################################
|
|
//
|
|
dpl_DCS *
|
|
BTL4VideoRenderer::ReadSKLFile(
|
|
Entity *entity,
|
|
dpl_DCS *parent_dcs,
|
|
const char *skeleton_filename,
|
|
ViewFrom view_type,
|
|
int *eye_count)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(skeleton_filename);
|
|
Check_Pointer(eye_count);
|
|
|
|
char
|
|
path[256];
|
|
strcpy(path, "video\\");
|
|
strcat(path, skeleton_filename);
|
|
|
|
NotationFile
|
|
*skeleton = new NotationFile(path);
|
|
Register_Object(skeleton);
|
|
|
|
if (skeleton->PageCount() == 0)
|
|
{
|
|
DEBUG_STREAM << "[skl] could not read " << path << "\n" << flush;
|
|
Unregister_Object(skeleton);
|
|
delete skeleton;
|
|
return NULL;
|
|
}
|
|
|
|
//
|
|
// Every node of this model shares one zone, switched on and flushed by
|
|
// the engine helper (L4VIDEO.CPP:1338).
|
|
//
|
|
dpl_ZONE
|
|
*zone = MakeNewZone();
|
|
|
|
int
|
|
node_count = 0,
|
|
object_count = 0;
|
|
|
|
//
|
|
// The ROOT page's DCS attaches under the caller's parent (the mech's
|
|
// RootRenderable DCS, so the model rides the entity's localToWorld) or,
|
|
// with no parent, straight to the scene -- the old bring-up behaviour,
|
|
// which parks the model at the world origin.
|
|
//
|
|
dpl_DCS
|
|
*root = RecurseSKLFile(
|
|
entity, parent_dcs, skeleton, "ROOT", 0, view_type,
|
|
zone, &node_count, &object_count, eye_count);
|
|
|
|
DEBUG_STREAM << "[skl] " << path
|
|
<< " -> " << node_count << " nodes, "
|
|
<< object_count << " objects, "
|
|
<< *eye_count << " eye\n" << flush;
|
|
|
|
//
|
|
// The engine guards this the same way (L4VIDEO.CPP, DPLReadEnvironment):
|
|
// ~NotationFile REWRITES the file when dirtyFlag is set, and these are
|
|
// the game's shipped .SKL data files.
|
|
//
|
|
Verify(!skeleton->IsDirty());
|
|
Unregister_Object(skeleton);
|
|
delete skeleton;
|
|
return root;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// RecurseSKLFile -- one page of the skeleton, then its children.
|
|
//#############################################################################
|
|
//
|
|
dpl_DCS *
|
|
BTL4VideoRenderer::RecurseSKLFile(
|
|
Entity *entity,
|
|
dpl_DCS *parent_dcs,
|
|
NotationFile *skeleton,
|
|
const char *page_name,
|
|
int recursion_depth,
|
|
ViewFrom view_type,
|
|
dpl_ZONE *zone,
|
|
int *node_count,
|
|
int *object_count,
|
|
int *eye_count)
|
|
{
|
|
Check(this);
|
|
Check(skeleton);
|
|
Check_Pointer(page_name);
|
|
|
|
if (!skeleton->PageExists(page_name))
|
|
{
|
|
DEBUG_STREAM << "[skl] missing page '" << page_name << "'\n" << flush;
|
|
return NULL;
|
|
}
|
|
|
|
//
|
|
// A guard, not a limit: the file is authored data and a bad parent= chain
|
|
// could otherwise recurse forever. The deepest real chain in MAD.SKL is
|
|
// nowhere near this.
|
|
//
|
|
if (recursion_depth > 32)
|
|
{
|
|
DEBUG_STREAM << "[skl] recursion too deep at '" << page_name
|
|
<< "'\n" << flush;
|
|
return NULL;
|
|
}
|
|
|
|
dpl_DCS
|
|
*dcs = dpl_NewDCS();
|
|
Check_Pointer(dcs);
|
|
dpl_SetDCSZone(dcs, zone);
|
|
|
|
//
|
|
// The node's LOCAL TRANSFORM, in the engine's own terms -- the wire
|
|
// convention is no longer inferred from captures, it is READ from the
|
|
// engine: RootRenderable's ctor (L4VIDRND.CPP:853) writes an entity pose
|
|
// into a DCS with
|
|
//
|
|
// *(Matrix4x4*)dpl_GetDCSMatrix(myDCS) = myEntity->localToWorld;
|
|
//
|
|
// so whatever Matrix4x4::operator=(const AffineMatrix&) produces IS the
|
|
// DCS matrix layout. Reading it (MATRIX.CPP:130): Matrix4x4 is ROW-major
|
|
// (MATRIX.HPP:113, entries[(Row<<2)+Column]) with rotation in rows 0-2,
|
|
// ZEROS in column 3, and the TRANSLATION IN ROW 3 -- entries 12/13/14.
|
|
//
|
|
// (The previous revision put translation at entries 3/7/11, quoting
|
|
// AffineMatrix as precedent. AffineMatrix does keep translation at
|
|
// 3/7/11 -- but because it is COLUMN-major, entries[(column<<2)+row]
|
|
// (AFFNMTRX.HPP:99); its (3,c) translation row lands at 3/7/11 by
|
|
// storage, not by convention. Matrix4x4 transposes that on copy. The
|
|
// 'last row made the maths blow up' claim attached to the old choice
|
|
// came from the contaminated bisect -- the crashes were the RIO fault.)
|
|
//
|
|
// Rotation comes from the page's pitch/yaw/roll through the engine's own
|
|
// Matrix4x4::operator=(const EulerAngles&), so the rotation ORDER is the
|
|
// engine's by construction, not a guess. MAD.SKL base-pose angles are
|
|
// all 0 or ~1e-3, so this is nearly identity today -- but it is the
|
|
// correct compose for any skeleton authored with real angles.
|
|
//
|
|
Scalar
|
|
tran_x = 0.0f,
|
|
tran_y = 0.0f,
|
|
tran_z = 0.0f,
|
|
rot_pitch = 0.0f,
|
|
rot_yaw = 0.0f,
|
|
rot_roll = 0.0f;
|
|
skeleton->GetEntry(page_name, "tranx", &tran_x);
|
|
skeleton->GetEntry(page_name, "trany", &tran_y);
|
|
skeleton->GetEntry(page_name, "tranz", &tran_z);
|
|
skeleton->GetEntry(page_name, "pitch", &rot_pitch);
|
|
skeleton->GetEntry(page_name, "yaw", &rot_yaw);
|
|
skeleton->GetEntry(page_name, "roll", &rot_roll);
|
|
|
|
Matrix4x4
|
|
node_matrix;
|
|
node_matrix = EulerAngles(rot_pitch, rot_yaw, rot_roll);
|
|
node_matrix(3,0) = tran_x;
|
|
node_matrix(3,1) = tran_y;
|
|
node_matrix(3,2) = tran_z;
|
|
|
|
//
|
|
// Write in place and flush -- the same idiom as RootRenderable's ctor
|
|
// (dpl_GetDCSMatrix + assign), not dpl_SetDCSMatrix.
|
|
//
|
|
float32
|
|
*dcs_matrix = dpl_GetDCSMatrix(dcs);
|
|
Check_Pointer(dcs_matrix);
|
|
*(Matrix4x4 *)dcs_matrix = node_matrix;
|
|
|
|
if (parent_dcs != NULL)
|
|
{
|
|
dpl_AddDCSToDCS(parent_dcs, dcs);
|
|
}
|
|
else
|
|
{
|
|
dpl_AddDCSToScene(dcs);
|
|
}
|
|
++(*node_count);
|
|
|
|
//
|
|
// This node's geometry, if it has any. Entity_Being_Created is already
|
|
// set by our caller so the library's C callback can tag the geometry with
|
|
// damage zones (L4VIDEO.CPP:4176).
|
|
//
|
|
const char
|
|
*object_name;
|
|
if (skeleton->GetEntry(page_name, "Object", &object_name) && object_name)
|
|
{
|
|
dpl_OBJECT
|
|
*object = dpl_LoadObject((char *)object_name, dpl_load_normal);
|
|
if (object != NULL)
|
|
{
|
|
dpl_INSTANCE
|
|
*instance = dpl_NewInstance();
|
|
Check_Pointer(instance);
|
|
dpl_SetInstanceObject(instance, object);
|
|
dpl_AddInstanceToDCS(dcs, instance);
|
|
dpl_FlushInstance(instance);
|
|
++(*object_count);
|
|
}
|
|
else
|
|
{
|
|
DEBUG_STREAM << "[skl] couldn't load object " << object_name
|
|
<< " for '" << page_name << "'\n" << flush;
|
|
}
|
|
}
|
|
|
|
dpl_FlushDCS(dcs);
|
|
|
|
//
|
|
// Children. Repeated "joint=" entries: the entry NAME is "joint" and the
|
|
// VALUE (dataReference) is the child page -- the same shape as the
|
|
// engine's objectpath= walk at L4VIDEO.CPP:1858.
|
|
//
|
|
NameList
|
|
*children = skeleton->MakeEntryList(page_name, "joint");
|
|
if (children != NULL)
|
|
{
|
|
Register_Object(children);
|
|
|
|
NameList::Entry
|
|
*entry;
|
|
for (entry = children->GetFirstEntry();
|
|
entry != NULL;
|
|
entry = entry->GetNextEntry())
|
|
{
|
|
const char
|
|
*child_page = (const char *)entry->dataReference;
|
|
if (child_page != NULL && *child_page != '\0')
|
|
{
|
|
RecurseSKLFile(
|
|
entity, dcs, skeleton, child_page, recursion_depth + 1,
|
|
view_type, zone, node_count, object_count, eye_count);
|
|
}
|
|
}
|
|
|
|
Unregister_Object(children);
|
|
delete children;
|
|
}
|
|
|
|
//
|
|
// "site=" children. Sites get NO draw component of their own -- the
|
|
// real pod's capture decodes exactly 26 DCS bodies for this skeleton,
|
|
// which is joints+root only, and this walk matched that count precisely
|
|
// BECAUSE it ignored sites. One site spawns hardware instead of
|
|
// geometry: siteeyepoint is the COCKPIT CAMERA.
|
|
//
|
|
// The authentic construction comes from the donor's decompile
|
|
// (bt411 btl4vid.cpp:462, decomp FUN_004579a8): the eye's offset matrix
|
|
// is the SITE'S OWN local rest transform, and the eye is parented on the
|
|
// site's PARENT segment's draw component -- this page's DCS -- NOT the
|
|
// hull root. World orientation and all live motion (torso twist, gait)
|
|
// then come from the parent-chain composition for free. For MAD.SKL the
|
|
// chain is jointtorso -> jointeye (trany +1.687, tranz -1.318) ->
|
|
// siteeyepoint (identity), which puts the eye in the canopy where the
|
|
// cockpit sits, instead of at the hull origin staring through the torso
|
|
// panels.
|
|
//
|
|
if (view_type == insideEntity)
|
|
{
|
|
NameList
|
|
*sites = skeleton->MakeEntryList(page_name, "site");
|
|
if (sites != NULL)
|
|
{
|
|
Register_Object(sites);
|
|
|
|
NameList::Entry
|
|
*site_entry;
|
|
for (site_entry = sites->GetFirstEntry();
|
|
site_entry != NULL;
|
|
site_entry = site_entry->GetNextEntry())
|
|
{
|
|
const char
|
|
*site_page = (const char *)site_entry->dataReference;
|
|
if (
|
|
site_page != NULL &&
|
|
strcmp(site_page, "siteeyepoint") == 0 &&
|
|
skeleton->PageExists(site_page)
|
|
)
|
|
{
|
|
Scalar
|
|
site_tx = 0.0f, site_ty = 0.0f, site_tz = 0.0f,
|
|
site_pitch = 0.0f, site_yaw = 0.0f, site_roll = 0.0f;
|
|
skeleton->GetEntry(site_page, "tranx", &site_tx);
|
|
skeleton->GetEntry(site_page, "trany", &site_ty);
|
|
skeleton->GetEntry(site_page, "tranz", &site_tz);
|
|
skeleton->GetEntry(site_page, "pitch", &site_pitch);
|
|
skeleton->GetEntry(site_page, "yaw", &site_yaw);
|
|
skeleton->GetEntry(site_page, "roll", &site_roll);
|
|
|
|
//
|
|
// Default ctor identities; the EulerAngles assignment
|
|
// writes ONLY the 3x3 rotation (AFFNMTRX.CPP:179), so
|
|
// the identity's zero translation survives it and the
|
|
// site translation goes in after.
|
|
//
|
|
LinearMatrix
|
|
site_offset;
|
|
site_offset =
|
|
EulerAngles(site_pitch, site_yaw, site_roll);
|
|
site_offset(3,0) = site_tx;
|
|
site_offset(3,1) = site_ty;
|
|
site_offset(3,2) = site_tz;
|
|
|
|
EulerAngles
|
|
*eyepoint_rotation =
|
|
(EulerAngles *)entity->GetAttributePointer(
|
|
"EyepointRotation");
|
|
DPLEyeRenderable
|
|
*cockpit_eye = new DPLEyeRenderable(
|
|
entity,
|
|
dplMainZone,
|
|
site_offset,
|
|
dcs,
|
|
dplMainView,
|
|
eyepoint_rotation);
|
|
Register_Object(cockpit_eye);
|
|
++(*eye_count);
|
|
|
|
if (getenv("BT_MER_LOG"))
|
|
{
|
|
DEBUG_STREAM << " [eye] cockpit eye on '"
|
|
<< page_name << "' offset=("
|
|
<< site_tx << "," << site_ty << ","
|
|
<< site_tz << ")\n" << flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
Unregister_Object(sites);
|
|
delete sites;
|
|
}
|
|
}
|
|
|
|
return dcs;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// MakeEntityRenderables -- the game level of the renderable factory.
|
|
//
|
|
// The engine's DPLRenderer::MakeEntityRenderables (L4VIDEO.CPP:4151) knows
|
|
// the ENGINE entity classes and calls DOWN to
|
|
// VideoRenderer::MakeEntityRenderables for anything else, which only prints
|
|
// Entity <id> class<n> couldn't figure out how to MakeEntityRenderables
|
|
// So every BT class has to be answered here.
|
|
//
|
|
// FIRST ANSWER: BTPlayer (class 3035) carries no graphics. The engine
|
|
// already does exactly this for its own PlayerClassID -- an empty case --
|
|
// and BT's player is simply a different id it cannot know about. This is
|
|
// the class the live pod run complained about.
|
|
//
|
|
// Everything else still chains to the engine, so this override can only
|
|
// ADD answers, never remove the ones DPLRenderer already gives.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
BTL4VideoRenderer::MakeEntityRenderables(
|
|
Entity *entity,
|
|
ResourceDescription *model_resource,
|
|
ViewFrom view_type)
|
|
{
|
|
Check(this);
|
|
Check(entity);
|
|
|
|
//
|
|
// RENDERABLE-BUILD TRACE (env BT_MER_LOG).
|
|
//
|
|
// The mission never launches because 530 renderer events queue at
|
|
// priority 0 during load and the background pump drains only one per
|
|
// seven frames, and the page fault lands on one of the LAST ~35 of them.
|
|
// So the fault belongs to a specific entity, not to elapsed time. The
|
|
// oldest log named it -- class 42, UnscalableTerrain -- but that was a
|
|
// different build, so this prints the class of every entity as its
|
|
// renderables are built and the last line before the fault names the
|
|
// culprit outright. One line per entity, not per frame: ~530 lines for a
|
|
// whole run, which the COM3 log carries without the 3x cost that per-frame
|
|
// logging brought.
|
|
//
|
|
if (getenv("BT_MER_LOG"))
|
|
{
|
|
static int
|
|
mer_count = 0;
|
|
|
|
mer_count++;
|
|
DEBUG_STREAM << "[mer] " << mer_count
|
|
<< " class=" << (int)entity->GetClassID()
|
|
<< " view=" << (int)view_type
|
|
<< " res=" << (model_resource ? 1 : 0)
|
|
<< endl << flush;
|
|
}
|
|
|
|
switch (entity->GetClassID())
|
|
{
|
|
case RegisteredClass::MechClassID:
|
|
//
|
|
// The mech's video resource is a SKELETON. Walk the chain the
|
|
// same way the engine does (L4VIDEO.CPP:4250) and hand every
|
|
// Skeleton entry to ReadSKLFile; anything else falls through to
|
|
// the engine, which knows what to do with plain objects.
|
|
//
|
|
{
|
|
if (model_resource == NULL)
|
|
{
|
|
break;
|
|
}
|
|
|
|
ChainOf<L4VideoObjectWrapper*>
|
|
video_chain(NULL);
|
|
L4VideoObjectWrapper::BuildVideoObjectChainFromResource(
|
|
&video_chain, model_resource);
|
|
|
|
ChainIteratorOf<L4VideoObjectWrapper*>
|
|
video_iterator(video_chain);
|
|
L4VideoObjectWrapper
|
|
*video_wrapper;
|
|
Logical
|
|
handled = False;
|
|
|
|
//
|
|
// BT_NO_SKL skips the skeleton build so the SAME binary can be
|
|
// run with and without it -- the only way to attribute the
|
|
// intermittent pod crash without a rebuild between samples.
|
|
//
|
|
if (getenv("BT_NO_SKL") != NULL)
|
|
{
|
|
break;
|
|
}
|
|
|
|
Entity_Being_Created = entity;
|
|
video_iterator.First();
|
|
while ((video_wrapper = video_iterator.ReadAndNext()) != NULL)
|
|
{
|
|
const L4VideoObject
|
|
*video_object = video_wrapper->GetVideoObject();
|
|
|
|
if (getenv("BT_MER_LOG"))
|
|
{
|
|
DEBUG_STREAM << " [vid] type="
|
|
<< (int)video_object->GetResourceType()
|
|
<< " file='" << video_object->GetObjectFilename()
|
|
<< "'" << endl << flush;
|
|
}
|
|
|
|
if (video_object->GetResourceType()
|
|
== L4VideoObject::Skeleton)
|
|
{
|
|
//
|
|
// THE ENGINE COMPOSITION, mirrored from the Mover branch
|
|
// of DPLRenderer::MakeEntityRenderables
|
|
// (L4VIDEO.CPP:4795-4860), which the base cannot apply
|
|
// here because it only accepts Object/Rubble resources --
|
|
// chaining it with a Skeleton just prints "wrong video
|
|
// resource type" and builds NOTHING (measured: zero
|
|
// vr_flush_dcs_artic records on the wire, camera frozen
|
|
// at the world origin, and the mech statue parked there
|
|
// with the eye inside it).
|
|
//
|
|
// 1. A DYNAMIC RootRenderable. Its ctor adds its DCS to
|
|
// the scene and seeds it from entity->localToWorld;
|
|
// its Execute re-flushes whenever the entity moves.
|
|
// That per-frame flush is the ONLY source of wire
|
|
// articulation (0x1f) for the vehicle -- without it
|
|
// nothing on the board ever moves. NULL graphical
|
|
// object exactly like the CameraShip inside-view case
|
|
// (L4VIDEO.CPP:4548): the skeleton supplies the
|
|
// geometry.
|
|
//
|
|
RootRenderable
|
|
*this_root = new RootRenderable(
|
|
entity,
|
|
RootRenderable::Dynamic,
|
|
NULL,
|
|
dplMainZone,
|
|
dpl_isect_mode_obj,
|
|
NULL);
|
|
Register_Object(this_root);
|
|
|
|
dpl_DCS
|
|
*root_DCS = this_root->GetDCS();
|
|
|
|
//
|
|
// 2. The skeleton hangs UNDER the root DCS -- so the whole
|
|
// model rides the entity's localToWorld instead of
|
|
// being parked at the world origin. The walk also
|
|
// builds the COCKPIT EYE when it meets the
|
|
// siteeyepoint site (donor construction: offset =
|
|
// the site's local rest, parent = the site's parent
|
|
// joint DCS -- so the eye sits in the canopy and
|
|
// rides torso twist through chain composition).
|
|
//
|
|
// THE INSIDE VIEW LOADS THE X-VARIANT SKELETON. The
|
|
// resource names one skeleton (mad.skl) for both views;
|
|
// the cockpit build is derived from it by the fleet-wide
|
|
// naming convention -- third letter X: MAD->MAX,
|
|
// AVA->AVX, BAT->BAX, BLH->BLX, FIR->FIX, JAK->JAX,
|
|
// LOK->LOX, with the numbered chassis following
|
|
// (MAD1->MAX1, LOK1->LOX1; all present in VIDEO/). The
|
|
// X skeleton carries the SAME 25-joint chain -- so the
|
|
// canopy eye and torso twist compose identically -- but
|
|
// its only geometry is the cockpit shell (MAX.SKL ->
|
|
// max_cop.bgf, the MAX_COP canopy with PUNCH-texel
|
|
// windows from the real-pod capture forensics). The
|
|
// donor names the same mechanism from the decomp side:
|
|
// inside = SkeletonType_A with '_cop' selection
|
|
// (btl4vid.hpp:678). Without this, the pilot sits
|
|
// inside the OUTSIDE model staring at torso panels.
|
|
//
|
|
int
|
|
eye_count = 0;
|
|
const char
|
|
*skeleton_name = video_object->GetObjectFilename();
|
|
char
|
|
inside_name[64];
|
|
dpl_DCS
|
|
*skl_result = NULL;
|
|
|
|
if (
|
|
view_type == insideEntity &&
|
|
strlen(skeleton_name) >= 3 &&
|
|
strlen(skeleton_name) < sizeof(inside_name)
|
|
)
|
|
{
|
|
strcpy(inside_name, skeleton_name);
|
|
inside_name[2] =
|
|
(inside_name[2] >= 'a' && inside_name[2] <= 'z')
|
|
? 'x' : 'X';
|
|
skl_result = ReadSKLFile(entity, root_DCS,
|
|
inside_name, view_type, &eye_count);
|
|
if (skl_result == NULL)
|
|
{
|
|
DEBUG_STREAM << "[skl] no cockpit variant '"
|
|
<< inside_name
|
|
<< "' -- falling back to the body skeleton\n"
|
|
<< flush;
|
|
}
|
|
}
|
|
if (skl_result == NULL)
|
|
{
|
|
ReadSKLFile(entity, root_DCS,
|
|
skeleton_name, view_type, &eye_count);
|
|
}
|
|
|
|
//
|
|
// 3. FALLBACK eyepoint only. A skeleton without a
|
|
// siteeyepoint still needs a camera for the inside
|
|
// view, and the hull root with a zero offset is the
|
|
// engine's own zero-construction default
|
|
// (L4VIDEO.CPP:4849). For MAD.SKL this no longer
|
|
// runs -- the walk builds the real cockpit eye.
|
|
//
|
|
if (view_type == insideEntity && eye_count == 0)
|
|
{
|
|
EulerAngles
|
|
*eyepoint_rotation =
|
|
(EulerAngles *)entity->GetAttributePointer(
|
|
"EyepointRotation");
|
|
DPLEyeRenderable
|
|
*this_eye = new DPLEyeRenderable(
|
|
entity,
|
|
dplMainZone,
|
|
LinearMatrix::Identity,
|
|
root_DCS,
|
|
dplMainView,
|
|
eyepoint_rotation);
|
|
Register_Object(this_eye);
|
|
}
|
|
handled = True;
|
|
}
|
|
}
|
|
Entity_Being_Created = NULL;
|
|
|
|
//
|
|
// Only chain the base when NO skeleton was found -- for a
|
|
// skeleton it contributes nothing but the complaint, and the
|
|
// root/eye it would otherwise build were built above.
|
|
//
|
|
if (!handled)
|
|
{
|
|
if (getenv("BT_MER_LOG"))
|
|
{
|
|
DEBUG_STREAM << " [chain] no skeleton -> DPLRenderer"
|
|
<< endl << flush;
|
|
}
|
|
DPLRenderer::MakeEntityRenderables(
|
|
entity, model_resource, view_type);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case RegisteredClass::BTPlayerClassID:
|
|
//
|
|
// No graphics -- the player is a control/scoring entity.
|
|
//
|
|
break;
|
|
|
|
default:
|
|
DPLRenderer::MakeEntityRenderables(entity, model_resource, view_type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void
|
|
BTL4VideoRenderer::LoadMissionImplementation(Mission *mission)
|
|
{
|
|
Check(this);
|
|
//
|
|
// CHAIN THE BASE. DPLRenderer::LoadMissionImplementation
|
|
// (L4VIDEO.CPP:6007) is NOT empty -- it reads the renderer environment
|
|
// and loads the name bitmaps. DPLReadEnvironment opens the
|
|
// notation file named by L4DPLCFG (SETENV.BAT defaults it to
|
|
// btdpl.ini) and hands its "main" page to DPLReadINIPage, which walks
|
|
// the compare/branch pages for this location/time and calls
|
|
// dpl_SetObjectFilePath / material / texmap from the objectpath=
|
|
// entries (L4VIDEO.CPP:1852). It is PRIVATE to DPLRenderer, so the
|
|
// game renderer reaches it only by chaining -- which is the whole
|
|
// point: an override here REPLACES the base, it does not extend it.
|
|
//
|
|
// WITHOUT IT every dpl_LoadObject returns NULL. The live pod run
|
|
// failed all 40 arena objects (sky / aw01..aw04 / afloor / bcor1 /
|
|
// bdet1 / bdet2 / bpip1) and ended in "NULL instance", while the
|
|
// SHIPPED binary on the SAME rig loaded every one of them. The
|
|
// models were never missing -- the loader simply had no paths, because
|
|
// the bring-up no-op that used to live here SUPPRESSED the base.
|
|
//
|
|
DPLRenderer::LoadMissionImplementation(mission);
|
|
}
|
|
|