From 35c750dc7c8773ebf85b0eb90a5f64df46f9d2db Mon Sep 17 00:00:00 2001 From: arcattack Date: Fri, 24 Jul 2026 18:36:02 -0500 Subject: [PATCH] Gitea #44: the reticle TARGET NAME PLATE (PNAME1-8) -- the target's callsign under the crosshair Reconstructed the last deferred piece of the 1995 reticle, decoded from the Execute disassembly @004cdcf0 [T1]: - selection: target_mech+0x190 (owning BTPlayer) -> player+0x1e0 (playerBitmapIndex), 1-based into the mesh table at this+0x2e8 - placement: re-placed every frame the aim moves -- scale 0.12 uniform, translate (K*retPos.x, K*retPos.y - 0.08, -1.0), K = 1/2.8 (the x87 long double @0x4cee64) -- so the label TRACKS THE AIM POINT, not screen centre - visibility: the LOCK attribute (this+0x184, cached +0x188) AND an owning player; reticle-off / simple-X also hide it - art: the plate quad UV-addresses a shared 128x64 bitslice texmap whose texels the pod OVERWROTE each mission with the egg's callsign rasters (original source commented out at L4VIDEO.cpp:5682-5710) -- the baked 'PLAYER n' art in BMAP.BSL is only the shipped fallback. Material colour is a neutral (0.5,0.5,0.5): grey-white, not phosphor green. Port: same geometry in reticle units (0.224 below the aim point, 0.336x0.084) drawn as the target's egg callsign texture through the reticle's own MapX/MapY mapping, so it follows the world view in every layout and BT_SHOT captures it. New: dpl2d_DrawTexturedRect, BTReticleTargetPlate, BTGetPlayerNameTexture. Also: kRetCaret corrected 0.02f -> 0.025f (_DAT_004cd7f4 read from the binary; the old value was a T3 guess, 20% small). KB: gauges-hud + open-questions corrected (the chain was filed as deferred and mis-attributed to 'the 3D marker'); TWO PNAME consumers now distinguished (this plate, and CameraShipHUDRenderable's ranking window which shipped 2026-07-18); new bgf-format section on the plate atlas. Rig-verified: 2-node relay, BT_GOTO=enemy -> '[hud] name plate: bmp=2 tex=1' and 'Boreas' rendered under the crosshair on the locked target. Solo clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg --- context/bgf-format.md | 47 +++++++++++++++ context/gauges-hud.md | 42 ++++++++++++- context/open-questions.md | 30 +++++++++- engine/MUNGA_L4/L4VIDEO.cpp | 24 ++++++++ game/reconstructed/btl4vid.cpp | 98 ++++++++++++++++++++++++++++-- game/reconstructed/dpl2d.cpp | 105 +++++++++++++++++++++++++++++++++ game/reconstructed/mech4.cpp | 54 +++++++++++++++++ 7 files changed, 390 insertions(+), 10 deletions(-) diff --git a/context/bgf-format.md b/context/bgf-format.md index ae91cc9..03ae9e8 100644 --- a/context/bgf-format.md +++ b/context/bgf-format.md @@ -71,6 +71,53 @@ A mech is not one BGF — it's a `.SKL` skeleton (a DCS tree of joints) whose ea `Object=part.bgf` is loaded + posed, then animated by `.ANI` clips. See [[asset-formats]] / [[locomotion]] for skeleton + animation. [T1] +## The PNAME/PLACE name-plate atlas (the only TEXT-bearing BGF geometry) [T1/T2] +`PNAME1-8.BGF` + `PLACE1-8.BGF` (`content/VIDEO/GEO/`, 235/238 bytes) are the MP +scoreboard's 16 billboard plates — each ONE flat quad (4 verts, `XYZ_UV`, `PCONN` ppf=4, +**no LOD chunk = 1 implicit LOD**, no normals, no `SV_SPECIAL`). PNAME = 1.0×0.25 m, +PLACE = 0.5×0.25 m, planar Z=0. Consumers: `FUN_00454a70` (the scoreboard, resource type +0x46) and `BTReticleRenderable` @004cc40c (PNAME only). See [[open-questions]]. + +**They are the only geometry in the corpus whose texture is TEXT**, which makes them the +sole asset that can prove BSL row order and slice mapping — and they CONFIRM the port's +pair-swapped nibble decode (`image.cpp:122-125`): slices 0→3 come out as "PLAYER 1/2", +"3/4", "5/6", "7/8" in exact index order. [T2 — decoded + read visually] + +One shared 128×64 texmap: `bmap:` → `VIDEO/MAT/BMAP.BMF` → 6 `TEXTURE` records +`bmap1_tex..bmap6_tex`, all `TEXTURE_MAP=bmap` with `BITSLICE` 0..5 → `VIDEO/TEX/BMAP.BSL` +(`DIV-BSL2`, 128×64, depth 4, texels @0x70). Cell map (UVs read straight from each BGF): + +| material | slice | plates → UV sub-rect | +|---|---|---| +| `name12_mtl`→`bmap1_tex` | 0 | PNAME1 `v[.5,1]`, PNAME2 `v[0,.5]` (full U; 128×32 cells) | +| `name34_mtl`→`bmap2_tex` | 1 | PNAME3 `v[.5,1]`, PNAME4 `v[0,.5]` | +| `name56_mtl`→`bmap3_tex` | 2 | PNAME5 `v[.5,1]`, PNAME6 `v[0,.5]` | +| `name78_mtl`→`bmap4_tex` | 3 | PNAME7 `v[.5,1]`, PNAME8 `v[0,.5]` | +| `place1234_mtl`→`bmap5_tex` | 4 | PLACE1-4 = 2×2 grid of 64×32 cells (`u[0,.5]/[.5,1]` × `v[.5,1]/[0,.5]`) | +| `place5678_mtl`→`bmap6_tex` | 5 | PLACE5-8 = same 2×2 grid | + +**The BSL is a RUNTIME COMPOSITING TARGET, not final art [T0 engine source].** +`DPLRenderer::MakeBitSliceStorage` (L4VIDEO.cpp:9888) allocates `uint32[128*64]`; +`LoadBitSliceTexture` merges a 1bpp egg BitMap into it; `FlushBitSliceTexture` +(L4VIDEO.cpp:5682, stubbed in 2007) did `dpl_LookupTexture("bmap:bmap1_tex")` + +`dpl_TexmapTexels2D(texmap, buf, 128, 64, 4)` — one upload rewrites ALL SIX slices. So the +1995 engine painted the **egg's callsign + ordinal bitmaps** onto these plates every mission; +the baked "PLAYER n" text is only the shipped DEFAULT. Two stacked 128×32 large-name bitmaps +per slice = exactly the PNAME v-halves; two stacked 128×32 ordinal bitmaps ("1st|2nd") = exactly +the PLACE 2×2 grid. Corollaries: +- **The shipped BMAP.BSL has only 4 distinct planes** — slice4 aliases slice0 and slice5 + aliases slice3 (nibble3==nibble7, nibble4==nibble6; nibbles 0/1 zero). The ordinal art was + never baked, so a naive PLACE draw shows "PLAY"/"ER 1"/… fragments. [T1 measured] +- The placeholder cells are stored **scanline-reversed** vs the compositor's top-down order + (`LoadBitSliceTexture` writes y=0..31 top-down), so a naive PNAME draw gives the RIGHT + number upside-down. Don't chase it — implement the compositor instead. [T1 / T3 fix choice] +- These 6 materials carry **no DIFFUSE/AMBIENT** — only `NAME`, `MATERIAL_TEXTURE` and an + **unhandled tag 0x0027** (12 B = {0.5,0.5,0.5}; only 41 uses in 1618 BMFs, likely SPECULAR + [T4]). `bgfload` therefore falls back to the gray placeholder `0xFFB0B0B8`; force WHITE for + these unlit text plates. And `decodeBSL` emits **alpha=255** for mono slices, so the plate + draws as an OPAQUE black rectangle — a billboard needs black→alpha-0 keying (which is exactly + what the 2D path's A4R4G4B4 `0xFFFF/0x0000` upload does). + ## Key Relationships - Detailed spec: `docs/BGF_FORMAT.md`. - Consumed by: [[rendering]] (the D3D9 draw path), [[locomotion]] (skeleton parts). diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 6adc5da..7fa98e0 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -543,8 +543,43 @@ and every instrument is now live [T2]:** wired task #38): lock requires a target AND your own HUD's host zone damage < **0.75** (a shot-up targeting computer loses lock) AND the targeted zone's damage < **1.0** (whole-mech target checks zone 0 — so a wreck's dead zone can't re-lock). The hotbox stays visible without - lock (box = HotBoxVector, ring = Lock — separate signals). The binary also hangs the PNAMEx.bgf - player-name mesh on the 3D marker here (3D chain still deferred). + lock (box = HotBoxVector, ring = Lock — separate signals). The SAME lock-change block drives the + PNAMEx.bgf **TARGET NAME PLATE** — the target's callsign floating under the crosshair. + **RECONSTRUCTED + RIG-VERIFIED 2026-07-24 (was "3D chain deferred"); awaiting live playtest.** + Mechanism, all [T1] from the Execute disassembly + (`reference/decomp/reticle_execute_004cdcf0.disasm.txt`) — the ONLY two functions that touch the + plate fields are the ctor @004cc40c and Execute @004cdcf0: + - **Mesh table** = `playerNameObject[]` at `this+0x2e8` (PNAME1 lands at +0x2ec), loaded + part_014.c:4430-4444; the array is 12 slots (`MAX_PLAYER_NAMES`), only 8 populated. + - **Selection** (@004cec98): `target_mech+0x190` (the owning BTPlayer) → `player+0x1e0` + (`playerBitmapIndex`) indexes that table **1-based** (the binary biases the base instead of + decrementing; no bounds check). Same 1-based egg `bitmapindex` the Comm pilot list uses. + - **Placement** (@004cdede): re-placed EVERY frame the aim moves — identity → scale 0.12 uniform + → translate `(K·reticlePos.x, K·reticlePos.y − 0.08, −1.0)`, `K` = the x87 long double + @0x4cee64 = **0.35714286 = 1/2.8** (the same 2.8 projection constant the hotbox uses). So the + plate **tracks the aim point**, 0.08 eye-space below it — NOT pinned under screen centre. + - **Visibility** = the **LOCK** attribute (the `Scalar*` at `this+0x184`, cached +0x188; + @004cebf9-@004cec47 branches to the hide path when it reads 0) AND the target having an owning + player. Reticle Off or simple-X (PrimaryHudOn clear) also hides it (@004cdd75/@004cddc1). + - **Art**: the plate is a bare 1.0×0.25 quad (4:1) whose material `bmap:name12_mtl` etc. + (content/VIDEO/MAT/BMAP.BMF) UV-addresses one shared 128×64 bitslice texmap — + 4 materials × 2 v-halves = the 8 player slots. `content/VIDEO/TEX/BMAP.BSL` ships **baked + "PLAYER 1..8" fallback art** (decoded 2026-07-24), but the pod OVERWRITES those texels at + runtime with the egg's 1bpp CALLSIGN rasters — the original source is commented out at + `engine/MUNGA_L4/L4VIDEO.cpp:5682-5710` (`FlushBitSliceTexture` → + `dpl_TexmapTexels2D(texmap, storage, 128, 64, 4)`, with the tell-tale warning "textures for + player names not defined"). So the authentic plate shows the **operator-set callsign**, with + "PLAYER n" only as the no-bitmap fallback. Material colour is a neutral (0.5,0.5,0.5) — the + plate is grey-white, NOT phosphor green like the reticle glyphs. + - **PORT**: same placement expressed in reticle units (÷K ⇒ 0.224 below the aim point, plate + 0.336×0.084), drawn as the target player's egg callsign texture + (`dpl2d_DrawTexturedRect` + `BTGetPlayerNameTexture` + `BTReticleTargetPlate`). + **Two PNAME consumers exist** — this reticle plate (PNAME only), and `FUN_00454a70` + (class key 0x46) = the engine's **`CameraShipHUDRenderable`**, the camera/broadcast seat's + followed-name banner + ranking window, which uses PNAME **and** PLACE1-8 as sorted + `[ordinal][callsign]` rows and is **already live** as screen quads (see [[multiplayer]] camera + seat). Its 1995 source is likewise commented out in `L4VIDRND.cpp:2562-2876` — the literal + `dpl_*` calls, a Rosetta stone for the binary's node API. - **Simple-X mode** (PrimaryHudOn off, mask 0x20): the minimal reticle — a small green cross (±0.02..0.08 arms) riding the aim translate (ctor @4689-4705 [T1]) — swapped for the full HUD by Draw's state switch. @@ -563,7 +598,8 @@ and every instrument is now live [T2]:** This recovery also CONFIRMS the HUD attr-table ids 4/5/6/8/0xA/0xB/0xC/0xD name↔use pairings (hud.hpp had flagged them uncertain; the OFFSETS were re-based 2026-07-19 — the table @5110b8 starts with id 3 FlickerRate@0x1D8, so Rotation=0x1DC…CompassHeading=0x214 Scalar; hud.hpp/ -CLASSMAP corrected, names/ids unchanged). Deferred: PNAME1-8.bgf 3D marker chain. (The canopy shell +CLASSMAP corrected, names/ids unchanged). The PNAME1-8.bgf target NAME PLATE is RECONSTRUCTED +(2026-07-24, §Lock ring above) — no longer deferred. (The canopy shell is now authentic and shows by default — see [[cockpit-view]]; `BT_HIDE_COCKPIT=1` hides it.) Player CALLSIGN labels (kill/damage feed, radar/target tags, score display) are 1bpp name diff --git a/context/open-questions.md b/context/open-questions.md index 19bc316..cfa0773 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -117,7 +117,29 @@ New unaccounted functionality no prior list knew (addresses verified absent from (hysteresis + key-repeat), no caller found; the one hole in "gauge system complete". Settle via vtable lookup (plausibly a config/roster scroller). - **FUN_00454a70** (193 ln) — the PNAME1-8/PLACE1-8.bgf MP name-billboard loader (the known - "MP target identification" gap's exact function). + "MP target identification" gap's exact function). ✅ **DECODED 2026-07-24 (not yet ported) + [T1/T2]** — it is the **MP SCOREBOARD/leaderboard**, built by the L4VIDEO video-resource + factory for **resource type 0x46** (part_008.c:2902-2911, gated `res+0x28 & 0xc == 0`, alloc + 0xbc). All 16 plates are ONE flat textured quad each (4 verts / 1 PCONN quad / no LOD chunk / + no normals): PNAME = 1.0×0.25 m, PLACE = 0.5×0.25 m, both planar Z=0, UNIT=metre, authored + 30-Nov-1995. **PNAME1-8 are literally the words "PLAYER 1".."PLAYER 8"**; **PLACE1-8 are the + rank ordinals "1ST".."8TH"** — a row reads `[PLACE][PNAME]` because the PLACE node is + translated x=−0.75 and spans ±0.25, so it ABUTS the PNAME plate's x∈[−0.5,+0.5] exactly. + Update fn **FUN_004553ac** re-parents each player's PNAME plate into row node `pnameGeo[rank]` + (rank from the per-player attr at `player+0x1cc`, players indexed by `player+0x1e0`), so the + rows SORT live. Row pitch = **0.24** (double @0x004551b8), rows DCS scale 0.12 @ (0.22,0.16,−0.5), + root DCS scale (0.2,0.2,1.0) @ (0,−0.15,−0.5). Materials/atlas map + the runtime-composite + mechanism: [[bgf-format]] §"The PNAME/PLACE name-plate atlas". The separate single-PNAME + consumer is `BTReticleRenderable` @004cc40c (no PLACE) — the in-cockpit designator label, + now **RECONSTRUCTED (2026-07-24, rig-verified)**: see [[gauges-hud]] §Lock ring. + The port's ranking display already ships the **2D** equivalent (egg ordinal+callsign bitmaps, + `CameraShipHUDRenderable::Render`), so this 3D chain is redundant for the scoreboard. + NOTE on the "PLAYER n" art: those baked words are the SHIPPED FALLBACK. The pod overwrites the + atlas texels at runtime with the egg's 1bpp callsign rasters — the original source survives + commented out at `engine/MUNGA_L4/L4VIDEO.cpp:5682-5710` (`FlushBitSliceTexture` → + `dpl_TexmapTexels2D(…,128,64,4)` + the warning "textures for player names not defined"). So an + authentic plate reads the player's CALLSIGN; "PLAYER n" only appears when the egg carries no + name bitmap. [T1] - ✅ **Explosion::SplashDamage @0042fad0 — RECONSTRUCTED (task #62, 2026-07-13) [T1/T2].** ONE call site, **Missile::Perform (FUN_004bef78, part_013.c:10097)**, in the collision branch (fires on ANY missile impact — mover OR world), gated on `missile+0x360`, radius = `missile+0x364`. **Radius source @@ -266,9 +288,11 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR executed as a D24S8 stencil cut; visible canopy = hull-minus-apertures ([[cockpit-view]] §FINAL). All 8 mechs render connected dark frames with clear viewports (the intermediate "frameless bhk1/loki/vulture/avatar" reading was wrong); thor matches the pod footage exactly. - Eye placement verified exact per-mech incl. Thor's +1.13 offset cockpit ([[cockpit-view]]). (b) the **3D marker + Eye placement verified exact per-mech incl. Thor's +1.13 offset cockpit ([[cockpit-view]]). (b) ~~the **3D marker chain** ([0x31c]/[0x320] dpl objects + the PNAME1-8.bgf player-name meshes over the locked - target — matters for MP). (c) the compass rotation SIGN is derived-not-verified [T3] (turn + target — matters for MP)~~ **DONE 2026-07-24: the target NAME PLATE is reconstructed + + rig-verified (the target's callsign under the crosshair, lock-gated) — [[gauges-hud]] §Lock + ring.** (c) the compass rotation SIGN is derived-not-verified [T3] (turn left → the stem should swing right; flip the sin sign in Draw if footage disagrees). - **✅ StatusMessagePool / MessageBoard — LIVE (2026-07-12) [T1/T2].** Decoded: FUN_0042e580 = the engine `Player::AddStatusMessage` (chain add at +0x1e4); FUN_0042e5a0 = `StatusMessageUpdate` diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 68a8204..d760224 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -9936,6 +9936,30 @@ void DPLRenderer::LoadNameBitmaps() LoadOrdinalBitmaps(); } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// BT: the RETICLE TARGET NAME PLATE's texture (the 1995 PNAMEx.bgf plate art). +// The plate meshes are quads UV-mapped onto a shared BITMAP material -- e.g. +// PNAME1.BGF and PNAME2.BGF both reference `bmap:name12_mtl` and differ only in +// their u-half (verified: the two files differ in 4 UV bytes) -- i.e. the plate +// shows the player's CALLSIGN raster, which on our side is exactly this +// per-player 128x32 name texture (4:1, the plate quad's 1.0 x 0.25 aspect). +// These load for EVERY mission from StartImplementation, so a cockpit session +// has them, not just a camera seat. The reticle TU (btl4vid.cpp) carries no +// d3d9 types, so hand the texture back opaquely. index0 is 0-BASED +// (playerBitmapIndex - 1). +// +void *BTGetPlayerNameTexture(int index0) +{ + DPLRenderer *renderer = + (l4_application != 0) ? l4_application->GetVideoRenderer() : 0; + if (renderer == 0 || index0 < 0 || index0 >= MAX_PLAYER_NAMES) + { + return 0; + } + return (void *)renderer->GetNameTexture(index0); +} + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // void diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index ffb97b2..0dbcc3e 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -1386,7 +1386,9 @@ static const float kRetTickMajor = 0.016f; // [0x82] static const float kRetBotX = -0.25f; // [0x84] bottom heading-ladder x0 static const float kRetBotY = 0.35f; // [0x85] heading ladder y static const float kRetBotSpan = 0.5f; // [0x86] -static const float kRetCaret = 0.02f; // _DAT_004cd7f4 (caret triangle size) [T3] +static const float kRetCaret = 0.025f; // _DAT_004cd7f4 (caret triangle size) + // [T1] read from the binary 2026-07-24 + // (was a 0.02f guess -- 20% small) static const int kRetTicksR = 13; // [9] right ladder tick count static const int kRetTicksB = 21; // [0xb] bottom ladder tick count static const float kRetMaxRange = 1200.0f; // ctor param 11 (0x44960000) @@ -1789,9 +1791,10 @@ void // the LOCK RING [0xb7] + its SPIN [0x9d] (Execute @4cebf9-4cee54 [T1]): // while a target is locked the ring+cross (subB9) draws at the reticle - // frame centre, spinning 4 degrees per frame; unlocked it clears. (The - // binary also hangs the PNAMEx.bgf player-name mesh on the 3D marker here - // -- the 3D marker chain remains deferred.) + // frame centre, spinning 4 degrees per frame; unlocked it clears. The SAME + // lock-change block drives the PNAMEx.bgf player NAME PLATE (the target's + // callsign under the crosshair) -- reconstructed at the end of this + // function, no longer deferred. { extern int gBTHudLockState; const int locked = (gBTHudLockState == 2); // the Lock attr rule, not just a target @@ -1856,6 +1859,93 @@ void extern int gBTHudPrimary; dpl2d_ExecuteList(gBTHudPrimary ? masterList : simpleXList, device); } + + // + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // THE TARGET NAME PLATE (PNAME1-8.bgf) -- the floating callsign under the + // crosshair. Reconstructed from the reticle Execute disassembly + // (reference/decomp/reticle_execute_004cdcf0.disasm.txt) [T1]: + // + // @004cdede, every frame while the full HUD is up: + // dpl_IdentityDCS (plate); + // dpl_ScaleDCS (plate, 0.12f, 0.12f, 0.12f); + // dpl_TranslateDCS(plate, K*reticlePos.x + -0.0f, + // K*reticlePos.y + -0.08f, -1.0f); + // dpl_FlushDCS (plate); + // K = the x87 long double @0x4cee64 = 0.35714286 = 1/2.8 -- the SAME + // 2.8 projection constant the hotbox uses, i.e. K converts the reticle's + // -1..+1 screen space into eye-space extents at z = -1. + // @004cec98: the plate's MESH is the target player's (see + // BTReticleTargetPlate in mech4.cpp), visibility 1; no owning player + // -> visibility 0. + // @004cdd75 / @004cddc1: reticle Off, or On-but-simple-X (PrimaryHudOn + // clear) -> visibility 0 and the cached target cleared. + // @004cebf9-@004cec47 -- THE LOCK GATE: the block is entered on a change + // of EITHER the target (Reticle+0x1c) or the LOCK attribute (the + // Scalar* at this+0x184, cached at +0x188), and when that lock value + // reads 0 it branches to the hide path (@004ced87). So the plate needs + // a real fire-control LOCK, not merely something under the crosshair -- + // the same `gBTHudLockState == 2` rule the lock RING uses below. + // + // So the plate TRACKS THE AIM POINT (it is not fixed under screen centre), + // sits 0.08 eye-space units below it, and shows the TARGET'S CALLSIGN. + // + // PORT: identical placement expressed in reticle units -- dividing the + // eye-space offsets by K (i.e. x2.8) puts the plate centre at + // (reticlePos.x, reticlePos.y +/- 0.08*2.8 = 0.224) + // spanning the plate quad's own 1.0 x 0.25 extents (content/VIDEO/GEO/ + // PNAME1.BGF, verified) scaled by 0.12 and back-converted: 0.336 x 0.084. + // The art is the player's callsign raster, exactly as the 1995 plate's + // `bmap:name12_mtl` material supplied it -- here the egg's 128x32 name + // texture (4:1 == the quad's aspect) via BTGetPlayerNameTexture. + // + { + extern int gBTHudPrimary; + extern int gBTHudLockState; // 2 == authentic LOCK + extern int BTReticleTargetPlate(void); // mech4.cpp + extern void *BTGetPlayerNameTexture(int index0); // L4VIDEO.cpp + extern void dpl2d_DrawTexturedRect(struct IDirect3DDevice9 *device, + void *texture, float cx, float cy, float w, float h, + unsigned long argb); + extern float gBTAimX, gBTAimY; + + const float kPlateScale = 0.12f; // dpl_ScaleDCS, uniform + const float kPlateW = 1.0f; // the PNAMEx quad's x extent + const float kPlateH = 0.25f; // ... and its y extent (4:1) + const float kInvK = 2.8f; // 1/K -- eye-space -> reticle + const float kPlateDY = 0.08f * kInvK; // = 0.224 below the aim point + // The plate's own material colour: content/VIDEO/MAT/BMAP.BMF gives + // `name12_mtl` (and each sibling) a NEUTRAL (0.5, 0.5, 0.5) -- the pod's + // plate is a grey-white label, NOT phosphor green like the reticle + // glyphs. 0.5 * 255 = 128. + const unsigned long kPlateARGB = 0xFF808080; + + const int plate = (gBTHudPrimary && gBTHudLockState == 2) + ? BTReticleTargetPlate() : 0; + if (plate > 0) + { + void *texture = BTGetPlayerNameTexture(plate - 1); + if (texture != 0) + { + // reticle y is +DOWN on screen (dpl2d MapY), eye-space y is + // +UP, so the binary's -0.08 eye offset is +kPlateDY here. + dpl2d_DrawTexturedRect(device, texture, + gBTAimX, gBTAimY + kPlateDY, + kPlateW * kPlateScale * kInvK, + kPlateH * kPlateScale * kInvK, + kPlateARGB); + } + if (getenv("BT_HUD_LOG")) + { + static int s_plog = 0; + if ((s_plog++ % 120) == 0) + DEBUG_STREAM << "[hud] name plate: bmp=" << plate + << " tex=" << (texture != 0) + << " at (" << gBTAimX << "," << (gBTAimY + kPlateDY) + << ")\n" << std::flush; + } + } + } } // diff --git a/game/reconstructed/dpl2d.cpp b/game/reconstructed/dpl2d.cpp index 1ab9352..eccf066 100644 --- a/game/reconstructed/dpl2d.cpp +++ b/game/reconstructed/dpl2d.cpp @@ -572,3 +572,108 @@ void dpl2d_ExecuteList(dpl2d_DISPLAY *list, IDirect3DDevice9 *device) 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); + + device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(VertexTex)); + + 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); +} diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 1565cab..31280ca 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -7103,3 +7103,57 @@ void BTCommitLookState(void *mech_v, int look_state) << " rearFiring=" << m->rearFiring << " yaw=" << yaw << " pitch=" << pitch << "\n" << std::flush; } + +// +//############################################################################# +// BTReticleTargetPlate -- the reticle NAME PLATE's player index (Gitea #43-b) +//############################################################################# +// +// The 1995 reticle renderable (BTReticleRenderable, ctor @004cc40c) loads +// PNAME1-8.bgf into a mesh table at this+0x2e8 and, every frame in Execute, +// picks one for the plate hung under the crosshair (disasm @004cec98 [T1]): +// +// edx = [this + 0x1e0] // the reticle's TARGET entity +// eax = edx ? [edx + 0x190] : 0 // mech+0x190 == the owning player +// if (eax) { // a piloted target +// edx = [eax + 0x1e0] // player+0x1e0 == playerBitmapIndex +// mesh = [this + edx*4 + 0x2e8] // 1-BASED: index 1 -> +0x2ec = PNAME1 +// dpl_SetInstanceObject(plate, mesh); +// dpl_SetInstanceVisibility(plate, 1); dpl_FlushInstance(plate); +// } else if (showing) { visibility 0; flush; } +// +// So the plate identifies the TARGET by its egg `bitmapindex` -- the same +// 1-based key the Comm pilot list and the radar labels use. This bridge lives +// in a complete-Mech TU (Reticle + Mech + Player all complete) and returns the +// 1-based index, or 0 for the binary's "hide the plate" case. +// +// GUARDED DEVIATION [T2, marked]: the binary reads `target+0x190` on ANY target +// entity; our compiled layouts differ per class (the databinding rule), so the +// mech test is explicit -- the same `IsDerivedFrom(Mech::ClassDerivations)` +// gate emitter.cpp uses. Our world-pick can target terrain and structures, +// which the 1995 pick could not, so this guard is load-bearing here. +// +int BTReticleTargetPlate(void) +{ + if (application == 0) + { + return 0; + } + Entity *view = application->GetViewpointEntity(); + if (view == 0 || !view->IsDerivedFrom(*Mech::GetClassDerivations())) + { + return 0; + } + Entity *target = ((Mech *)view)->targetReticle.targetEntity; + if (target == 0 || !target->IsDerivedFrom(*Mech::GetClassDerivations())) + { + return 0; + } + Player *owner = ((Mech *)target)->GetPlayerLink(); + if (owner == 0) + { + return 0; + } + int index = owner->playerBitmapIndex; + return (index > 0 && index <= 8) ? index : 0; +}