From ca988db9ed70d1003bb46e4bd27d13f3071e480d Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 13 Aug 2026 07:26:15 -0500 Subject: [PATCH] radar: draw the MAP GEOMETRY -- staticEntities was empty on two stacked gates Playtester DOS-binary comparison: our radar showed blips + wedge but NO map outlines (the DOS reference draws the whole arena). MapDisplay::DrawStatic was always a faithful, working drawer of the rtype-18 GaugeImage outlines (the same records docs/MAPS.html renders) -- it drew nothing because staticEntityList was empty, from TWO stacked port regressions; fixing either alone is insufficient: 1. GaugeRenderer::RebuildEntityGrid (per frame) Clear()ed BOTH grids and refilled from the DYNAMIC host iterators only -- the 528 static-flagged (0x20C) UnscalableTerrain map placements, which carry most arena outlines (arenall: 59 static vs 3 dynamic), were structurally excluded, and the wipe also destroyed anything the interest feed contributed. 2. The notify feed's static gate was transcribed as Terrain::GetClassDerivations() -- but the shipped content has ZERO Terrain records (census: 1008x UnscalableTerrain + 789x CulturalIcon, both deriving from Entity in our headers), so the predicate was dead code and rejected 100% of map geometry. @0x4e6ef8 was almost certainly UnscalableTerrain's derivation (or the shipped machine's MUNGA derived UnscalableTerrain from Terrain). [T3 on which; the corrected gate is what the content requires.] FIX: - staticEntities is now PERSISTENT: filled ONCE per mission inside RebuildEntityGrid (AllEntityIterator; keep UnscalableTerrain-derived entities carrying a rtype-18 GaugeImage -- covers the static walls AND the dynamic CulturalIcon props the 2026-07-12 phantom-red-pip fix routed here). Deterministic whether or not the interest feed delivers. - Per-frame RebuildEntityGrid now rebuilds only the MOVING grid (Mover filter + dead-mech skip unchanged). - Lifecycle: cleared + refill-armed at LinkToEntity (the pod process hosts many missions); the gate-corrected remove-notify prunes destroyed statics; the gate-corrected add-notify is guarded pre-fill so nothing double-adds. - BT_RADAR_LOG=1 prints '[radar] static grid filled: N'. Verified live (glass, DEV.EGG): fill reports 326 map entities; structure outlines now draw on the radar at the correct zoom; blips + sweep wedge unregressed. KB: GAUGE_COMPOSITE row 24 + the Phase-4 staticEntities deferral resolved. Co-Authored-By: Claude Opus 4.8 --- docs/GAUGE_COMPOSITE.md | 5 ++- engine/MUNGA/GAUGREND.cpp | 67 +++++++++++++++++++++++++++------ engine/MUNGA/GAUGREND.h | 5 +++ game/reconstructed/btl4grnd.cpp | 21 ++++++++++- 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/docs/GAUGE_COMPOSITE.md b/docs/GAUGE_COMPOSITE.md index fc484dd..51e8bf4 100644 --- a/docs/GAUGE_COMPOSITE.md +++ b/docs/GAUGE_COMPOSITE.md @@ -629,7 +629,8 @@ attributes published on Emitter+Myomers — see the AUDIT finding A addendum bel Byte-exact re-base of the PoweredSubsystem:HeatSink LEAF (Sensor/Emitter/Myomers) → radarPercent heat penalty (delete the sensor.cpp:287-288 guard — ✅ LANDED: the guard is deleted and the leaf is byte-exact, sensor.cpp:407-413) + the raw-offset cluster reads (partially locked since — MechWeapon byte-exact); -radar pip/name symbology + staticEntities (task #17, under reconstruction 2026-07-12); +radar pip/name symbology + staticEntities (task #17: pips landed the real L4GaugeImage path; +✅ staticEntities RESOLVED 2026-08-13, branch radar-static-geometry — see row 24); CycleDisplayMode→ModeManager mask (✅ RESOLVED 2026-07-19, Gitea #6 [T2]: @4d1ae4 relabeled the NotifyOfDisplayModeChange override (vtbl+0x4C) and made virtual; desktop 'N'/pad RightThumb → CycleDisplayModeNow; secondary schematic swap dama→crit→heat pixel-verified via @@ -687,7 +688,7 @@ reconfigure/externalConfigure); parse-skip list EMPTY ([gskip]=0), all 50 attr b | 21 | Eng myomer page: seek-step lamp | Myomers@0x320 INFERRED (ctor @004c8df4 not in the decomp export) | marked in code | T3 | CORRECT (best-effort; flagged) | | 22 | Eng page: linked heat-sink number (hs+0x1d4 guarded raw) | PrepEngr numeric #2 | marked BEST-EFFORT (guarded) | T3 | CORRECT (guarded; flagged) | | 23 | sec: radar map (wedge/blips/scale) | Mech Radar* attrs; wedge ← Torso::currentTwist@0x1D8 (Gitea #1) | #1 [T2]; LIVE: blips + wedge render, zoom via SetTargetRange | T2 | CORRECT | -| 24 | sec: radar pip/name symbology | GetVideoObject/LookUpPip/GetNameID | task #17 under reconstruction — cross-blip stand-in | T3 | DEFERRED-FEED | +| 24 | sec: radar pip/name symbology + MAP GEOMETRY | GetVideoObject/LookUpPip/GetNameID; MapDisplay::DrawStatic ← staticEntities | pips: the real L4GaugeImage path landed (task #17). **Map geometry RESOLVED 2026-08-13** (branch radar-static-geometry, playtester DOS comparison): DrawStatic was always faithful but staticEntities was EMPTY — (1) the per-frame RebuildEntityGrid Clear()+dynamic-only refill structurally excluded the 528 static-flagged UnscalableTerrain placements (arenall: 59 static vs 3 dynamic), and (2) the notify gate was transcribed as Terrain, which matches ZERO shipped records (1008 UnscalableTerrain + 789 CulturalIcon derive from Entity — @0x4e6ef8 was almost certainly UnscalableTerrain's derivation). Fix: staticEntities is now persistent, filled once per mission (AllEntityIterator, UnscalableTerrain-derived + rtype-18 pip; `[radar] static grid filled: N` under BT_RADAR_LOG), reset at LinkToEntity, pruned by the gate-corrected remove notify. Verified live: 326 entities filled, arena outlines draw | T2 | CORRECT | | 25 | sec: headingPointer needle + numeric | YawPitchRoll decomposition (engine-convention port of euler[0]) | LIVE: 089→120 while turning under BT_GOTO | T2 | CORRECT | | 26 | sec: numericSpeed | LinearSpeed (abs(adv)/dt) | LIVE: 0 parked → 176-182 walking | T2 | CORRECT (units question stays open) | | 27 | sec: digitalClock MISSION TIME | engine mission clock (format enum arg) | LIVE: 09:19→06:04 counting | T0/T2 | CORRECT | diff --git a/engine/MUNGA/GAUGREND.cpp b/engine/MUNGA/GAUGREND.cpp index 56eaf30..b094d15 100644 --- a/engine/MUNGA/GAUGREND.cpp +++ b/engine/MUNGA/GAUGREND.cpp @@ -2798,6 +2798,7 @@ GaugeRenderer::GaugeRenderer(): { Test_Tell("GaugeRenderer::GaugeRenderer()\n"); Check_Pointer(this); + staticEntitiesFilled = False; // radar map geometry: filled per mission int i; @@ -3015,6 +3016,13 @@ void Test_Tell("GaugeRenderer::LinkToEntity(" << entity << ")\n"); + // RADAR MAP GEOMETRY (2026-08-13): a new mission link invalidates the + // persistent static grid -- clear it and let the next RebuildEntityGrid + // refill from the new mission's entities (the pod process hosts many + // missions back to back; stale pointers must not cross rounds). + staticEntities.Clear(); + staticEntitiesFilled = False; + // //-------------------------------------------------------------------- // Inform all gauges @@ -3613,7 +3621,6 @@ void Check(this); movingEntities.Clear(); - staticEntities.Clear(); if (application == NULL) { @@ -3629,13 +3636,18 @@ void // projectiles that AllEntityIterator would flood the grid with -- iterate the // dynamic masters (the local + AI/dummy vehicles) plus the dynamic replicants // (peer vehicles in multiplayer). - // CLASSIFICATION (the phantom-red-pip fix, 2026-07-12): the dynamic - // iterators ALSO surface non-Mover world entities (the map's CulturalIcon - // props, class 0x5E, register dynamic on this port) -- unsorted, they drew - // through the radar's MOVING loop as RED contacts ("multiple pips with one - // player"). Sort by Mover derivation: vehicles into the moving grid (red - // pips), everything else into the static grid (the dim silhouette layer - // DrawStatic draws) -- the follow-up the old note deferred. + // + // STATIC GRID SPLIT (2026-08-13, the missing-radar-map-geometry fix): the + // per-frame rebuild used to Clear() + refill staticEntities from the SAME + // dynamic iterators -- which structurally excluded the map's STATIC-flagged + // UnscalableTerrain placements (528 of the 1071 outlined map records; on the + // arena maps essentially every wall/building), so MapDisplay::DrawStatic had + // nothing to draw and the radar showed no map. staticEntities is now + // PERSISTENT: filled ONCE per mission below (every UnscalableTerrain-derived + // entity carrying a rtype-18 GaugeImage -- static AND dynamic, so the + // CulturalIcon props the 2026-07-12 phantom-red-pip fix routed here still + // arrive), pruned by NotifyOfBecomingUninterestingEntity, reset at + // LinkToEntity. The per-frame loops below rebuild only the MOVING grid. { HostManager::DynamicMasterEntityIterator master_iterator(host_manager); master_iterator.First(); @@ -3655,8 +3667,6 @@ void if (st != 2 && st != 9) movingEntities.Add(entity); } - else - staticEntities.Add(entity); } } { @@ -3671,10 +3681,43 @@ void if (st != 2 && st != 9) movingEntities.Add(entity); } - else - staticEntities.Add(entity); } } + + // + // THE ONE-TIME STATIC FILL. Authoritative regardless of the interest + // feed's health: walk EVERY entity once per mission and keep the + // UnscalableTerrain-derived ones that carry a gauge representation (the + // rtype-18 GaugeImage in their model list -- the same records + // docs/MAPS.html renders). The class gate is UnscalableTerrain, NOT + // Terrain: the shipped content has ZERO Terrain records -- its map + // geometry is 1008x UnscalableTerrain + 789x CulturalIcon (a subclass), + // and our headers derive UnscalableTerrain from Entity, so the old + // Terrain gate matched nothing (see the btl4grnd.cpp note on @0x4e6ef8). + // Clear() first so any pre-fill interest-feed adds can't duplicate. + // + if (!staticEntitiesFilled) + { + staticEntities.Clear(); + HostManager::AllEntityIterator all_iterator(host_manager); + Entity *entity; + int kept = 0; + while ((entity = all_iterator.ReadAndNext()) != NULL) + { + if (!entity->IsDerivedFrom(*UnscalableTerrain::GetClassDerivations())) + continue; + if (application->GetResourceFile()->SearchList( + entity->GetResourceID(), + ResourceDescription::GaugeImageStreamResourceType) == NULL) + continue; + staticEntities.Add(entity); + ++kept; + } + staticEntitiesFilled = True; + if (getenv("BT_RADAR_LOG")) + DEBUG_STREAM << "[radar] static grid filled: " << kept + << " map entities with gauge outlines\n" << std::flush; + } Check_Fpu(); } diff --git a/engine/MUNGA/GAUGREND.h b/engine/MUNGA/GAUGREND.h index c46933d..73ebe7e 100644 --- a/engine/MUNGA/GAUGREND.h +++ b/engine/MUNGA/GAUGREND.h @@ -673,6 +673,11 @@ protected: GraphicsPort *graphicsPort[maximumGraphicsPorts]; GaugeEntityList movingEntities; GaugeEntityArray staticEntities; + // RADAR MAP GEOMETRY (2026-08-13): staticEntities is PERSISTENT, filled + // ONCE per mission by RebuildEntityGrid's authoritative walk (and pruned by + // NotifyOfBecomingUninterestingEntity). Reset at LinkToEntity so a pod + // process that hosts many missions refills per round. + Logical staticEntitiesFilled; LampManager *lampManager; GaugeAlarmManager *gaugeAlarmManager; diff --git a/game/reconstructed/btl4grnd.cpp b/game/reconstructed/btl4grnd.cpp index 560c507..151e663 100644 --- a/game/reconstructed/btl4grnd.cpp +++ b/game/reconstructed/btl4grnd.cpp @@ -362,7 +362,21 @@ void { movingEntities.Add(entity); // FUN_004434f4, this+0x94 } - else if (entity->IsDerivedFrom(*Terrain::GetClassDerivations())) // 0x4e6ef8 + // + // GATE CORRECTED 2026-08-13 (the missing-radar-map-geometry fix): this + // was transcribed as Terrain::GetClassDerivations() -- but the shipped + // content has ZERO Terrain records (census: 1008x UnscalableTerrain + + // 789x CulturalIcon, both deriving from Entity in our headers), so the + // Terrain predicate was dead code and rejected 100% of map geometry. + // The binary's @0x4e6ef8 must have been UnscalableTerrain's derivation + // (or the shipped-machine MUNGA derived UnscalableTerrain from + // Terrain). [T3 on which; the corrected gate matches the content.] + // The add is guarded on the one-time fill (GAUGREND RebuildEntityGrid) + // not having run -- post-fill notifications would double-add, and the + // fill's Clear()+walk supersedes any pre-fill adds. + // + else if (!staticEntitiesFilled + && entity->IsDerivedFrom(*UnscalableTerrain::GetClassDerivations())) { staticEntities.Add(entity); // FUN_00443864, this+0xb0 } @@ -409,7 +423,10 @@ void { movingEntities.Remove(entity); // FUN_00443510, this+0x94 } - else if (entity->IsDerivedFrom(*Terrain::GetClassDerivations())) // 0x4e6ef8 + // Gate corrected 2026-08-13 to match the ADD side (see the note there): + // UnscalableTerrain, not Terrain. Post-fill this is the prune path for + // a destroyed/expiring static -- it leaves the persistent radar grid. + else if (entity->IsDerivedFrom(*UnscalableTerrain::GetClassDerivations())) { staticEntities.Remove(entity); // FUN_00443930, this+0xb0 }