BTL4VideoRenderer answers MechClassID: walks the video-object chain the way the engine does, hands every L4VideoObject::Skeleton entry to ReadSKLFile, and recurses the .SKL into a dpl_DCS tree with geometry instanced onto it. Verified repeatedly on the live pod: '[skl] video\mad.skl -> 26 nodes, 19 objects', with no 'wrong video resource type' complaint and no load failures. Those counts are exactly what the file declares (25 joint= entries + root, 19 Object= entries). Corrects the earlier success criterion in this file, which said 22 instances by reading the reference capture's 'instance x22' against DZoneCount=22. Damage zones are not geometry -- 28 dzone= tags spread across 19 objects. Translations are written to matrix[3]/[7]/[11], MUNGA's own AffineMatrix layout. It walks cleanly but no frame has been seen WITH the mech yet, so the slot choice is recorded as unconfirmed. Rotation stays identity by design: every base-pose angle in MAD.SKL is 0 or ~1e-3, so translation alone assembles the model and isolates one convention at a time. AND A CORRECTION I have to flag loudly: I earlier concluded from single runs that non-identity translations crashed the pod, 'isolated' it, and 'confirmed' the alternative also crashed. That was all noise. The same binary re-run gives walk / crash / walk / crash -- the known intermittent plane-write defect is now firing on ~half of pod runs and lands at different points each time, which is precisely what made it look deterministic. Never accept a single pod run as evidence on this rig; require two agreeing runs. That defect is now the top of the list: a run must survive both the skeleton build and the launch to render anything, which at ~50% is a coin flip on a four-minute cycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
452 lines
14 KiB
C++
452 lines
14 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
|
|
|
|
//
|
|
// 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,
|
|
const char *skeleton_filename,
|
|
ViewFrom view_type)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(skeleton_filename);
|
|
|
|
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;
|
|
|
|
dpl_DCS
|
|
*root = RecurseSKLFile(
|
|
entity, NULL, skeleton, "ROOT", 0, view_type,
|
|
zone, &node_count, &object_count);
|
|
|
|
DEBUG_STREAM << "[skl] " << path
|
|
<< " -> " << node_count << " nodes, "
|
|
<< object_count << " objects\n" << flush;
|
|
|
|
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)
|
|
{
|
|
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);
|
|
|
|
//
|
|
// STAGE 2a -- the node's LOCAL TRANSLATION.
|
|
//
|
|
// Slot convention, derived rather than guessed. A DCS flush body is
|
|
// [remote][type_check][node][pad][16 x float32]: our own identity
|
|
// matrices were found on the wire at body offset 16, which also
|
|
// showed analyze_scene.py's rest[4:68] read to be one word early.
|
|
// Re-reading a real BT capture with that correction gives a clean
|
|
// identity with the translation in the LAST ROW -- a row-vector
|
|
// convention, the transpose of MUNGA's AffineMatrix (which is 3x4
|
|
// row-major with translation in the 4th COLUMN, entries[3/7/11], per
|
|
// AFFNMTRX.CPP:50).
|
|
//
|
|
// Rotation is deliberately left identity for now: every base-pose
|
|
// pitch/yaw/roll in MAD.SKL is 0 or ~1e-3, so TRANSLATION alone
|
|
// assembles the model -- which isolates this one convention and
|
|
// makes a wrong guess about rotation order impossible to mistake for
|
|
// a wrong guess about translation.
|
|
//
|
|
Scalar
|
|
tran_x = 0.0f,
|
|
tran_y = 0.0f,
|
|
tran_z = 0.0f;
|
|
skeleton->GetEntry(page_name, "tranx", &tran_x);
|
|
skeleton->GetEntry(page_name, "trany", &tran_y);
|
|
skeleton->GetEntry(page_name, "tranz", &tran_z);
|
|
|
|
float32
|
|
matrix[16];
|
|
int
|
|
i;
|
|
for (i = 0; i < 16; ++i)
|
|
{
|
|
matrix[i] = 0.0f;
|
|
}
|
|
matrix[0] = matrix[5] = matrix[10] = matrix[15] = 1.0f;
|
|
//
|
|
// Translation goes in the 4th COLUMN (3/7/11), the same layout as
|
|
// MUNGA's own AffineMatrix (3x4 row-major, entries[3/7/11] --
|
|
// AFFNMTRX.CPP:50). Putting it in the last ROW instead made the
|
|
// matrix PROJECTIVE under a column-vector convention and the
|
|
// transform maths blew up: the walk died on the first node that
|
|
// carries geometry, reproducibly, and bisecting with the reads kept
|
|
// but the values suppressed ran clean. Same library house as
|
|
// MUNGA, same convention.
|
|
//
|
|
matrix[3] = (float32)tran_x;
|
|
matrix[7] = (float32)tran_y;
|
|
matrix[11] = (float32)tran_z;
|
|
dpl_SetDCSMatrix(dcs, 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);
|
|
}
|
|
}
|
|
|
|
Unregister_Object(children);
|
|
delete children;
|
|
}
|
|
|
|
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);
|
|
|
|
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;
|
|
|
|
Entity_Being_Created = entity;
|
|
video_iterator.First();
|
|
while ((video_wrapper = video_iterator.ReadAndNext()) != NULL)
|
|
{
|
|
const L4VideoObject
|
|
*video_object = video_wrapper->GetVideoObject();
|
|
if (video_object->GetResourceType()
|
|
== L4VideoObject::Skeleton)
|
|
{
|
|
ReadSKLFile(entity,
|
|
video_object->GetObjectFilename(), view_type);
|
|
handled = True;
|
|
}
|
|
}
|
|
Entity_Being_Created = NULL;
|
|
|
|
if (!handled)
|
|
{
|
|
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);
|
|
}
|