Respawn visuals: warp was in the wrong render pass; render never un-wrecked

Two render bugs behind "warp never appears" + "respawned mech stays a sunk wreck":

1. Warp invisible: the tsphere was drawn in PASS_ALPHABLEND, but DrawMesh skips
   any op whose alphaTest != (pass==PASS_ALPHABLEND) (L4D3D.cpp:1045) and
   tsphere's op is opaque -> every op skipped, nothing drawn. Flag the sphere's
   ops for the alpha pass, and draw it as an explicit translucent-blue ADDITIVE
   glow (state saved+restored) so it reads as a warp shimmer, not an opaque ball
   or (as shipped) nothing. Tint via BT_WARP_COLOR.

2. Render stays wrecked on respawn: SwapToWreck hides the body + hangs a sinking
   dbr hulk with render_tree.wrecked a one-way latch; Mech::Reset healed the sim
   but nothing reversed the render, so the reborn mech kept rendering as the sunk
   hulk. Add RebuildMechRenderables (the heal direction of RemakeEntity): drop the
   hulk/debris, clear wrecked, restore every body segment to its now-intact mesh.
   Mech::Reset calls it via BTRebuildMechModel.

Smoke-verified: tsphere loads with its op alpha-flagged; on each respawn
"[BTrender] respawn: rebuilt intact model (12 segs restored, hulk dropped)"; no
crash. Both are local render (the respawning node's own screen); remote-observer
replication of the warp+un-wreck rides the deferred WriteUpdateRecord death path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-10 07:26:48 -05:00
co-authored by Claude Opus 4.8
parent 247e51e1e1
commit d07dd0bf4e
3 changed files with 179 additions and 1 deletions
+165 -1
View File
@@ -770,6 +770,103 @@ void
}
//
//#############################################################################
// 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<Entity*, MechRenderTree>::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 (mirrors the load in
// RemakeEntityRenderables / MakeMechRenderables, for the healed graphic state).
//
JointedMover *jointed_mover = (JointedMover *)entity;
EntitySegment::SkeletonType skeletonType =
(EntitySegment::SkeletonType)render_tree.viewSkeleton;
EntitySegment::SegmentTableIterator segment_iterator(jointed_mover->segmentTable);
EntitySegment *segment;
int restored = 0;
while ((segment = segment_iterator.ReadAndNext()) != NULL)
{
if (segment->IsSiteSegment() != 0)
continue;
int segment_slot = segment->GetIndex();
std::map<int, HierarchicalDrawComponent*>::iterator r =
render_tree.segRenderable.find(segment_slot);
if (r == render_tree.segRenderable.end() || r->second == NULL)
continue;
Enumeration seg_gstate = 0; // ExistsGraphicState (healed)
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();
CString *object_name = segment->GetVideoObjectName(skeletonType, seg_gstate);
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';
strcat(filename, ".bgf");
new_object = d3d_OBJECT::LoadObject(GetDevice(), filename);
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;
}
}
if (new_object != NULL)
{
r->second->SetDrawObj(new_object);
++restored;
}
render_tree.segGState[segment_slot] = (int)seg_gstate;
}
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)
@@ -790,6 +887,18 @@ void BTRemakeMechModel(Entity *entity)
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);
}
//
//#############################################################################
@@ -2184,9 +2293,20 @@ void
{
gTLocSphereTried = 1;
gTLocSphere = d3d_OBJECT::LoadObject(device, "tsphere.bgf");
if (gTLocSphere != 0)
{
// Route the sphere's (opaque) draw-ops INTO the alpha-blend pass.
// DrawMesh skips any op whose alphaTest != (pass==PASS_ALPHABLEND)
// (L4D3D.cpp:1045), so an un-flagged tsphere renders NOTHING in the
// alpha pass -- the warp was invisible for exactly this reason.
for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op)
gTLocSphere->GetDrawOp(op)->alphaTest = true;
}
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] tsphere.bgf load "
<< (gTLocSphere ? "OK" : "FAILED") << "\n" << std::flush;
<< (gTLocSphere ? "OK" : "FAILED")
<< " ops=" << (gTLocSphere ? gTLocSphere->GetDrawOpCount() : 0)
<< "\n" << std::flush;
}
if (gTLocSphere == 0)
{
@@ -2229,7 +2349,51 @@ void
m._41 = gWarpX; m._42 = gWarpY; m._43 = gWarpZ; m._44 = 1.0f;
gTLocSphere->SetLocalToWorld(m);
// Draw as a translucent BLUE additive glow -- a shimmering warp, not an
// opaque view-blocking ball. We are inside the alpha pass (ALPHABLENDENABLE
// + ZWRITE off already set, L4VIDEO.cpp:7762), but the blend factors + colour
// are indeterminate here (beams/pfx drew first), so set them explicitly and
// RESTORE every state we touch (a leaked LIGHTING/TFACTOR corrupts the scene).
DWORD sSrc, sDst, sLight, sTFactor, sCOp, sCA1, sAOp, sAA1;
device->GetRenderState(D3DRS_SRCBLEND, &sSrc);
device->GetRenderState(D3DRS_DESTBLEND, &sDst);
device->GetRenderState(D3DRS_LIGHTING, &sLight);
device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTFactor);
device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp);
device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1);
device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp);
device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1);
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE); // additive glow
device->SetRenderState(D3DRS_LIGHTING, FALSE);
{
// ARGB blue tint + intensity; BT_WARP_COLOR=AARRGGBB (hex) 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) : 0x9040A0FF;
if (s_warpColor == 0) s_warpColor = 0x9040A0FF;
}
device->SetRenderState(D3DRS_TEXTUREFACTOR, s_warpColor);
}
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
device->SetTextureStageState(0, D3DTSS_COLORARG1, 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, sTFactor);
device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp);
device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp);
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1);
}
//===========================================================================//
+6
View File
@@ -648,6 +648,12 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
void
RemakeEntityRenderables(Entity *entity);
// The HEAL direction of RemakeEntity: on respawn (Mech::Reset) drop the
// wreck hulk/debris + restore every body segment to its now-intact mesh
// (the wreck swap, SwapToWreck, is one-way; this reverses it).
void
RebuildMechRenderables(Entity *entity);
protected:
//
// Per-mech render-tree bookkeeping so RemakeEntityRenderables can find each
+8
View File
@@ -1066,6 +1066,14 @@ void
SetPreRunFlag();
if (interestCount == 0) interestCount = 1;
// --- RENDER un-wreck: drop the sunk dbr hulk + restore the intact body ---
// (the wreck swap is one-way on the render side; Reset heals the sim but
// the mech keeps rendering as the sunk hulk without this).
{
extern void BTRebuildMechModel(Entity *entity);
BTRebuildMechModel((Entity *)this);
}
// --- broadcast the reset state to replicants (FUN_004a4c54(this, 0x1f)) ---
ForceUpdate();