#81 follow-ups: peer warp must not stomp the POV vortex; panel draw probe; mech3 ghost symbols

Two field observations from watching the 4-node stress, both run down:

1. "A respawn happened without the blue vortex" -- REAL.  The translocation
   effect is ONE global slot shared by the POV lifecycle and the world-anchored
   peer sphere (a port extension).  A peer's un-wreck arriving while the local
   pilot's own collapse/wait/expand was in flight overwrote gWarpPhase/gWarpPOV
   and killed the POV vortex.  Invisible before the respawn fix only because
   overlapping respawns barely existed; now they are routine.  Fix: the POV
   lifecycle owns the slot -- BTStartWarpEffect self-skips while it is active
   ([tloc] peer warp SKIPPED).  2-node bench: 20/20 POV collapse+expand pairs,
   9 peer spheres played, 11 correctly skipped; solo: 8/8 pairs unchanged.

2. "Comms panel counted no deaths" -- panel machinery CORRECT; the 4-node
   zeros were the bench's own CPU crush (4 core-pinned instances starved the
   PilotList to <0.6 Hz, so rows redrew minutes-stale).  Added the arbiter:
   [score] panel DRAW slot/pilot/kills/deaths edge log (BT_SCORE_LOG) -- the
   2-node rerun drew 0->9 / 0->11 live on both nodes, local AND replicated
   (SBMIRROR rows confirm owner->replicant tally flow on all 4 stress nodes).

