From 63c1c5a4603461618ffc7b726b7cf4ec7e2ad0f2 Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 9 Jul 2026 21:41:57 -0500 Subject: [PATCH] Implement the translocation sphere -- the "blue warp" respawn effect (task #52) BTTranslocationRenderable was a no-op stub; now reconstructed over the engine's POVTranslocateRenderable: loads tsphere.bgf and runs a collapse-on-arrival / expand-on-reveal sphere (scale 100->1 over 1.3s, then 1->150 over 1.0s, rotating) keyed on the player's SimulationState dial. Drawn direct from the render loop by BTDrawTranslocationSpheres (beside BTDrawBeams, PASS_ALPHABLEND) -- the same accommodation the beams/reticle use. The asset loads by FILENAME (tsphere.bgf), not through the RES table -- which is why every resource-name search missed the effect for three rounds. Trigger wiring: btl4vid.cpp MakeEntityRenderables builds the sphere for the LOCAL player (the authentic wiring builds it only for replicants + a POV fade for self, but peer player-attribute replication isn't wired -- on a replicant SimulationState/ DropZoneLocation read uninitialised). btplayer.cpp pulses SimulationState DropZoneAcquired->VehicleTranslocated at respawn (a 1.4s flip timer stands in for the engine's +1s drop-zone re-post) and writes the respawn origin into the DropZoneLocation attribute. Verified 2-node (BT_TLOC_LOG): on respawn the sphere collapses (~100->5) then expands (->150) at the valid drop-zone origin, deduped to <=2 active, respawn cycle un-regressed, no render errors. Visual appearance (colour/size) still needs a live look; the authentic see-others'-spheres path needs player replication. Co-Authored-By: Claude Opus 4.8 --- context/multiplayer.md | 22 +++- engine/MUNGA_L4/L4VIDEO.cpp | 9 ++ game/reconstructed/btl4vid.cpp | 200 ++++++++++++++++++++++++++++++++ game/reconstructed/btl4vid.hpp | 25 ++++ game/reconstructed/btplayer.cpp | 37 ++++++ game/reconstructed/btstubs.cpp | 5 +- 6 files changed, 290 insertions(+), 8 deletions(-) diff --git a/context/multiplayer.md b/context/multiplayer.md index b2ba466..a56b6ea 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -221,11 +221,23 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on 1)` (FUN_00458d2c, alloc 0x40, control state 1); our own POV instead gets `BTPOVStartEndRenderable` (the mission fade in/out). So peers watch each other's translocation sphere; your own death is a POV fade. - - **Why unseen:** `BTTranslocationRenderable` (declared btl4vid.hpp:320) is a NO-OP STUB in - `btstubs.cpp:365` (`: BTRenderableBase(entity) {}`). Porting it = reimplement over the engine's - `POVTranslocateRenderable` (its `d3d_OBJECT::LoadObject("tsphere.bgf")` + Execute state machine are - already in L4VIDRND.cpp; the DPL instance/DCS calls are commented — render via our d3d_OBJECT + - Scene renderable path, same infra as the beam/reticle renderables). TODO — task #52 whirlwind visual. + - **✅ DONE + LOG-VERIFIED (2026-07-09, task #52):** `BTTranslocationRenderable` was a no-op stub + in `btstubs.cpp`; now reconstructed in `btl4vid.cpp` (state machine + `d3d_OBJECT::LoadObject + ("tsphere.bgf")`), drawn direct from the render loop by `BTDrawTranslocationSpheres` (hooked in + L4VIDEO.cpp beside `BTDrawBeams`, PASS_ALPHABLEND) — the same direct-draw accommodation the beams/ + reticle use since our VideoComponent tree is partial. Trigger: the LOCAL player's own + `SimulationState` dial, pulsed `DropZoneAcquired(1)`→`VehicleTranslocated(2)` at respawn (btplayer.cpp + create branch + a 1.4 s flip timer in `PlayerSimulation`), with the respawn origin written into the + player's `DropZoneLocation` attribute (the sphere's draw position). Wiring (btl4vid.cpp + `MakeEntityRenderables`, BTPlayerClassID) originally built the sphere only for REPLICANT players + (third-party view) + a POV fade for self; extended to build it for the LOCAL player too, since peer + player-attribute replication (SimulationState/DropZoneLocation) isn't wired (on a replicant both read + 0xCD… uninitialised — confirmed via `BT_TLOC_LOG`). Verified 2-node: on B's respawn the sphere + COLLAPSES (scale ~100→5) then EXPANDS (→150) at the valid drop-zone origin, deduped to ≤2 active, + respawn cycle un-regressed, no render errors. Env gate `BT_TLOC_LOG`. REMAINING (follow-up): the + visual appearance (colour/translucency/size on screen) is unverified by logs — needs a live look; + and the AUTHENTIC peer path (see OTHERS' spheres) needs player SimulationState/DropZoneLocation + replication — until then the local-player sphere stands in. NOTE the respawn-cycle SIM path is separate + done: `DropZoneReply` (FUN_004bffd0) → `Mech::Reset` (@0049fb74) + the level-2 "translocated" cockpit alarm; `CreatePlayerVehicle` (FUN_004bfcac, disassembled — make+link only, matches RP analog). The sphere is a pure RENDER-side watcher on the diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index f485fe7..f06593f 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -7788,6 +7788,15 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte BTDrawPfx(mDevice, &viewTransform, (float)dT); } + // BT translocation spheres (task #52): the "blue warp" that collapses onto a + // dying mech and expands to reveal the reborn one -- one per watched player, + // keyed on its SimulationState dial. Same alpha pass as the beams/pfx. + { + extern void BTDrawTranslocationSpheres(LPDIRECT3DDEVICE9 dev, + const D3DXMATRIX *view, float dt, Time frame_time); + BTDrawTranslocationSpheres(mDevice, &viewTransform, (float)dT, mTargetRenderTime); + } + // The BT targeting reticle / weapon pips (2D screen space, cockpit view // only -- the dpl2d layer; see game/reconstructed/dpl2d.cpp). { diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index 3306f2b..4ff5262 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -197,6 +197,24 @@ void dplMainZone, dplDeathZone, sim_state, fogRed, fogGreen, fogBlue, fogNear, fogFar, 3 /* MissionStartingState */, 4 /* MissionEndingState */); + + // + // BRING-UP (task #52): ALSO give the LOCAL player the translocation + // SPHERE (the authentic wiring shows it only on OTHERS -- replicant + // branch above -- because peer player-attribute replication of + // SimulationState/DropZoneLocation isn't wired yet; both read + // uninitialised on a replicant). Driven by the local player's OWN + // SimulationState dial (pulsed to DropZoneAcquired->Translocated at + // respawn, btplayer.cpp) at its own DropZoneLocation -- so you see + // the warp collapse+expand around your reinsertion. Same control + // state (1 = DropZoneAcquired). Remove once player replication lands. + // + Point3D *local_drop = + (Point3D *)entity->GetAttributePointer("DropZoneLocation"); + if (local_drop) + new BTTranslocationRenderable( + entity, VideoRenderable::Watcher, GetMainView(), + sim_state, local_drop, 1); } break; } @@ -2088,3 +2106,185 @@ Logical } //===========================================================================// +// BTTranslocationRenderable -- the "blue warp" translocation sphere (task #52) +// +// Reconstructed from the engine's POVTranslocateRenderable (L4VIDRND.cpp:1749): +// a sphere (tsphere.bgf) that COLLAPSES onto the mech when the watched player +// enters the control state (DropZoneAcquired), holds while dead, then EXPANDS +// to reveal the reborn mech when the state leaves it (VehicleTranslocated). +// Rotates 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, called beside BTDrawBeams), same accommodation +// the weapon beams + reticle use since our VideoComponent tree is partial. +//===========================================================================// +namespace { + // Timings + scales are the engine's #defines (L4VIDRND.cpp:1765-1772). + const float TLOC_COLLAPSE_TIME = 1.3f; + const float TLOC_COLLAPSE_START_SCALE = 100.0f; + const float TLOC_EXPAND_TIME = 1.0f; + const float TLOC_EXPAND_END_SCALE = 150.0f; + const float TLOC_ROTATE_RATE = 0.5f * 0.01745329222222f; // rad/frame + + BTTranslocationRenderable *gTLocFx[64]; + int gTLocFxCount = 0; + d3d_OBJECT *gTLocSphere = 0; + int gTLocSphereTried = 0; +} + +BTTranslocationRenderable::BTTranslocationRenderable( + Entity *entity, int /*execution_type*/, dpl_VIEW * /*this_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) +{ + // A player's render tree is rebuilt on respawn, so drop any prior (now + // stale) sphere for the SAME entity from the active walk -- only the newest + // steps + draws (the old renderable objects are owned/freed by their tree). + for (int i = 0; i < gTLocFxCount; ++i) + if (gTLocFx[i] != 0 && gTLocFx[i]->myWatchedEntity == entity) + { + gTLocFx[i] = gTLocFx[--gTLocFxCount]; + --i; + } + if (gTLocFxCount < 64) + gTLocFx[gTLocFxCount++] = this; + if (getenv("BT_TLOC_LOG")) + DEBUG_STREAM << "[tloc] renderable created for entity " << entity->GetEntityID() + << " control=" << myControlState << " (active=" << gTLocFxCount << ")\n" << std::flush; +} + +BTTranslocationRenderable::~BTTranslocationRenderable() +{ + for (int i = 0; i < gTLocFxCount; ++i) + if (gTLocFx[i] == this) + { + gTLocFx[i] = gTLocFx[--gTLocFxCount]; + break; + } +} + +// +// Step every active translocation sphere's state machine and draw the visible +// ones. Called from the render loop (L4VIDEO.cpp) with the frame's view matrix +// + dt, so the sphere occludes against the depth already laid down by the world. +// +void + BTDrawTranslocationSpheres(LPDIRECT3DDEVICE9 device, const D3DXMATRIX *view, + float dt, Time frame_time) +{ + if (gTLocFxCount == 0) + return; + + if (gTLocSphere == 0 && !gTLocSphereTried) + { + gTLocSphereTried = 1; + gTLocSphere = d3d_OBJECT::LoadObject(device, "tsphere.bgf"); + if (getenv("BT_TLOC_LOG")) + DEBUG_STREAM << "[tloc] tsphere.bgf load " + << (gTLocSphere ? "OK" : "FAILED") << "\n" << std::flush; + } + if (gTLocSphere == 0) + return; + + const int log_on = getenv("BT_TLOC_LOG") ? 1 : 0; + static int s_logTick = 0; + const int log_now = log_on && ((++s_logTick % 30) == 1); + + for (int i = 0; i < gTLocFxCount; ++i) + { + BTTranslocationRenderable *fx = gTLocFx[i]; + if (fx == 0 || fx->myTrigger == 0 || fx->myDropZone == 0) + continue; + + const unsigned st = fx->myTrigger->GetState(); + float scale = 1.0f; + + switch (fx->mySphereState) + { + case BTTranslocationRenderable::TLoc_Idle: + if (st == fx->myControlState) + { + fx->mySphereState = BTTranslocationRenderable::TLoc_Collapse; + fx->myTimer = 0.0f; + fx->mySphereVisible = true; + } + break; + + case BTTranslocationRenderable::TLoc_Collapse: + { + fx->myTimer += dt; + float left = 1.0f - (fx->myTimer / TLOC_COLLAPSE_TIME); // 1 -> 0 + if (left <= 0.0f) + { + scale = 1.0f; + fx->mySphereState = BTTranslocationRenderable::TLoc_Wait; + fx->myTimer = 0.0f; + } + else + scale = left * TLOC_COLLAPSE_START_SCALE + 1.0f; // 101 -> 1 + break; + } + + case BTTranslocationRenderable::TLoc_Wait: + scale = 1.0f; + if (st != fx->myControlState) // respawned/translocated + { + fx->mySphereState = BTTranslocationRenderable::TLoc_Expand; + fx->myTimer = 0.0f; + } + break; + + case BTTranslocationRenderable::TLoc_Expand: + { + fx->myTimer += dt; + float used = fx->myTimer / TLOC_EXPAND_TIME; // 0 -> 1 + if (used >= 1.0f) + { + scale = 1.0f; + fx->mySphereState = BTTranslocationRenderable::TLoc_Idle; + fx->mySphereVisible = false; + } + else + scale = used * TLOC_EXPAND_END_SCALE + 1.0f; // 1 -> 151 + break; + } + } + + if (log_now) + DEBUG_STREAM << "[tloc] fx=" << i << " trigger=" << st + << " ctrl=" << fx->myControlState << " state=" << fx->mySphereState + << " scale=" << scale << " vis=" << (int)fx->mySphereVisible + << " at=(" << fx->myDropZone->x << "," << fx->myDropZone->y + << "," << fx->myDropZone->z << ")\n" << std::flush; + + if (!fx->mySphereVisible) + continue; + + fx->myRotateY += TLOC_ROTATE_RATE; + + // world = scale * rotateY(myRotateY), translated to the drop zone. + const float s = scale; + const float c = cosf(fx->myRotateY); + const float sn = sinf(fx->myRotateY); + D3DXMATRIX m; + m._11 = s * c; m._12 = 0.0f; m._13 = -s * sn; m._14 = 0.0f; + m._21 = 0.0f; m._22 = s; m._23 = 0.0f; m._24 = 0.0f; + m._31 = s * sn; m._32 = 0.0f; m._33 = s * c; m._34 = 0.0f; + m._41 = (float)fx->myDropZone->x; + m._42 = (float)fx->myDropZone->y; + m._43 = (float)fx->myDropZone->z; + m._44 = 1.0f; + + gTLocSphere->SetLocalToWorld(m); + gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time); + } +} + +//===========================================================================// diff --git a/game/reconstructed/btl4vid.hpp b/game/reconstructed/btl4vid.hpp index bde45a6..d161894 100644 --- a/game/reconstructed/btl4vid.hpp +++ b/game/reconstructed/btl4vid.hpp @@ -317,6 +317,16 @@ class BTMarkerWatcherRenderable: // // Drop-zone translocation effect renderable (FUN_00458d2c, alloc 0x40). // +// THE "BLUE WARP" (task #52): the engine analog is POVTranslocateRenderable +// (L4VIDRND.cpp:1749), which loads tsphere.bgf and runs a collapse-on-death / +// expand-on-respawn sphere keyed on the entity's SimulationState dial vs a +// control state. This was a no-op stub; now reconstructed. The sphere +// COLLAPSES (scale 100->1 over 1.3s) when the watched player enters the +// control state (DropZoneAcquired), holds, then EXPANDS (1->150 over 1.0s) +// when it leaves (VehicleTranslocated) -- revealing the reborn mech. Rotates +// throughout. Drawn (direct-draw, like the weapon beams) by +// BTDrawTranslocationSpheres from the render loop. +// class BTTranslocationRenderable: public BTRenderableBase { @@ -328,6 +338,21 @@ class BTTranslocationRenderable: StateIndicator *effect_trigger, Point3D *drop_zone, int effect_control_state); + ~BTTranslocationRenderable(); + + // Sphere state machine (public so the file-static render walk in + // btl4vid.cpp can step + draw each active effect). Mirrors + // POVTranslocateRenderable's IdleState/InitialCollapse/WaitForReincarnate/ + // ExpandReveal. + enum { TLoc_Idle = 0, TLoc_Collapse, TLoc_Wait, TLoc_Expand }; + Entity *myWatchedEntity; // the player this sphere belongs to (dedupe key) + StateIndicator *myTrigger; // the SimulationState dial we watch + Point3D *myDropZone; // world position to render the sphere at + unsigned myControlState; // state value that starts the collapse + int mySphereState; // TLoc_* + float myTimer; // seconds elapsed in the current timed phase + float myRotateY; // accumulated spin + bool mySphereVisible; }; // diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index f18ddc1..148e3b6 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -204,6 +204,11 @@ static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0, static const char *SelfDestructName = "self destruct"; // &DAT_00524b38 static const Scalar RespawnDelay = 5.0f; // death -> drop-zone hunt (@004c0830) + +// WARP (task #52): countdown from the respawn's DropZoneAcquired pulse to the +// VehicleTranslocated flip that makes the translocation sphere EXPAND (reveal). +// One local player per node, so a file-static holds it. <0 = idle. +static Scalar sWarpExpandTimer = -1.0f; static const Scalar TicksPerSecond = 1.0f; // (see note in PlayerSimulation) // @@ -754,6 +759,19 @@ void Player::PlayerSimulation(time_slice); // FUN_0042e168 + // WARP (task #52): once the collapse has had time to play, flip the + // SimulationState dial off DropZoneAcquired so the translocation sphere + // leaves its "wait" phase and EXPANDS (reveals the reborn mech). + if (sWarpExpandTimer > 0.0f) + { + sWarpExpandTimer -= time_slice; + if (sWarpExpandTimer <= 0.0f) + { + sWarpExpandTimer = -1.0f; + SetSimulationState(VehicleTranslocatedState); // state 2 -> expand + } + } + if ( (lastPerformance - lastConsoleUpdate) / TicksPerSecond >= CONSOLE_UPDATE_INTERVAL // _DAT_004c08fc || application->GetApplicationState() == Application::EndingMission // app+0x88 == 6 @@ -1047,6 +1065,25 @@ void AlwaysExecute(); // param_1[10] &= ~2 (run every frame) deathCount = 0; // param_1[0x80] + + // + // WARP (task #52): fire the translocation sphere at our reinsertion. + // Publish the drop-zone position into our DropZoneLocation attribute (the + // sphere renderable draws there) and pulse the SimulationState dial to + // DropZoneAcquired -- state 1, the sphere's COLLAPSE trigger. + // PlayerSimulation flips it to VehicleTranslocated ~1.4 s later, which the + // sphere reads as "reincarnate" and EXPANDS to reveal the reborn mech. + // (Authentic trigger = the player's own SimulationState + DropZoneLocation + // attribute; the flip-timer stands in for the engine's +1 s drop-zone + // re-post choreography, which our sever-and-recreate respawn skips.) + // + { + Point3D *drop_attr = (Point3D *)GetAttributePointer("DropZoneLocation"); + if (drop_attr != 0) + *drop_attr = message->dropZoneLocation.linearPosition; + } + SetSimulationState(DropZoneAcquiredState); // state 1 -> collapse + sWarpExpandTimer = 1.4f; } else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80] { diff --git a/game/reconstructed/btstubs.cpp b/game/reconstructed/btstubs.cpp index 4565d5c..c19458a 100644 --- a/game/reconstructed/btstubs.cpp +++ b/game/reconstructed/btstubs.cpp @@ -362,9 +362,8 @@ BTMarkerWatcherRenderable::BTMarkerWatcherRenderable( Entity *entity, int, dpl_VIEW *, dpl_DCS *) : BTRenderableBase(entity) {} -BTTranslocationRenderable::BTTranslocationRenderable( - Entity *entity, int, dpl_VIEW *, StateIndicator *, Point3D *, int) - : BTRenderableBase(entity) {} +// BTTranslocationRenderable (the "blue warp" translocation sphere) is now a real +// reconstruction in btl4vid.cpp (task #52) -- no longer a stub here. BTPOVStartEndRenderable::BTPOVStartEndRenderable( Entity *entity, int, dpl_VIEW *, dpl_ZONE *, dpl_ZONE *, StateIndicator *,