//===========================================================================// // 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 #if !defined(EMITTER_HPP) # include // Emitter/PPC (the reticle's per-weapon pip wiring) #endif #pragma hdrstop #if !defined(BTL4VID_HPP) # include #endif #if !defined(MECH_HPP) # include // Mech / JointedMover segment table + subsystems #endif #if !defined(MECHWEAP_HPP) # include // MechWeapon::GetClassDerivations (reticle pip) #endif #if !defined(NOTATION_HPP) # include #endif #if !defined(NAMELIST_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(TERRAIN_HPP) # include // Terrain::GetClassDerivations (world-pick target, task #41) #endif #include #include #include // clock() -- the threat-trail ages (task #37) // WORLD-PICK TARGET (task #41): a live Terrain entity mech4 cites in // mech+0x388 when the boresight pick lands on the ground (the binary's target // slot holds non-mech world entities). Captured in MakeEntityRenderables. Entity *gBTTerrainEntity = 0; // // Material-name substitution placeholders. Mirrors RPL4VideoRenderer's // color_parameter/badge_parameter, plus BT's patch/serno. // static const char * const colorParameter = "%color%"; // @0051d188 static const char * const badgeParameter = "%badge%"; // @0051d18c static const char * const patchParameter = "%patch%"; // @0051d190 static const char * const sernoParameter = "%serno%"; // @0051d194 // // Radial spacing between adjacent weapon pips along the reticle (_DAT_004cdce8). // static const float PIP_SPACING = 0.03f; // _DAT_004cdce8 (a DOUBLE: 0.03 -- // verified from the exe; the 0.01 guess // overlapped the 0.028-wide pips) // // One-character serial number stamped into %serno% material names; advances // '0'..'9' then 'A' each mech loaded. (DAT @0051d1b5.) // static char gSerno = '0'; // // BattleTech entity / subsystem ClassIDs touched by the dispatch switches that // were not recoverable from the surviving headers (the rest -- MechClassID, // BTPlayerClassID, ReservoirClassID, EmitterClassID, PPCClassID -- resolve via // the BT registration headers). Values from CLASSMAP.md / the recovered enum. // enum { MechMarkerClassID = 0xBBA, // timestamp / beacon marker MechWeaponClassID = 0xBCD, // projectile-weapon tracer SearchLightClassID = 0xBD8 // searchlight subsystem }; extern NameList *materialSubstitutionList; // DAT_004f1aac extern Entity *Entity_Being_Created; // DAT_004f1aa8 // //############################################################################# // MakeEntityRenderables //############################################################################# // // @004d0774 // // The ClassID dispatch (analogue of RPL4VideoRenderer::MakeEntityRenderables). // void BTL4VideoRenderer::MakeEntityRenderables( Entity *entity, ResourceDescription *model_resource, ViewFrom view_type) { Entity_Being_Created = entity; // DAT_004f1aa8 HierarchicalDrawComponent *mech_root = NULL; switch (entity->GetClassID()) // entity[0x04] { case MechClassID: // 0xBB9 { // // Fog for the mech's main view, then load the colour/badge/patch // material substitutions, build the whole mech, and tear the // substitution list back down. // SetFogStyle(updateFogSetting); // FUN_0045d3cc(this,0x68) SetupMaterialSubstitutionList(entity); // FUN_004d0cc0 mech_root = MakeMechRenderables( // FUN_004cef28 entity, model_resource, view_type); TearDownMaterialSubstitutionList(); // FUN_004d11e8 // NB: the RootRenderable built by MakeMechRenderables registers // itself with the renderer (AddRenderable) and hooks to the entity's // localToWorld in its ctor -- no explicit AddDynamicRenderable here // (unlike the 1996 VideoComponent path). (void)mech_root; break; } case MechMarkerClassID: // 0xBBA (timestamp/marker beacon) { d3d_OBJECT *marker = LoadObject("tmst_c"); // FUN_00498448 BTRootRenderable *root = // FUN_00453578, alloc 100 new BTRootRenderable( entity, VideoRenderable::Dynamic, marker, GetScene(), 1, 0); // watcher that keeps the marker oriented (FUN_00458c58, alloc 0x120) new BTMarkerWatcherRenderable( entity, 0, GetMainView() /* this[0x2cc] */, root->GetDCS()); break; } case BTPlayerClassID: // 0xBDA { StateIndicator *sim_state = (StateIndicator *)entity->GetAttributePointer(1 /* SimulationState */); if ((entity->GetInstance() & 0xC) == 4) // ReplicantInstance { // // Third-party view: drop-zone translocation effect. // Point3D *drop_zone = (Point3D *)entity->GetAttributePointer("DropZoneLocation"); // @0051d73a if (sim_state && drop_zone) { new BTTranslocationRenderable( // FUN_00458d2c, alloc 0x40 entity, VideoRenderable::Watcher, GetMainView(), sim_state, drop_zone, 1); } } else if (sim_state) { // // Our own POV start/end (mission fade in/out) using the fog // colour + near/far planes stored on the renderer. // new BTPOVStartEndRenderable( // FUN_00454394, alloc 0x50 entity, VideoRenderable::Watcher, GetMainView(), dplMainZone, dplDeathZone, sim_state, fogRed, fogGreen, fogBlue, fogNear, fogFar, 3 /* MissionStartingState */, 4 /* MissionEndingState */); } break; } default: { // // Unknown / non-mech entities (terrain, cavern world geometry, props, // landmarks, doorframes, eyecandy, ...) route to the DPL per-entity // builder -- exactly as RP's default does (RPL4VID.cpp:1436). That // builder loads each entity's video object(s) (.bgf via // d3d_OBJECT::LoadObject -> LoadObjectBGF) and hangs them on a // Root/Static/DCS-instance renderable, which is how the cavern world // gets onto the screen. (Previously this deferred to the no-op // VideoRenderer grandparent -> world drew nothing.) The uninitialised // `this_instance` in the CulturalIcon/Landmark arm has been fixed in // L4VIDEO.cpp. // // WORLD-PICK TARGET support (task #41): the boresight pick can hit // TERRAIN (the binary's target slot holds non-mech world entities -- // HudSimulation part_013.c:5620 explicitly handles a target without // damage zones). Capture a live Terrain entity for mech4's ground // pick to cite in mech+0x388 (the pick POINT carries the geometry; // which specific instance matters only to damage, which never routes // to terrain). { extern Entity *gBTTerrainEntity; if (gBTTerrainEntity == 0 && entity->IsDerivedFrom(*Terrain::GetClassDerivations())) gBTTerrainEntity = entity; } DPLRenderer::MakeEntityRenderables( entity, model_resource, view_type); break; } } Entity_Being_Created = NULL; } // //############################################################################# // MakeMechRenderables //############################################################################# // // @004cef28 (6157 bytes -- the main world-view builder) // // Build the renderable tree for one mech and submit it to the scene. This is // the BattleTech analogue of RPL4VideoRenderer::MakeJointedMoverRenderables. // HierarchicalDrawComponent* BTL4VideoRenderer::MakeMechRenderables( Entity *entity, ResourceDescription *model_resource, // (unused; tree built from segment table) ViewFrom type) { // // RECONSTRUCTION NOTE (WinTesla port): // The shipped 1996 BattleTech built this tree from a bespoke pre-DPL // renderable hierarchy (BTRootRenderable / BTHingeRenderable / ...) that // parented on raw dpl_DCS* handles and drove the Division IG board. That // hierarchy was never ported to WinTesla -- the engine here replaced it with // the D3D-backed VideoRenderable family (RootRenderable / HingeRenderable / // BallJointRenderable / BallTranslateJointRenderable / DPLStaticChildRenderable // / DPLEyeRenderable, see MUNGA_L4/L4VIDRND). Those renderables self-register // with the renderer, build their own DCS, and parent on the PARENT RENDERABLE // (a HierarchicalDrawComponent*), not a dpl_DCS*. So this body is rebuilt the // RP way (mirrors RPL4VideoRenderer::MakeJointedMoverRenderables, the // segment-table variant) using the engine renderables, which is what actually // gets mech geometry onto the screen. The BT-specific 2D reticle + weapon/ // effect renderables (BTReticleRenderable, beams, tracers, searchlight) still // depend on the unported dpl2d_ layer and are deferred -- TODO(bring-up). // JointedMover *jointed_mover = (JointedMover *)entity; // //~~~~~~~~~~~~~~~~~~~~~~~ // Inside or Outside view: pick skeleton variant + intersect mode/mask. //~~~~~~~~~~~~~~~~~~~~~~~ // bool inDeathZone; dpl_ISECT_MODE intersect_mode; // stub type (empty); kept for parity uint32 intersect_mask; EntitySegment::SkeletonType skeletonType; // // DEBUG(bring-up): external chase camera. The player POV mech is normally // built with the INSIDE skeleton -- the camera sits AT the cockpit eyepoint // with no world geometry ahead, so the frame is black. To make the mech // BODY visible, treat the player's own mech as an OUTSIDE build (which loads // the full body geometry) and, after the renderable tree is built, install a // fixed external camera a few mech-heights in FRONT looking back at the mech. // Other (already-outside) mechs keep their normal no-camera body build. // TODO(bring-up): replace with a real spectator/chase view-mode toggle wired // through BTL4Application; see RECONCILE.md. // const bool buildDebugChaseCamera = (type == insideEntity); if (buildDebugChaseCamera) type = outsideEntity; if (type == insideEntity) { inDeathZone = true; intersect_mask = 0; skeletonType = EntitySegment::SkeletonType_A; // 4 } else { inDeathZone = false; intersect_mask = INTERSECT_ALL; // 0xffffffff skeletonType = EntitySegment::SkeletonType_N; // 0 } // // Root renderable for this entity. Its ctor calls AddRenderable(this) and // binds to entity->localToWorld, so the whole tree is driven from the entity // position every frame. // RootRenderable *this_root = new RootRenderable( entity, VideoRenderable::Dynamic, NULL, inDeathZone, intersect_mode, intersect_mask); // // Start (or reset) this mech's RemakeEntity bookkeeping: record the skeleton // variant now; the per-segment renderables + initial graphic states are filled // in as the tree is built below (see RemakeEntityRenderables). // MechRenderTree &render_tree = mMechRenderTrees[entity]; render_tree = MechRenderTree(); render_tree.skeletonType = (int)skeletonType; render_tree.viewSkeleton = (int)skeletonType; render_tree.rootRenderable = this_root; render_tree.wrecked = 0; if (getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[BTrender] tracking mech tree for entity " << (void*)entity << " classID=" << entity->GetClassID() << " (" << mMechRenderTrees.size() << " tracked)\n" << std::flush; // // Per-segment renderable array (the parent for each segment's children). // int segment_count = jointed_mover->segmentCount; // [0x318] HierarchicalDrawComponent **dcs_array = new HierarchicalDrawComponent*[segment_count]; for (int i = 0; i < segment_count; ++i) dcs_array[i] = NULL; // bring-up diagnostics (counts geometry actually loaded vs. requested) int dbg_obj_requested = 0, dbg_obj_loaded = 0, dbg_eye = 0; DEBUG_STREAM << "[BTrender] MakeMechRenderables: " << segment_count << " segments, view=" << (int)type << "\n" << std::flush; JointSubsystem *joint_subsystem = jointed_mover->GetJointSubsystem(); // [0x31c] // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Walk the EntitySegment table. For each segment: build its offset matrix, // find its parent renderable, choose its joint renderable, load and hang its // geometry. Site segments (eyepoint, gun ports) are handled specially. //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // EntitySegment::SegmentTableIterator segment_iterator(jointed_mover->segmentTable /* [0x300] */); EntitySegment *segment; while ((segment = segment_iterator.ReadAndNext()) != NULL) // vtbl+0x28 { LinearMatrix offset_matrix; offset_matrix = segment->GetBaseOffset(); // [0x74] // // Parent renderable: root if the segment has no parent, else the // renderable already built for its parent segment. // HierarchicalDrawComponent *parent_DCS; if (!segment->GetParent() /* [0xc4] */) { parent_DCS = this_root; } else { parent_DCS = dcs_array[segment->GetParentIndex() /* [0xc8] */]; } // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Site segment? The eyepoint site builds the camera (DPLEyeRenderable) // for the inside view; other sites carry no body geometry here (their // subsystem effects are deferred -- TODO(bring-up)). //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // if (segment->IsSiteSegment() /* [0x10] */ != 0) { // The authentic COCKPIT EYEPOINT. Built for the true inside view // AND for the player's chase build (buildDebugChaseCamera), so the // V-key toggle can switch to it -- the pod's only view was this // eyepoint; the chase camera is the port's usability addition. if ((type == insideEntity || buildDebugChaseCamera) && strcmp((const char *)segment->GetName() /* [0x11c] */, "siteeyepoint") == 0) // @0051d290 { EulerAngles *eye_rot = (EulerAngles *)entity->GetAttributePointer("EyepointRotation"); // @0051d29d // // AUTHENTIC eye (decomp FUN_004579a8 @part_007.c:9274-9325; caller part_014.c:5525-5566): // the eye offset matrix is the siteeyepoint segment's LOCAL rest transform (GetBaseOffset, // segment+0x74 -- already in offset_matrix above), and the eye is parented on its PARENT // segment's draw component (parent_DCS) -- NOT the root, NOT the full GetSegmentToEntity, // NOT an upright hack. World orientation + all live motion (torso twist, gait) come from // the parent-chain composition, exactly as the decomp's dpl_AddDCSToDCS hierarchy does; // EyepointRotation is combined in Execute as baseOffset * R, and VIEW = inverse(eyeWorld) // (FUN_004c22c4 @part_013.c:11788) -- so the look/up axes fall out of the eye's own basis // with no per-mech forward-axis assumption. // mEyeCockpit = new DPLEyeRenderable( entity, offset_matrix, parent_DCS, eye_rot); if (type == insideEntity) // true inside build: it IS the camera mCamera = mEyeCockpit; dbg_eye = 1; } continue; } // // Load this segment's geometry (skeleton-variant .bgf), if any. // d3d_OBJECT *this_object = NULL; // Select the segment's model VARIANT by its damage zone's graphic state. // The engine keys video-object names by {skeleton, damage_graphic_state} // (SEGMENT.h:172): a Destroyed zone (GetGraphicState()==1) returns the // destroyed/damaged model, so a wrecked segment visibly comes apart. The // recon previously passed ONLY the skeleton type, leaving the state at its // default 0 (Exists) -> always the intact model = no visible damage. Enumeration seg_gstate = 0; // ExistsGraphicState { int zone_index = segment->GetPrimaryDamageZone(); // SEGMENT.h:107 (a zone INDEX) if (zone_index >= 0 && zone_index < entity->damageZoneCount && entity->damageZones[zone_index] != 0) seg_gstate = entity->damageZones[zone_index]->GetGraphicState(); // DAMAGE.h:196 } CString *object_name = segment->GetVideoObjectName(skeletonType, seg_gstate); // FUN_00424084 if (object_name != NULL) { char filename[44]; strcpy(filename, (const char *)*object_name); int len = (int)strlen(filename); if (len >= 4) filename[len - 4] = '\0'; // strip ".bgf" strcat(filename, ".bgf"); // d3d_OBJECT::LoadObject wants the extension this_object = d3d_OBJECT::LoadObject(GetDevice(), filename); ++dbg_obj_requested; if (this_object != NULL) ++dbg_obj_loaded; else DEBUG_STREAM << "[BTrender] no mesh for '" << filename << "' (expects VIDEO\\*.x)\n" << std::flush; // SHADOW PROXY (task #20): the binary's shadow is the flat *_tshd.bgf // silhouette posed by jointshadow/jointtshadow (model-record // ShadowJointName @0xB4, part_012.c:10285). Tag it so d3d_OBJECT // draws it translucent in the blend pass instead of opaque black; // alphaTest=true routes it there (HierarchicalDrawComponent::Execute, // L4VIDRND.cpp:149, schedules the pass per-drawOp on alphaTest). if (this_object != NULL && strstr(filename, "tshd") != NULL) { this_object->SetIsShadow(1); for (int op = 0; op < this_object->GetDrawOpCount(); ++op) this_object->GetDrawOp(op)->alphaTest = true; } } // // Determine joint type: static (-1 -> Static) or look up the joint in // the JointSubsystem's joint table. // int segment_slot = segment->GetIndex() /* [0xcc] */; Joint *this_joint = NULL; Joint::JointType joint_type; if (segment->GetJointIndex() /* [0xc0] */ == -1) { joint_type = Joint::StaticJointType; // 3 } else { this_joint = joint_subsystem->GetJoint(segment->GetJointIndex()); // FUN_0041d3b3 joint_type = this_joint->GetJointType() /* [0x10] */; } // // Build the appropriate engine joint renderable, recording it in the // per-segment array so children can parent to it. // HierarchicalDrawComponent *child; switch (joint_type) { case Joint::BallJointType: // 4 { child = new BallJointRenderable( entity, VideoRenderable::Dynamic, this_object, inDeathZone, intersect_mode, intersect_mask, parent_DCS, &offset_matrix, &this_joint->GetEulerAngles()); break; } case Joint::BallTranslationJointType: // 5 { child = new BallTranslateJointRenderable( entity, VideoRenderable::Dynamic, this_object, inDeathZone, intersect_mode, intersect_mask, parent_DCS, &offset_matrix, &this_joint->GetEulerAngles(), &this_joint->GetTranslation()); break; } case Joint::StaticJointType: // 3 { child = new DPLStaticChildRenderable( entity, inDeathZone, this_object, intersect_mode, intersect_mask, offset_matrix, parent_DCS); break; } default: // 0,1,2 HingeX/Y/Z { child = new HingeRenderable( entity, VideoRenderable::Dynamic, this_object, inDeathZone, intersect_mode, intersect_mask, parent_DCS, &offset_matrix, &this_joint->GetHinge() /* [0xc] */); break; } } dcs_array[segment_slot] = child; // Record this segment's renderable + the graphic state it was built with, // so a later damage-state change can swap its mesh in place (RemakeEntity). render_tree.segRenderable[segment_slot] = child; render_tree.segGState[segment_slot] = (int)seg_gstate; } delete [] dcs_array; DEBUG_STREAM << "[BTrender] mech tree built: meshes " << dbg_obj_loaded << "/" << dbg_obj_requested << " loaded, eye=" << dbg_eye << "\n" << std::flush; // // If this mech DIED before its tree was built (a fast kill during mission // creation), apply the remembered wreck swap now. // { extern int BTTakePendingWreck(Entity *entity); if (BTTakePendingWreck(entity)) SwapToWreck(entity); } // // The TARGETING RETICLE (the main-view HUD): built for the player's mech, // per the 1996 inside-view path (@part_014.c:5127-5158 constructs the // 0x358 BTReticleRenderable, then :5390-5436 registers one pip per weapon // from its TargetWithinRange / WeaponRange / PipPosition / PipColor / // PipExtendedRange / SimulationState attributes). Drawn by BTDrawReticle // in the cockpit view only. // if (buildDebugChaseCamera) { extern BTReticleRenderable *BTBuildReticle(Entity *mech); BTBuildReticle(entity); } // DEV: BT_START_INSIDE=1 begins in the cockpit view (also exercises the // inside-skeleton swap headlessly). if (buildDebugChaseCamera && getenv("BT_START_INSIDE")) SetViewInside(1); // // TODO(bring-up): inside-view targeting reticle (BTReticleRenderable + // AddWeapon pips) and the per-subsystem weapon/effect renderables (PPC/ // emitter beams, projectile tracers, coolant, searchlight) are NOT built // here yet -- they depend on the dpl2d_ 2D display-list layer (stubbed) and // the BT effect renderables (stubbed). Adding them is the HUD / weapons // render bring-up step; they are not required to get the mech body drawn. // // // DEBUG(bring-up): install the fixed external chase camera for the player's // own mech. The eye renderable parents on this_root, so when the render // tree executes (RootRenderable::Execute pushes entity->localToWorld onto the // matrix stack) the camera's offset is composed with the mech's world matrix // -- i.e. the camera tracks the mech. DPLEyeRenderable looks down its local // +Z axis (row 2) with local +Y as up (row 1) from its translation (row 3); // D3DXMatrixLookAtRH re-derives the basis from pos/at/up. // // Mech-local frame (from the .skl): +Y up, the mech FACES -Z (gun ports / // eyepoint are at -Z), and the Avatar is ~10-12 units tall (hip at y~5.3, // eyepoint ~y9). Place the camera in FRONT (-Z) and above, looking back // toward the mech centre. // if (buildDebugChaseCamera) { // CHASE view (task #15 usability): the mech faces -Z, so the original // debug placement (camera at -Z, "in front, looking back at its face") // made W walk the mech TOWARD the viewer -- hopelessly disorienting to // drive. Default is now BEHIND (+Z) and above, looking forward over the // shoulder: press forward, the mech walks away from you; turns read // correctly. env BT_CAM=face restores the old face-on animation view. const char *camMode = getenv("BT_CAM"); const bool faceView = (camMode != 0 && camMode[0] == 'f'); float camPx = 0.0f, camPy = faceView ? 9.0f : 11.0f; float camPz = faceView ? -28.0f : 28.0f; // -Z front / +Z behind const float tgtX = 0.0f, tgtY = 6.0f; const float tgtZ = faceView ? 0.0f : -6.0f; // chase: look ahead of the mech // DEBUG(bring-up): BT_CAM_Y / BT_CAM_Z override the fixed chase-camera offset -- // raising it clears mound-shoulder OCCLUSION, but NOT genuine geometry clipping // where the mech is stopped on a steep slope and the terrain rises through its // legs (that is a collision-vs-visual issue, not a camera one). if (const char *cy = getenv("BT_CAM_Y")) camPy = (float)atof(cy); if (const char *cz = getenv("BT_CAM_Z")) camPz = (float)atof(cz); // look direction (local +Z of the camera) = normalize(target - pos) float zx = tgtX - camPx, zy = tgtY - camPy, zz = tgtZ - camPz; float zl = (float)sqrt(zx*zx + zy*zy + zz*zz); if (zl < 1e-6f) zl = 1.0f; zx /= zl; zy /= zl; zz /= zl; // world up const float ux = 0.0f, uy = 1.0f, uz = 0.0f; // right (local +X) = up x forward float xx = uy*zz - uz*zy, xy = uz*zx - ux*zz, xz = ux*zy - uy*zx; float xl = (float)sqrt(xx*xx + xy*xy + xz*xz); if (xl < 1e-6f) xl = 1.0f; xx /= xl; xy /= xl; xz /= xl; // recomputed up (local +Y) = forward x right float yx = zy*xz - zz*xy, yy = zz*xx - zx*xz, yz = zx*xy - zy*xx; // CAMERA-BASIS CONVENTION (task #56 follow-through): the view is now the // authentic VIEW = inverse(eyeWorld) (DPLEyeRenderable::Execute), under // which the camera looks along -Z of its own basis -- so the basis rows // are right/up/BACK, not right/up/look. This chase matrix was authored // for the old LookAt (row2 = look); convert by a 180-degree turn about // local Y (negate the X and Z rows -- determinant stays +1). LinearMatrix debugOffset; // identity debugOffset(0,0) = -xx; debugOffset(0,1) = -xy; debugOffset(0,2) = -xz; // X row (right, flipped) debugOffset(1,0) = yx; debugOffset(1,1) = yy; debugOffset(1,2) = yz; // Y row (up) debugOffset(2,0) = -zx; debugOffset(2,1) = -zy; debugOffset(2,2) = -zz; // Z row (BACK = -look) debugOffset(3,0) = camPx; debugOffset(3,1) = camPy; debugOffset(3,2) = camPz; // W row (pos) mEyeChase = new DPLEyeRenderable(entity, debugOffset, this_root, NULL); // Respect the pilot's CHOSEN view across renderable rebuilds: this build // used to stomp mCamera back to chase on every remake (damage swaps, // start-inside), silently flipping the active eye out from under the V // toggle (and the aim-ray camera feed with it). mCamera = (mViewInside && mEyeCockpit != 0) ? mEyeCockpit : mEyeChase; DEBUG_STREAM << "[BTrender] external debug chase camera installed at (" << camPx << "," << camPy << "," << camPz << ") looking at (" << tgtX << "," << tgtY << "," << tgtZ << ") -- V toggles the cockpit eyepoint" << (mEyeCockpit ? "" : " (COCKPIT EYE MISSING)") << "\n" << std::flush; } return this_root; } // //############################################################################# // RemakeEntityRenderables (the render "RemakeEntity" state -- damage swap) //############################################################################# // // A damage zone's graphic state changed (a segment became Destroyed or Gone). // Walk this mech's segments and, for any whose graphic state now differs from // what its renderable was built with, re-pick the video-object variant by the // new graphic state and swap it onto the joint renderable IN PLACE. Execute() // re-reads graphicalObject each frame, so the wrecked mesh shows next frame. // No teardown: the component dtor does not cascade to children (L4VIDRND.cpp:104), // so a rebuild would leak -- the authentic behaviour is an in-place mesh swap. // void BTL4VideoRenderer::RemakeEntityRenderables(Entity *entity) { std::map::iterator tree_it = mMechRenderTrees.find(entity); if (tree_it == mMechRenderTrees.end()) { if (getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[BTrender] RemakeEntity: no render tree for entity " << (void*)entity << " (" << mMechRenderTrees.size() << " tracked)\n" << std::flush; return; // tree not built yet -- Make will read the state } MechRenderTree &render_tree = tree_it->second; if (render_tree.wrecked) return; // already the dbr hulk -- nothing left to swap JointedMover *jointed_mover = (JointedMover *)entity; EntitySegment::SkeletonType skeletonType = (EntitySegment::SkeletonType)render_tree.viewSkeleton; // the DISPLAYED set EntitySegment::SegmentTableIterator segment_iterator(jointed_mover->segmentTable); EntitySegment *segment; int swapped = 0, checked = 0, mapped = 0; while ((segment = segment_iterator.ReadAndNext()) != NULL) { if (segment->IsSiteSegment() != 0) continue; ++checked; int segment_slot = segment->GetIndex(); std::map::iterator r = render_tree.segRenderable.find(segment_slot); if (r == render_tree.segRenderable.end() || r->second == NULL) continue; ++mapped; // // Current graphic state for this segment (from its damage zone). // Enumeration seg_gstate = 0; // ExistsGraphicState int zone_index = segment->GetPrimaryDamageZone(); if (zone_index >= 0 && zone_index < entity->damageZoneCount && entity->damageZones[zone_index] != 0) seg_gstate = entity->damageZones[zone_index]->GetGraphicState(); if ((int)seg_gstate == render_tree.segGState[segment_slot]) continue; // unchanged -- nothing to swap render_tree.segGState[segment_slot] = (int)seg_gstate; // // Re-pick + load the segment's video-object variant for the new graphic // state (same construction as the initial build in MakeMechRenderables). // CString *object_name = segment->GetVideoObjectName(skeletonType, seg_gstate); if (getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[BTrender] seg '" << (const char *)segment->GetName() << "' slot " << segment_slot << " -> gstate " << (int)seg_gstate << " variant=" << (object_name ? (const char *)*object_name : "(none)") << "\n" << std::flush; d3d_OBJECT *new_object = NULL; if (object_name != NULL) { char filename[44]; strcpy(filename, (const char *)*object_name); int len = (int)strlen(filename); if (len >= 4) filename[len - 4] = '\0'; // strip ".bgf" strcat(filename, ".bgf"); new_object = d3d_OBJECT::LoadObject(GetDevice(), filename); if (new_object == NULL && getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[BTrender] damaged variant '" << filename << "' FAILED to load (expects VIDEO\\*.x)\n" << std::flush; if (new_object != NULL && strstr(filename, "tshd") != NULL) { new_object->SetIsShadow(1); for (int op = 0; op < new_object->GetDrawOpCount(); ++op) new_object->GetDrawOp(op)->alphaTest = true; } } // // GoneGraphicState (blown off): no mesh -> hide the segment. Destroyed/ // Exists: swap to the variant if it loaded; otherwise keep the current // mesh (don't blank a segment merely because a damaged .bgf is missing). // if (new_object != NULL) r->second->SetDrawObj(new_object); else if ((int)seg_gstate == DamageZone::GoneGraphicState) r->second->SetDrawObj(NULL); ++swapped; } if (swapped != 0 || getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[BTrender] RemakeEntity: " << swapped << " mesh(es) swapped (" << mapped << " body segs mapped of " << checked << " checked)\n" << std::flush; } // //############################################################################# // RebuildMechRenderables (the HEAL direction of RemakeEntity -- respawn) //############################################################################# // // SwapToWreck hides every body segment and hangs a sinking dbr hulk on the root, // latching render_tree.wrecked (one-way). On respawn Mech::Reset heals the sim // state, but the render stays the sunk hulk unless we reverse the swap: drop the // hulk/debris and restore every segment to its now-intact mesh (the zones are // healed, so GetGraphicState() == Exists). This is the same in-place mesh swap // as RemakeEntityRenderables, forced (RemakeEntity early-returns while wrecked). // int BTTakePendingWreck(Entity *entity); // defined below (SwapToWreck section) void BTL4VideoRenderer::RebuildMechRenderables(Entity *entity) { std::map::iterator tree_it = mMechRenderTrees.find(entity); if (tree_it == mMechRenderTrees.end()) { BTTakePendingWreck(entity); // clear a queued (never-applied) wreck return; } MechRenderTree &render_tree = tree_it->second; // // Drop the wreck hulk + strewn debris (the renderables leak -- the component // dtor does not cascade, same as the wreck swap -- but hiding them removes // them from the draw and stops TickWreck from sinking a nulled tree). // if (render_tree.wreckHulk != NULL) render_tree.wreckHulk->SetDrawObj(NULL); if (render_tree.wreckDebris != NULL) render_tree.wreckDebris->SetDrawObj(NULL); render_tree.wreckHulk = NULL; render_tree.wreckDebris = NULL; render_tree.wrecked = 0; render_tree.wreckAge = 0.0f; // // Restore every body segment to its now-intact mesh via the shared view-skeleton // applier, which ALSO re-asserts the inside-view rules (SkeletonType_A + '_cop' // canopy suppression). Without this, respawning while in cockpit view rebuilt // the full OUTSIDE torso around the eyepoint -> the eye ended up inside opaque // geometry -> black viewport until the next V-toggle. Only the mech the local // camera views FROM (the player's own) gets the inside treatment; a replicant // (a peer's mech) is always drawn with its outside skeleton. // Entity *viewpoint = (application != 0) ? application->GetViewpointEntity() : 0; int inside = (entity == viewpoint) ? mViewInside : 0; int restored = ApplyViewSkeleton(entity, inside); if (getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[BTrender] respawn: rebuilt intact model (" << restored << " segs restored, hulk dropped)\n" << std::flush; } // //############################################################################# // BTRemakeMechModel (sim-side bridge -- see btl4vid.hpp) //############################################################################# // // Reaches the live renderer and refreshes a mech's visible model after its // damage graphic state changed. Called from MechDeathHandler (sim TU). The // frame loop is single-threaded (sim + render share the main thread; only the // network RX socket runs on its own thread), so loading geometry here is safe. // void BTRemakeMechModel(Entity *entity) { if (entity == NULL || application == NULL) return; BTL4VideoRenderer *renderer = (BTL4VideoRenderer *)application->GetVideoRenderer(); if (renderer != NULL) renderer->RemakeEntityRenderables(entity); } // Sim-side bridge for the respawn render un-wreck (Mech::Reset calls this to // restore the intact model after healing). void BTRebuildMechModel(Entity *entity) { if (entity == NULL || application == NULL) return; BTL4VideoRenderer *renderer = (BTL4VideoRenderer *)application->GetVideoRenderer(); if (renderer != NULL) renderer->RebuildMechRenderables(entity); } // //############################################################################# // SwapToWreck (ExplosionScripts effect 104, reconstructed) //############################################################################# // // The authentic death chain: the victim's per-mech death ModelList // ('blhdead'/'lokdead'/... resources 22-25) dispatches effect 104, whose 1996 // script (@0045xxxx, part_008.c:2663 case 4) swaps in the burning WRECK: the // destroyed hulk mesh + flame meshes with flicker sweeps and a slow settle. // The 2007 port stubbed the whole script layer. This reconstruction does the // core swap: hide every segment mesh and hang the victim's own "dbr" // hulk on the tree root (the 1996 code hardcoded thrdbr.bgf -- a dev shortcut; // every mech ships its hulk: BLHDBR/MADDBR/LOKDBR/... + GENDBR the generic // fallback). Burning comes from the effect layer (the death list also fires // the 1007 boom + 1001 smoke plume; the wreck re-arms the plume while it // stands). Mesh flames (flamesml/flamebig + sweep flicker) are a noted // follow-up. // static std::map gBTPendingWrecks; int BTTakePendingWreck(Entity *entity) { std::map::iterator it = gBTPendingWrecks.find(entity); if (it == gBTPendingWrecks.end()) return 0; gBTPendingWrecks.erase(it); return 1; } void BTL4VideoRenderer::SwapToWreck(Entity *victim) { std::map::iterator tree_it = mMechRenderTrees.find(victim); if (tree_it == mMechRenderTrees.end()) { gBTPendingWrecks[victim] = 1; // died before the tree was built return; } MechRenderTree &render_tree = tree_it->second; if (render_tree.wrecked) return; // // The victim's model prefix, from any segment's intact video-object name // (e.g. "blh_rfot.bgf" -> "blh" -> "blhdbr.bgf"). // char hulk_name[44]; hulk_name[0] = '\0'; { JointedMover *jm = (JointedMover *)victim; EntitySegment::SegmentTableIterator it(jm->segmentTable); EntitySegment *segment; while ((segment = it.ReadAndNext()) != NULL) { if (segment->IsSiteSegment() != 0) continue; CString *nm = segment->GetVideoObjectName( (EntitySegment::SkeletonType)render_tree.skeletonType, 0); if (nm != NULL && strlen((const char *)*nm) >= 3) { strncpy(hulk_name, (const char *)*nm, 3); hulk_name[3] = '\0'; strcat(hulk_name, "dbr.bgf"); break; } } } d3d_OBJECT *hulk = (hulk_name[0] != '\0') ? d3d_OBJECT::LoadObject(GetDevice(), hulk_name) : NULL; if (hulk == NULL) { DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name << "' missing -> gendbr.bgf fallback\n" << std::flush; hulk = d3d_OBJECT::LoadObject(GetDevice(), "gendbr.bgf"); } // // The strewn-debris field that accompanies the standing hulk (the 1996 // script pairs them: thrdbr + ldbr, parented together, sinking together). // d3d_OBJECT *debris = d3d_OBJECT::LoadObject(GetDevice(), "ldbr.bgf"); // // Hide the body; hang the wreck pieces on the tree root (identity offset -- // the root renderable already pushes the wreck's localToWorld, so they sit // at the mech's ground position with its death yaw). // for (std::map::iterator r = render_tree.segRenderable.begin(); r != render_tree.segRenderable.end(); ++r) { if (r->second != NULL) r->second->SetDrawObj(NULL); } if (render_tree.rootRenderable != NULL) { dpl_ISECT_MODE isect_mode; LinearMatrix identity(True); if (hulk != NULL) render_tree.wreckHulk = new DPLStaticChildRenderable( victim, false /* main zone */, hulk, isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable); if (debris != NULL) render_tree.wreckDebris = new DPLStaticChildRenderable( victim, false /* main zone */, debris, isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable); } render_tree.wrecked = 1; render_tree.wreckAge = 0.0f; DEBUG_STREAM << "[BTrender] wreck swap: victim -> '" << (hulk_name[0] ? hulk_name : "gendbr.bgf") << (hulk ? "'" : "' (LOAD FAILED -- body hidden only)") << (debris ? " + ldbr debris" : "") << "\n" << std::flush; } // //############################################################################# // TickWreck -- the wreck's quadratic SINK (the 1996 burial) //############################################################################# // // FUN_00456410 (the 1996 sink renderable): offsetY = rate * t^2, hulk rate // -0.025 (armed 0.25s after the boom by a sweep trigger). The ~7-unit hulk is // fully underground ~17s after the kill -- the wreck visual "fades away" by // burial; the ENTITY (sim/collision) stays, per the wreck-stays rule. Once // buried, the pieces are hidden and the sink stops. // int BTL4VideoRenderer::TickWreck(Entity *victim, float dt) { std::map::iterator tree_it = mMechRenderTrees.find(victim); if (tree_it == mMechRenderTrees.end()) return 1; // no tree yet -- not buried MechRenderTree &render_tree = tree_it->second; if (!render_tree.wrecked) return 1; // not swapped yet if (render_tree.wreckHulk == NULL && render_tree.wreckDebris == NULL) return 0; // already buried render_tree.wreckAge += dt; float sink = -0.025f * render_tree.wreckAge * render_tree.wreckAge; // the authored rate if (sink < -8.0f) { // fully buried -> hide + stop ticking if (render_tree.wreckHulk) render_tree.wreckHulk->SetDrawObj(NULL); if (render_tree.wreckDebris) render_tree.wreckDebris->SetDrawObj(NULL); render_tree.wreckHulk = NULL; render_tree.wreckDebris = NULL; DEBUG_STREAM << "[BTrender] wreck buried (sink complete)\n" << std::flush; return 0; } if (render_tree.wreckHulk) render_tree.wreckHulk->SetOffsetTranslation(0.0f, sink, 0.0f); if (render_tree.wreckDebris) render_tree.wreckDebris->SetOffsetTranslation(0.0f, sink, 0.0f); return 1; } // // Sim-side bridge (UpdateDeathState drives the sink each dead frame). // int BTWreckSinkTick(Entity *victim, float dt) { if (victim == NULL || application == NULL) return 1; BTL4VideoRenderer *renderer = (BTL4VideoRenderer *)application->GetVideoRenderer(); if (renderer == NULL) return 1; return renderer->TickWreck(victim, dt); } // // Engine-side bridge (the ExplosionClassID dispatch calls this on effect 104). // void BTSwapMechToWreck(Entity *victim) { if (victim == NULL || application == NULL) return; BTL4VideoRenderer *renderer = (BTL4VideoRenderer *)application->GetVideoRenderer(); if (renderer != NULL) renderer->SwapToWreck(victim); } // //############################################################################# // BTReticleRenderable -- ctor (@004cc40c) + Draw (Execute @004cdcf0 is in an // un-exported gap; the draw dynamics here are [T3], the GLYPHS are [T1]) //############################################################################# // // The live reticle instance (the player's; drawn by BTDrawReticle in the // cockpit view only). // static BTReticleRenderable *gBTReticle = 0; extern Scalar gBTHudRangeStorage; // live target range (defined below) static int gBTHudInside = 0; // cockpit view live (set by SetViewInside) // the dpl2d rasteriser (dpl2d.cpp; device type opaque here) extern void dpl2d_ExecuteList(dpl2d_DISPLAY *list, struct IDirect3DDevice9 *device); void BTSetHudTargetRange(Scalar range) { gBTHudRangeStorage = range; } void BTSetHudInside(int inside) { gBTHudInside = inside; } // // FUN_004cd938 -- the tick-LADDER builder: `count` ticks stepped along an axis // (dir 0/1 = +x/-x travel with vertical ticks, 2/3 = +y/-y with horizontal // ticks), a MAJOR tick (halfMajor) every (majorEvery+1)th, minor otherwise. // static void BTReticleTickLadder(dpl2d_DISPLAY *list, int count, int majorEvery, float span, float x0, float y0, int dir, float halfMinor, float halfMajor) { float step = span / (float)(count - 1); float tickMinX = 0, tickMinY = 0, tickMajX = 0, tickMajY = 0; float stepX = 0, stepY = 0; switch (dir) { case 0: stepX = step; tickMajY = halfMajor; tickMinY = halfMinor; break; case 1: stepX = -step; tickMajY = halfMajor; tickMinY = halfMinor; break; case 2: stepY = step; tickMajX = halfMajor; tickMinX = halfMinor; break; case 3: stepY = -step; tickMajX = halfMajor; tickMinX = halfMinor; break; } float x = x0, y = y0; int untilMajor = 0; dpl2d_OpenLines(list); for (int i = 0; i < count; ++i) { if (untilMajor == 0) { dpl2d_AddPoint(list, x + tickMajX, y + tickMajY); dpl2d_AddPoint(list, x - tickMajX, y - tickMajY); untilMajor = majorEvery; } else { dpl2d_AddPoint(list, x + tickMinX, y + tickMinY); dpl2d_AddPoint(list, x - tickMinX, y - tickMinY); --untilMajor; } x += stepX; y += stepY; } dpl2d_CloseLines(list); } // // The authentic calibration constants (the binary ctor's own values). // static const float kRetOriginX = 0.35f; // [0x7f] right range-ladder x static const float kRetOriginY = 0.25f; // [0x80] ladder bottom y static const float kRetScaleY = 0.5f; // [0x81] ladder span static const float kRetTickMinor = 0.008f; // [0x83] static const float kRetTickMajor = 0.016f; // [0x82] static const float kRetBotX = -0.25f; // [0x84] bottom heading-ladder x0 static const float kRetBotY = 0.35f; // [0x85] heading ladder y static const float kRetBotSpan = 0.5f; // [0x86] static const float kRetCaret = 0.02f; // _DAT_004cd7f4 (caret triangle size) [T3] static const int kRetTicksR = 13; // [9] right ladder tick count static const int kRetTicksB = 21; // [0xb] bottom ladder tick count static const float kRetMaxRange = 1200.0f; // ctor param 11 (0x44960000) BTReticleRenderable::BTReticleRenderable(Entity *entity, Scalar *range_attr) : VideoRenderable(entity, VideoRenderable::Dynamic) { weaponCount = 0; originX = kRetOriginX; originY = kRetOriginY; scaleY = kRetScaleY; biasX = kRetTickMajor; maxRange = kRetMaxRange; minRange = 0.0f; rangeScale= maxRange - minRange; // [0x8d] -- AddWeapon divides by it rangeAttr2= range_attr; pipsBuilt = 0; // recovered-Execute dynamic state lockShown = 0; lockSpinDeg= 0.0f; masterList = dpl2d_NewDisplayList(); simpleXList = dpl2d_NewDisplayList(); aimDotList = dpl2d_NewDisplayList(); rangeCaretR = dpl2d_NewDisplayList(); rangeCaretB = dpl2d_NewDisplayList(); headingList = dpl2d_NewDisplayList(); bottomAnchor = dpl2d_NewDisplayList(); leftArrow = dpl2d_NewDisplayList(); rightArrow = dpl2d_NewDisplayList(); crossList = dpl2d_NewDisplayList(); subB6 = dpl2d_NewDisplayList(); subB7 = dpl2d_NewDisplayList(); subB8 = dpl2d_NewDisplayList(); subB9 = dpl2d_NewDisplayList(); subBA = dpl2d_NewDisplayList(); // // The MASTER list (@4511-4601, faithfully transcribed). Colours: green // 0.75 for the frame, yellow for the range caret; widths 1/2/3. // dpl2d_DISPLAY *m = masterList; dpl2d_Begin(m, 1); dpl2d_SetLineWidth(m, 1.0f); dpl2d_FullScreenClip(m); dpl2d_SetColor(m, 0.75f, 0.0f, 0.0f); dpl2d_CallList(m, crossList); // [0xa1] target-box slot (empty until lock; // Draw rebuilds it: designator ring at the // target's screen point / off-screen arrows) dpl2d_SetColor(m, 0.0f, 0.75f, 0.0f); // the AIM GROUP (task #36): [0x9a] is the aim TRANSLATE -- CallList state // persists to the caller (the dpl2d inline-include semantic), so the slew // translate it carries positions the dot + crosses that follow. The // PushState/PopState pair contains it so the fixed frame (tick ladders, // tapes) stays put. [T3 -- the binary Execute is un-exported; mechanism // per the engine ReticleRenderable's position-list pattern, T0.] dpl2d_PushState(m); dpl2d_CallList(m, aimDotList); // [0x9a] the aim translate dpl2d_OpenPolypoint(m); // centre dot dpl2d_AddPoint(m, 0.0f, 0.0f); dpl2d_ClosePolypoint(m); dpl2d_OpenLines(m); // inner cross (gap at centre) dpl2d_AddPoint(m, 0.04f, 0.0f); dpl2d_AddPoint(m, 0.10f, 0.0f); dpl2d_AddPoint(m, -0.04f, 0.0f); dpl2d_AddPoint(m, -0.10f, 0.0f); dpl2d_AddPoint(m, 0.0f, 0.04f); dpl2d_AddPoint(m, 0.0f, 0.10f); dpl2d_AddPoint(m, 0.0f, -0.04f); dpl2d_AddPoint(m, 0.0f, -0.10f); dpl2d_CloseLines(m); dpl2d_SetLineWidth(m, 3.0f); dpl2d_OpenLines(m); // heavy outer cross dpl2d_AddPoint(m, 0.10f, 0.0f); dpl2d_AddPoint(m, 0.16f, 0.0f); dpl2d_AddPoint(m, -0.10f, 0.0f); dpl2d_AddPoint(m, -0.16f, 0.0f); dpl2d_AddPoint(m, 0.0f, 0.10f); dpl2d_AddPoint(m, 0.0f, 0.16f); dpl2d_AddPoint(m, 0.0f, -0.10f); dpl2d_AddPoint(m, 0.0f, -0.16f); dpl2d_CloseLines(m); dpl2d_PopState(m); // contain the aim translate dpl2d_SetLineWidth(m, 1.0f); // the RIGHT range ladder (13 ticks up the right side, dir 3 = -y travel) BTReticleTickLadder(m, kRetTicksR, 1, kRetScaleY, kRetOriginX, kRetOriginY, 3, kRetTickMinor, kRetTickMajor); // the range group (ctor @4546-4560 [T1], color-corrected vs the reference // screenshot which CONFIRMS the binary): the YELLOW width-2 state applies // to the CALLED live list (the range BAR + caret translate the Draw // rebuilds); the caret TRIANGLE itself is GREEN width 1 (@4550-4551 -- // SetLineWidth(1) + SetColor(0,0.75,0) BEFORE the polyline). dpl2d_PushState(m); dpl2d_SetLineWidth(m, 2.0f); dpl2d_SetColor(m, 0.75f, 0.75f, 0.0f); // yellow: the live bar dpl2d_CallList(m, rangeCaretR); dpl2d_SetLineWidth(m, 1.0f); dpl2d_SetColor(m, 0.0f, 0.75f, 0.0f); // green: the caret triangle dpl2d_OpenPolyline(m); dpl2d_AddPoint(m, kRetOriginX - kRetTickMajor, kRetOriginY); dpl2d_AddPoint(m, kRetOriginX - kRetCaret - kRetTickMajor, kRetOriginY + kRetCaret); dpl2d_AddPoint(m, kRetOriginX - kRetCaret - kRetTickMajor, kRetOriginY - kRetCaret); dpl2d_AddPoint(m, kRetOriginX - kRetTickMajor, kRetOriginY); // close @4558 dpl2d_ClosePolyline(m); dpl2d_PopState(m); // the BOTTOM heading ladder (21 ticks across, dir 0 = +x travel) BTReticleTickLadder(m, kRetTicksB, 1, kRetBotSpan, kRetBotX, kRetBotY, 0, kRetTickMinor, kRetTickMajor); // the heading carets (over/under triangles at the ladder centre, shifted // by the called heading translate) // (ctor @4565-4587 [T1], color-corrected like the range caret: yellow // width 2 for the CALLED live twist-deflection list, then GREEN width 1 // @4569-4570 for the bowtie triangles. The binary pops state between the // over- and under-caret (@4579) -- same green/1 either way.) { float cx = kRetBotX + kRetBotSpan * 0.5f; // _DAT_004cd7f8 = 0.5 [T1] dpl2d_PushState(m); dpl2d_SetLineWidth(m, 2.0f); dpl2d_SetColor(m, 0.75f, 0.75f, 0.0f); // yellow: the live deflection line dpl2d_CallList(m, rangeCaretB); dpl2d_SetLineWidth(m, 1.0f); dpl2d_SetColor(m, 0.0f, 0.75f, 0.0f); // green: the bowtie carets dpl2d_OpenPolyline(m); dpl2d_AddPoint(m, cx, kRetBotY - kRetTickMajor); dpl2d_AddPoint(m, cx + kRetCaret, kRetBotY - kRetCaret - kRetTickMajor); dpl2d_AddPoint(m, cx - kRetCaret, kRetBotY - kRetCaret - kRetTickMajor); dpl2d_AddPoint(m, cx, kRetBotY - kRetTickMajor); dpl2d_ClosePolyline(m); dpl2d_OpenPolyline(m); dpl2d_AddPoint(m, cx, kRetBotY + kRetTickMajor); dpl2d_AddPoint(m, cx + kRetCaret, kRetBotY + kRetCaret + kRetTickMajor); dpl2d_AddPoint(m, cx - kRetCaret, kRetBotY + kRetCaret + kRetTickMajor); dpl2d_AddPoint(m, cx, kRetBotY + kRetTickMajor); dpl2d_ClosePolyline(m); dpl2d_PopState(m); } // COMPASS group (Execute @4ce6e0-4ce7e4 [T1]): [0x278] bottomAnchor holds a // rotate(CompassHeading rad->deg) + translate to (botX, botY - 3*tickMajor // - 0.03) -- the compass rose (circle + north stem, authored at the origin) // spins with the mech heading at the bottom-left of the twist tape. The // THREAT trail [0x2e8] draws in the same frame: 0.05-unit direction marks // from the compass centre toward recent damage sources (fresh = red, aging // = yellow, expired at 6s -- Execute @4ce3e2-4ce6ce [T1]). dpl2d_PushState(m); dpl2d_CallList(m, bottomAnchor); // [0x9e] the compass rotate+translate dpl2d_Circle(m, 0.0f, 0.0f, 0.03f, 0); // the rose ring [T3 radius] dpl2d_OpenLines(m); dpl2d_AddPoint(m, 0.0f, -0.04f); // the north stem dpl2d_AddPoint(m, 0.0f, -0.005f); dpl2d_CloseLines(m); dpl2d_CallList(m, subBA); // [0xba] the threat-direction trail dpl2d_PopState(m); dpl2d_CallList(m, subB6); // [0xb6] the composed weapon pips dpl2d_CallList(m, headingList); // [0x9d] the LOCK-RING SPIN matrix (4 deg/frame) dpl2d_CallList(m, subB7); // [0xb7] the lock-ring slot (subB9 when locked) dpl2d_End(m); dpl2d_Compile(m); // // The green centre-ring sub-lists ([0xb8]/[0xb9]) -- the lock indicator // rings the binary Execute swaps in on target state. // dpl2d_Begin(subB8, 1); dpl2d_SetColor(subB8, 0.0f, 0.75f, 0.0f); dpl2d_Circle(subB8, 0.0f, 0.0f, 0.12f, 0); dpl2d_End(subB8); dpl2d_Compile(subB8); dpl2d_Begin(subB9, 1); dpl2d_SetColor(subB9, 0.0f, 0.75f, 0.0f); dpl2d_Circle(subB9, 0.0f, 0.0f, 0.12f, 0); dpl2d_OpenLines(subB9); dpl2d_AddPoint(subB9, 0.14f, 0.0f); dpl2d_AddPoint(subB9, 0.10f, 0.0f); dpl2d_AddPoint(subB9, -0.14f, 0.0f); dpl2d_AddPoint(subB9, -0.10f, 0.0f); dpl2d_AddPoint(subB9, 0.0f, 0.14f); dpl2d_AddPoint(subB9, 0.0f, 0.10f); dpl2d_AddPoint(subB9, 0.0f, -0.14f); dpl2d_AddPoint(subB9, 0.0f, -0.10f); dpl2d_CloseLines(subB9); dpl2d_End(subB9); dpl2d_Compile(subB9); // // The off-screen turn ARROWS ([0x9f]/[0xa0]) -- big width-12 chevrons at // x = +-1.2..1.5 (screen edges), half-alpha; positioned/enabled by the // un-exported Execute -> built but not drawn statically. // dpl2d_Begin(leftArrow, 1); dpl2d_SetLineWidth(leftArrow, 12.0f); dpl2d_OpenLines(leftArrow); dpl2d_AddPoint(leftArrow, -1.2f, -0.30011f); dpl2d_AddPoint(leftArrow, -1.5f, 0.0f); dpl2d_AddPoint(leftArrow, -1.5f, 0.0f); dpl2d_AddPoint(leftArrow, -1.2f, 0.30011f); dpl2d_CloseLines(leftArrow); dpl2d_SetLineWidth(leftArrow, 1.0f); dpl2d_End(leftArrow); dpl2d_Compile(leftArrow); dpl2d_Begin(rightArrow, 1); dpl2d_SetLineWidth(rightArrow, 12.0f); dpl2d_OpenLines(rightArrow); dpl2d_AddPoint(rightArrow, 1.2f, 0.30011f); dpl2d_AddPoint(rightArrow, 1.5f, 0.0f); dpl2d_AddPoint(rightArrow, 1.5f, 0.0f); dpl2d_AddPoint(rightArrow, 1.2f, -0.30011f); dpl2d_CloseLines(rightArrow); dpl2d_SetLineWidth(rightArrow, 1.0f); dpl2d_End(rightArrow); dpl2d_Compile(rightArrow); // // The SIMPLE X [0x99] (ctor @4689-4705 [T1]): the minimal reticle used // when PrimaryHudOn is OFF -- a small green cross (arms +-0.02..0.08) // riding the same aim translate. Draw switches master <-> this on the // element-mask bit (the recovered Execute's state-list logic @4cdd9d). // dpl2d_Begin(simpleXList, 1); dpl2d_SetLineWidth(simpleXList, 1.0f); dpl2d_FullScreenClip(simpleXList); dpl2d_SetColor(simpleXList, 0.0f, 0.75f, 0.0f); dpl2d_CallList(simpleXList, aimDotList); // slews with the crosshair dpl2d_OpenLines(simpleXList); dpl2d_AddPoint(simpleXList, -0.08f, 0.0f); dpl2d_AddPoint(simpleXList, -0.02f, 0.0f); dpl2d_AddPoint(simpleXList, 0.02f, 0.0f); dpl2d_AddPoint(simpleXList, 0.08f, 0.0f); dpl2d_AddPoint(simpleXList, 0.0f, -0.08f); dpl2d_AddPoint(simpleXList, 0.0f, -0.02f); dpl2d_AddPoint(simpleXList, 0.0f, 0.02f); dpl2d_AddPoint(simpleXList, 0.0f, 0.08f); dpl2d_CloseLines(simpleXList); dpl2d_End(simpleXList); dpl2d_Compile(simpleXList); // empty placeholders (filled per frame / on lock) dpl2d_Begin(crossList, 1); dpl2d_End(crossList); dpl2d_Compile(crossList); dpl2d_Begin(aimDotList, 1); dpl2d_End(aimDotList); dpl2d_Compile(aimDotList); dpl2d_Begin(rangeCaretR, 1); dpl2d_End(rangeCaretR); dpl2d_Compile(rangeCaretR); dpl2d_Begin(rangeCaretB, 1); dpl2d_End(rangeCaretB); dpl2d_Compile(rangeCaretB); dpl2d_Begin(headingList, 1); dpl2d_End(headingList); dpl2d_Compile(headingList); dpl2d_Begin(bottomAnchor,1); dpl2d_End(bottomAnchor);dpl2d_Compile(bottomAnchor); dpl2d_Begin(subB6, 1); dpl2d_End(subB6); dpl2d_Compile(subB6); dpl2d_Begin(subB7, 1); dpl2d_End(subB7); dpl2d_Compile(subB7); dpl2d_Begin(subBA, 1); dpl2d_End(subBA); dpl2d_Compile(subBA); } BTReticleRenderable::~BTReticleRenderable() { if (gBTReticle == this) gBTReticle = 0; } // // Per-frame draw. Rebuild the live translate lists (the range caret slides // along its ladder with the target range -- the ctor's translate(0, // -scaleY * rangeFraction) at @4608-4613), then draw the master, then each // weapon's pip: the LIT pip (list A) while its within-range flag is up, the // dark ring (list B) otherwise. [T3 dynamics / T1 geometry] // void BTReticleRenderable::Draw(struct IDirect3DDevice9 *device) { // the range caret translate, from the live target range Scalar range = (rangeAttr2 != 0) ? *rangeAttr2 : 0.0f; if (range < minRange) range = minRange; if (range > maxRange) range = maxRange; Scalar frac = (range - minRange) / (maxRange - minRange); // [0x26c] = the range BAR (a line from the ladder TOP down to the caret // height) + the caret translate (Execute @4ceb16-4cebf8: AddPoint(originX, // originY-scaleY), AddPoint(originX, originY-scaleY*frac), then the // SetMatrix(translate(0, -scaleY*frac)) the caret triangles ride). dpl2d_Begin(rangeCaretR, 1); dpl2d_OpenLines(rangeCaretR); dpl2d_AddPoint(rangeCaretR, originX, originY - scaleY); dpl2d_AddPoint(rangeCaretR, originX, originY - scaleY * frac); dpl2d_CloseLines(rangeCaretR); { Scalar six[6] = { 1, 0, 0, 1, 0, -scaleY * frac }; dpl2d_ConcatMatrix(rangeCaretR, six); } dpl2d_End(rangeCaretR); dpl2d_Compile(rangeCaretR); // the AIM translate [0x9a] (Execute @4cde59-4cdedd [T1]: rebuilt on slew // move with SetMatrix(translate(reticlePosition))). { extern float gBTAimX, gBTAimY; Scalar t6[6] = { 1, 0, 0, 1, gBTAimX, gBTAimY }; dpl2d_Begin(aimDotList, 1); dpl2d_ConcatMatrix(aimDotList, t6); dpl2d_End(aimDotList); dpl2d_Compile(aimDotList); } // the TORSO-TWIST TAPE carets [0x9c] (Execute @4ce7e5-4cea9a [T1]): the // bottom 21-tick tape is the TWIST indicator -- a deflection line from the // tape centre plus the over/under carets translated by // offset = -/+(span/2) * (RotationOfTorsoHorizontal / twist limit) // (attrs 4/5/6: the live twist over the per-side limits; full deflection = // the torso hard against its stop). The fixed-torso BLH reads 0 (centred). { extern float gBTHudTwist, gBTHudTwistLimit; float off = 0.0f; if (gBTHudTwistLimit > 1e-4f) { off = -(kRetBotSpan * 0.5f) * (gBTHudTwist / gBTHudTwistLimit); if (off < -kRetBotSpan * 0.5f) off = -kRetBotSpan * 0.5f; if (off > kRetBotSpan * 0.5f) off = kRetBotSpan * 0.5f; } const float cx = kRetBotX + kRetBotSpan * 0.5f; dpl2d_Begin(rangeCaretB, 1); dpl2d_OpenLines(rangeCaretB); dpl2d_AddPoint(rangeCaretB, cx, kRetBotY); dpl2d_AddPoint(rangeCaretB, cx + off, kRetBotY); dpl2d_CloseLines(rangeCaretB); { Scalar t6[6] = { 1, 0, 0, 1, off, 0 }; dpl2d_ConcatMatrix(rangeCaretB, t6); } dpl2d_End(rangeCaretB); dpl2d_Compile(rangeCaretB); } // the COMPASS rotate [0x9e] (Execute @4ce6e0-4ce7e4 [T1]): the rose spins // by CompassHeading (radians; the binary converts x57.2958 for its degree // recorder) and sits at (botX, botY - 3*tickMajor - 0.03). { extern float gBTHudHeading; const float c = (float)cos((double)gBTHudHeading); const float s = (float)sin((double)gBTHudHeading); Scalar r6[6] = { c, s, -s, c, kRetBotX, kRetBotY - 3.0f * kRetTickMajor - 0.03f }; dpl2d_Begin(bottomAnchor, 1); dpl2d_ConcatMatrix(bottomAnchor, r6); dpl2d_End(bottomAnchor); dpl2d_Compile(bottomAnchor); } // the THREAT trail [0xba] (Execute @4ce3e2-4ce6ce [T1]): direction marks // from the compass centre toward recent damage sources. Each mark is a // 0.05-unit line along the (mech-local x,z) attack direction; FRESH marks // (< 2s) draw red, aging ones yellow, expired (> 6s) drop. { extern int BTTakeHudThreats(float out_xz[][2], float out_age[], int max_n); float txz[16][2]; float tage[16]; const int n = BTTakeHudThreats(txz, tage, 16); dpl2d_Begin(subBA, 1); if (n > 0) { // stale (yellow) first, then fresh (red) -- the binary's color split dpl2d_SetColor(subBA, 0.75f, 0.75f, 0.0f); for (int pass = 0; pass < 2; ++pass) { const int wantFresh = (pass == 1); if (pass == 1) dpl2d_SetColor(subBA, 0.75f, 0.0f, 0.0f); for (int i = 0; i < n; ++i) { const int isFresh = (tage[i] < 2.0f); if (isFresh != wantFresh) continue; dpl2d_OpenLines(subBA); dpl2d_AddPoint(subBA, 0.0f, 0.0f); dpl2d_AddPoint(subBA, txz[i][0] * 0.05f, txz[i][1] * 0.05f); dpl2d_CloseLines(subBA); } } dpl2d_SetColor(subBA, 0.0f, 0.75f, 0.0f); // restore green } dpl2d_End(subBA); dpl2d_Compile(subBA); } // the WEAPON PIPS [0xb6] (Execute @4ce2c2-4ce3e1 [T1]): the composed pip // list the master calls. Per weapon: skip unless its GROUP is displayed // (weaponMode & elementMask low bits); HIDE it when destroyed (attr 1 == // 1); the LIT pip (A) when the fire cycle is LOADED (attr 0x1c == 2, our // source: rechargeLevel >= 1), else the dark ring (B, charging). Range // plays NO part -- the binary never reads TargetWithinRange here. { extern int gBTHudGroupMask; // element-mask low bits (0xF = all) int dirty = 0; for (int i = 0; i < weaponCount; ++i) { const int destroyed = (simStateAttr[i] != 0 && *simStateAttr[i] == 1); const int loaded = (cycleReady[i] != 0 && *cycleReady[i] >= 0.999f); if (destroyed != simStateCache[i] || loaded != alarmCache[i]) { simStateCache[i] = destroyed; alarmCache[i] = loaded; dirty = 1; } } static int s_lastMask = -1; if (gBTHudGroupMask != s_lastMask) { s_lastMask = gBTHudGroupMask; dirty = 1; } if (dirty || !pipsBuilt) { pipsBuilt = 1; dpl2d_Begin(subB6, 1); for (int i = 0; i < weaponCount; ++i) { if ((weaponMode[i] & gBTHudGroupMask) == 0) continue; // group not displayed if (simStateCache[i]) continue; // destroyed: no pip at all dpl2d_CallList(subB6, alarmCache[i] ? pipDisplayListA[i] : pipDisplayListB[i]); } dpl2d_End(subB6); dpl2d_Compile(subB6); } } // the LOCK RING [0xb7] + its SPIN [0x9d] (Execute @4cebf9-4cee54 [T1]): // while a target is locked the ring+cross (subB9) draws at the reticle // frame centre, spinning 4 degrees per frame; unlocked it clears. (The // binary also hangs the PNAMEx.bgf player-name mesh on the 3D marker here // -- the 3D marker chain remains deferred.) { extern int gBTHudLockState; const int locked = (gBTHudLockState == 2); // the Lock attr rule, not just a target if (locked != lockShown) { lockShown = locked; dpl2d_Begin(subB7, 1); if (locked) dpl2d_CallList(subB7, subB9); dpl2d_End(subB7); dpl2d_Compile(subB7); } if (locked) { lockSpinDeg += 4.0f; // per FRAME, the binary's rate if (lockSpinDeg >= 360.0f) lockSpinDeg -= 360.0f; const float a = lockSpinDeg * 0.0174532925f; const float c = (float)cos((double)a), s = (float)sin((double)a); Scalar r6[6] = { c, s, -s, c, 0, 0 }; dpl2d_Begin(headingList, 1); dpl2d_ConcatMatrix(headingList, r6); dpl2d_End(headingList); dpl2d_Compile(headingList); } } // the TARGET HOTBOX / edge arrows [0xa1] (Execute @4cdf6f-4ce28b [T1]): // the box is a RECTANGLE hugging the target's projected extents -- x +-4 // around the hotbox point, +1 above / -11.5 below it (the authored pod // mech envelope) -- switching to the left/right edge arrow when both // edges pass +-1.6 or the target is behind. { extern int gBTHudLockState; extern float gBTHudLockWorld[3]; // the target's hotbox point (top) extern int BTProjectHotBox(const float top[3], float *xl, float *xr, float *yt, float *yb, int *side); dpl2d_Begin(crossList, 1); if (gBTHudLockState != 0) { float xl, xr, yt, yb; int side; if (BTProjectHotBox(gBTHudLockWorld, &xl, &xr, &yt, &yb, &side)) { dpl2d_OpenPolyline(crossList); // the closed hotbox rectangle dpl2d_AddPoint(crossList, xl, yt); dpl2d_AddPoint(crossList, xl, yb); dpl2d_AddPoint(crossList, xr, yb); dpl2d_AddPoint(crossList, xr, yt); dpl2d_ClosePolyline(crossList); } else { dpl2d_CallList(crossList, (side < 0) ? leftArrow : rightArrow); } } dpl2d_End(crossList); dpl2d_Compile(crossList); } // the STATE-list switch (Execute @4cdd9d [T1]): PrimaryHudOn (element mask // 0x20) selects the full HUD; off = just the simple aim cross. { extern int gBTHudPrimary; dpl2d_ExecuteList(gBTHudPrimary ? masterList : simpleXList, device); } } // // The render-loop hook: draw the player's reticle over the finished 3D frame, // COCKPIT VIEW ONLY (the 1996 build constructed it only for insideEntity). // void BTDrawReticle(struct IDirect3DDevice9 *device) { if (gBTReticle != 0 && gBTHudInside) gBTReticle->Draw(device); } // // Build the player's reticle + register one pip per weapon (the 1996 wiring: // @part_014.c:5386-5436). The binary's gate is IsDerivedFrom(0x511830) -- // MechWeapon::ClassDerivations [T1: the loop hard-aborts on missing // WeaponRange/PipPosition/... attrs, which only MechWeapons publish] -- so // EVERY mounted weapon registers a pip: lasers, PPCs AND missile launchers. // Per weapon it reads WeaponRange / PipPosition / TargetWithinRange / // PipExtendedRange / PipColor / SimulationState (attrs 1 + 0x1c) / RearFiring; // mode 1 = front (RearFiring==0), which every BLH weapon is. // BTReticleRenderable *BTBuildReticle(Entity *mech) { if (gBTReticle != 0) return gBTReticle; extern void BTSetHudTargetRange(Scalar range); // (self; range fed by mech4) extern Scalar gBTHudRangeStorage; // defined below gBTReticle = new BTReticleRenderable(mech, &gBTHudRangeStorage); Mech *m = (Mech *)mech; for (int wi = 0; wi < m->GetSubsystemCount(); ++wi) { Subsystem *ws = m->GetSubsystem(wi); if (ws == 0) continue; if (!ws->IsDerivedFrom(MechWeapon::ClassDerivations)) // 0x511830 continue; MechWeapon *wp = (MechWeapon *)ws; RGBColor pc = wp->PipColor(); float r = (float)pc.Red, g = (float)pc.Green, b = (float)pc.Blue; if (r < 0.0f || g < 0.0f || b < 0.0f) { r = 0.78f; g = 0.08f; b = 0.02f; } DEBUG_STREAM << "[hud] pip: classID=" << (int)ws->GetClassID() << " pos=" << wp->PipPosition() << " range=" << wp->WeaponRange() << " ext=" << wp->PipExtendedRange() << " rgb=(" << r << "," << g << "," << b << ")\n" << std::flush; gBTReticle->AddWeapon( wp->WeaponRange(), wp->PipPosition(), (int *)wp->WithinRangePtr(), wp->PipExtendedRange(), r, g, b, wp->RechargeLevelPtr(), // attr 0x1c analog: loaded when >= 1 2, 3, (int *)wp->SimulationStatePtr(), // attr 1: damage state (1 = destroyed) 1, 1 /* Front group (RearFiring==0 on every BLH weapon) */); } DEBUG_STREAM << "[hud] reticle built: " << gBTReticle->WeaponCount() << " weapon pip(s) registered\n" << std::flush; return gBTReticle; } // the live target-range storage the reticle's caret binds to Scalar gBTHudRangeStorage = 0.0f; // // THREAT trail store (recovered Execute @4ce3e2-4ce6ce): timestamped attack // directions pushed on player damage (mech.cpp handler); Draw ages them -- // fresh < 2s (red), expired > 6s (dropped). World-frame (x,z) directions: // they draw inside the compass's rotated frame, so the marks stay // world-referenced on the rose like a true compass bearing. // struct BTHudThreat { float x, z; clock_t born; }; static BTHudThreat gBTHudThreats[16]; static int gBTHudThreatCount = 0; void BTPushHudThreat(float wx, float wz) { float len = sqrtf(wx * wx + wz * wz); if (len < 1e-4f) return; if (gBTHudThreatCount >= 16) // drop the oldest { for (int i = 1; i < 16; ++i) gBTHudThreats[i - 1] = gBTHudThreats[i]; gBTHudThreatCount = 15; } BTHudThreat &t = gBTHudThreats[gBTHudThreatCount++]; t.x = wx / len; t.z = wz / len; t.born = clock(); } int BTTakeHudThreats(float out_xz[][2], float out_age[], int max_n) { const clock_t now = clock(); int n = 0; for (int i = 0; i < gBTHudThreatCount; ++i) { const float age = (float)(now - gBTHudThreats[i].born) / (float)CLOCKS_PER_SEC; if (age > 6.0f) continue; // expired gBTHudThreats[n] = gBTHudThreats[i]; // compact in place if (n < max_n) { out_xz[n][0] = gBTHudThreats[n].x; out_xz[n][1] = gBTHudThreats[n].z; out_age[n] = age; } ++n; } gBTHudThreatCount = n; return (n < max_n) ? n : max_n; } // //############################################################################# // BTReticleRenderable::AddWeapon //############################################################################# // // @004cdac0 // // Append one weapon range/pip marker to the reticle (max 10). Stores the // weapon's attribute pointers in parallel arrays indexed by weaponCount, then // pre-builds the two 2D display lists (the pip glyph + its extended-range arc) // at the screen position computed from the (clamped) weapon range. // void BTReticleRenderable::AddWeapon( Scalar weapon_range, // param_2 int pip_position, // param_3 int *within_range_value, // param_4 TargetWithinRange int extended_range, // param_5 PipExtendedRange Scalar pip_red, // param_6..8 PipColor Scalar pip_green, Scalar pip_blue, Scalar *cycle_ready, // param_9 (port: rechargeLevel) int const2, // param_10 (2 = loaded) int const3, // param_11 (3 = charging) int *sim_state_value, // param_12 weapon attr 1 int const1, // param_13 (1 = destroyed) int weapon_mode) // param_14 (group bit) { if (this->weaponCount /* [0x38] */ >= 10) { Fail("Tried to display too many weapons"); // @0051d24f, line 0x338 } int n = this->weaponCount; // // Record this weapon's control state in the parallel arrays -- the exact // store order of @004cdac0 (part_014.c:4827-4837). The caches hold the // pip's DERIVED display flags (loaded / destroyed) for the Execute-style // change detection in Draw. // this->stateConst3[n] /* [0x64+n*4] = param_11 */ = const3; this->stateConst2[n] /* [0x8c+n*4] = param_10 */ = const2; this->stateConst1[n] /* [0xb4+n*4] = param_13 */ = const1; this->cycleReady[n] /* [0x130+n*4] = param_9 */ = cycle_ready; this->alarmCache[n] /* [0xdc+n*4] */ = (*cycle_ready >= 0.999f); this->simStateAttr[n] /* [0x158+n*4] = param_12 */ = sim_state_value; this->simStateCache[n] /* [0x104+n*4] */ = (*sim_state_value == const1); this->withinRangePtr[n] /* [0x18c+n*4] = param_4 */ = within_range_value; this->withinRangeCache[n] /* [0x1b4+n*4] = *param_4 */ = *within_range_value; this->weaponMode[n] /* [0x3c+n*4] = param_14 */ = weapon_mode; dpl2d_DISPLAY *pip_list = dpl2d_NewDisplayList(); // FUN_00487f34 dpl2d_DISPLAY *arc_list = dpl2d_NewDisplayList(); this->pipDisplayListA[n] /* [0x2b0+n*4] */ = pip_list; this->pipDisplayListB[n] /* [0x288+n*4] */ = arc_list; // // Clamp range into [minRange .. maxRange]. // if (weapon_range >= this->minRange /* [0x230] */) { if (weapon_range > this->maxRange /* [0x22c] */) weapon_range = this->maxRange; } else { weapon_range = this->minRange; } // // Screen position of this pip from the reticle's calibrated geometry. // float x = this->originX /* [0x1fc] */ + this->biasX /* [0x208] */ + (float)pip_position * PIP_SPACING /* _DAT_004cdce8 */; float y = -this->scaleY /* [0x204] */ * ((weapon_range - this->minRange) / this->rangeScale /* [0x234] */) + this->originY /* [0x200] */; // // Pip glyph display list: a coloured ring (+ a small filled marker when // this is an extended-range / "rear" weapon). // dpl2d_Begin(pip_list, 1); // FUN_00487fbc dpl2d_SetColor(pip_list, pip_red, pip_green, pip_blue); // param_6,7,8 dpl2d_Circle(pip_list, x, y, 0.012f /* 0x3c449ba6 */, 1); dpl2d_SetColor(pip_list, 0, 0, 0); dpl2d_Circle(pip_list, x, y, 0.014f /* 0x3c656042 */, 0); if (extended_range != 0) // param_5 { dpl2d_SetColor(pip_list, 0.7f, 0.7f, 0.7f); // 0x3f333333 dpl2d_PushMatrix(pip_list); dpl2d_MoveTo(pip_list, x, y); dpl2d_PopMatrix(pip_list); } dpl2d_End(pip_list); dpl2d_Compile(pip_list); // // Extended-range arc display list (black outline ring). // dpl2d_Begin(arc_list, 1); dpl2d_SetColor(arc_list, 0, 0, 0); dpl2d_Circle(arc_list, x, y, 0.014f, 0); dpl2d_End(arc_list); dpl2d_Compile(arc_list); this->weaponCount = n + 1; } // //############################################################################# // SetupMaterialSubstitutionList //############################################################################# // // @004d0cc0 // // Read the "vehicletable" resource and build the per-mech material-name // substitution list, expanding the %color% / %badge% / %patch% / %serno% // placeholders. Directly parallels RPL4VideoRenderer::SetupMaterialSubstitution- // List (which handles %color%/%badge%); BT adds %patch% and a per-load // incrementing %serno% (serial number, "0".."9","A"...). // void BTL4VideoRenderer::SetupMaterialSubstitutionList(Entity *entity) { // // One-shot cache of the placeholder string lengths. // static int colorLen = -1, badgeLen, patchLen, sernoLen; // guards @0051d19c..d1b4 if (colorLen < 0) { colorLen = strlen(colorParameter); badgeLen = strlen(badgeParameter); patchLen = strlen(patchParameter); sernoLen = strlen(sernoParameter); } // // Fetch + lock the vehicle table resource, copy it out, and parse it as a // NotationFile. // ResourceDescription *res = application->GetResourceFile()->FindResourceDescription( // FUN_00406ff8 "vehicletable" /* @0051d941 */, ResourceDescription::VehicleTableResourceType); if (res == NULL) return; res->Lock(); long len = (long)res->resourceSize; // [0x40] char *copy = new char[len]; memcpy(copy, res->resourceAddress /* [0x3c] */, len); // FUN_004d4918 res->Unlock(); NotationFile *veh_tbl = new NotationFile(); // FUN_00403e84 veh_tbl->ReadText(copy, len); // FUN_00404d00 delete [] copy; // // Look up this mech's colour / badge / patch codes from the table, using // the egg-supplied names carried on the entity (badge=resourceNameA @0x844, // color=resourceNameB @0x848, patch=resourceNameC @0x84c). // // The recovered code read these directly off the BattleTech mech egg // (Mech::vehicleColor / vehicleBadge / vehiclePatch) and Fail()ed on a miss. // TODO(bring-up): the reconstructed Mech carries those names as ref-counted // creation-name objects (resourceNameA/B/C) whose backing string is the // transient MakeMessage buffer -- not safely readable here yet, and the // attribute-index path ("VehicleColor"/...) is not wired, so it returns // garbage that FindNote then deref-crashed on (btl4vid.cpp:808). Follow the // RP analogue (RPL4VID.cpp:1562) which simply tolerates a missing colour/ // badge: a NULL egg name or a table miss leaves veh_* == NULL and the // placeholder substitution below drops to default materials. Re-wire the // real egg names (a named Mech accessor) once the Mech layout is mapped. // const char *egg_color = NULL; // [0x848] resourceNameB (color) const char *egg_badge = NULL; // [0x844] resourceNameA (badge) const char *egg_patch = NULL; // [0x84c] resourceNameC (patch) const char *veh_color = NULL, *veh_badge = NULL, *veh_patch = NULL; if (egg_color && !veh_tbl->GetEntry("color", egg_color, &veh_color)) // @0051d94e { DEBUG_STREAM << " Color value '" << egg_color << "' from egg not found in vehicle table\n"; // @0051d954 veh_color = NULL; } if (egg_badge && !veh_tbl->GetEntry("badge", egg_badge, &veh_badge)) // @0051d9b8 { DEBUG_STREAM << " Badge value '" << egg_badge << "' from egg not found in vehicle table\n"; veh_badge = NULL; } if (egg_patch && !veh_tbl->GetEntry("patch", egg_patch, &veh_patch)) // @0051da22 { DEBUG_STREAM << " Patch value '" << egg_patch << "' from egg not found in vehicle table\n"; veh_patch = NULL; } // // Generic substitution list, then expand placeholders per entry. // materialSubstitutionList = veh_tbl->MakeEntryList("substitute"); // @0051da8c, DAT_004f1aac for (NameList::Entry *entry = materialSubstitutionList->GetFirstEntry(); entry != NULL; entry = entry->GetNextEntry()) { char buffer[80]; char *dst = buffer; const char *src = entry->GetChar(); *dst = '\0'; const char *pc; while ((pc = strchr(src, '%')) != NULL) // FUN_004d49f4 { int n = (int)(pc - src); const char *resume = src; if (n != 0) { memcpy(dst, src, n); dst += n; resume = pc; } if (!strncmp(pc, sernoParameter, sernoLen)) { // // %serno% -> the current one-character serial (gSerno, which // increments '0'->'9'->'A' each mech loaded). // if (gSerno /* @0051d1b5 */ != '\0') *dst++ = gSerno; src = resume + sernoLen; } else if (!strncmp(pc, colorParameter, colorLen)) { if (veh_color) { strcpy(dst, veh_color); dst += strlen(veh_color); } src = resume + colorLen; } else if (!strncmp(pc, badgeParameter, badgeLen)) { if (veh_badge) { strcpy(dst, veh_badge); dst += strlen(veh_badge); } src = resume + badgeLen; } else if (!strncmp(pc, patchParameter, patchLen)) { if (veh_patch) { strcpy(dst, veh_patch); dst += strlen(veh_patch); } src = resume + patchLen; } else { *dst++ = *resume; // stray '%' src = resume + 1; } } strcpy(dst, src); // tail // // Store the expanded copy back into the list entry. // char *result = new char[strlen(buffer) + 1]; strcpy(result, buffer); entry->dataReference = result; } delete veh_tbl; // // Advance the global serial number ('9' wraps to 'A') and install the // per-frame material-name substitution callback. // if (gSerno == '9') gSerno = 'A'; else gSerno = gSerno + 1; dpl_SetMaterialNameCallback(substituteMaterial); // FUN_0049664c(FUN_00459eb8) } // //############################################################################# // TearDownMaterialSubstitutionList //############################################################################# // // @004d11e8 // // Free the expanded substitution strings + the list, and clear the DPL // material-name callback. // void BTL4VideoRenderer::TearDownMaterialSubstitutionList() { if (materialSubstitutionList != NULL) { for (NameList::Entry *entry = materialSubstitutionList->GetFirstEntry(); entry != NULL; entry = entry->GetNextEntry()) { char *p = entry->GetChar(); if (p) { delete [] p; entry->dataReference = NULL; } } delete materialSubstitutionList; materialSubstitutionList = NULL; } // dpl_SetMaterialNameCallback(NULL); } //===========================================================================// // BTL4VideoRenderer ctor/dtor/TestInstance //---------------------------------------------------------------------------// // TODO(bring-up): the shipped BT ctor took the 1995 IG-board calibration tuple // (rate/complexity/priority/interest/depth) and drove the Division renderer. // WinTesla replaced that renderer with the D3D DPLRenderer, whose ctor now needs // (HWND, width, height, fullscreen, interest_type, depth). The old calibration // args have no D3D analogue, so for the first link we forward the interest/depth // and bind the renderer to the active window at the pod main-view size (800x600). // Real window/size wiring belongs in the BTL4Application video bring-up. //===========================================================================// BTL4VideoRenderer::BTL4VideoRenderer( RendererRate /*calibration_rate*/, RendererComplexity /*calibration_complexity*/, RendererPriority /*calibration_priority*/, InterestType interest_type, InterestDepth depth_calibration ) : DPLRenderer(::GetActiveWindow(), 800, 600, false, interest_type, depth_calibration) { Check_Pointer(this); mEyeCockpit = 0; mEyeChase = 0; mViewInside = 0; } // // // ApplyViewSkeleton -- select + load each body segment's displayed mesh for the // given view (inside = SkeletonType_A / outside = the mech's own skeletonType), // honoring the live damage graphic state, the inside-view '_cop' canopy // suppression, and the shadow (tshd) flagging. Shared by SetViewInside (the live // V-toggle) and RebuildMechRenderables (the respawn un-wreck) so the cockpit view // stays self-consistent ACROSS the death/respawn transition -- the respawn path // used to restore the full OUTSIDE torso around the cockpit eyepoint (no '_cop' // suppression, and viewSkeleton left stale), which enclosed the eye in opaque // geometry -> the "black viewport until you press V" bug. Records viewSkeleton so // a later rebuild restores the RIGHT skeleton. Returns the count of shown meshes. // int BTL4VideoRenderer::ApplyViewSkeleton(Entity *viewpoint, int inside) { std::map::iterator tree_it = mMechRenderTrees.find(viewpoint); if (tree_it == mMechRenderTrees.end()) return 0; MechRenderTree &render_tree = tree_it->second; render_tree.viewSkeleton = inside ? (int)EntitySegment::SkeletonType_A : render_tree.skeletonType; JointedMover *jm = (JointedMover *)viewpoint; EntitySegment::SegmentTableIterator it(jm->segmentTable); EntitySegment *segment; int shown = 0, hidden = 0; while ((segment = it.ReadAndNext()) != NULL) { if (segment->IsSiteSegment() != 0) continue; int slot = segment->GetIndex(); std::map::iterator r = render_tree.segRenderable.find(slot); if (r == render_tree.segRenderable.end() || r->second == NULL) continue; // Live graphic state from the zone (healed=Exists / damaged / gone) -- read // fresh so a just-healed respawn shows the intact mesh, not a stale cache. Enumeration gstate = 0; int zone_index = segment->GetPrimaryDamageZone(); if (zone_index >= 0 && zone_index < viewpoint->damageZoneCount && viewpoint->damageZones[zone_index] != 0) gstate = viewpoint->damageZones[zone_index]->GetGraphicState(); CString *nm = segment->GetVideoObjectName( (EntitySegment::SkeletonType)render_tree.viewSkeleton, gstate); // The cockpit canopy shell (*_cop -- the frame around the eyepoint) SHOWS by // default: with the authentic eye (baseOffset + parent-segment DCS + inverse // view) and the loader's single-sided dark-ramp treatment it renders as the // dark frame with the world through the openings (task #55, verified vs // gameplay footage). BT_HIDE_COCKPIT=1 hides it (diagnostic). if (inside && nm != NULL && strstr((const char *)*nm, "_cop") != NULL && getenv("BT_HIDE_COCKPIT")) nm = NULL; d3d_OBJECT *obj = NULL; if (nm != NULL) { char filename[44]; strcpy(filename, (const char *)*nm); int len = (int)strlen(filename); if (len >= 4) filename[len - 4] = '\0'; strcat(filename, ".bgf"); obj = d3d_OBJECT::LoadObject(GetDevice(), filename); if (obj != NULL && strstr(filename, "tshd") != NULL) { obj->SetIsShadow(1); for (int op = 0; op < obj->GetDrawOpCount(); ++op) obj->GetDrawOp(op)->alphaTest = true; } } r->second->SetDrawObj(obj); render_tree.segGState[slot] = (int)gstate; if (obj) ++shown; else ++hidden; } DEBUG_STREAM << "[view] skeleton " << (inside ? "A (inside)" : "N (outside)") << ": " << shown << " segment mesh(es) shown, " << hidden << " hidden\n" << std::flush; return shown; } // // The V-key view toggle: switch the live camera between the authentic cockpit // eyepoint and the port's external chase camera. A missing eye (e.g. the // cockpit eye on a mech with no siteeyepoint) leaves the current view. // void BTL4VideoRenderer::SetViewInside(int inside) { mViewInside = inside; // persists across renderable rebuilds if (inside && mEyeCockpit != 0) mCamera = mEyeCockpit; else if (!inside && mEyeChase != 0) mCamera = mEyeChase; // // Swap the player's DISPLAYED skeleton with the view: the INSIDE view uses // the inside-skeleton mesh set (SkeletonType_A -- most body segments have // no inside mesh, so the pilot isn't wrapped in his own torso textures; // the authentic pod view worked exactly this way), the chase view restores // the full outside set. Damage graphic states are respected per segment. // // While WRECKED the body is the sinking hulk, so skip the live mesh swap -- // but still RECORD the chosen skeleton so the respawn rebuild restores the // right one (RebuildMechRenderables re-applies these same rules on un-wreck). // Entity *viewpoint = (application != 0) ? application->GetViewpointEntity() : 0; std::map::iterator tree_it = mMechRenderTrees.find(viewpoint); if (tree_it != mMechRenderTrees.end()) { if (!tree_it->second.wrecked) ApplyViewSkeleton(viewpoint, inside); else tree_it->second.viewSkeleton = inside ? (int)EntitySegment::SkeletonType_A : tree_it->second.skeletonType; } DEBUG_STREAM << "[view] " << (inside ? "COCKPIT eyepoint" : "external chase") << (mCamera == mEyeCockpit ? " (cockpit live)" : " (chase live)") << "\n" << std::flush; // the HUD overlay draws in the cockpit view only { extern void BTSetHudInside(int inside); BTSetHudInside(mCamera == mEyeCockpit ? 1 : 0); } } // // Sim-side bridge (the mech4 keyboard poll drives it). // void BTSetViewInside(int inside) { if (application == NULL) return; BTL4VideoRenderer *renderer = (BTL4VideoRenderer *)application->GetVideoRenderer(); if (renderer != NULL) renderer->SetViewInside(inside); } BTL4VideoRenderer::~BTL4VideoRenderer() { } Logical BTL4VideoRenderer::TestInstance() const { return True; } //===========================================================================// // The "blue warp" translocation sphere (task #52) // // Reconstructed from the engine's POVTranslocateRenderable (L4VIDRND.cpp:1749): // a sphere (tsphere.bgf) that COLLAPSES onto the respawn point then EXPANDS to // reveal the reborn mech, rotating throughout. Loaded by FILENAME (not via the // RES table) -- which is why every resource-name search missed it. Drawn direct // from the render loop (BTDrawTranslocationSpheres, beside BTDrawBeams). // // SELF-CONTAINED ONE-SHOT: the engine keys this off the player's SimulationState // dial, but in our reconstruction that dial ALSO drives the camera/POV + // targeting, so pulsing it for the sphere regressed all of those (inside-view, // no-fire). Instead the respawn path (btplayer.cpp) calls BTStartWarpEffect at // the drop-zone origin and the effect plays its own collapse->expand -- touching // nothing but the render. (The BTTranslocationRenderable objects the entity // tree still builds for the replicant/POV wiring are now inert.) //===========================================================================// namespace { // The engine #defines VERBATIM (L4VIDRND.cpp:1763-1770): collapse from 100x down // to 1x over 1.3s, throb at 1x, then expand 1x -> 150x over 1.0s. TRANSLATE_LIMIT // is the WaitForReincarnate wobble amplitude. Only these five are live (the // ROTATE_* / TRANSLATE_RATE members are dead -- no geometry rotation). const float TLOC_COLLAPSE_TIME = 1.3f; // COLLAPSE_TIME const float TLOC_EXPAND_TIME = 1.0f; // EXPAND_TIME const float TLOC_TRANSLATE_LIMIT = 2.0f; // wait wobble amplitude float gWarpCollapseScale = 100.0f; // COLLAPSE_START_SCALE float gWarpExpandScale = 150.0f; // EXPAND_END_SCALE d3d_OBJECT *gTLocSphere = 0; int gTLocSphereTried = 0; // The single active warp one-shot (one local player per node). Faithful state // machine: 0 Idle, 1 InitialCollapse, 3 WaitForReincarnate (world masked, wobble), // 2 ExpandReveal (L4VIDRND.cpp POVTranslocateRenderable states). int gWarpPhase = 0; float gWarpT = 0.0f; // phase clock (collapse/expand) float gWaitClock = 0.0f; // WaitForReincarnate elapsed (wobble + stuck-black failsafe) float gWarpSpin = 0.0f; // accumulated throat-axis (Z) spin -- the SPIRAL (decomp FUN_00453dc4) float gWarpX = 0.0f, gWarpY = 0.0f, gWarpZ = 0.0f; int gWarpPOV = 0; // 1 = centre on the local eye (own death/respawn); 0 = world-anchored (peer) int gWarpMasked = 0; // 1 while we have raised the SetIsDead world mask } extern void BTSetWorldDead(int dead); // L4VIDRND.cpp bridge -> l4_application->SetIsDead // The renderable objects the entity tree builds for the translocation wiring are // inert now (the warp is the self-contained one-shot below); the ctor/dtor just // satisfy MakeEntityRenderables. BTTranslocationRenderable::BTTranslocationRenderable( Entity *entity, int, dpl_VIEW *, StateIndicator *effect_trigger, Point3D *drop_zone, int effect_control_state) : BTRenderableBase(entity), myWatchedEntity(entity), myTrigger(effect_trigger), myDropZone(drop_zone), myControlState((unsigned)effect_control_state), mySphereState(TLoc_Idle), myTimer(0.0f), myRotateY(0.0f), mySphereVisible(false) { } BTTranslocationRenderable::~BTTranslocationRenderable() { } static void BTWarpApplyScaleEnv() { if (const char *s = getenv("BT_WARP_SCALE")) { float v = (float)atof(s); if (v > 0.0f) { gWarpCollapseScale = v; gWarpExpandScale = v * 1.5f; } } } // // LOCAL DEATH -> InitialCollapse (engine: trigger becomes == control state, Idle -> // InitialCollapse, L4VIDRND.cpp:1900-1911). The sphere collapses 100x -> 1x onto // your own eye over 1.3s (world still visible), then raises the SetIsDead world mask // and THROBS (WaitForReincarnate) until the respawn kicks the expand. POV only. // void BTStartWarpCollapsePOV() { BTWarpApplyScaleEnv(); gWarpPhase = 1; // InitialCollapse gWarpT = 0.0f; gWaitClock = 0.0f; gWarpPOV = 1; gWarpX = gWarpY = gWarpZ = 0.0f; if (getenv("BT_TLOC_LOG")) DEBUG_STREAM << "[tloc] warp COLLAPSE (POV) start\n" << std::flush; } // // LOCAL RESPAWN -> ExpandReveal (engine: trigger becomes != control state, // WaitForReincarnate -> ExpandReveal + SetIsDead(false), L4VIDRND.cpp:1987-1989). // Drops the world mask and blasts the sphere 1x -> 150x, revealing the reborn world. // void BTStartWarpExpandPOV() { BTWarpApplyScaleEnv(); if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } // == SetIsDead(false) :1989 gWarpPhase = 2; // ExpandReveal gWarpT = 0.0f; gWarpPOV = 1; gWarpX = gWarpY = gWarpZ = 0.0f; if (getenv("BT_TLOC_LOG")) DEBUG_STREAM << "[tloc] warp EXPAND (POV) start\n" << std::flush; } // // World-anchored warp (a PORT EXTENSION -- the authentic effect is POV only): an // OBSERVER seeing a peer respawn over THERE. No world mask (the observer is alive); // expand-reveal at the peer's world point. // void BTStartWarpEffect(float x, float y, float z) { BTWarpApplyScaleEnv(); gWarpPhase = 2; // ExpandReveal gWarpT = 0.0f; gWarpPOV = 0; gWarpX = x; gWarpY = y; gWarpZ = z; if (getenv("BT_TLOC_LOG")) DEBUG_STREAM << "[tloc] warp EXPAND (world) at (" << x << "," << y << "," << z << ")\n" << std::flush; } // // FAILSAFE: drop the world mask + end the effect. MUST be called on every path // where a collapse fired but no respawn/expand can follow (mission end, out of // lives, dropped DropZoneReply) -- otherwise the SetIsDead world stays BLACK forever. // void BTWarpForceUnmask() { if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } if (gWarpPhase == 3) gWarpPhase = 0; } // // Play the warp one-shot: tsphere.bgf COLLAPSES onto the respawn point (scale // -> 1 over 1.3s) then EXPANDS to reveal the reborn mech (1 -> max over 1.0s), // rotating. Alpha pass (beside the beams) so it blends + Z-tests vs the world. // void BTDrawTranslocationSpheres(LPDIRECT3DDEVICE9 device, const D3DXMATRIX *view, float dt, Time frame_time) { // DIAG (off by default): BT_WARP_SELFTEST=1 forces a steady POV warp // (held in WaitForReincarnate, world masked) so the swirl can be frame-captured and // compared against the original (capture.png) in a solo game -- no death/respawn // needed. Remove once the visual is signed off. static int s_selftest = -1; if (s_selftest < 0) s_selftest = getenv("BT_WARP_SELFTEST") ? 1 : 0; if (s_selftest && gWarpPhase == 0) { gWarpPhase = 3; gWarpPOV = 1; gWaitClock = 0.0f; gWarpX = gWarpY = gWarpZ = 0.0f; if (!gWarpMasked) { BTSetWorldDead(1); gWarpMasked = 1; } } if (gWarpPhase == 0) return; if (gTLocSphere == 0 && !gTLocSphereTried) { gTLocSphereTried = 1; gTLocSphere = d3d_OBJECT::LoadObject(device, "tsphere.bgf"); if (gTLocSphere != 0) { for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op) { L4DRAWOP *dop = gTLocSphere->GetDrawOp(op); // THE SWIRL MOTION. The authentic material scrolls its texture // (tsphere_scr_tex SPECIAL "SCROLL 0.0 0.0 0.1 0.5"), but the port only // picks scroll up from a per-texture .met file (L4D3D.cpp:640), which // tsphere has none of, and the ramp-bake path leaves doScroll=false -- // so our swirl was FROZEN. Set the authored scroll rates here so // SetTextureScrolling animates it (u -0.1/s, v +0.5/s -> the churning // swirl that "spins around"), REPEAT wrap so the scroll tiles. dop->texture.doScroll = getenv("BT_WARP_NOSCROLL") ? false : true; // DIAG toggle dop->texture.scrollUDelta = 0.1f; dop->texture.scrollVDelta = 0.5f; dop->texture.wrap_u = L4TEXOP::REPEAT; dop->texture.wrap_v = L4TEXOP::REPEAT; // The pass routing (drawAsSky for POV / alphaTest for the peer overlay) // is set per-frame below, since it differs by mode. } } if (getenv("BT_TLOC_LOG")) DEBUG_STREAM << "[tloc] tsphere.bgf load " << (gTLocSphere ? "OK" : "FAILED") << " ops=" << (gTLocSphere ? gTLocSphere->GetDrawOpCount() : 0) << (gTLocSphere ? " center=(" : "") << (gTLocSphere ? gTLocSphere->mCullCenter.x : 0.0f) << "," << (gTLocSphere ? gTLocSphere->mCullCenter.y : 0.0f) << "," << (gTLocSphere ? gTLocSphere->mCullCenter.z : 0.0f) << ") r=" << (gTLocSphere ? gTLocSphere->GetRadius() : 0.0f) << "\n" << std::flush; } if (gTLocSphere == 0) { gWarpPhase = 0; return; } // ===== The POVTranslocateRenderable::Execute() state machine (L4VIDRND.cpp) ===== float scale = 1.0f; if (gWarpPhase == 1) // InitialCollapse (:1928): 101 -> 1 { gWarpT += dt; float left = 1.0f - (gWarpT / TLOC_COLLAPSE_TIME); if (left <= 0.0f) // collapse finished (:1935) { scale = 1.0f; gWarpPhase = 3; // -> WaitForReincarnate (:1946) gWaitClock = 0.0f; // rebaseline for the wobble (:1945) if (gWarpPOV) { BTSetWorldDead(1); gWarpMasked = 1; } // SetIsDead(true) (:1947) } else scale = left * gWarpCollapseScale + 1.0f; // (pct_left*100)+1 } else if (gWarpPhase == 3) // WaitForReincarnate (:1978): throb black { gWaitClock += dt; scale = 1.0f; // FAILSAFE: the respawn kicks Wait->Expand (BTStartWarpExpandPOV). If it never // arrives (mission end / out of lives / dropped DropZoneReply) the SetIsDead // world would stay BLACK forever -- so time out and un-mask after 12s. if (!s_selftest && gWaitClock > 12.0f) { if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } gWarpPhase = 0; return; } } else // ExpandReveal (:2013): 1 -> 151 { gWarpT += dt; float used = gWarpT / TLOC_EXPAND_TIME; if (used >= 1.0f) // reveal done (:2030) { if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } // safety (mask should already be off) gWarpPhase = 0; return; } scale = used * gWarpExpandScale + 1.0f; // (pct_used*150)+1 } if (getenv("BT_TLOC_LOG")) { static int s_lt = 0; if ((++s_lt % 15) == 1) DEBUG_STREAM << "[tloc] warp phase=" << gWarpPhase << " scale=" << scale << " pov=" << gWarpPOV << " masked=" << gWarpMasked << "\n" << std::flush; } // PLACEMENT (authentic POVTranslocateRenderable, L4VIDRND.cpp:1812 "rotated and // scaled around the VTV"): the sphere is PURE SCALE -- NO geometry spin. The // engine's myRotateY is dead code; the swirl is entirely the texture SCROLL. // - POV (your OWN respawn): centre the sphere ON your eye and orient it to the // view: world = Scale(s) * inverse(view). In view space that puts the sphere // at the origin, so you sit at its centre looking out through the swirl -- the // authentic tunnel. (World-fixing it at the mech's feet was the "blob from my // own perspective / not aligned to my orientation" the user reported.) // - Non-POV (observing a PEER respawn): anchor at the peer's world point so the // swirl plays over there -- pure scale, then translate. const float s = scale; // PLACEMENT -- the authentic VTV/eye parenting (L4VIDRND.cpp:2069-2074): the mesh // LOCAL ORIGIN is the eye, so world = localToWorld * inverse(view) with NO recenter. // tsphere is authored OFF-origin ON PURPOSE (centre +8.25y) so the eye sits ~0.38r // low INSIDE the sphere; recentring it to dead-centre (my earlier attempt) was WRONG // and only worsened the funnel. In WaitForReincarnate the localToWorld is a pure // Lissajous TRANSLATION (the throb, :1996-2003); otherwise a pure SCALE (no spin -- // myRotateY is dead; the swirl is the texture scroll). D3DXMATRIX local; if (gWarpPhase == 3) // WaitForReincarnate throb D3DXMatrixTranslation(&local, (float)(cos(gWaitClock * 3.33) * TLOC_TRANSLATE_LIMIT), (float)(sin(gWaitClock * 2.5) * TLOC_TRANSLATE_LIMIT), 0.0f); else D3DXMatrixScaling(&local, s, s, s); // THE SPIRAL (the piece I wrongly dropped): a continuous per-frame SPIN about the // throat axis (mesh local Z). The 1995 binary's translocate Execute (decomp // FUN_00453dc4) accumulates an angle and writes a Z-rotation into the sphere every // frame; the WinTesla port STUBBED it out and I followed the stub, calling // myRotateY "dead". Spin + the axial V texture-scroll = a HELIX = the smooth // spiralling vortex, and the rotation SWEEPS the coarse 12 facet edges so the mesh // stops reading as a faceted "sphincter". BT_WARP_SPIN = rad/s (default 4). static float s_spinRate = -1.0e9f; if (s_spinRate == -1.0e9f) { const char *sv = getenv("BT_WARP_SPIN"); s_spinRate = sv ? (float)atof(sv) : 4.0f; } gWarpSpin += s_spinRate * dt; if (gWarpSpin > 6.2831853f) gWarpSpin -= 6.2831853f; else if (gWarpSpin < 0.0f) gWarpSpin += 6.2831853f; D3DXMATRIX spin; D3DXMatrixRotationZ(&spin, gWarpSpin); // tsphere is a 12-facet BICONE tunnel; its long axis (the vortex throat) is the // mesh LOCAL Z, which Scale*inverse(view) already points down the view forward -- // so NO reorientation is needed (default tilt 0). BT_WARP_TILT (deg, pitch about // X) is kept only to NUDGE the off-axis convergence toward screen-centre if wanted // (the eye sits 8.25 below the throat axis, authentically, so it reads a touch // high). static float s_tiltDeg = -1.0e9f; if (s_tiltDeg == -1.0e9f) { const char *tv = getenv("BT_WARP_TILT"); s_tiltDeg = tv ? (float)atof(tv) : 0.0f; } D3DXMATRIX tilt; D3DXMatrixRotationX(&tilt, s_tiltDeg * (float)(3.14159265358979 / 180.0)); // EYE-ON-AXIS: bintA is smooth CLOUD noise (no rings of its own); the vortex rings // come from that cloud mapped in POLAR (U=angle around the Z throat, V=radius) -- // which only reads as CONCENTRIC rings when you look straight DOWN the throat axis. // The eye is authentically 8.25 below the axis, which skews the rings to diagonal // bands (the "sphincter"). BT_WARP_EYE_UP shifts the eye ONTO the axis (mesh local // +Y) so the rings go concentric; default 8.25 (the throat-axis offset), =0 for the // raw authentic off-axis view. static float s_eyeUp = -1.0e9f; if (s_eyeUp == -1.0e9f) { const char *ev = getenv("BT_WARP_EYE_UP"); s_eyeUp = ev ? (float)atof(ev) : 8.25f; } D3DXMATRIX eyeUp; D3DXMatrixTranslation(&eyeUp, 0.0f, -s_eyeUp, 0.0f); D3DXMATRIX world; if (gWarpPOV && view != 0) { D3DXMATRIX invView; D3DXMatrixInverse(&invView, 0, view); // throat-axis -> origin (eyeUp), SPIN about Z, scale/throb, tilt, eye->world. // eyeUp before spin so the rotation is about the THROAT, not the off-axis eye. world = eyeUp * spin * local * tilt * invView; } else { D3DXMATRIX anchorM; D3DXMatrixTranslation(&anchorM, gWarpX, gWarpY, gWarpZ); world = local * anchorM; // peer overlay: external view, keep upright } gTLocSphere->SetLocalToWorld(world); // The swirl COLOUR: the "sky" ramp already bakes the bintA cloud blue->white // (bgfload.cpp un-gate; L4D3D.cpp:480), and we MODULATE that by the material's // authored EMISSIVE {0.7,0.5,1} = 0xB380FF (the lavender-blue energy cast) which // the LIGHTING-off path would otherwise drop -- that lavender is the original's // signature colour. BT_WARP_COLOR=AARRGGBB overrides. static DWORD s_warpColor = 0; if (s_warpColor == 0) { const char *wc = getenv("BT_WARP_COLOR"); s_warpColor = wc ? (DWORD)strtoul(wc, 0, 16) : 0xFFB380FF; if (s_warpColor == 0) s_warpColor = 0xFFB380FF; } if (gWarpPOV) { // ===== POV: draw as SKY, exactly like the engine (isDeathDraw -> drawAsSky, // L4VIDRND.cpp:1802-1808). OPAQUE + backface-cull (CW) + z-test ON is what makes // an inside-viewed dome render CLEAN: no translucent double-blend, no coincident // double-winding z-fight (that WAS the "glitchy funnel"), and the huge expand // shell is occluded by the world coming back on = the reveal. The world itself // is blacked by the SetIsDead mask (raised at collapse-end), so during the throb // you see ONLY the swirling dome. Sky-pass states: L4VIDEO.cpp:7526 (cull CW), // 7568-7570 (zwrite on / blend off), 7693 (light off). Leave ZENABLE default. ===== for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op) { L4DRAWOP *dop = gTLocSphere->GetDrawOp(op); dop->drawAsSky = true; dop->alphaTest = false; dop->drawAsDecal = false; } DWORD sZW, sAB, sCull, sLight, sTF, sCOp, sCA1, sCA2, sAOp, sAA1; DWORD sMin, sMag, sMip, sAniso, sFog; device->GetRenderState(D3DRS_ZWRITEENABLE, &sZW); device->GetRenderState(D3DRS_ALPHABLENDENABLE, &sAB); device->GetRenderState(D3DRS_CULLMODE, &sCull); device->GetRenderState(D3DRS_LIGHTING, &sLight); device->GetRenderState(D3DRS_FOGENABLE, &sFog); device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTF); device->GetSamplerState(0, D3DSAMP_MINFILTER, &sMin); device->GetSamplerState(0, D3DSAMP_MAGFILTER, &sMag); device->GetSamplerState(0, D3DSAMP_MIPFILTER, &sMip); device->GetSamplerState(0, D3DSAMP_MAXANISOTROPY, &sAniso); device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp); device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1); device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2); device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp); device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1); device->SetRenderState(D3DRS_ZWRITEENABLE, TRUE); device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); // OPAQUE -> single write/pixel { // DIAG: BT_WARP_CULL = none|cw|ccw (default cw). The mesh is double-sided // (every tri emitted fwd+rev); if CW doesn't cleanly cull one winding the two // coincident surfaces z-fight into radial sparkle "spokes". static DWORD s_cull = 0xffffffff; if (s_cull == 0xffffffff) { const char *cv = getenv("BT_WARP_CULL"); s_cull = (cv && cv[0]=='n') ? D3DCULL_NONE : (cv && cv[1]=='c') ? D3DCULL_CCW : D3DCULL_CW; } device->SetRenderState(D3DRS_CULLMODE, s_cull); } device->SetRenderState(D3DRS_LIGHTING, FALSE); device->SetRenderState(D3DRS_FOGENABLE, FALSE); // tsphere_mtl is "IMMUNE 1" (fog-immune) -- world fog was washing the swirl device->SetRenderState(D3DRS_TEXTUREFACTOR, s_warpColor); // FILTERING: era-authentic ISOTROPIC linear+mip (default). My earlier // anisotropic "fix" was the SPOKE driver: on the grazing funnel wall the texel // footprint is stretched RADIALLY (down the throat/V), so anisotropy averages // ALONG that (smears radially) while keeping azimuthal sharpness -> the soft // concentric rings collapse into radial STREAKS/spokes. Plain isotropic linear // keeps the rings soft + concentric. BT_WARP_ANISO=N re-enables it (diag only). { static int s_aniso = -2; if (s_aniso == -2) { const char *a = getenv("BT_WARP_ANISO"); s_aniso = a ? atoi(a) : 0; } if (s_aniso > 0) { device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC); device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR); device->SetSamplerState(0, D3DSAMP_MAXANISOTROPY, (DWORD)s_aniso); } else // force isotropic linear (undo any anisotropy a prior draw left set) { device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); // Trilinear mips. (The grainy radial "spokes" were NOT a mip problem -- they were // the texture-scroll precision collapse, fixed in L4D3D::SetTextureScrolling via fmod. // BT_WARP_MIP=0 forces base-level-only (diag).) { static int s_mip=-1; if(s_mip<0){const char*mv=getenv("BT_WARP_MIP"); s_mip=(mv&&mv[0]=='0')?0:1;} device->SetSamplerState(0, D3DSAMP_MIPFILTER, s_mip?D3DTEXF_LINEAR:D3DTEXF_NONE); } } } // The BANDS are the per-VERTEX Gouraud colour (concentric shade-ramp contours // baked from the geometry in bgfload::finish); the bintA texture (lifted to a // gentle range) MODULATES churn within them. COLOROP = TEXTURE x DIFFUSE(bands). // BT_WARP_TEXMOD=0 shows the bands ALONE (diagnostic: is the geometry shade right). device->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1); // vertex colour = diffuse { static int s_texmod = -1; // Ramp is BAKED into the texture now -> default SELECTARG1(TEXTURE) (else branch). // BT_WARP_TEXMOD=1 re-enables the TEXTURExDIFFUSE modulate (diag: double-tints). if (s_texmod < 0) { const char *t = getenv("BT_WARP_TEXMOD"); s_texmod = (t && t[0] == '1') ? 1 : 0; } if (s_texmod) { device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // bintA cloud churn device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); // the band colour } else { device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // baked lavender ramp -- direct } } device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); gTLocSphere->Draw(PASS_SKY, view, frame_time); device->SetSamplerState(0, D3DSAMP_MINFILTER, sMin); device->SetSamplerState(0, D3DSAMP_MAGFILTER, sMag); device->SetSamplerState(0, D3DSAMP_MIPFILTER, sMip); device->SetSamplerState(0, D3DSAMP_MAXANISOTROPY, sAniso); device->SetRenderState(D3DRS_ZWRITEENABLE, sZW); device->SetRenderState(D3DRS_ALPHABLENDENABLE, sAB); device->SetRenderState(D3DRS_CULLMODE, sCull); device->SetRenderState(D3DRS_LIGHTING, sLight); device->SetRenderState(D3DRS_FOGENABLE, sFog); device->SetRenderState(D3DRS_TEXTUREFACTOR, sTF); device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp); device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1); device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2); device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp); device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1); } else { // ===== Peer overlay (port extension): the observer is ALIVE (no world mask), so // a sky-drawn sphere would be occluded by the world -> invisible. Draw it in the // alpha pass, Z-tested + translucent, so the swirl reads at the peer's spot. ===== for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op) { L4DRAWOP *dop = gTLocSphere->GetDrawOp(op); dop->alphaTest = true; dop->drawAsSky = false; dop->drawAsDecal = false; } const DWORD peerColor = (s_warpColor == 0xFFFFFFFF) ? 0xB0FFFFFF : s_warpColor; // ~70% alpha DWORD sSrc, sDst, sLight, sTF, sCOp, sCA1, sCA2, sAOp, sAA1; device->GetRenderState(D3DRS_SRCBLEND, &sSrc); device->GetRenderState(D3DRS_DESTBLEND, &sDst); device->GetRenderState(D3DRS_LIGHTING, &sLight); device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTF); device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp); device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1); device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2); device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp); device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1); device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); device->SetRenderState(D3DRS_LIGHTING, FALSE); device->SetRenderState(D3DRS_TEXTUREFACTOR, peerColor); device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR); device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time); device->SetRenderState(D3DRS_SRCBLEND, sSrc); device->SetRenderState(D3DRS_DESTBLEND, sDst); device->SetRenderState(D3DRS_LIGHTING, sLight); device->SetRenderState(D3DRS_TEXTUREFACTOR, sTF); device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp); device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1); device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2); device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp); device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1); } // DIAG (off by default): BT_WARP_SELFSHOT= dumps a backbuffer FRAME // SEQUENCE (_00.png ..) once the warp has settled, so the swirl can be turned // into a GIF / compared to capture.png WITHOUT bringing the window to the foreground. // Retained as the warp's visual-verification harness (needed for the open peer/collapse work). { const char *shotPath = getenv("BT_WARP_SELFSHOT"); if (shotPath && gWarpPhase != 0) { static int s_shotN = 0; ++s_shotN; // 40 frames, every 3rd, starting at frame 45 (~0.75s in) -> ~2s of the // spinning/churning swirl at 60fps. if (s_shotN >= 45 && ((s_shotN - 45) % 3 == 0)) { int idx = (s_shotN - 45) / 3; if (idx < 40) { char fn[600]; _snprintf(fn, sizeof fn, "%s_%02d.png", shotPath, idx); IDirect3DSurface9 *bb = 0; if (SUCCEEDED(device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &bb)) && bb) { D3DXSaveSurfaceToFileA(fn, D3DXIFF_PNG, bb, 0, 0); bb->Release(); } } } } } } //===========================================================================//