From 81dda84e9b122195a2e68b74d9a6567c38c2034d Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Tue, 4 Aug 2026 16:34:06 -0500 Subject: [PATCH] #108 forensics block (pre-717): ghost detector + IDs + field envs The night-11 instrumentation that makes the next ghost/K-D report diagnosable instead of anecdotal: - UNGATED [ghost] stale-replicant detector: ReadUpdateRecord stamps every applied record; the death-handler tick logs ONE line per starvation episode (>600 frames, unburied) with entity id + mode + last position, plus a GHOST matchlog record. Verified both ways: zero false positives on a healthy 2-node session; fires on both nodes at frame 601 after a mid-session relay kill. - Entity IDs on the render forensics (MakeMechRenderables / RemakeEntity / wreck-swap fallbacks) and the replicant un-wreck line un-gated -- ghost triage no longer needs players to set envs. - players/*.bat (steam + both joins): BT_MATCHLOG/BT_SCORE_LOG/BT_DEATH_LOG on for every field session. Co-Authored-By: Claude Fable 5 --- game/reconstructed/btl4vid.cpp | 12 +++++--- game/reconstructed/mech.cpp | 7 +++++ game/reconstructed/mech.hpp | 3 ++ game/reconstructed/mechdmg.cpp | 55 ++++++++++++++++++++++++++++++---- game/reconstructed/mechdmg.hpp | 8 +++++ players/join.bat | 7 +++++ players/join_lan.bat | 7 +++++ players/play_steam.bat | 7 +++++ 8 files changed, 96 insertions(+), 10 deletions(-) diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index 22adbc5..12aba7d 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -401,7 +401,8 @@ HierarchicalDrawComponent* // bring-up diagnostics (counts geometry actually loaded vs. requested) int dbg_obj_requested = 0, dbg_obj_loaded = 0, dbg_eye = 0; DEBUG_STREAM << "[BTrender] MakeMechRenderables: " << segment_count - << " segments, view=" << (int)type << "\n" << std::flush; + << " segments, view=" << (int)type + << " entity=" << entity->GetEntityID() << "\n" << std::flush; JointSubsystem *joint_subsystem = jointed_mover->GetJointSubsystem(); // [0x31c] @@ -884,7 +885,8 @@ void if (swapped != 0 || getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[BTrender] RemakeEntity: " << swapped << " mesh(es) swapped (" << mapped << " body segs mapped of " - << checked << " checked)\n" << std::flush; + << checked << " checked) entity=" + << entity->GetEntityID() << "\n" << std::flush; } @@ -1171,13 +1173,15 @@ void if (hulk != NULL && hulk->GetVertCount() == 0) { DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name - << "' is an EMPTY placeholder -> gendbr.bgf fallback\n" << std::flush; + << "' is an EMPTY placeholder -> gendbr.bgf fallback (entity=" + << victim->GetEntityID() << ")\n" << std::flush; hulk = NULL; } if (hulk == NULL) { DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name - << "' missing -> gendbr.bgf fallback\n" << std::flush; + << "' missing -> gendbr.bgf fallback (entity=" + << victim->GetEntityID() << ")\n" << std::flush; hulk = d3d_OBJECT::LoadObject(GetDevice(), "gendbr.bgf"); } diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index f9124ae..e8062e2 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -2424,6 +2424,13 @@ Logical void Mech::ReadUpdateRecord(Simulation::UpdateRecord *message) { + // #108 ghost forensics: stamp the staleness detector on every applied + // record (replicants only -- the detector lives on the death handler). + if (GetInstance() == ReplicantInstance) + { + extern void BTMechNoteUpdateApplied(void *mech_v); + BTMechNoteUpdateApplied((void *)this); + } // RECORD-CADENCE probe (BT_RXJIT): quantify how EVENLY records arrive (wall-clock // ms between arrivals) -- the jitter that makes the peer's corrections random. if (getenv("BT_RXJIT") && GetInstance() == ReplicantInstance) diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index e8f1bd7..e9c8e85 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -1119,6 +1119,9 @@ protected: // from its exported consumers + the RP VTV::DeathShutdown analog) --- Logical IsMechDestroyed(); // damage-side death flag: graphicAlarm level >= 9 void UpdateDeathState(Scalar dt); // death freeze + subsystem DeathShutdown + wreck smoke + // --- #108 ghost-forensics accessors (PORT; the fields are protected) --- + void *DeathHandlerPtr() { return (void *)deathHandler; } + int WreckBuried() const { return collisionVolumeCount == 0; } // --- clip loaders (mech3) ------------------------------------------- ResourceDescription::ResourceID * diff --git a/game/reconstructed/mechdmg.cpp b/game/reconstructed/mechdmg.cpp index 628c343..e83a27a 100644 --- a/game/reconstructed/mechdmg.cpp +++ b/game/reconstructed/mechdmg.cpp @@ -1193,8 +1193,20 @@ extern void BTSpawnDamageEffect(Mech *mech, int effect_resource, int segment_ind // unported "RemakeEntity" render state). Entity* param -> Mech* binds fine. extern void BTRemakeMechModel(Entity *entity); +// +// #108 bridge: Mech::ReadUpdateRecord (mech.cpp, incomplete-type TU for the +// handler) stamps the ghost detector through this. +// +void + BTMechNoteUpdateApplied(void *mech_v) +{ + Mech *m = (Mech *)mech_v; + if (m != 0 && m->DeathHandlerPtr() != 0) + ((MechDeathHandler *)m->DeathHandlerPtr())->NoteUpdateApplied(); +} + MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984 - : owner(mech), prevMode(0) + : owner(mech), prevMode(0), ghostFrames(0), ghostLogged(0) { // per-zone last-damage cache, zeroed (binary this[0x10], size mech+0x11c). int count = (mech != 0 && mech->damageZoneCount > 0) ? mech->damageZoneCount : 0; @@ -1217,6 +1229,35 @@ void if (owner == 0) return; + // #108 GHOST DETECTOR (PORT, ungated -- the night-9 forensic): a visible + // REPLICANT that stops receiving update records is exactly what "everyone + // shoots thin air" looks like from the other side. ReadUpdateRecord + // stamps arrivals (NoteUpdateApplied); ~20s of silence on an unburied + // replicant logs ONE [ghost] line per episode with the identity + last + // position, then re-arms when records resume. Frame-counted (Tick has no + // dt); 600 frames ~ 10-20s at field rates. + if (owner->GetInstance() == Entity::ReplicantInstance) + { + ++ghostFrames; + if (ghostFrames > 600 && !ghostLogged + && !owner->WreckBuried()) // buried wrecks are expected-silent + { + ghostLogged = 1; + DEBUG_STREAM << "[ghost] replicant " << owner->GetEntityID() + << " has received NO update records for " << ghostFrames + << " frames (mode " << (int)owner->MovementMode() + << ") last pos (" << owner->localOrigin.linearPosition.x + << "," << owner->localOrigin.linearPosition.z + << ") -- stale-replicant / ghost signature (#108)\n" << std::flush; + if (BTMatchLogActive()) + BTMatchLog("GHOST", "mech=%d:%d frames=%d mode=%d x=%.1f z=%.1f", + BTMatchHostOf(owner->GetEntityID()), (int)owner->GetEntityID(), + ghostFrames, (int)owner->MovementMode(), + (float)owner->localOrigin.linearPosition.x, + (float)owner->localOrigin.linearPosition.z); + } + } + // REPLICANT UN-WRECK (#94): fire on the DEATH-STATE EXIT edge of the // replicated SimulationState dial (MovementMode 9/2 are the death modes; // they never revert except through Mech::Reset, whose SetMovementMode(0) @@ -1251,11 +1292,13 @@ void BTStartWarpEffect((float)owner->localOrigin.linearPosition.x, (float)owner->localOrigin.linearPosition.y, (float)owner->localOrigin.linearPosition.z); - if (getenv("BT_DEATH_LOG")) - DEBUG_STREAM << "[respawn] replicant un-wrecked + warp (mode " - << oldMode << "->" << mode << ") at (" - << owner->localOrigin.linearPosition.x << "," - << owner->localOrigin.linearPosition.z << ")\n" << std::flush; + // #108 forensics: UNGATED -- one line per peer respawn; a field + // night's ghost triage needs these without asking players for envs. + DEBUG_STREAM << "[respawn] replicant " << owner->GetEntityID() + << " un-wrecked + warp (mode " + << oldMode << "->" << mode << ") at (" + << owner->localOrigin.linearPosition.x << "," + << owner->localOrigin.linearPosition.z << ")\n" << std::flush; } } diff --git a/game/reconstructed/mechdmg.hpp b/game/reconstructed/mechdmg.hpp index b36373f..c64cd05 100644 --- a/game/reconstructed/mechdmg.hpp +++ b/game/reconstructed/mechdmg.hpp @@ -363,6 +363,11 @@ class MechDeathHandler MechDeathHandler(Mech *mech); // @0042a984 ~MechDeathHandler(); // @0042a9f4 void Tick(); // @0042aa2c (the Performance) + // #108 forensics (PORT, ungated): the replicant staleness detector. + // Mech::ReadUpdateRecord stamps every applied record; Tick counts + // frames since -- a visible replicant starved of records IS the ghost + // signature, logged once per episode with id + age + last position. + void NoteUpdateApplied() { ghostFrames = 0; ghostLogged = 0; } private: Mech *owner; // @0x14 @@ -378,6 +383,9 @@ class MechDeathHandler // authentic un-wreck rides the type-0 graphic-state hook // (Mech::ReadUpdateRecord case 0), which our port has not reconstructed. int prevMode; + // #108 ghost detector state (PORT) + int ghostFrames; + int ghostLogged; }; #endif diff --git a/players/join.bat b/players/join.bat index d30de33..7a657e7 100644 --- a/players/join.bat +++ b/players/join.bat @@ -27,6 +27,13 @@ rem Clear any INHERITED BT_LOG: with it set the exe uses that path verbatim rem and, with no BT_LOG_APPEND, TRUNCATES it -- the exact evidence loss this rem change removes. Empty forces the per-day self-named file + append. set BT_LOG= +rem #108 FORENSICS (night-11): every field session records the match ledger +rem (matchlog also auto-arms on -net; forcing covers menu-relaunch paths) and +rem the score/death chain lines -- the ghost/K-D triage needs them from every +rem player, not just the ones we can reach for env setup. +set BT_MATCHLOG=1 +set BT_SCORE_LOG=1 +set BT_DEATH_LOG=1 cd content rem JOIN-GAME menu (2026-07-22): pick your CALLSIGN + MECH, click JOIN -- rem your choice is sent to the operator and lands in the mission roster. diff --git a/players/join_lan.bat b/players/join_lan.bat index 9eb1545..d65c9b6 100644 --- a/players/join_lan.bat +++ b/players/join_lan.bat @@ -30,6 +30,13 @@ rem Clear any INHERITED BT_LOG: with it set the exe uses that path verbatim rem and, with no BT_LOG_APPEND, TRUNCATES it -- the exact evidence loss this rem change removes. Empty forces the per-day self-named file + append. set BT_LOG= +rem #108 FORENSICS (night-11): every field session records the match ledger +rem (matchlog also auto-arms on -net; forcing covers menu-relaunch paths) and +rem the score/death chain lines -- the ghost/K-D triage needs them from every +rem player, not just the ones we can reach for env setup. +set BT_MATCHLOG=1 +set BT_SCORE_LOG=1 +set BT_DEATH_LOG=1 cd content rem JOIN-GAME menu (2026-07-22): pick your CALLSIGN + MECH, click JOIN -- rem your choice is sent to the operator and lands in the mission roster. diff --git a/players/play_steam.bat b/players/play_steam.bat index edb4fd4..46648cf 100644 --- a/players/play_steam.bat +++ b/players/play_steam.bat @@ -29,6 +29,13 @@ rem Clear any INHERITED BT_LOG: with it set the exe uses that path verbatim rem and, with no BT_LOG_APPEND, TRUNCATES it -- the exact evidence loss this rem change removes. Empty forces the per-day self-named file + append. set BT_LOG= +rem #108 FORENSICS (night-11): every field session records the match ledger +rem (matchlog also auto-arms on -net; forcing covers menu-relaunch paths) and +rem the score/death chain lines -- the ghost/K-D triage needs them from every +rem player, not just the ones we can reach for env setup. +set BT_MATCHLOG=1 +set BT_SCORE_LOG=1 +set BT_DEATH_LOG=1 cd content rem ------------------------------------------------------------------ rem OUR GENERATION ONLY (2026-07-27). The wait loop below used to hold