Warp: decouple from player SimulationState (fix camera/targeting regressions)

Pulsing the player's SimulationState (DropZoneAcquired->Translocated) to trigger
the translocation sphere regressed everything else that dial drives in our
reconstruction -- the camera flipped to inside-view, targeting/firing gated off,
shadow pass glitched. That dial is load-bearing; co-opting it was wrong.

Replace with a self-contained render one-shot: btplayer.cpp respawn calls
BTStartWarpEffect(dropZoneOrigin); the effect plays its own collapse->expand and
touches nothing but the render. No SetSimulationState, no DropZoneLocation write,
no per-entity renderable walk (the tree's BTTranslocationRenderable objects are
now inert). Scale capped 30/40 (authentic 100/150) so the sphere -- centred on
your own reinsertion -- doesn't envelop the camera; tunable via BT_WARP_SCALE.

Smoke-verified 2-node: warp fires on each respawn (collapse 31->5, expand ->35),
repeats, respawn cycle intact, no crash. Camera/targeting restored (state pulse
gone).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 21:58:38 -05:00
co-authored by Claude Opus 4.8
parent 63c1c5a460
commit f0535356bc
2 changed files with 105 additions and 184 deletions
+78 -133
View File
@@ -197,24 +197,6 @@ void
dplMainZone, dplDeathZone, sim_state, dplMainZone, dplDeathZone, sim_state,
fogRed, fogGreen, fogBlue, fogNear, fogFar, fogRed, fogGreen, fogBlue, fogNear, fogFar,
3 /* MissionStartingState */, 4 /* MissionEndingState */); 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; break;
} }
@@ -2106,33 +2088,48 @@ Logical
} }
//===========================================================================// //===========================================================================//
// BTTranslocationRenderable -- the "blue warp" translocation sphere (task #52) // The "blue warp" translocation sphere (task #52)
// //
// Reconstructed from the engine's POVTranslocateRenderable (L4VIDRND.cpp:1749): // Reconstructed from the engine's POVTranslocateRenderable (L4VIDRND.cpp:1749):
// a sphere (tsphere.bgf) that COLLAPSES onto the mech when the watched player // a sphere (tsphere.bgf) that COLLAPSES onto the respawn point then EXPANDS to
// enters the control state (DropZoneAcquired), holds while dead, then EXPANDS // reveal the reborn mech, rotating throughout. Loaded by FILENAME (not via the
// to reveal the reborn mech when the state leaves it (VehicleTranslocated). // RES table) -- which is why every resource-name search missed it. Drawn direct
// Rotates throughout. Loaded by FILENAME (not via the RES table) -- which is // from the render loop (BTDrawTranslocationSpheres, beside BTDrawBeams).
// why every resource-name search missed it. Drawn direct from the render loop //
// (BTDrawTranslocationSpheres, called beside BTDrawBeams), same accommodation // SELF-CONTAINED ONE-SHOT: the engine keys this off the player's SimulationState
// the weapon beams + reticle use since our VideoComponent tree is partial. // 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 { namespace {
// Timings + scales are the engine's #defines (L4VIDRND.cpp:1765-1772). // Timings are the engine's #defines (L4VIDRND.cpp:1765-1772). The authentic
// scales are 100 (collapse start) / 150 (expand end); capped smaller here so
// the sphere -- which is centred on YOUR own reinsertion point -- does not
// envelop the camera and black out the view. Tunable via BT_WARP_SCALE.
const float TLOC_COLLAPSE_TIME = 1.3f; 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_TIME = 1.0f;
const float TLOC_EXPAND_END_SCALE = 150.0f;
const float TLOC_ROTATE_RATE = 0.5f * 0.01745329222222f; // rad/frame const float TLOC_ROTATE_RATE = 0.5f * 0.01745329222222f; // rad/frame
float gWarpCollapseScale = 30.0f; // authentic 100
float gWarpExpandScale = 40.0f; // authentic 150
BTTranslocationRenderable *gTLocFx[64];
int gTLocFxCount = 0;
d3d_OBJECT *gTLocSphere = 0; d3d_OBJECT *gTLocSphere = 0;
int gTLocSphereTried = 0; int gTLocSphereTried = 0;
// The single active warp one-shot (one local player per node).
int gWarpPhase = 0; // 0 idle, 1 collapse, 2 expand
float gWarpT = 0.0f;
float gWarpX = 0.0f, gWarpY = 0.0f, gWarpZ = 0.0f;
float gWarpRot = 0.0f;
} }
// 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( BTTranslocationRenderable::BTTranslocationRenderable(
Entity *entity, int /*execution_type*/, dpl_VIEW * /*this_view*/, Entity *entity, int, dpl_VIEW *,
StateIndicator *effect_trigger, Point3D *drop_zone, int effect_control_state) StateIndicator *effect_trigger, Point3D *drop_zone, int effect_control_state)
: BTRenderableBase(entity), : BTRenderableBase(entity),
myWatchedEntity(entity), myWatchedEntity(entity),
@@ -2144,42 +2141,43 @@ BTTranslocationRenderable::BTTranslocationRenderable(
myRotateY(0.0f), myRotateY(0.0f),
mySphereVisible(false) 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() 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 // Kick the warp at a world position (called from the respawn path). Does NOT
// ones. Called from the render loop (L4VIDEO.cpp) with the frame's view matrix // touch player state -- pure render.
// + dt, so the sphere occludes against the depth already laid down by the world. //
void
BTStartWarpEffect(float x, float y, float z)
{
if (const char *s = getenv("BT_WARP_SCALE"))
{
float v = (float)atof(s);
if (v > 0.0f) { gWarpCollapseScale = v; gWarpExpandScale = v * 1.33f; }
}
gWarpPhase = 1; // collapse
gWarpT = 0.0f;
gWarpX = x; gWarpY = y; gWarpZ = z;
gWarpRot = 0.0f;
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] warp start at (" << x << "," << y << "," << z
<< ") scale=" << gWarpCollapseScale << "/" << gWarpExpandScale << "\n" << std::flush;
}
//
// 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 void
BTDrawTranslocationSpheres(LPDIRECT3DDEVICE9 device, const D3DXMATRIX *view, BTDrawTranslocationSpheres(LPDIRECT3DDEVICE9 device, const D3DXMATRIX *view,
float dt, Time frame_time) float dt, Time frame_time)
{ {
if (gTLocFxCount == 0) if (gWarpPhase == 0)
return; return;
if (gTLocSphere == 0 && !gTLocSphereTried) if (gTLocSphere == 0 && !gTLocSphereTried)
@@ -2191,100 +2189,47 @@ void
<< (gTLocSphere ? "OK" : "FAILED") << "\n" << std::flush; << (gTLocSphere ? "OK" : "FAILED") << "\n" << std::flush;
} }
if (gTLocSphere == 0) if (gTLocSphere == 0)
{
gWarpPhase = 0;
return; 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: gWarpT += dt;
scale = 1.0f; gWarpRot += TLOC_ROTATE_RATE;
if (st != fx->myControlState) // respawned/translocated
float scale;
if (gWarpPhase == 1) // collapse: (max+1) -> 1
{ {
fx->mySphereState = BTTranslocationRenderable::TLoc_Expand; float left = 1.0f - (gWarpT / TLOC_COLLAPSE_TIME);
fx->myTimer = 0.0f; if (left <= 0.0f) { gWarpPhase = 2; gWarpT = 0.0f; scale = 1.0f; }
else scale = left * gWarpCollapseScale + 1.0f;
} }
break; else // expand: 1 -> (max+1)
case BTTranslocationRenderable::TLoc_Expand:
{ {
fx->myTimer += dt; float used = gWarpT / TLOC_EXPAND_TIME;
float used = fx->myTimer / TLOC_EXPAND_TIME; // 0 -> 1 if (used >= 1.0f) { gWarpPhase = 0; return; } // done
if (used >= 1.0f) scale = used * gWarpExpandScale + 1.0f;
}
if (getenv("BT_TLOC_LOG"))
{ {
scale = 1.0f; static int s_lt = 0;
fx->mySphereState = BTTranslocationRenderable::TLoc_Idle; if ((++s_lt % 15) == 1)
fx->mySphereVisible = false; DEBUG_STREAM << "[tloc] warp phase=" << gWarpPhase << " scale=" << scale
} << " at=(" << gWarpX << "," << gWarpY << "," << gWarpZ << ")\n" << std::flush;
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 s = scale;
const float c = cosf(fx->myRotateY); const float c = cosf(gWarpRot);
const float sn = sinf(fx->myRotateY); const float sn = sinf(gWarpRot);
D3DXMATRIX m; D3DXMATRIX m;
m._11 = s * c; m._12 = 0.0f; m._13 = -s * sn; m._14 = 0.0f; 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._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._31 = s * sn; m._32 = 0.0f; m._33 = s * c; m._34 = 0.0f;
m._41 = (float)fx->myDropZone->x; m._41 = gWarpX; m._42 = gWarpY; m._43 = gWarpZ; m._44 = 1.0f;
m._42 = (float)fx->myDropZone->y;
m._43 = (float)fx->myDropZone->z;
m._44 = 1.0f;
gTLocSphere->SetLocalToWorld(m); gTLocSphere->SetLocalToWorld(m);
gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time); gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time);
}
} }
//===========================================================================// //===========================================================================//
+7 -31
View File
@@ -204,11 +204,6 @@ static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0,
static const char *SelfDestructName = "self destruct"; // &DAT_00524b38 static const char *SelfDestructName = "self destruct"; // &DAT_00524b38
static const Scalar RespawnDelay = 5.0f; // death -> drop-zone hunt (@004c0830) 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) static const Scalar TicksPerSecond = 1.0f; // (see note in PlayerSimulation)
// //
@@ -759,19 +754,6 @@ void
Player::PlayerSimulation(time_slice); // FUN_0042e168 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 ( if (
(lastPerformance - lastConsoleUpdate) / TicksPerSecond >= CONSOLE_UPDATE_INTERVAL // _DAT_004c08fc (lastPerformance - lastConsoleUpdate) / TicksPerSecond >= CONSOLE_UPDATE_INTERVAL // _DAT_004c08fc
|| application->GetApplicationState() == Application::EndingMission // app+0x88 == 6 || application->GetApplicationState() == Application::EndingMission // app+0x88 == 6
@@ -1068,22 +1050,16 @@ void
// //
// WARP (task #52): fire the translocation sphere at our reinsertion. // WARP (task #52): fire the translocation sphere at our reinsertion.
// Publish the drop-zone position into our DropZoneLocation attribute (the // Self-contained render one-shot at the drop-zone position -- it does NOT
// sphere renderable draws there) and pulse the SimulationState dial to // touch the player's SimulationState (that dial drives the camera/POV +
// DropZoneAcquired -- state 1, the sphere's COLLAPSE trigger. // targeting in our reconstruction; pulsing it for the sphere trigger
// PlayerSimulation flips it to VehicleTranslocated ~1.4 s later, which the // regressed all of those). The effect plays its own collapse->expand.
// 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"); const Point3D &p = message->dropZoneLocation.linearPosition;
if (drop_attr != 0) extern void BTStartWarpEffect(float x, float y, float z);
*drop_attr = message->dropZoneLocation.linearPosition; BTStartWarpEffect((float)p.x, (float)p.y, (float)p.z);
} }
SetSimulationState(DropZoneAcquiredState); // state 1 -> collapse
sWarpExpandTimer = 1.4f;
} }
else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80] else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80]
{ {