diff --git a/context/open-questions.md b/context/open-questions.md index 9686dd6..cbd1dbc 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -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 `::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). diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index 0458c80..91ce9ef 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -121,6 +121,17 @@ mislanch.cpp's extern but not projweap.cpp's → first AUTOCANNON shot AV'd). ** bridge-signature change, `grep -rn "extern .*"` 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 `::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 diff --git a/context/translocation-warp.md b/context/translocation-warp.md index c119b58..b10b055 100644 --- a/context/translocation-warp.md +++ b/context/translocation-warp.md @@ -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. diff --git a/docs/GHOST_MECH_ANALYSIS.md b/docs/GHOST_MECH_ANALYSIS.md index a4e6ed8..f2e6465 100644 --- a/docs/GHOST_MECH_ANALYSIS.md +++ b/docs/GHOST_MECH_ANALYSIS.md @@ -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 / diff --git a/game/reconstructed/btl4gau3.cpp b/game/reconstructed/btl4gau3.cpp index bb74bda..b81a38b 100644 --- a/game/reconstructed/btl4gau3.cpp +++ b/game/reconstructed/btl4gau3.cpp @@ -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; diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index bccd496..9cddc24 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -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; diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index a2cdcd9..213a469 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -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 + // `&::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) ------------------------------------ diff --git a/game/reconstructed/mech3.cpp b/game/reconstructed/mech3.cpp index 45aa124..1bae3d3 100644 --- a/game/reconstructed/mech3.cpp +++ b/game/reconstructed/mech3.cpp @@ -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 ::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 == "")