Camera-seat ranking window LIVE: the 1995 broadcast overlay reimplemented

The stubbed dpl-instance ranking display is reborn as screen-space
quads in CameraShipHUDRenderable::Render:
  - followed-player callsign banner (bottom center; the old direct
    index was correct -- Execute already converts to the 0-based
    texture slot)
  - the RANKING WINDOW: one row per scoring player, [ordinal][callsign]
    in rank order right of center; visibility = the Director's
    authentic flash logic (10s on / 15s off, solid final 30s); rows
    follow LIVE playerRank pointers so they re-sort as scores change
  - discovery: each 128x32 ordinal bitmap packs TWO ordinals side by
    side ('1st|2nd', '3rd|4th' -- why 4 bitmaps serve 8 players);
    draw = texture rank/2 with a u-half selected by rank parity
  - alpha-blended A4R4G4B4 white-on-transparent textures x green
    diffuse = the authentic green look

BT_SHOT moved AFTER the 2D pass -- it captured the backbuffer pre-HUD,
so overlays were on screen but invisible to screenshots (cost one
false debugging round).  GetOrdinalTexture accessor added.

Screenshot-verified: '1st MAVERICK' standings + MAVERICK banner over
live auto-directed coverage; 2-node mech smoke PASS (un-regressed;
the new render path only executes when a CameraDirector HUD exists).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 16:32:23 -05:00
co-authored by Claude Fable 5
parent 4459262b9c
commit 0a77d8e54b
5 changed files with 214 additions and 95 deletions
+111 -4
View File
@@ -3045,17 +3045,124 @@ void CameraShipHUDRenderable::Execute()
VideoRenderable::Execute();
}
//
// BT port (camera-seat bring-up, 2026-07-18): draw one screen-space textured
// quad (u/v 0..1) -- shared by the followed-callsign banner and the ranking
// window rows. The name/ordinal bitmaps load black-on-transparent
// (LoadBitSliceTexture, A4R4G4B4), so alpha blending keys them over the scene.
//
static void CameraHUDDrawQuad(
LPDIRECT3DDEVICE9 device,
LPDIRECT3DTEXTURE9 texture,
float x, float y, float w, float h,
float u0 = 0.0f, float u1 = 1.0f)
{
if (texture == NULL)
{
return;
}
L4VERTEX_2D_TEX quad[4];
memset(quad, 0, sizeof(quad));
const DWORD color = D3DCOLOR_XRGB(0, 128, 0);
for (int i = 0; i < 4; ++i)
{
quad[i].z = 0.0f;
quad[i].rhw = 1.0f;
quad[i].color = color;
}
quad[0].x = x + w; quad[0].y = y; quad[0].u = u1; quad[0].v = 0.0f;
quad[1].x = x; quad[1].y = y; quad[1].u = u0; quad[1].v = 0.0f;
quad[2].x = x + w; quad[2].y = y + h; quad[2].u = u1; quad[2].v = 1.0f;
quad[3].x = x; quad[3].y = y + h; quad[3].u = u0; quad[3].v = 1.0f;
device->SetTexture(0, texture);
device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad,
sizeof(L4VERTEX_2D_TEX));
}
void CameraShipHUDRenderable::Render(int pass, const D3DXMATRIX *viewTransform)
{
if (application->GetApplicationState() == Application::RunningMission && oldFollowedPlayerIndex >= 0 && oldFollowedPlayerIndex < MAX_PLAYER_NAMES)
if (application->GetApplicationState() != Application::RunningMission)
{
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
return;
}
LPDIRECT3DDEVICE9 device = myRenderer->GetDevice();
device->SetFVF(L4VERTEX_2D_TEX_FVF);
device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
device->SetFVF(L4VERTEX_2D_TEX_FVF);
//
// The followed player's callsign banner (bottom center). Execute already
// converts the 1-based goalPlayerIndex to the 0-based texture slot.
//
if (oldFollowedPlayerIndex >= 0 && oldFollowedPlayerIndex < MAX_PLAYER_NAMES)
{
device->SetStreamSource(0, mVB, 0, sizeof(L4VERTEX_2D_TEX));
device->SetTexture(0, myRenderer->GetNameTexture(oldFollowedPlayerIndex));
device->SetTexture(0,
myRenderer->GetNameTexture(oldFollowedPlayerIndex));
device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
}
//
// THE RANKING WINDOW (the un-stubbed 1995 display): one row per scoring
// player -- [1st/2nd/... ordinal][callsign] -- in RANK order, right of
// center (the old DCS placement: window at x .22 y .16, ordinal left of
// the name). Visibility is the Director's flash logic (10s on / 15s off,
// solid for the final 30s). playerRank[bmp-1] -> &playerRanking (LIVE
// pointers captured at ctor), so the rows re-sort as the score changes.
//
if (getenv("BT_CAM_LOG"))
{
static int s_rlog = 0;
if ((s_rlog++ % 300) == 0)
DEBUG_STREAM << "[cam] HUD render: followed="
<< oldFollowedPlayerIndex
<< " rankwin=" << (displayRankingWindow
? (int)*displayRankingWindow : -1)
<< " count=" << playerCount << "\n" << std::flush;
}
if (displayRankingWindow != NULL && *displayRankingWindow
&& playerRank != NULL && playerCount > 0)
{
const float width = myRenderer->GetWidth();
const float height = myRenderer->GetHeight();
const float nameW = width * 0.20f;
const float ordW = width * 0.10f;
const float rowH = nameW * (32.0f / 128.0f); // bitmap aspect
const float pad = width * 0.01f;
const float x0 = width * 0.55f;
const float y0 = height * 0.16f;
for (int bmp = 0; bmp < playerCount && bmp < MAX_PLAYER_NAMES; ++bmp)
{
if (playerRank[bmp] == NULL)
{
continue;
}
int rank = *playerRank[bmp];
if (getenv("BT_CAM_LOG"))
{
static int s_rowlog = 0;
if ((s_rowlog++ % 300) == 0)
DEBUG_STREAM << "[cam] rank row: bmp=" << bmp
<< " rank=" << rank << "\n" << std::flush;
}
if (rank < 0 || rank >= MAX_PLAYER_NAMES)
{
continue; // non-scoring (director/camera)
}
const float y = y0 + rank * (rowH * 1.15f);
// each 128x32 ordinal bitmap packs TWO ordinals side by side
// ("1st|2nd", "3rd|4th", ...) -- texture rank/2, u-half by parity
CameraHUDDrawQuad(device, myRenderer->GetOrdinalTexture(rank / 2),
x0, y, ordW, rowH,
(rank & 1) ? 0.5f : 0.0f, (rank & 1) ? 1.0f : 0.5f);
CameraHUDDrawQuad(device, myRenderer->GetNameTexture(bmp),
x0 + ordW + pad, y, nameW, rowH);
}
}
device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~