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
716 lines
25 KiB
C++
716 lines
25 KiB
C++
//===========================================================================//
|
|
// File: dpl2d.cpp //
|
|
// Project: BattleTech port (WinTesla / btl4) //
|
|
//---------------------------------------------------------------------------//
|
|
// libDPL 2D vector display-list layer, re-hosted over Direct3D 9. //
|
|
// //
|
|
// OPCODE-FAITHFUL rework (task #35 / phase-02): every dpl2d_ recorder in the //
|
|
// binary self-identifies via its debug-name string (part_010.c 0x487f34.. //
|
|
// 0x4888c0) -- the full API + opcode map is recorded in //
|
|
// phases/phase-02-dpl2d-reticle.md. This TU implements that model: //
|
|
// //
|
|
// OpenDisplayList(mode) .. CloseDisplayList .. FlushDisplayList //
|
|
// AddOpenPolypoint(2)/AddClosePolypoint(3) -- a POINT set //
|
|
// AddOpenPolyline(4)/AddClosePolyline(5) -- a CLOSED loop //
|
|
// AddOpenLines(6)/AddCloseLines(7) -- SEPARATE SEGMENT PAIRS //
|
|
// AddPoint(8, x, y) -- vertex of the open prim //
|
|
// AddCircle(9, x, y, r, fill) //
|
|
// AddSetColor(0xF, r, g, b) //
|
|
// AddSetMatrix(0x10, 2x3) / AddConcatMatrix(0x11, 2x3) //
|
|
// AddPushState(0x12) / AddPopState(0x13) //
|
|
// AddSetLineWidth(0x15, w) //
|
|
// AddCallDisplayList(list) -- nested glyph reuse //
|
|
// AddFullScreenClipRegion -- clip reset (no-op here) //
|
|
// //
|
|
// Coordinates are normalised view space (the reticle builder's constants //
|
|
// are fractions like 0.04/0.1/0.35); dpl2d_ExecuteList maps them to the //
|
|
// current viewport and draws XYZRHW primitives with state save/restore. //
|
|
// //
|
|
// NOTE the OLD trio dpl2d_PushMatrix/MoveTo/PopMatrix was a misreading of //
|
|
// AddOpenPolypoint/AddPoint/AddClosePolypoint ("draw a point"); the aliases //
|
|
// below keep old callers compiling while mapping to the true semantics. //
|
|
//===========================================================================//
|
|
|
|
#include <bt.hpp> // Scalar + engine prelude
|
|
#pragma hdrstop
|
|
|
|
#if !defined(BTL4VID_HPP)
|
|
# include <btl4vid.hpp> // dpl2d_DISPLAY (opaque) + dpl2d_* prototypes
|
|
#endif
|
|
#if !defined(DPL2D_HPP)
|
|
# include <dpl2d.hpp>
|
|
#endif
|
|
|
|
#include <d3d9.h>
|
|
#include <vector>
|
|
#include <math.h>
|
|
|
|
// the presented (client) aspect -- L4VIDEO.cpp (gWindowAspect); GLOBAL scope
|
|
// on purpose (an extern inside the anonymous namespace = a different symbol
|
|
// -> /FORCE garbage call; task #44)
|
|
extern float BTGetPresentAspect();
|
|
|
|
namespace
|
|
{
|
|
struct Vertex2D
|
|
{
|
|
float x, y, z, rhw;
|
|
DWORD color;
|
|
};
|
|
const DWORD kVertex2DFVF = (D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
|
|
|
|
const int kCircleSegments = 32; // tessellation of AddCircle
|
|
const float kPi = 3.14159265358979323846f;
|
|
|
|
inline float ClampUnit(float v)
|
|
{
|
|
return (v < 0.0f) ? 0.0f : (v > 1.0f ? 1.0f : v);
|
|
}
|
|
|
|
struct Vec2 { float x, y; };
|
|
|
|
// 2x3 affine (row vectors): [x y 1] * | m00 m01 |
|
|
// | m10 m11 |
|
|
// | m20 m21 |
|
|
struct Mat2x3
|
|
{
|
|
float m[6]; // m00 m01 m10 m11 m20 m21
|
|
void Identity() { m[0]=1; m[1]=0; m[2]=0; m[3]=1; m[4]=0; m[5]=0; }
|
|
Vec2 Apply(float x, float y) const
|
|
{
|
|
Vec2 r;
|
|
r.x = x * m[0] + y * m[2] + m[4];
|
|
r.y = x * m[1] + y * m[3] + m[5];
|
|
return r;
|
|
}
|
|
static Mat2x3 Mul(const Mat2x3 &a, const Mat2x3 &b) // a then b
|
|
{
|
|
Mat2x3 r;
|
|
r.m[0] = a.m[0]*b.m[0] + a.m[1]*b.m[2];
|
|
r.m[1] = a.m[0]*b.m[1] + a.m[1]*b.m[3];
|
|
r.m[2] = a.m[2]*b.m[0] + a.m[3]*b.m[2];
|
|
r.m[3] = a.m[2]*b.m[1] + a.m[3]*b.m[3];
|
|
r.m[4] = a.m[4]*b.m[0] + a.m[5]*b.m[2] + b.m[4];
|
|
r.m[5] = a.m[4]*b.m[1] + a.m[5]*b.m[3] + b.m[5];
|
|
return r;
|
|
}
|
|
};
|
|
|
|
//
|
|
// One recorded command (the opcode stream, structurally).
|
|
//
|
|
struct Command
|
|
{
|
|
enum Kind
|
|
{
|
|
kPoints, // vertex run: a point per vertex
|
|
kLineStrip, // vertex run: SEGMENT PAIRS (LINELIST; task #44)
|
|
kPolyLoop, // vertex run: closed loop
|
|
kCircleFill,
|
|
kCircleOutline,
|
|
kSetColor,
|
|
kSetWidth,
|
|
kPushState,
|
|
kPopState,
|
|
kSetMatrix,
|
|
kConcatMatrix,
|
|
kCallList
|
|
} kind;
|
|
DWORD color; // kSetColor (prebaked ARGB)
|
|
float a, b, c, d; // circle x,y,r,fill / width in a
|
|
Mat2x3 mat; // kSetMatrix/kConcatMatrix
|
|
void *callee; // kCallList (Dpl2dList*)
|
|
std::vector<Vec2> verts; // vertex runs
|
|
};
|
|
}
|
|
|
|
struct Dpl2dList
|
|
{
|
|
bool recording;
|
|
bool compiled;
|
|
int mode;
|
|
std::vector<Command> commands;
|
|
// the currently-open vertex primitive (between AddOpenX .. AddCloseX)
|
|
int openKind; // -1 none, else Command::Kind
|
|
std::vector<Vec2> openVerts;
|
|
|
|
Dpl2dList() : recording(false), compiled(false), mode(0), openKind(-1) {}
|
|
};
|
|
|
|
static inline Dpl2dList *AsList(dpl2d_DISPLAY *handle)
|
|
{
|
|
return reinterpret_cast<Dpl2dList *>(handle);
|
|
}
|
|
|
|
static void FlushOpenPrimitive(Dpl2dList *self)
|
|
{
|
|
if (self->openKind < 0)
|
|
return;
|
|
Command cmd;
|
|
cmd.kind = (Command::Kind)self->openKind;
|
|
cmd.verts = self->openVerts;
|
|
self->commands.push_back(cmd);
|
|
self->openKind = -1;
|
|
self->openVerts.clear();
|
|
}
|
|
|
|
//===========================================================================//
|
|
// Recorder -- the dpl2d_ entry points (prototypes in btl4vid.hpp)
|
|
//===========================================================================//
|
|
|
|
dpl2d_DISPLAY *dpl2d_NewDisplayList()
|
|
{
|
|
return reinterpret_cast<dpl2d_DISPLAY *>(new Dpl2dList);
|
|
}
|
|
|
|
void dpl2d_Begin(dpl2d_DISPLAY *list, int mode) // OpenDisplayList
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
self->recording = true;
|
|
self->compiled = false;
|
|
self->mode = mode;
|
|
self->openKind = -1;
|
|
self->openVerts.clear();
|
|
self->commands.clear();
|
|
}
|
|
|
|
void dpl2d_End(dpl2d_DISPLAY *list) // CloseDisplayList
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
FlushOpenPrimitive(self);
|
|
self->recording = false;
|
|
}
|
|
|
|
void dpl2d_Compile(dpl2d_DISPLAY *list) // FlushDisplayList
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
self->compiled = true;
|
|
}
|
|
|
|
void dpl2d_SetColor(dpl2d_DISPLAY *list, Scalar red, Scalar green, Scalar blue)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
Command cmd;
|
|
cmd.kind = Command::kSetColor;
|
|
cmd.color = D3DCOLOR_COLORVALUE(
|
|
ClampUnit((float)red), ClampUnit((float)green), ClampUnit((float)blue), 1.0f);
|
|
self->commands.push_back(cmd);
|
|
}
|
|
|
|
void dpl2d_SetLineWidth(dpl2d_DISPLAY *list, Scalar width)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
Command cmd;
|
|
cmd.kind = Command::kSetWidth;
|
|
cmd.a = (float)width;
|
|
self->commands.push_back(cmd);
|
|
}
|
|
|
|
void dpl2d_PushState(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
Command cmd; cmd.kind = Command::kPushState;
|
|
self->commands.push_back(cmd);
|
|
}
|
|
|
|
void dpl2d_PopState(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
Command cmd; cmd.kind = Command::kPopState;
|
|
self->commands.push_back(cmd);
|
|
}
|
|
|
|
void dpl2d_SetMatrix(dpl2d_DISPLAY *list, const Scalar *six)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0 || six == 0) return;
|
|
Command cmd; cmd.kind = Command::kSetMatrix;
|
|
for (int i = 0; i < 6; ++i) cmd.mat.m[i] = (float)six[i];
|
|
self->commands.push_back(cmd);
|
|
}
|
|
|
|
void dpl2d_ConcatMatrix(dpl2d_DISPLAY *list, const Scalar *six)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0 || six == 0) return;
|
|
Command cmd; cmd.kind = Command::kConcatMatrix;
|
|
for (int i = 0; i < 6; ++i) cmd.mat.m[i] = (float)six[i];
|
|
self->commands.push_back(cmd);
|
|
}
|
|
|
|
void dpl2d_CallList(dpl2d_DISPLAY *list, dpl2d_DISPLAY *callee)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0 || callee == 0) return;
|
|
Command cmd; cmd.kind = Command::kCallList;
|
|
cmd.callee = AsList(callee);
|
|
self->commands.push_back(cmd);
|
|
}
|
|
|
|
void dpl2d_OpenPolypoint(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
FlushOpenPrimitive(self);
|
|
self->openKind = Command::kPoints;
|
|
}
|
|
|
|
void dpl2d_ClosePolypoint(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
FlushOpenPrimitive(self);
|
|
}
|
|
|
|
void dpl2d_OpenLines(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
FlushOpenPrimitive(self);
|
|
self->openKind = Command::kLineStrip;
|
|
}
|
|
|
|
void dpl2d_CloseLines(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
FlushOpenPrimitive(self);
|
|
}
|
|
|
|
void dpl2d_OpenPolyline(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
FlushOpenPrimitive(self);
|
|
self->openKind = Command::kPolyLoop;
|
|
}
|
|
|
|
void dpl2d_ClosePolyline(dpl2d_DISPLAY *list)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
FlushOpenPrimitive(self);
|
|
}
|
|
|
|
void dpl2d_AddPoint(dpl2d_DISPLAY *list, Scalar x, Scalar y)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
if (self->openKind < 0)
|
|
self->openKind = Command::kPoints; // tolerate a stray vertex
|
|
Vec2 v = { (float)x, (float)y };
|
|
self->openVerts.push_back(v);
|
|
}
|
|
|
|
void dpl2d_Circle(dpl2d_DISPLAY *list, Scalar x, Scalar y, Scalar radius, int fill)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0) return;
|
|
FlushOpenPrimitive(self);
|
|
Command cmd;
|
|
cmd.kind = fill ? Command::kCircleFill : Command::kCircleOutline;
|
|
cmd.a = (float)x; cmd.b = (float)y; cmd.c = (float)radius;
|
|
self->commands.push_back(cmd);
|
|
}
|
|
|
|
void dpl2d_FullScreenClip(dpl2d_DISPLAY * /*list*/)
|
|
{
|
|
// clip reset -- the executor always draws to the full viewport
|
|
}
|
|
|
|
//
|
|
// LEGACY ALIASES (the earlier AddWeapon transcription used these names for
|
|
// what the binary's opcodes reveal to be the point primitive).
|
|
//
|
|
void dpl2d_PushMatrix(dpl2d_DISPLAY *list) { dpl2d_OpenPolypoint(list); }
|
|
void dpl2d_MoveTo(dpl2d_DISPLAY *list, Scalar x, Scalar y) { dpl2d_AddPoint(list, x, y); }
|
|
void dpl2d_PopMatrix(dpl2d_DISPLAY *list) { dpl2d_ClosePolypoint(list); }
|
|
|
|
//===========================================================================//
|
|
// Executor -- rasterise a compiled list on the supplied device.
|
|
//===========================================================================//
|
|
|
|
namespace
|
|
{
|
|
struct ExecState
|
|
{
|
|
DWORD color;
|
|
float width;
|
|
Mat2x3 mat;
|
|
};
|
|
|
|
//
|
|
// COORDINATE MODEL (from the reticle's authored constants): centred origin,
|
|
// +x right, +y down, unit = half the PRESENTED height on both axes (keeps
|
|
// circles round ON SCREEN; the reticle content spans roughly +-0.5).
|
|
//
|
|
// ASPECT PRE-COMPENSATION (task #44): the list draws into the fixed
|
|
// backbuffer, which the present STRETCHES into the client area. For the
|
|
// on-screen result to be square at any window shape, the backbuffer x-unit
|
|
// is (bbW/2)/presentAspect -- equal to bbH/2 on a 4:3 window (unchanged),
|
|
// pre-squished on wider windows so the stretch restores square.
|
|
//
|
|
inline float XUnit(float vw, float vh)
|
|
{
|
|
// NB the extern lives at GLOBAL scope (below the namespace ends /
|
|
// above in the TU) -- an extern declared INSIDE an anonymous
|
|
// namespace mangles as `anonymous-namespace'::... = a DIFFERENT,
|
|
// unresolved symbol, which /FORCE turns into a garbage call target
|
|
// (crashed exactly so; the /FORCE gotcha).
|
|
float a = BTGetPresentAspect();
|
|
return (a > 0.1f) ? (vw * 0.5f) / a : (vh * 0.5f);
|
|
}
|
|
inline float MapX(float x, float ox, float vw, float vh)
|
|
{ return ox + vw * 0.5f + x * XUnit(vw, vh); }
|
|
inline float MapY(float y, float oy, float vh)
|
|
{ return oy + vh * 0.5f + y * (vh * 0.5f); }
|
|
|
|
void DrawVertexRun(IDirect3DDevice9 *device, const Command &cmd,
|
|
const ExecState &st, float ox, float oy, float vw, float vh)
|
|
{
|
|
if (cmd.verts.empty())
|
|
return;
|
|
static std::vector<Vertex2D> verts;
|
|
verts.clear();
|
|
for (size_t i = 0; i < cmd.verts.size(); ++i)
|
|
{
|
|
Vec2 p = st.mat.Apply(cmd.verts[i].x, cmd.verts[i].y);
|
|
Vertex2D v;
|
|
v.x = MapX(p.x, ox, vw, vh); v.y = MapY(p.y, oy, vh);
|
|
v.z = 0.0f; v.rhw = 1.0f; v.color = st.color;
|
|
verts.push_back(v);
|
|
}
|
|
if (cmd.kind == Command::kPoints)
|
|
{
|
|
// a point renders as a small filled quad (D3D9 point size is
|
|
// unreliable on the fixed function path); ~width pixels square.
|
|
float half = (st.width > 1.0f) ? st.width : 1.0f;
|
|
for (size_t i = 0; i < verts.size(); ++i)
|
|
{
|
|
Vertex2D q[4] = { verts[i], verts[i], verts[i], verts[i] };
|
|
q[0].x -= half; q[0].y -= half;
|
|
q[1].x += half; q[1].y -= half;
|
|
q[2].x -= half; q[2].y += half;
|
|
q[3].x += half; q[3].y += half;
|
|
device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, q, sizeof(Vertex2D));
|
|
}
|
|
}
|
|
else if (cmd.kind == Command::kLineStrip)
|
|
{
|
|
// SEPARATE SEGMENTS (task #44): OpenLines/CloseLines points come in
|
|
// PAIRS -- every ctor use is segment pairs (the 4 cross arms, one
|
|
// pair per ladder tick, the compass stem, the lock ring's 4 ticks,
|
|
// each threat mark). Drawing them as a connected STRIP joined the
|
|
// ticks into zigzags, hung diagonals off the crosshair arms, and
|
|
// drew chords across the lock ring (user screenshot). LINELIST =
|
|
// one line per point pair, the binary's semantics.
|
|
if (verts.size() >= 2)
|
|
device->DrawPrimitiveUP(D3DPT_LINELIST,
|
|
(UINT)(verts.size() / 2), &verts[0], sizeof(Vertex2D));
|
|
}
|
|
else // kPolyLoop -- closed
|
|
{
|
|
if (verts.size() >= 2)
|
|
{
|
|
verts.push_back(verts[0]);
|
|
device->DrawPrimitiveUP(D3DPT_LINESTRIP,
|
|
(UINT)verts.size() - 1, &verts[0], sizeof(Vertex2D));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExecuteListInner(Dpl2dList *self, IDirect3DDevice9 *device,
|
|
ExecState &st, std::vector<ExecState> &stack,
|
|
float ox, float oy, float vw, float vh, int depth)
|
|
{
|
|
if (self == 0 || depth > 8)
|
|
return;
|
|
Vertex2D ring[kCircleSegments + 2];
|
|
|
|
for (size_t i = 0; i < self->commands.size(); ++i)
|
|
{
|
|
const Command &cmd = self->commands[i];
|
|
switch (cmd.kind)
|
|
{
|
|
case Command::kSetColor: st.color = cmd.color; break;
|
|
case Command::kSetWidth: st.width = cmd.a; break;
|
|
case Command::kPushState: stack.push_back(st); break;
|
|
case Command::kPopState:
|
|
if (!stack.empty()) { st = stack.back(); stack.pop_back(); }
|
|
break;
|
|
case Command::kSetMatrix: st.mat = cmd.mat; break;
|
|
case Command::kConcatMatrix:
|
|
st.mat = Mat2x3::Mul(cmd.mat, st.mat);
|
|
break;
|
|
case Command::kCallList:
|
|
// INLINE include: a called list's state changes (matrices,
|
|
// colours) PERSIST into the caller -- the reticle's range
|
|
// caret is a called list holding just a translate that shifts
|
|
// the master's subsequent geometry.
|
|
ExecuteListInner((Dpl2dList *)cmd.callee, device,
|
|
st, stack, ox, oy, vw, vh, depth + 1);
|
|
break;
|
|
case Command::kPoints:
|
|
case Command::kLineStrip:
|
|
case Command::kPolyLoop:
|
|
DrawVertexRun(device, cmd, st, ox, oy, vw, vh);
|
|
break;
|
|
case Command::kCircleFill:
|
|
case Command::kCircleOutline:
|
|
{
|
|
Vec2 p = st.mat.Apply(cmd.a, cmd.b);
|
|
const float cx = MapX(p.x, ox, vw, vh);
|
|
const float cy = MapY(p.y, oy, vh);
|
|
const float rx = cmd.c * XUnit(vw, vh); // pre-squished in bb space,
|
|
const float ry = cmd.c * (vh * 0.5f); // round after the present stretch
|
|
if (cmd.kind == Command::kCircleFill)
|
|
{
|
|
ring[0].x = cx; ring[0].y = cy; ring[0].z = 0.0f;
|
|
ring[0].rhw = 1.0f; ring[0].color = st.color;
|
|
for (int s = 0; s <= kCircleSegments; ++s)
|
|
{
|
|
float a = (2.0f * kPi * s) / kCircleSegments;
|
|
ring[s + 1].x = cx + cosf(a) * rx;
|
|
ring[s + 1].y = cy + sinf(a) * ry;
|
|
ring[s + 1].z = 0.0f; ring[s + 1].rhw = 1.0f;
|
|
ring[s + 1].color = st.color;
|
|
}
|
|
device->DrawPrimitiveUP(D3DPT_TRIANGLEFAN,
|
|
kCircleSegments, ring, sizeof(Vertex2D));
|
|
}
|
|
else
|
|
{
|
|
for (int s = 0; s <= kCircleSegments; ++s)
|
|
{
|
|
float a = (2.0f * kPi * s) / kCircleSegments;
|
|
ring[s].x = cx + cosf(a) * rx;
|
|
ring[s].y = cy + sinf(a) * ry;
|
|
ring[s].z = 0.0f; ring[s].rhw = 1.0f;
|
|
ring[s].color = st.color;
|
|
}
|
|
device->DrawPrimitiveUP(D3DPT_LINESTRIP,
|
|
kCircleSegments, ring, sizeof(Vertex2D));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void dpl2d_ExecuteList(dpl2d_DISPLAY *list, IDirect3DDevice9 *device)
|
|
{
|
|
Dpl2dList *self = AsList(list);
|
|
if (self == 0 || device == 0 || self->commands.empty())
|
|
return;
|
|
|
|
D3DVIEWPORT9 vp;
|
|
if (FAILED(device->GetViewport(&vp)))
|
|
return;
|
|
|
|
// BT_HUD_LOG: one-shot geometry ground truth -- the viewport the HUD maps
|
|
// into + where key reticle x/y land (diagnosing scale/placement reports).
|
|
{
|
|
static int s_logged = -1;
|
|
if (s_logged < 0) s_logged = getenv("BT_HUD_LOG") ? 1 : 0;
|
|
if (s_logged == 1)
|
|
{
|
|
s_logged = 2;
|
|
DEBUG_STREAM << "[hud-geom] viewport x=" << vp.X << " y=" << vp.Y
|
|
<< " w=" << vp.Width << " h=" << vp.Height
|
|
<< " MapX(0.35)=" << (vp.X + vp.Width*0.5f + 0.35f*(vp.Height*0.5f))
|
|
<< " MapX(-0.25)=" << (vp.X + vp.Width*0.5f - 0.25f*(vp.Height*0.5f))
|
|
<< " MapY(0.25)=" << (vp.Y + vp.Height*0.5f + 0.25f*(vp.Height*0.5f))
|
|
<< " MapY(0.35)=" << (vp.Y + vp.Height*0.5f + 0.35f*(vp.Height*0.5f))
|
|
<< "\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
DWORD oldLighting, oldZ, oldCull, oldAlpha, oldSrc, oldDst, oldFog;
|
|
device->GetRenderState(D3DRS_LIGHTING, &oldLighting);
|
|
device->GetRenderState(D3DRS_ZENABLE, &oldZ);
|
|
device->GetRenderState(D3DRS_CULLMODE, &oldCull);
|
|
device->GetRenderState(D3DRS_ALPHABLENDENABLE, &oldAlpha);
|
|
device->GetRenderState(D3DRS_SRCBLEND, &oldSrc);
|
|
device->GetRenderState(D3DRS_DESTBLEND, &oldDst);
|
|
device->GetRenderState(D3DRS_FOGENABLE, &oldFog);
|
|
IDirect3DBaseTexture9 *oldTex = 0;
|
|
device->GetTexture(0, &oldTex);
|
|
|
|
device->SetTexture(0, 0);
|
|
device->SetRenderState(D3DRS_LIGHTING, FALSE);
|
|
device->SetRenderState(D3DRS_ZENABLE, FALSE);
|
|
device->SetRenderState(D3DRS_FOGENABLE, FALSE);
|
|
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
|
device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
|
|
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
|
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
|
device->SetFVF(kVertex2DFVF);
|
|
|
|
ExecState st;
|
|
st.color = 0xFFFFFFFF;
|
|
st.width = 1.0f;
|
|
st.mat.Identity();
|
|
std::vector<ExecState> stack;
|
|
ExecuteListInner(self, device,
|
|
st, stack, (float)vp.X, (float)vp.Y, (float)vp.Width, (float)vp.Height, 0);
|
|
|
|
device->SetTexture(0, oldTex);
|
|
if (oldTex) oldTex->Release();
|
|
device->SetRenderState(D3DRS_LIGHTING, oldLighting);
|
|
device->SetRenderState(D3DRS_ZENABLE, oldZ);
|
|
device->SetRenderState(D3DRS_FOGENABLE, oldFog);
|
|
device->SetRenderState(D3DRS_CULLMODE, oldCull);
|
|
device->SetRenderState(D3DRS_ALPHABLENDENABLE, oldAlpha);
|
|
device->SetRenderState(D3DRS_SRCBLEND, oldSrc);
|
|
device->SetRenderState(D3DRS_DESTBLEND, oldDst);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// dpl2d_DrawTexturedRect -- a TEXTURED quad in RETICLE coordinates
|
|
//#############################################################################
|
|
//
|
|
// The reticle's target NAME PLATE (the 1995 PNAMEx.bgf plate) is a UV-mapped
|
|
// quad, so it cannot ride the line/polyline display lists above -- but it must
|
|
// land in the SAME space, because the binary places it from the same
|
|
// `Reticle::reticlePosition` that translates the aim group (reticle Execute
|
|
// @004cdede [T1 disasm]). So it reuses this TU's authentic mapping: MapX's
|
|
// aspect-corrected x unit + MapY's half-height y unit, in the CURRENT viewport
|
|
// (which is the world sub-rect under the cockpit-surround layout, not the whole
|
|
// backbuffer -- so the plate follows the view like the rest of the reticle).
|
|
//
|
|
// cx/cy = quad CENTRE, w/h = full extents, all in reticle units. The texture
|
|
// arrives as void* because the caller (game/reconstructed/btl4vid.cpp) carries
|
|
// no d3d9 types. State is saved/restored exactly as dpl2d_ExecuteList does
|
|
// (leaking a texture stage or the alpha state into the next frame's 3D pass is
|
|
// the "floating rocks" class of bug).
|
|
//
|
|
void dpl2d_DrawTexturedRect(
|
|
IDirect3DDevice9 *device,
|
|
void *texture,
|
|
float cx,
|
|
float cy,
|
|
float w,
|
|
float h,
|
|
unsigned long argb)
|
|
{
|
|
if (device == 0 || texture == 0)
|
|
return;
|
|
|
|
D3DVIEWPORT9 vp;
|
|
if (FAILED(device->GetViewport(&vp)))
|
|
return;
|
|
|
|
const float ox = (float)vp.X, oy = (float)vp.Y;
|
|
const float vw = (float)vp.Width, vh = (float)vp.Height;
|
|
const float xunit = XUnit(vw, vh); // aspect-corrected, as MapX
|
|
const float yunit = vh * 0.5f; // as MapY
|
|
|
|
const float px = MapX(cx, ox, vw, vh);
|
|
const float py = MapY(cy, oy, vh);
|
|
const float hw = (w * 0.5f) * xunit;
|
|
const float hh = (h * 0.5f) * yunit;
|
|
|
|
struct VertexTex
|
|
{
|
|
float x, y, z, rhw;
|
|
DWORD color;
|
|
float u, v;
|
|
};
|
|
const DWORD kVertexTexFVF =
|
|
(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1);
|
|
|
|
VertexTex quad[4];
|
|
memset(quad, 0, sizeof(quad));
|
|
for (int i = 0; i < 4; ++i)
|
|
{
|
|
quad[i].z = 0.0f;
|
|
quad[i].rhw = 1.0f;
|
|
quad[i].color = (DWORD)argb; // modulates the plate art (its material colour)
|
|
}
|
|
// tri-strip order: TR, TL, BR, BL
|
|
quad[0].x = px + hw; quad[0].y = py - hh; quad[0].u = 1.0f; quad[0].v = 0.0f;
|
|
quad[1].x = px - hw; quad[1].y = py - hh; quad[1].u = 0.0f; quad[1].v = 0.0f;
|
|
quad[2].x = px + hw; quad[2].y = py + hh; quad[2].u = 1.0f; quad[2].v = 1.0f;
|
|
quad[3].x = px - hw; quad[3].y = py + hh; quad[3].u = 0.0f; quad[3].v = 1.0f;
|
|
|
|
DWORD oldLighting = 0, oldZ = 0, oldCull = 0;
|
|
DWORD oldAlpha = 0, oldSrc = 0, oldDst = 0, oldFog = 0;
|
|
device->GetRenderState(D3DRS_LIGHTING, &oldLighting);
|
|
device->GetRenderState(D3DRS_ZENABLE, &oldZ);
|
|
device->GetRenderState(D3DRS_CULLMODE, &oldCull);
|
|
device->GetRenderState(D3DRS_ALPHABLENDENABLE, &oldAlpha);
|
|
device->GetRenderState(D3DRS_SRCBLEND, &oldSrc);
|
|
device->GetRenderState(D3DRS_DESTBLEND, &oldDst);
|
|
device->GetRenderState(D3DRS_FOGENABLE, &oldFog);
|
|
IDirect3DBaseTexture9 *oldTex = 0;
|
|
device->GetTexture(0, &oldTex);
|
|
|
|
device->SetRenderState(D3DRS_LIGHTING, FALSE);
|
|
device->SetRenderState(D3DRS_ZENABLE, FALSE);
|
|
device->SetRenderState(D3DRS_FOGENABLE, FALSE);
|
|
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
|
device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
|
|
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
|
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
|
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);
|
|
device->SetRenderState(D3DRS_LIGHTING, oldLighting);
|
|
device->SetRenderState(D3DRS_ZENABLE, oldZ);
|
|
device->SetRenderState(D3DRS_FOGENABLE, oldFog);
|
|
device->SetRenderState(D3DRS_CULLMODE, oldCull);
|
|
device->SetRenderState(D3DRS_ALPHABLENDENABLE, oldAlpha);
|
|
device->SetRenderState(D3DRS_SRCBLEND, oldSrc);
|
|
device->SetRenderState(D3DRS_DESTBLEND, oldDst);
|
|
}
|