diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 4a5d68c..0cddb36 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -77,6 +77,18 @@ Identify a class at runtime with `sub->GetClassID()` (== `sub+4`). [T1] - **Heat leaf:** `PoweredSubsystem : HeatSink : HeatableSubsystem : MechSubsystem` (Emitter/PPC/Sensor/Myomers/weapons chain here). [T1] - **Watcher branch:** `Torso/Gyroscope/Searchlight/ThermalSight/HUD : PowerWatcher : HeatWatcher : MechSubsystem`; `AmmoBin : HeatWatcher`. Uses a 0xC connection + 0x54 alarm (NOT the 4/8-byte heat-leaf types). [T1] +### Derivation-tag addresses (`FUN_0041a1a4(obj, tag)` = IsDerivedFrom) +| Tag | Class | Evidence | +|---|---|---| +| **0x511830** | **MechWeapon**::ClassDerivations | The reticle AddWeapon loop (part_014.c:5386) hard-aborts on missing WeaponRange/PipPosition/… attrs for every member; part_012.c:9344 counts ready weapons; the Mech ctor roster at this[0x1ef] (`weaponRoster`, ex-mislabel "poweredSubsystems") ORs weapon `capabilityFlags`@+0x334. [T1] | +| 0x50f4bc | PoweredSubsystem | btl4gau2 gauge wave. [T1] | +| 0x50e3ec | HeatableSubsystem | btl4gaug widget gate. [T1] | +| 0x50e604 | HeatSink | btl4gaug widget gate. [T1] | +| 0x51155c | roster at Mech this[0x1eb] (`heatableSubsystems` label) | tag ≠ 0x50e3ec — the label is UNVERIFIED [T4]; arbitrate before relying on it. | +| 0x50e4fc | roster at Mech this[499] (`damageableSubsystems` label) | [T3] | +| 0x50bdb4 | Mech | emitter.cpp destroyed-check. [T1] | +| 0x5121a8 | ProjectileWeapon-family (checked after 0x511830; reads ammo state @+0x364) | [T3] | + --- ## 3. Mech struct offsets (`Mech*`, binary layout) diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 94a981a..2a2ad3a 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -84,7 +84,24 @@ see [[open-questions]] `mech+0x190`. The **MessageBoard** feed needs **StatusMes stub) + the per-player status queue. `SeekVoltageGraph`'s 4 Seek* attrs are a cluster-child (not config-called). [T2] +## Cockpit HUD reticle (main screen, inside view) — LIVE +`BTReticleRenderable` (0x358 bytes, ctor @004cc40c) draws over the finished 3D frame in cockpit +view only, via the recovered **dpl2d** 2D display-list API (recorders @0x487f34-0x488630; opcode +map + coordinate model in `phases/phase-02-dpl2d-reticle.md`; port: `game/reconstructed/dpl2d.cpp`). +Geometry is the ctor's hardcoded calibration (originX 0.35, originY 0.25, scaleY 0.5, right range +ladder 0..1200 m, bottom heading tape, center cross + dot; tick ladders via FUN_004cd938). The +range caret binds to the live target range (`BTSetHudTargetRange`, fed by mech4's targeting step). +**Weapon pips:** the build loop (part_014.c:5386) registers EVERY subsystem +`IsDerivedFrom(0x511830 = MechWeapon)` — lasers, PPCs AND missile launchers (BLH = 7 pips) — via +`AddWeapon` @004cdac0 (verified store map in btl4vid.hpp), reading WeaponRange/PipPosition/ +PipColor/PipExtendedRange/TargetWithinRange/SimulationState(attrs 1+0x1c)/RearFiring. Pip A (lit, +authored PipColor) when TargetWithinRange, else the dark ring B. [T2 geometry+wiring] +The binary's per-frame `Execute` @004cdcf0 sits in an UN-EXPORTED decomp gap — the Draw dynamics +(caret slide, pip lit/dark; lock rings/turn arrows built but static) are best-effort [T3]. +Deferred: PNAME1-8.bgf 3D pip meshes + the 3D marker chain; the `blx_cop` canopy shell renders +black and is hidden by name filter (`BT_INSIDE_COCKPIT=1` shows it) — see [[open-questions]]. + ## Key Relationships -- Full history: `docs/GAUGE_COMPOSITE.md`. +- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`. - Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state. - Renders on: [[pod-hardware]] MFD surfaces. diff --git a/context/open-questions.md b/context/open-questions.md index 322f470..3c4b5ac 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -51,6 +51,15 @@ authentic path scoped. these are MODE flags — in the basic test mission the inert Myomers / dormant valve is likely AUTHENTIC (advanced damage off); wiring makes them RESPOND when a mission enables the mode, not necessarily change the basic-mission behavior. The MessageBoard feed is separate (StatusMessagePool, below). +- **Cockpit HUD leftovers (task #35 residue, 2026-07-08)** — the reticle + 7 weapon pips are LIVE + (see [[gauges-hud]] + `phases/phase-02-dpl2d-reticle.md`), but three pieces remain: (a) the + **blx_cop canopy shell** renders BLACK (untextured/unlit in our inside view) so it's hidden by a + name filter — `BT_INSIDE_COCKPIT=1` shows it; the authentic look needs its material/lighting + path; (b) the **PNAME1-8.bgf 3D pip meshes** + the 3D target-designator marker chain (the ctor's + 15-arg form binds 8 HUD-subsystem attrs we bypass); (c) the binary per-frame **Execute + @004cdcf0** is in an un-exported decomp gap — the Draw dynamics (lock rings, off-screen turn + arrows, blink states, the 3 cached "SimulationState" constants 2/3/1) are best-effort [T3]; + a Ghidra re-export of the 0x4cdcf0 region would let us transcribe it properly. - **StatusMessagePool (NULL stub, btstubs.cpp:62)** — the per-player status-message queue. BTPlayer+0x1dc is never populated → MessageBoard is empty (authentic for bring-up). What resolves it: wire the pool + decode `AddStatusMessage @0042e580` + the `Player__StatusMessage` diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 0e5f200..0d3739f 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -7539,6 +7539,13 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte BTDrawPfx(mDevice, &viewTransform, (float)dT); } + // The BT targeting reticle / weapon pips (2D screen space, cockpit view + // only -- the dpl2d layer; see game/reconstructed/dpl2d.cpp). + { + extern void BTDrawReticle(struct IDirect3DDevice9 *device); + BTDrawReticle(mDevice); + } + // // And don't forget particles too // diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index 6c29c62..bd5a3aa 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -35,6 +35,9 @@ // #include +#if !defined(EMITTER_HPP) +# include // Emitter/PPC (the reticle's per-weapon pip wiring) +#endif #pragma hdrstop #if !defined(BTL4VID_HPP) @@ -525,6 +528,20 @@ HierarchicalDrawComponent* SwapToWreck(entity); } + // + // The TARGETING RETICLE (the main-view HUD): built for the player's mech, + // per the 1996 inside-view path (@part_014.c:5127-5158 constructs the + // 0x358 BTReticleRenderable, then :5390-5436 registers one pip per weapon + // from its TargetWithinRange / WeaponRange / PipPosition / PipColor / + // PipExtendedRange / SimulationState attributes). Drawn by BTDrawReticle + // in the cockpit view only. + // + if (buildDebugChaseCamera) + { + extern BTReticleRenderable *BTBuildReticle(Entity *mech); + BTBuildReticle(entity); + } + // DEV: BT_START_INSIDE=1 begins in the cockpit view (also exercises the // inside-skeleton swap headlessly). if (buildDebugChaseCamera && getenv("BT_START_INSIDE")) @@ -937,6 +954,347 @@ void BTSwapMechToWreck(Entity *victim) } +// +//############################################################################# +// BTReticleRenderable -- ctor (@004cc40c) + Draw (Execute @004cdcf0 is in an +// un-exported gap; the draw dynamics here are [T3], the GLYPHS are [T1]) +//############################################################################# +// +// The live reticle instance (the player's; drawn by BTDrawReticle in the +// cockpit view only). +// +static BTReticleRenderable *gBTReticle = 0; +extern Scalar gBTHudRangeStorage; // live target range (defined below) +static int gBTHudInside = 0; // cockpit view live (set by SetViewInside) + +// the dpl2d rasteriser (dpl2d.cpp; device type opaque here) +extern void dpl2d_ExecuteList(dpl2d_DISPLAY *list, struct IDirect3DDevice9 *device); + +void BTSetHudTargetRange(Scalar range) { gBTHudRangeStorage = range; } +void BTSetHudInside(int inside) { gBTHudInside = inside; } + +// +// FUN_004cd938 -- the tick-LADDER builder: `count` ticks stepped along an axis +// (dir 0/1 = +x/-x travel with vertical ticks, 2/3 = +y/-y with horizontal +// ticks), a MAJOR tick (halfMajor) every (majorEvery+1)th, minor otherwise. +// +static void + BTReticleTickLadder(dpl2d_DISPLAY *list, int count, int majorEvery, + float span, float x0, float y0, int dir, float halfMinor, float halfMajor) +{ + float step = span / (float)(count - 1); + float tickMinX = 0, tickMinY = 0, tickMajX = 0, tickMajY = 0; + float stepX = 0, stepY = 0; + switch (dir) + { + case 0: stepX = step; tickMajY = halfMajor; tickMinY = halfMinor; break; + case 1: stepX = -step; tickMajY = halfMajor; tickMinY = halfMinor; break; + case 2: stepY = step; tickMajX = halfMajor; tickMinX = halfMinor; break; + case 3: stepY = -step; tickMajX = halfMajor; tickMinX = halfMinor; break; + } + float x = x0, y = y0; + int untilMajor = 0; + dpl2d_OpenLines(list); + for (int i = 0; i < count; ++i) + { + if (untilMajor == 0) + { + dpl2d_AddPoint(list, x + tickMajX, y + tickMajY); + dpl2d_AddPoint(list, x - tickMajX, y - tickMajY); + untilMajor = majorEvery; + } + else + { + dpl2d_AddPoint(list, x + tickMinX, y + tickMinY); + dpl2d_AddPoint(list, x - tickMinX, y - tickMinY); + --untilMajor; + } + x += stepX; y += stepY; + } + dpl2d_CloseLines(list); +} + +// +// The authentic calibration constants (the binary ctor's own values). +// +static const float kRetOriginX = 0.35f; // [0x7f] right range-ladder x +static const float kRetOriginY = 0.25f; // [0x80] ladder bottom y +static const float kRetScaleY = 0.5f; // [0x81] ladder span +static const float kRetTickMinor = 0.008f; // [0x83] +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 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) + +BTReticleRenderable::BTReticleRenderable(Entity *entity, Scalar *range_attr) + : VideoRenderable(entity, VideoRenderable::Dynamic) +{ + weaponCount = 0; + originX = kRetOriginX; originY = kRetOriginY; + scaleY = kRetScaleY; biasX = kRetTickMajor; + maxRange = kRetMaxRange; minRange = 0.0f; + rangeScale= maxRange - minRange; // [0x8d] -- AddWeapon divides by it + rangeAttr2= range_attr; + + masterList = dpl2d_NewDisplayList(); + aimDotList = dpl2d_NewDisplayList(); + rangeCaretR = dpl2d_NewDisplayList(); + rangeCaretB = dpl2d_NewDisplayList(); + headingList = dpl2d_NewDisplayList(); + bottomAnchor = dpl2d_NewDisplayList(); + leftArrow = dpl2d_NewDisplayList(); + rightArrow = dpl2d_NewDisplayList(); + crossList = dpl2d_NewDisplayList(); + subB6 = dpl2d_NewDisplayList(); + subB7 = dpl2d_NewDisplayList(); + subB8 = dpl2d_NewDisplayList(); + subB9 = dpl2d_NewDisplayList(); + subBA = dpl2d_NewDisplayList(); + + // + // The MASTER list (@4511-4601, faithfully transcribed). Colours: green + // 0.75 for the frame, yellow for the range caret; widths 1/2/3. + // + dpl2d_DISPLAY *m = masterList; + dpl2d_Begin(m, 1); + dpl2d_SetLineWidth(m, 1.0f); + dpl2d_FullScreenClip(m); + dpl2d_SetColor(m, 0.75f, 0.0f, 0.0f); + dpl2d_CallList(m, crossList); // [0xa1] target-box slot (empty until lock) + dpl2d_SetColor(m, 0.0f, 0.75f, 0.0f); + dpl2d_CallList(m, aimDotList); // [0x9a] the aim translate + dpl2d_OpenPolypoint(m); // centre dot + dpl2d_AddPoint(m, 0.0f, 0.0f); + dpl2d_ClosePolypoint(m); + dpl2d_OpenLines(m); // inner cross (gap at centre) + dpl2d_AddPoint(m, 0.04f, 0.0f); dpl2d_AddPoint(m, 0.10f, 0.0f); + dpl2d_AddPoint(m, -0.04f, 0.0f); dpl2d_AddPoint(m, -0.10f, 0.0f); + dpl2d_AddPoint(m, 0.0f, 0.04f); dpl2d_AddPoint(m, 0.0f, 0.10f); + dpl2d_AddPoint(m, 0.0f, -0.04f); dpl2d_AddPoint(m, 0.0f, -0.10f); + dpl2d_CloseLines(m); + dpl2d_SetLineWidth(m, 3.0f); + dpl2d_OpenLines(m); // heavy outer cross + dpl2d_AddPoint(m, 0.10f, 0.0f); dpl2d_AddPoint(m, 0.16f, 0.0f); + dpl2d_AddPoint(m, -0.10f, 0.0f); dpl2d_AddPoint(m, -0.16f, 0.0f); + dpl2d_AddPoint(m, 0.0f, 0.10f); dpl2d_AddPoint(m, 0.0f, 0.16f); + dpl2d_AddPoint(m, 0.0f, -0.10f); dpl2d_AddPoint(m, 0.0f, -0.16f); + dpl2d_CloseLines(m); + dpl2d_SetLineWidth(m, 1.0f); + // the RIGHT range ladder (13 ticks up the right side, dir 3 = -y travel) + BTReticleTickLadder(m, kRetTicksR, 1, kRetScaleY, + kRetOriginX, kRetOriginY, 3, kRetTickMinor, kRetTickMajor); + // the range CARET (yellow triangle; its called list holds the live + // translate the Draw rebuilds each frame from the target range) + dpl2d_PushState(m); + dpl2d_SetLineWidth(m, 2.0f); + dpl2d_SetColor(m, 0.75f, 0.75f, 0.0f); + dpl2d_CallList(m, rangeCaretR); + dpl2d_OpenPolyline(m); + dpl2d_AddPoint(m, kRetOriginX - kRetTickMajor, kRetOriginY); + dpl2d_AddPoint(m, kRetOriginX - kRetCaret - kRetTickMajor, kRetOriginY + kRetCaret); + dpl2d_AddPoint(m, kRetOriginX - kRetCaret - kRetTickMajor, kRetOriginY - kRetCaret); + dpl2d_ClosePolyline(m); + dpl2d_PopState(m); + // the BOTTOM heading ladder (21 ticks across, dir 0 = +x travel) + BTReticleTickLadder(m, kRetTicksB, 1, kRetBotSpan, + kRetBotX, kRetBotY, 0, kRetTickMinor, kRetTickMajor); + // the heading carets (over/under triangles at the ladder centre, shifted + // by the called heading translate) + { + float cx = kRetBotX + kRetBotSpan * 0.5f; // _DAT_004cd7f8 = 0.5 [T1] + dpl2d_PushState(m); + dpl2d_SetLineWidth(m, 2.0f); + dpl2d_SetColor(m, 0.75f, 0.75f, 0.0f); + dpl2d_CallList(m, rangeCaretB); + dpl2d_OpenPolyline(m); + dpl2d_AddPoint(m, cx, kRetBotY - kRetTickMajor); + dpl2d_AddPoint(m, cx + kRetCaret, kRetBotY - kRetCaret - kRetTickMajor); + dpl2d_AddPoint(m, cx - kRetCaret, kRetBotY - kRetCaret - kRetTickMajor); + dpl2d_ClosePolyline(m); + dpl2d_OpenPolyline(m); + dpl2d_AddPoint(m, cx, kRetBotY + kRetTickMajor); + dpl2d_AddPoint(m, cx + kRetCaret, kRetBotY + kRetCaret + kRetTickMajor); + dpl2d_AddPoint(m, cx - kRetCaret, kRetBotY + kRetCaret + kRetTickMajor); + dpl2d_ClosePolyline(m); + dpl2d_PopState(m); + } + // bottom-anchor group: a small arc + drop line under the centre + dpl2d_PushState(m); + dpl2d_CallList(m, bottomAnchor); + dpl2d_Circle(m, 0.0f, 0.0f, 0.03f, 0); // 0x3cf5c28f-family arc [T3 radius] + dpl2d_OpenLines(m); + dpl2d_AddPoint(m, 0.0f, -0.04f); + dpl2d_AddPoint(m, 0.0f, -0.005f); + dpl2d_CloseLines(m); + dpl2d_CallList(m, subBA); + dpl2d_PopState(m); + dpl2d_CallList(m, subB6); + dpl2d_CallList(m, headingList); + dpl2d_CallList(m, subB7); + dpl2d_End(m); + dpl2d_Compile(m); + + // + // The green centre-ring sub-lists ([0xb8]/[0xb9]) -- the lock indicator + // rings the binary Execute swaps in on target state. + // + dpl2d_Begin(subB8, 1); + dpl2d_SetColor(subB8, 0.0f, 0.75f, 0.0f); + dpl2d_Circle(subB8, 0.0f, 0.0f, 0.12f, 0); + dpl2d_End(subB8); dpl2d_Compile(subB8); + dpl2d_Begin(subB9, 1); + dpl2d_SetColor(subB9, 0.0f, 0.75f, 0.0f); + dpl2d_Circle(subB9, 0.0f, 0.0f, 0.12f, 0); + dpl2d_OpenLines(subB9); + dpl2d_AddPoint(subB9, 0.14f, 0.0f); dpl2d_AddPoint(subB9, 0.10f, 0.0f); + dpl2d_AddPoint(subB9, -0.14f, 0.0f); dpl2d_AddPoint(subB9, -0.10f, 0.0f); + dpl2d_AddPoint(subB9, 0.0f, 0.14f); dpl2d_AddPoint(subB9, 0.0f, 0.10f); + dpl2d_AddPoint(subB9, 0.0f, -0.14f); dpl2d_AddPoint(subB9, 0.0f, -0.10f); + dpl2d_CloseLines(subB9); + dpl2d_End(subB9); dpl2d_Compile(subB9); + + // + // The off-screen turn ARROWS ([0x9f]/[0xa0]) -- big width-12 chevrons at + // x = +-1.2..1.5 (screen edges), half-alpha; positioned/enabled by the + // un-exported Execute -> built but not drawn statically. + // + dpl2d_Begin(leftArrow, 1); + dpl2d_SetLineWidth(leftArrow, 12.0f); + dpl2d_OpenLines(leftArrow); + dpl2d_AddPoint(leftArrow, -1.2f, -0.30011f); + dpl2d_AddPoint(leftArrow, -1.5f, 0.0f); + dpl2d_AddPoint(leftArrow, -1.5f, 0.0f); + dpl2d_AddPoint(leftArrow, -1.2f, 0.30011f); + dpl2d_CloseLines(leftArrow); + dpl2d_SetLineWidth(leftArrow, 1.0f); + dpl2d_End(leftArrow); dpl2d_Compile(leftArrow); + dpl2d_Begin(rightArrow, 1); + dpl2d_SetLineWidth(rightArrow, 12.0f); + dpl2d_OpenLines(rightArrow); + dpl2d_AddPoint(rightArrow, 1.2f, 0.30011f); + dpl2d_AddPoint(rightArrow, 1.5f, 0.0f); + dpl2d_AddPoint(rightArrow, 1.5f, 0.0f); + dpl2d_AddPoint(rightArrow, 1.2f, -0.30011f); + dpl2d_CloseLines(rightArrow); + dpl2d_SetLineWidth(rightArrow, 1.0f); + dpl2d_End(rightArrow); dpl2d_Compile(rightArrow); + + // empty placeholders (filled per frame / on lock) + dpl2d_Begin(crossList, 1); dpl2d_End(crossList); dpl2d_Compile(crossList); + dpl2d_Begin(aimDotList, 1); dpl2d_End(aimDotList); dpl2d_Compile(aimDotList); + dpl2d_Begin(rangeCaretR, 1); dpl2d_End(rangeCaretR); dpl2d_Compile(rangeCaretR); + dpl2d_Begin(rangeCaretB, 1); dpl2d_End(rangeCaretB); dpl2d_Compile(rangeCaretB); + dpl2d_Begin(headingList, 1); dpl2d_End(headingList); dpl2d_Compile(headingList); + dpl2d_Begin(bottomAnchor,1); dpl2d_End(bottomAnchor);dpl2d_Compile(bottomAnchor); + dpl2d_Begin(subB6, 1); dpl2d_End(subB6); dpl2d_Compile(subB6); + dpl2d_Begin(subB7, 1); dpl2d_End(subB7); dpl2d_Compile(subB7); + dpl2d_Begin(subBA, 1); dpl2d_End(subBA); dpl2d_Compile(subBA); +} + +BTReticleRenderable::~BTReticleRenderable() +{ + if (gBTReticle == this) + gBTReticle = 0; +} + +// +// Per-frame draw. Rebuild the live translate lists (the range caret slides +// along its ladder with the target range -- the ctor's translate(0, +// -scaleY * rangeFraction) at @4608-4613), then draw the master, then each +// weapon's pip: the LIT pip (list A) while its within-range flag is up, the +// dark ring (list B) otherwise. [T3 dynamics / T1 geometry] +// +void + BTReticleRenderable::Draw(struct IDirect3DDevice9 *device) +{ + // the range caret translate, from the live target range + Scalar range = (rangeAttr2 != 0) ? *rangeAttr2 : 0.0f; + if (range < minRange) range = minRange; + if (range > maxRange) range = maxRange; + Scalar frac = (range - minRange) / (maxRange - minRange); + Scalar six[6] = { 1, 0, 0, 1, 0, -scaleY * frac }; + dpl2d_Begin(rangeCaretR, 1); + dpl2d_ConcatMatrix(rangeCaretR, six); + dpl2d_End(rangeCaretR); + dpl2d_Compile(rangeCaretR); + + dpl2d_ExecuteList(masterList, device); + + for (int i = 0; i < weaponCount; ++i) + { + int inRange = 0; + if (withinRangePtr[i] != 0) // [0x18c] TargetWithinRange + inRange = (*withinRangePtr[i] != 0); + dpl2d_ExecuteList(inRange ? pipDisplayListA[i] : pipDisplayListB[i], device); + } +} + +// +// The render-loop hook: draw the player's reticle over the finished 3D frame, +// COCKPIT VIEW ONLY (the 1996 build constructed it only for insideEntity). +// +void BTDrawReticle(struct IDirect3DDevice9 *device) +{ + if (gBTReticle != 0 && gBTHudInside) + gBTReticle->Draw(device); +} + +// +// Build the player's reticle + register one pip per weapon (the 1996 wiring: +// @part_014.c:5386-5436). The binary's gate is IsDerivedFrom(0x511830) -- +// MechWeapon::ClassDerivations [T1: the loop hard-aborts on missing +// WeaponRange/PipPosition/... attrs, which only MechWeapons publish] -- so +// EVERY mounted weapon registers a pip: lasers, PPCs AND missile launchers. +// Per weapon it reads WeaponRange / PipPosition / TargetWithinRange / +// PipExtendedRange / PipColor / SimulationState (attrs 1 + 0x1c) / RearFiring; +// mode 1 = front (RearFiring==0), which every BLH weapon is. +// +BTReticleRenderable *BTBuildReticle(Entity *mech) +{ + if (gBTReticle != 0) + return gBTReticle; + + extern void BTSetHudTargetRange(Scalar range); // (self; range fed by mech4) + extern Scalar gBTHudRangeStorage; // defined below + + gBTReticle = new BTReticleRenderable(mech, &gBTHudRangeStorage); + + Mech *m = (Mech *)mech; + for (int wi = 0; wi < m->GetSubsystemCount(); ++wi) + { + Subsystem *ws = m->GetSubsystem(wi); + if (ws == 0) + continue; + if (!ws->IsDerivedFrom(MechWeapon::ClassDerivations)) // 0x511830 + continue; + MechWeapon *wp = (MechWeapon *)ws; + RGBColor pc = wp->PipColor(); + float r = (float)pc.Red, g = (float)pc.Green, b = (float)pc.Blue; + if (r < 0.0f || g < 0.0f || b < 0.0f) { r = 0.78f; g = 0.08f; b = 0.02f; } + gBTReticle->AddWeapon( + wp->WeaponRange(), + wp->PipPosition(), + (int *)wp->WithinRangePtr(), + wp->PipExtendedRange(), + r, g, b, + (int *)wp->SimulationStatePtr(), // attr 0x1c "SimulationState" + 2, 3, + (int *)wp->SimulationStatePtr(), // attr 1 "SimulationState" + 1, + 1 /* front (RearFiring==0 on every BLH weapon) */); + } + DEBUG_STREAM << "[hud] reticle built: " << gBTReticle->WeaponCount() + << " weapon pip(s) registered\n" << std::flush; + return gBTReticle; +} + +// the live target-range storage the reticle's caret binds to +Scalar gBTHudRangeStorage = 0.0f; + // //############################################################################# // BTReticleRenderable::AddWeapon @@ -951,19 +1309,19 @@ void BTSwapMechToWreck(Entity *victim) // void BTReticleRenderable::AddWeapon( - Scalar weapon_range, - int pip_position, - StateIndicator *within_range_attr, - int extended_range, - Scalar pip_red, - Scalar pip_green, - Scalar pip_blue, - StateIndicator *sim_state, - int arg9, // constant 2 from caller - int arg10, // constant 3 from caller - StateIndicator *sim_state2, - int arg12, // constant 1 from caller - int weapon_mode) // 1 front / 2 rear + Scalar weapon_range, // param_2 + int pip_position, // param_3 + int *within_range_value, // param_4 TargetWithinRange + int extended_range, // param_5 PipExtendedRange + Scalar pip_red, // param_6..8 PipColor + Scalar pip_green, + Scalar pip_blue, + int *alarm_value, // param_9 weapon attr 0x1c + int const2, // param_10 (constant 2) + int const3, // param_11 (constant 3) + int *sim_state_value, // param_12 weapon attr 1 + int const1, // param_13 (constant 1) + int weapon_mode) // param_14 (1 front / 2 rear) { if (this->weaponCount /* [0x38] */ >= 10) { @@ -973,18 +1331,20 @@ void int n = this->weaponCount; // - // Record this weapon's control attributes in the parallel arrays. + // Record this weapon's control state in the parallel arrays -- the exact + // store order of @004cdac0 (part_014.c:4827-4837). The binary read the + // two attr objects' values at +0x14; the port takes value pointers direct. // - this->pipColor[n] /* [0x64+n*4] */ = pip_red; - this->withinRangeAttr[n] /* [0x8c+n*4] */ = within_range_attr; - this->simState[n] /* [0xb4+n*4] */ = sim_state; - this->simState2Ptr[n] /* [0x130+n*4] */ = sim_state2; - this->simState2Value[n] /* [0xdc+n*4] */ = *(int *)(sim_state2 + 0x14); - this->arg12Ptr[n] /* [0x158+n*4] */ = arg12; - this->arg12Value[n] /* [0x104+n*4] */ = *(int *)(arg12 + 0x14); - this->rangeAttr[n] /* [0x18c+n*4] */ = (Scalar *)within_range_attr; // param_4 - this->rangeCache[n] /* [0x1b4+n*4] */ = *(Scalar *)within_range_attr; - this->weaponMode[n] /* [0x3c+n*4] */ = weapon_mode; + this->stateConst3[n] /* [0x64+n*4] = param_11 */ = const3; + this->stateConst2[n] /* [0x8c+n*4] = param_10 */ = const2; + this->stateConst1[n] /* [0xb4+n*4] = param_13 */ = const1; + this->alarmAttr[n] /* [0x130+n*4] = param_9 */ = alarm_value; + this->alarmCache[n] /* [0xdc+n*4] = *(param_9+0x14) */ = *alarm_value; + this->simStateAttr[n] /* [0x158+n*4] = param_12 */ = sim_state_value; + this->simStateCache[n] /* [0x104+n*4] = *(param_12+0x14) */ = *sim_state_value; + this->withinRangePtr[n] /* [0x18c+n*4] = param_4 */ = within_range_value; + this->withinRangeCache[n] /* [0x1b4+n*4] = *param_4 */ = *within_range_value; + this->weaponMode[n] /* [0x3c+n*4] = param_14 */ = weapon_mode; dpl2d_DISPLAY *pip_list = dpl2d_NewDisplayList(); // FUN_00487f34 dpl2d_DISPLAY *arc_list = dpl2d_NewDisplayList(); @@ -1352,6 +1712,12 @@ void DEBUG_STREAM << "[view] " << (inside ? "COCKPIT eyepoint" : "external chase") << (mCamera == mEyeCockpit ? " (cockpit live)" : " (chase live)") << "\n" << std::flush; + + // the HUD overlay draws in the cockpit view only + { + extern void BTSetHudInside(int inside); + BTSetHudInside(mCamera == mEyeCockpit ? 1 : 0); + } } // diff --git a/game/reconstructed/btl4vid.hpp b/game/reconstructed/btl4vid.hpp index 447730d..0e4761f 100644 --- a/game/reconstructed/btl4vid.hpp +++ b/game/reconstructed/btl4vid.hpp @@ -453,75 +453,117 @@ class BTReticleRenderable: public VideoRenderable { public: - BTReticleRenderable( // @004cc40c + // + // PORT ctor. The binary ctor (@004cc40c, 15 args) received the mech's + // TargetReticle attribute + 8 HUD-subsystem attributes; its CALIBRATION + // however is its own hardcoded constants (0.35/0.25/0.5/0.016/0.008 -- + // the pip-row origin/scale and the two tick-ladder frames), which this + // port ctor reproduces directly. The one live input the glyphs consume + // is the target RANGE (drives the range-scale caret); it binds to a + // Scalar the targeting step updates. The 3D marker chain + the + // PNAME1-8.bgf pip meshes (the floating 3D target designator) are a + // separate deferred piece -- see phases/phase-02-dpl2d-reticle.md. + // + BTReticleRenderable( // @004cc40c (port signature) Entity *entity, - int execution_type, - Reticle **my_reticle, - dpl_VIEW *this_view, - void *pip_position_template, - void *pip_color_template, - void *range_template, - void *range_lo_template, - void *range_hi_template, - Scalar max_range, - int flag, - void *sim_template_a, - void *sim_template_b, - void *sim_template_c); + Scalar *range_attr); ~BTReticleRenderable(); + // + // Per-frame draw (the binary Execute @004cdcf0 is in an UN-EXPORTED + // gap; the dynamics here are [T3]: draw the master reticle, then each + // weapon's lit pip when its within-range flag is up, else the dark + // ring). Called from the render loop, cockpit view only. + // + void + Draw(struct IDirect3DDevice9 *device); + + int WeaponCount() const { return weaponCount; } + // // Append a weapon range/pip marker to the reticle. @004cdac0 + // Parameter order/count is the binary's; the two attr-object args + // (param_9 / param_12 -- weapon attrs 0x1c and 1, BOTH named + // "SimulationState" per the caller's abort strings @51d526/51d577) + // are adapted to direct value pointers (the binary read the value at + // attr+0x14; we have no 1995 attr-object layout -- databinding trap). + // The caller passes the constants 2 / 3 / 1 verbatim. // void AddWeapon( - Scalar weapon_range, - int pip_position, - StateIndicator *within_range_attr, - int extended_range, - Scalar pip_red, - Scalar pip_green, - Scalar pip_blue, - StateIndicator *sim_state, - int arg9, - int arg10, - StateIndicator *sim_state2, - int arg12, - int weapon_mode); - - void - Execute(); + Scalar weapon_range, // param_2 + int pip_position, // param_3 + int *within_range_value, // param_4 TargetWithinRange + int extended_range, // param_5 PipExtendedRange + Scalar pip_red, // param_6..8 PipColor + Scalar pip_green, + Scalar pip_blue, + int *alarm_value, // param_9 weapon attr 0x1c + int const2, // param_10 (constant 2) + int const3, // param_11 (constant 3) + int *sim_state_value, // param_12 weapon attr 1 + int const1, // param_13 (constant 1) + int weapon_mode); // param_14 (1 front / 2 rear) protected: // - // Per-weapon parallel arrays (cap 10). + // Per-weapon parallel arrays (cap 10). Slot mapping is the verified + // store order of AddWeapon @004cdac0 (part_014.c:4827-4837) [T1]; the + // three caller constants and the cached attr values are consumed by the + // UN-EXPORTED Execute @004cdcf0, so their semantics are [T4]. // int weaponCount; // +0x38 - int weaponMode[10]; // +0x3c - Scalar pipColor[10]; // +0x64 - StateIndicator *withinRangeAttr[10]; // +0x8c - StateIndicator *simState[10]; // +0xb4 - int simState2Value[10]; // +0xdc - int arg12Value[10]; // +0x104 - StateIndicator *simState2Ptr[10]; // +0x130 - int arg12Ptr[10]; // +0x158 - Scalar *rangeAttr[10]; // +0x18c - Scalar rangeCache[10]; // +0x1b4 + int weaponMode[10]; // +0x3c param_14 + int stateConst3[10]; // +0x64 param_11 (3) + int stateConst2[10]; // +0x8c param_10 (2) + int stateConst1[10]; // +0xb4 param_13 (1) + int alarmCache[10]; // +0xdc *param_9 at add time + int simStateCache[10]; // +0x104 *param_12 at add time + int *alarmAttr[10]; // +0x130 param_9 + int *simStateAttr[10]; // +0x158 param_12 + int *withinRangePtr[10]; // +0x18c param_4 + int withinRangeCache[10]; // +0x1b4 *param_4 at add time dpl2d_DISPLAY *pipDisplayListB[10]; // +0x288 dpl2d_DISPLAY *pipDisplayListA[10]; // +0x2b0 // - // Calibrated reticle geometry. + // Calibrated reticle geometry (the ctor's authentic constants). // - Scalar originX; // +0x1fc - Scalar originY; // +0x200 - Scalar scaleY; // +0x204 - Scalar biasX; // +0x208 - Scalar maxRange; // +0x22c - Scalar minRange; // +0x230 - Scalar rangeScale; // +0x234 + Scalar originX; // +0x1fc [0x7f] = 0.35 + Scalar originY; // +0x200 [0x80] = 0.25 + Scalar scaleY; // +0x204 [0x81] = 0.5 + Scalar biasX; // +0x208 [0x82] = 0.016 + Scalar maxRange; // +0x22c [0x8b] = 1200 + Scalar minRange; // +0x230 [0x8c] = 0 + Scalar rangeScale; // +0x234 [0x8f] + + // + // The reticle's own display lists (the ctor's [0x97..0xa1, 0xb6..0xba] + // set, as transcribed) + the live range binding. + // + Scalar *rangeAttr2; // [0x89] the target-range source + dpl2d_DISPLAY *masterList; // [0x98] the composed reticle + dpl2d_DISPLAY *aimDotList; // [0x9a] centre aim dot + dpl2d_DISPLAY *rangeCaretR; // [0x9b] right-ladder caret + dpl2d_DISPLAY *rangeCaretB; // [0x9c] bottom-ladder carets + dpl2d_DISPLAY *headingList; // [0x9d] heading sub-list + dpl2d_DISPLAY *bottomAnchor; // [0x9e] bottom-ladder anchor + dpl2d_DISPLAY *leftArrow; // [0x9f] left range arrow + dpl2d_DISPLAY *rightArrow; // [0xa0] right range arrow + dpl2d_DISPLAY *crossList; // [0xa1] the dotted cross + dpl2d_DISPLAY *subB6; // [0xb6..0xba] aux sub-lists + dpl2d_DISPLAY *subB7; + dpl2d_DISPLAY *subB8; // [0xb8] green centre ring + dpl2d_DISPLAY *subB9; // [0xb9] green ring + cross + dpl2d_DISPLAY *subBA; }; +// +// The live reticle draw hook (called from the engine render loop after the +// 3D scene; draws only in the cockpit view). +// +extern void BTDrawReticle(struct IDirect3DDevice9 *device); + // //############################################################################# // BTL4VideoRenderer diff --git a/game/reconstructed/btstubs.cpp b/game/reconstructed/btstubs.cpp index 1be3aad..4565d5c 100644 --- a/game/reconstructed/btstubs.cpp +++ b/game/reconstructed/btstubs.cpp @@ -389,26 +389,8 @@ BTLightConnection::BTLightConnection( Entity *entity, int, dpl_INSTANCE *, int, int *) : BTRenderableBase(entity) {} -//===========================================================================// -// BTReticleRenderable ctor/dtor/Execute (AddWeapon lives in btl4vid.cpp). -//---------------------------------------------------------------------------// -// TODO(bring-up): real ctor @004cc40c builds the reticle 2D display lists. -//===========================================================================// -BTReticleRenderable::BTReticleRenderable( - Entity *entity, int, Reticle **, dpl_VIEW *, - void *, void *, void *, void *, void *, Scalar, int, - void *, void *, void *) - : VideoRenderable(entity, VideoRenderable::Dynamic), weaponCount(0) -{ -} - -BTReticleRenderable::~BTReticleRenderable() -{ -} - -void BTReticleRenderable::Execute() -{ -} +// (BTReticleRenderable ctor/dtor/Draw now live in btl4vid.cpp -- the real +// @004cc40c reconstruction replaced the old bring-up stubs here.) //===========================================================================// // VideoComponent (legacy renderable base) -- declared in L4VIDRND.h but NOT diff --git a/game/reconstructed/dpl2d.cpp b/game/reconstructed/dpl2d.cpp index 881c074..cc03bb9 100644 --- a/game/reconstructed/dpl2d.cpp +++ b/game/reconstructed/dpl2d.cpp @@ -341,6 +341,16 @@ namespace Mat2x3 mat; }; + // + // COORDINATE MODEL (from the reticle's authored constants): centred origin, + // +x right, +y down, unit = half the viewport HEIGHT on both axes (keeps + // circles round; the reticle content spans roughly +-0.5). + // + inline float MapX(float x, float ox, float vw, float vh) + { return ox + vw * 0.5f + x * (vh * 0.5f); } + 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) { @@ -352,7 +362,7 @@ namespace { Vec2 p = st.mat.Apply(cmd.verts[i].x, cmd.verts[i].y); Vertex2D v; - v.x = ox + p.x * vw; v.y = oy + p.y * vh; + 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); } @@ -389,11 +399,11 @@ namespace } void ExecuteListInner(Dpl2dList *self, IDirect3DDevice9 *device, - ExecState st, float ox, float oy, float vw, float vh, int depth) + ExecState &st, std::vector &stack, + float ox, float oy, float vw, float vh, int depth) { if (self == 0 || depth > 8) return; - std::vector stack; Vertex2D ring[kCircleSegments + 2]; for (size_t i = 0; i < self->commands.size(); ++i) @@ -412,8 +422,12 @@ namespace 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, ox, oy, vw, vh, depth + 1); + st, stack, ox, oy, vw, vh, depth + 1); break; case Command::kPoints: case Command::kLineStrip: @@ -424,9 +438,9 @@ namespace case Command::kCircleOutline: { Vec2 p = st.mat.Apply(cmd.a, cmd.b); - const float cx = ox + p.x * vw; - const float cy = oy + p.y * vh; - const float r = cmd.c * vw; // width-relative -> stays round + const float cx = MapX(p.x, ox, vw, vh); + const float cy = MapY(p.y, oy, vh); + const float r = cmd.c * (vh * 0.5f); // height-unit -> stays round if (cmd.kind == Command::kCircleFill) { ring[0].x = cx; ring[0].y = cy; ring[0].z = 0.0f; @@ -497,8 +511,9 @@ void dpl2d_ExecuteList(dpl2d_DISPLAY *list, IDirect3DDevice9 *device) st.color = 0xFFFFFFFF; st.width = 1.0f; st.mat.Identity(); + std::vector stack; ExecuteListInner(self, device, - st, (float)vp.X, (float)vp.Y, (float)vp.Width, (float)vp.Height, 0); + st, stack, (float)vp.X, (float)vp.Y, (float)vp.Width, (float)vp.Height, 0); device->SetTexture(0, oldTex); if (oldTex) oldTex->Release(); diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 8f0d4af..be03fb6 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -678,7 +678,7 @@ Mech::Mech( Wword(0x1e8) = Wword(0x1e9) = Wword(0x1ea) = 0; heatableSubsystems.Construct(0); // FUN_004a4dab(this+0x1eb) - poweredSubsystems.Construct(0); // FUN_004a4df6(this+0x1ef) + weaponRoster.Construct(0); // FUN_004a4df6(this+0x1ef) damageableSubsystems.Construct(0); // FUN_004a4e41(this+499) for (int i = 0; i < 5; ++i) // FUN_0043ad4f x5 @@ -1033,12 +1033,12 @@ Mech::Mech( heatableSubsystems.Add((Subsystem *)s); } } - for (int id = 2; id < subsystemCount; ++id) // powered roster + for (int id = 2; id < subsystemCount; ++id) // weapon roster (0x511830 = MechWeapon) { SubProxy *s = (SubProxy *)subsystemArray[id]; if (s != 0 && s->IsDerivedFrom(0x511830)) { - poweredSubsystems.Add((Subsystem *)s); + weaponRoster.Add((Subsystem *)s); stateFlags |= s->capabilityFlags; // |= *(s+0x334) -> Wword(0x104) } } diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index f645d3c..f37d91f 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -408,7 +408,12 @@ struct ShotDescriptor ReconChain controllableSubsystems; // @0x65c this[0x197] (FUN_00427768) ReconChain watchedSubsystems; // @0x6bc this[0x1af] (FUN_00427768) ReconChain heatableSubsystems; // @0x7ac this[0x1eb] (class 0x51155c) - ReconChain poweredSubsystems; // @0x7bc this[0x1ef] (class 0x511830) + ReconChain weaponRoster; // @0x7bc this[0x1ef] (class 0x511830 = + // MechWeapon::ClassDerivations -- the + // reticle AddWeapon loop @part_014.c:5386 + // asserts weapon attrs on every member. + // Was mislabeled "poweredSubsystems"; + // PoweredSubsystem is 0x50f4bc.) ReconChain damageableSubsystems; // @0x7cc this[499] (class 0x50e4fc) int weaponCount; // @0x448 this[0x112] diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 079c669..3ab1131 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -2331,6 +2331,14 @@ void MECH_TARGET_ENTITY(this) = gEnemyMech; // mech+0x388 (HasActiveTarget gate) MECH_TARGET_SUBIDX(this) = -1; // mech+0x38c whole-mech target + // feed the HUD's range caret (the reticle's right-ladder pointer) + { + extern void BTSetHudTargetRange(Scalar range); + float hddx = enemyPos.x - localOrigin.linearPosition.x; + float hddz = enemyPos.z - localOrigin.linearPosition.z; + BTSetHudTargetRange((Scalar)sqrtf(hddx*hddx + hddz*hddz)); + } + float ddx = enemyPos.x - localOrigin.linearPosition.x; float ddy = enemyPos.y - localOrigin.linearPosition.y; float ddz = enemyPos.z - localOrigin.linearPosition.z; diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 30311a3..09600d0 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -787,7 +787,7 @@ void { // TODO(look-eyepoint): the commit block below writes the eyepoint pose at // binary offsets whose Mech-member mapping is CONFLICTED (mech+0x360 is - // claimed by mechName, +0x410 by stateFlags, +0x7bc by poweredSubsystems -- + // claimed by mechName, +0x410 by stateFlags, +0x7bc by weaponRoster -- // and the old draft even routed them through the TORSO pointer). Arbitrate // those labels against the raw before enabling; the port has no eyepoint // consumer yet. Look-state SELECTION above stays live (this-only). diff --git a/game/reconstructed/mechweap.hpp b/game/reconstructed/mechweap.hpp index febc3c2..99ceda7 100644 --- a/game/reconstructed/mechweap.hpp +++ b/game/reconstructed/mechweap.hpp @@ -241,6 +241,18 @@ class CockpitHud; void MuzzlePoint(Point3D &p) { GetMuzzlePoint(p); } const RGBColor &PipColor() const { return pipColor; } + // + // Reticle/HUD accessors (the pip registration wiring @part_014.c:5390: + // per weapon the 1996 build reads TargetWithinRange / WeaponRange / + // PipPosition / PipColor / PipExtendedRange / SimulationState). + // + Scalar WeaponRange() const { return weaponRange; } + int PipPosition() const { return pipPosition; } + int PipExtendedRange() const { return pipExtendedRange; } + void *WithinRangePtr() { return &targetWithinRange; } + void *WeaponAlarmPtr() { return &weaponAlarm; } + void *SimulationStatePtr() { return &simulationState; } // attr 1 / 0x1c ("SimulationState") + protected: // @004b9864 -- build an aim orientation (atan2 of a direction vector). diff --git a/phases/phase-02-dpl2d-reticle.md b/phases/phase-02-dpl2d-reticle.md index 99e0dfd..49ffb16 100644 --- a/phases/phase-02-dpl2d-reticle.md +++ b/phases/phase-02-dpl2d-reticle.md @@ -1,7 +1,16 @@ # Phase 02 — dpl2d API recovery + BTReticleRenderable reconstruction (task #35) **Goal:** the main-view HUD (targeting reticle + up to 10 per-weapon pips). -**Status:** API fully recovered [T1]; ctor/Execute transcription + wiring IN PROGRESS. +**Status:** COMPLETE (steps 1-5 live, verified 2026-07-08). dpl2d opcode model + CallList +inline-include semantics in `game/reconstructed/dpl2d.cpp`; ctor glyph program + FUN_004cd938 +tick-ladder helper transcribed in `btl4vid.cpp`; per-weapon registration via the binary's real +gate `IsDerivedFrom(0x511830 = MechWeapon::ClassDerivations)` (part_014.c:5386) — **7 pips** on +the BLH (3 lasers + 2 PPCs + 2 MissileLaunchers), AddWeapon @004cdac0 store map verified +(part_014.c:4827-4837; both state attrs are named "SimulationState", strings @51d526/51d577). +Draw hook = `BTDrawReticle(device)` after the 3D scene, cockpit view only. The binary Execute +@004cdcf0 is an UN-EXPORTED gap → Draw dynamics are best-effort [T3] (caret slide from live +target range; pip A lit on TargetWithinRange else ring B). Step 6 (blx_cop canopy shell) and the +PNAME1-8.bgf 3D pip meshes remain DEFERRED — tracked in context/open-questions.md. ## The recovered dpl2d recorder API (libDPL 2D display lists) [T1] diff --git a/scratchpad/bgfbounds.py b/scratchpad/bgfbounds.py index 6008e9a..76007d0 100644 --- a/scratchpad/bgfbounds.py +++ b/scratchpad/bgfbounds.py @@ -40,7 +40,7 @@ def bounds(path): % (path.split('\\')[-1], nverts, lo[0], hi[0], lo[1], hi[1], lo[2], hi[2])) base = r'C:\git\bt411\content\VIDEO\GEO\%s' -for f in ['ERMLASER.BGF', 'PPC.BGF', 'LLASER.BGF', 'SLASER.BGF', 'MLASER.BGF']: +for f in ['BLX_COP.BGF']: try: bounds(base % f) except Exception as e: