K/D scoreboard ROOT-CAUSED (#45) + respawn latch fix (#57) + plate stage ops (#58)

FIXES (compile clean; exe link pending -- a game client still holds btl4.exe):

#57 respawn latch: deathPending was released in ONE place (btplayer.cpp:343) and
only if a LATER re-post happened to find the mech alive -- so if the +5s re-post
landed before the drop-zone reply, or never arrived, the flag stayed set while
the pilot respawned and fought on, and their NEXT death hit the dedup at :391
and was swallowed whole (no warp, no tally, no hunt) for the rest of the
process.  Tonight 3 of 3 deaths in the final round were swallowed, each by a
player who had respawned successfully earlier in the same process.  Clear moved
to the actual completion point (after Mech::Reset at the drop zone) + the four
abandon paths that also stranded it.  Cycle logging promoted to always-on
(START / RESET / SWALLOWED warning) + a RESPAWN matchlog record.

#58 reticle callsign plate drew as a SOLID RECTANGLE on some maps: my
dpl2d_DrawTexturedRect bound a texture but never set COLOROP/ALPHAOP, and
L4D3D.cpp:1085 sets both to SELECTARG1 from TFACTOR (ignores the texture, fills
with a constant).  Which state was live depended on what the 3D pass drew last
-- hence 'depends on the level'.  Now sets MODULATE texture x diffuse for both
channels and restores.  Latent sibling noted (CameraHUDDrawQuad, same omission).

ROOT-CAUSE DOC (no code): docs/KD_SCOREBOARD_PLAN.md.  Headlines:
 - the port reads DEATHS from the WRONG FIELD: the binary's PilotList draws
   +0x27c/+0x280; we labelled +0x280 'pad_0x280', never write it, and
   substituted the engine's Player::deathCount (+0x200, the respawn identity
   number seeded to -2) -> remote rows read a clamped fake 0 by construction;
 - BTPlayer::VehicleDeadMessageHandler @0x4c05c4 is MISSING from our decomp
   export (functions_index.tsv jumps 4c052c -> 4c083c, verified) -- that silence
   is what produced the 'pad' belief.  An absent function is not evidence of an
   absent behaviour;
 - neither counter rides an update record, so divergence never self-heals and
   bystanders show 0/0 all mission;  BTPlayerCountObservedDeath is unreachable
   dead code;
 - 'a kill I didn't earn' = last-hitter-takes-all via lastInflictingID, and
   COLLISIONS count (5 of 47 deaths had a type=0 killing blow);
 - the phantom kill is authentic 1995 (the kill handler bumps the victim's
   killCount too);
 - btplayer.cpp:453 does simulationFlags |= 0x1 where the binary does
   ForceUpdate() -- bit 0 is DelayWatchersFlag and nothing clears it, so the
   first death of any pilot permanently disables ExecuteWatchers on that
   player's simulation.  Filing separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
This commit is contained in:
arcattack
2026-07-25 01:50:07 -05:00
co-authored by Claude Opus 5
parent 326023812a
commit 5174c638ce
4 changed files with 325 additions and 4 deletions
+36
View File
@@ -664,8 +664,44 @@ void dpl2d_DrawTexturedRect(
device->SetFVF(kVertexTexFVF);
device->SetTexture(0, (IDirect3DTexture9 *)texture);
//
// TEXTURE STAGE OPS -- explicitly, every time (Gitea #58). Binding a
// texture is NOT enough: the fixed-function stage still has whatever
// COLOROP/ALPHAOP the last drawer left, and this TU's own list path runs
// UNtextured (`SetTexture(0, 0)`), so nothing here establishes them. In
// particular `L4D3D.cpp:1085-1088` sets
// COLOROP = ALPHAOP = D3DTOP_SELECTARG1 with ARG1 = D3DTA_TFACTOR
// which makes the pipeline ignore the texture completely and fill with a
// constant colour -- i.e. the callsign plate renders as a SOLID RECTANGLE.
// Whether that state happens to be live depends on what the 3D scene drew
// last, which is why the field report was "sometimes, depends on the level
// and the background". MODULATE texture x diffuse for BOTH channels makes
// the draw deterministic: colour = texel * diffuse, alpha = texel * diffuse,
// so a 0x0000 texel (the transparent background of the 1bpp raster upload,
// L4VIDEO.cpp LoadBitSliceTexture) stays invisible.
//
DWORD oCOp = 0, oCA1 = 0, oCA2 = 0, oAOp = 0, oAA1 = 0, oAA2 = 0;
device->GetTextureStageState(0, D3DTSS_COLOROP, &oCOp);
device->GetTextureStageState(0, D3DTSS_COLORARG1, &oCA1);
device->GetTextureStageState(0, D3DTSS_COLORARG2, &oCA2);
device->GetTextureStageState(0, D3DTSS_ALPHAOP, &oAOp);
device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &oAA1);
device->GetTextureStageState(0, D3DTSS_ALPHAARG2, &oAA2);
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
device->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(VertexTex));
device->SetTextureStageState(0, D3DTSS_COLOROP, oCOp);
device->SetTextureStageState(0, D3DTSS_COLORARG1, oCA1);
device->SetTextureStageState(0, D3DTSS_COLORARG2, oCA2);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, oAOp);
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, oAA1);
device->SetTextureStageState(0, D3DTSS_ALPHAARG2, oAA2);
device->SetTexture(0, oldTex);
if (oldTex) oldTex->Release();
device->SetFVF(kVertex2DFVF);