Also: the mech3 offline-authoring stubs declared every <Subsystem>::DefaultData
as Entity__SharedData while the real statics are Simulation__SharedData (this
engine derives Entity FROM Simulation) -- ~20 ghost symbols /FORCE silently
resolved to garbage.  DefaultData half fixed (SubsystemDefaultData now returns
the true common base Simulation::SharedData); the CreateStreamedSubsystem stub
signatures remain wrong (nested SubsystemResource* + ResourceFile*), are
cold (no callers), and are tracked in open-questions + gotchas §6 stub-typedef
corollary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-07-30 10:10:29 -05:00
co-authored by Claude Fable 5
parent 830a976192
commit bfa1b04990
8 changed files with 106 additions and 15 deletions
+8
View File
@@ -879,6 +879,14 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
## Content build sub-project (low priority)
- Lab/other-build maps (`des`/`burnt`/`frstrm`) are source-only (`.map` in CONTENT/BT/MAPS/); would
need compiling into a RES via the DOS `btl4tool.exe`. The 8 RES maps cover testing.
- **Offline subsystem-authoring dispatch is un-linked (known, cold, 2026-07-30).** mech3.cpp's
`Mech::CreateSubsystemStream` dispatches to 20 per-class `CreateStreamedSubsystem` stubs whose
guessed signatures (`void*`/`int`) don't match the real modules (nested `<X>::SubsystemResource*`
+ trailing `ResourceFile*`, e.g. mechtech.cpp:407) — all 20 are /FORCE-silenced unresolved
externals. No callers exist, so it cannot crash today; before wiring any authoring tool through
it, convert each to a per-module bridge fn in a complete-type TU (gotchas §6 stub-typedef
corollary). The parallel `DefaultData` half of the table WAS the same trap and is fixed
(`Simulation__SharedData`).
## Key Relationships
- Feeds from: every subsystem/render topic (their deferral notes collect here).
+11
View File
@@ -121,6 +121,17 @@ mislanch.cpp's extern but not projweap.cpp's → first AUTOCANNON shot AV'd). **
bridge-signature change, `grep -rn "extern .*<name>"` and update every declaration; then grep
the fresh link output for the symbol name** — the pre-existing LNK2019 wall camouflages new
entries if you only eyeball it.
**Stub-typedef corollary (mech3 tool path, 2026-07-30):** a LOCAL stub TU that re-declares a
sibling class's STATIC MEMBER with the wrong typedef mangles to a **ghost symbol** — mech3.cpp
declared every `<Subsystem>::DefaultData` as `Mech::SharedData` (= `Entity__SharedData`) while
the real definitions are the inherited `Simulation__SharedData` (in THIS engine `Entity` derives
from `Simulation`; `Entity__SharedData : public Simulation::SharedData`, ENTITY3.h:9) → ~20
unresolved externals, all /FORCE-silenced, all cold (offline authoring dispatch has no callers).
Fixed for the DefaultData statics; the `CreateStreamedSubsystem` stub SIGNATURES are still wrong
(real ones take the class's NESTED `SubsystemResource*` + trailing `ResourceFile*`) and stay
unresolved-by-design until bridged per-module ([[open-questions]]). **Rule: when a "benign" LNK
wall exists, `tail` on the build output hides the fleet — always grep the FULL log; and verify a
stub's member TYPE against `dumpbin /symbols` of the defining obj, not against what looks right.**
**Duplicate-GLOBAL corollary (glass per-display windows, 2026-07-20):** a global DEFINED in two
libs (the 1995 headers declare free globals without `inline`/`extern`, so `application`,
`ghWnd`, … exist in BOTH `munga_engine` and `bt410_l4`) links under `/FORCE:MULTIPLE` with
+6 -1
View File
@@ -121,7 +121,12 @@ Local player's own death → `BTStartWarpCollapsePOV()` (btplayer.cpp `VehicleDe
(local-guarded). `BTWarpForceUnmask()` on mission-end / no-DropZones. **Peer warp IS wired + visible
(`160b78e`):** an observer sees a peer's death/respawn warp — the replicant un-wreck fires the
world-anchored `BTStartWarpEffect(x,y,z)` at the peer's position (mechdmg.cpp:1074, gated to
`ReplicantInstance`; `simulationState` rides every update-record header so the observer tracks the
`ReplicantInstance`; the effect machine is a SINGLE global slot, and since 2026-07-30 the local
pilot's own POV lifecycle OWNS it — `BTStartWarpEffect` self-skips while `gWarpPhase!=0 && gWarpPOV`
(`[tloc] peer warp SKIPPED`), because a peer's un-wreck landing mid-collapse/wait/expand used to
overwrite the machine and kill the POV vortex ("my respawn had no blue whirlwind"). Invisible
before the #81 respawn fix only because overlapping respawns barely existed; 2-node bench: 20/20
POV collapse+expand pairs, 9 peer spheres played, 11 correctly skipped; `simulationState` rides every update-record header so the observer tracks the
peer's state). The ONLY remaining nuance [T3, non-gating]: the observer's peer sphere is anchored to
the peer's WORLD position rather than the peer's authentic `DropZoneLocation` (that attribute isn't
replicated) — a fidelity refinement, not a missing effect.
+9
View File
@@ -27,6 +27,15 @@ one increment per death (13/13/15/15) — the DEATHS column the comms panel draw
kill-credit path evaluated all 56 deaths and correctly declined each self-kill (`NOCREDIT self=1`),
KILLS untouched by the fix (separate `ScoreMessage` path). Bench: `scratchpad/night6/mp4_stress.sh`
+ `content/MP4.EGG`.
**Two observations from watching the 4-node run, both run down (2026-07-30):** (1) *"comms panel
counted no deaths"* — the panel machinery is CORRECT (2-node rerun with a draw-value probe: both
rows on both nodes drew 0→9/0→11 live, local + replicated); the 4-node zeros were the artificial
load (4 core-pinned instances → PilotList Execute starved to <0.6 Hz, panel minutes-stale). Probe
stays in (`[score] panel DRAW`, BT_SCORE_LOG). (2) *"a respawn had no blue vortex"* — REAL and now
fixed: the warp effect is one global slot and a PEER's un-wreck sphere (`BTStartWarpEffect`) could
stomp the local pilot's own POV vortex mid-lifecycle; fixed by POV-priority self-skip
(btl4vid.cpp; `context/translocation-warp.md`). Pre-fix this never showed because overlapping
respawns barely existed — the ghost fix CREATED the traffic that exposed it.
**Verified:** solo **17 consecutive death/respawn cycles**, every one `START``RESET`, 0 swallowed /
0 mismatch / 0 crash (pre-fix this strands permanently after cycle 1). Two-node MP over a real
network path with cross-machine drop-zone replies: A 11 cycles, B 12, 0 swallowed / 0 mismatch /
+21 -2
View File
@@ -547,8 +547,27 @@ void
// briefly redirected to Player::deathCount@0x200, but that is the respawn-handshake
// identity (seeded -2), which is why it needed a display clamp; +0x280 was never
// dead, only unwritten. Both counters now replicate owner->replicant.
e.nameDisplay->Draw(&localView, (Scalar)BTPilotKills(pilot)); // KILLS (killCount)
e.mechDisplay->Draw(&localView, (Scalar)BTPilotDeaths(pilot)); // DEATHS (deathTally @0x280)
int drawnKills = BTPilotKills(pilot);
int drawnDeaths = BTPilotDeaths(pilot);
e.nameDisplay->Draw(&localView, (Scalar)drawnKills); // KILLS (killCount)
e.mechDisplay->Draw(&localView, (Scalar)drawnDeaths); // DEATHS (deathTally @0x280)
// DIAG (BT_SCORE_LOG): the exact value handed to the panel numerics, edge-
// logged per slot. This is the arbiter between "the tally moved but the
// panel drew stale zeros" (this line shows 0) and "the panel drew it but
// the on-screen surface didn't" (this line shows N).
if (getenv("BT_SCORE_LOG"))
{
static int s_lastK[8] = { -1,-1,-1,-1,-1,-1,-1,-1 };
static int s_lastD[8] = { -1,-1,-1,-1,-1,-1,-1,-1 };
if (s_lastK[currentSlot] != drawnKills || s_lastD[currentSlot] != drawnDeaths)
{
s_lastK[currentSlot] = drawnKills;
s_lastD[currentSlot] = drawnDeaths;
DEBUG_STREAM << "[score] panel DRAW slot " << currentSlot
<< " pilot " << pilot << " kills=" << drawnKills
<< " deaths=" << drawnDeaths << "\n" << std::flush;
}
}
}
++currentSlot;
+14
View File
@@ -2878,6 +2878,20 @@ void BTStartWarpExpandPOV()
//
void BTStartWarpEffect(float x, float y, float z)
{
// ONE warp slot (the authentic effect is POV-only; this world-anchored peer
// sphere is the port extension). The local pilot's own death/respawn
// lifecycle OWNS the slot: a peer's un-wreck landing mid-collapse/wait/expand
// would overwrite gWarpPhase/gWarpPOV and kill the POV vortex ("my respawn
// had no blue vortex"). Harmless before the #81 respawn fix only because
// overlapping respawns barely existed; now they are routine. Skip the peer
// sphere instead -- the observer still sees the un-wreck swap itself.
if (gWarpPhase != 0 && gWarpPOV)
{
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] peer warp SKIPPED (own POV warp active, phase "
<< gWarpPhase << ")\n" << std::flush;
return;
}
BTWarpApplyScaleEnv();
gWarpPhase = 2; // ExpandReveal
gWarpT = 0.0f;
+9 -1
View File
@@ -1134,7 +1134,15 @@ protected:
NotationFile *model_file, const char *model_name,
NotationFile *model_notation,
const ResourceDirectories *directories);
static SharedData *
// Returns the BASE shared-data type: the subsystem blocks are
// Simulation__SharedData and only Mech's own is the derived
// Entity__SharedData (ENTITY3.h:9 -- Entity__SharedData : public
// Simulation::SharedData), so the common type of the mixed table is
// the Simulation one. Declaring this as Mech::SharedData made every
// `&<Subsystem>::DefaultData` stub in mech3.cpp mangle to a symbol
// that does not exist -- ~20 unresolved externals /FORCE silently
// resolved to garbage (reconstruction-gotchas #3).
static Simulation::SharedData *
SubsystemDefaultData(const char *type_name);
// --- simulation / damage (mech4) ------------------------------------
+28 -11
View File
@@ -93,19 +93,36 @@
// Reconstruction stand-ins LOCAL to this translation unit.
// The offline "Subsystems" authoring path dispatches each streamed component
// to its subsystem class's static CreateStreamedSubsystem / DefaultData.
// Those ~21 classes live in sibling modules not visible here (and PPC /
// GaussRifle / SubsystemMessageManager have no reconstructed class at all),
// so the uniform factory surface is declared here as minimal stubs. The
// bodies/data are resolved at link against the real subsystem modules; these
// declarations only let the dispatch compile. Kept LOCAL so they never
// collide with the real sibling classes.
// Those ~21 classes live in sibling modules not visible here, so the uniform
// factory surface is declared here as minimal stubs. The bodies/data are
// resolved at link against the real subsystem modules; these declarations
// only let the dispatch compile. Kept LOCAL so they never collide with the
// real sibling classes.
// ⚠ The DefaultData member TYPE must be Simulation__SharedData -- the type
// the real Subsystem-family definitions have (every sibling declares it via
// the inherited SharedData typedef, dumpbin: ?DefaultData@X@@2VSimulation__
// SharedData@@A). Declaring it as Mech::SharedData (== Entity__SharedData)
// mangles to a DIFFERENT symbol, and /FORCE turned all ~20 of the resulting
// unresolved externals into silent garbage pointers instead of link errors
// (reconstruction-gotchas #3). Cold in-game (this is the tool path), but a
// landmine for the authoring tools. FIXED for the DefaultData statics.
// ⚠ STILL UNRESOLVED (known, cold): the CreateStreamedSubsystem stubs below
// do NOT match the real modules' signatures -- the real ones take the
// class's NESTED <X>::SubsystemResource * (arg 4) and a trailing
// ResourceFile * (e.g. mechtech.cpp:407), where these stubs guessed
// void* / int. All 20 factory references therefore stay unresolved under
// /FORCE. Harmless while the offline authoring dispatch has no callers,
// but before wiring a tool through CreateSubsystemStream these must become
// per-module BRIDGE functions in complete-type TUs (the project's bridge
// convention) -- the nested resource types make direct stubs impossible.
// Tracked in context/open-questions.md.
//===========================================================================//
typedef Mech::SharedData SharedData; // namespace-scope alias (== Entity__SharedData)
// 7-arg (non-weapon) streamed-subsystem factory shape.
#define STREAMED_SUBSYS(NAME) \
struct NAME { \
static SharedData DefaultData; \
static Simulation__SharedData DefaultData; \
static int CreateStreamedSubsystem(NotationFile *, const char *, \
const char *, void *, NotationFile *, \
const ResourceDirectories *, int); \
@@ -113,7 +130,7 @@ typedef Mech::SharedData SharedData; // namespace-scope alias (== Entity__Shared
// 8-arg (weapon) streamed-subsystem factory shape (leading pass counter).
#define STREAMED_WEAPON(NAME) \
struct NAME { \
static SharedData DefaultData; \
static Simulation__SharedData DefaultData; \
static int CreateStreamedSubsystem(int, NotationFile *, \
const char *, const char *, void *, NotationFile *, \
const ResourceDirectories *, int); \
@@ -141,13 +158,13 @@ STREAMED_WEAPON(AmmoBin);
// MechTech is forward-declared in mech.hpp; complete it (non-weapon shape).
struct MechTech
{
static SharedData DefaultData;
static Simulation__SharedData DefaultData;
static int CreateStreamedSubsystem(NotationFile *, const char *,
const char *, void *, NotationFile *,
const ResourceDirectories *, int);
};
// MechControlsMapper is forward-declared in mech.hpp; only its DefaultData is used.
struct MechControlsMapper { static SharedData DefaultData; };
struct MechControlsMapper { static Simulation__SharedData DefaultData; };
// SubsystemMessageManager: leading pass, no trailing index, no DefaultData.
struct SubsystemMessageManager
{
@@ -905,7 +922,7 @@ fail:
// CLASSMAP / the per-module reconstructions). Returns 0 for an unknown name.
//###########################################################################
//###########################################################################
/*static*/ SharedData *
/*static*/ Simulation::SharedData *
Mech::SubsystemDefaultData(const char *type_name)
{
if (Streq(type_name, "")) return &Mech::DefaultData; // 0050bde4 (DAT_0050dc1f == "")