//===========================================================================// // 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 #pragma hdrstop #if !defined(BTL4VID_HPP) # include #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; // // 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) { 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 class 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 video_chain(NULL); L4VideoObjectWrapper::BuildVideoObjectChainFromResource( &video_chain, model_resource); ChainIteratorOf 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 (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); }