diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 43ddfb9..01a8657 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -9963,6 +9963,22 @@ void void DPLRenderer::SortAndReloadNameBitmaps() { + // + // NULL-GUARD (2026-07-26, the #35-class sweep). This resort runs in the + // round-STOP path (the end-of-round "circle") and derefs the current + // mission and the mission player raw -- the binary could, because a pod's + // mission world always existed when it ran. The port's MP stop/teardown + // windows (double-stop, abort-during-stop) can leave either briefly NULL + // -- the exact shape of the 515 audio-clip crash. The refresh is + // cosmetic; skip it when the world is mid-teardown. + // + if (application == 0 + || application->GetCurrentMission() == 0 + || application->GetMissionPlayer() == 0 + || application->GetEntityManager() == 0) + { + return; + } // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Get the Entity Group of Players @@ -9993,6 +10009,15 @@ void DPLRenderer::SortAndReloadNameBitmaps() // void DPLRenderer::LoadOrdinalBitmaps() { + // NULL-GUARD (2026-07-26, the #35-class sweep): runs from the round-stop + // resort above; player/mission can be mid-teardown NULL in the port's MP + // windows. Ordinals are the camera-director's cosmetic overlay -- skip. + if (application == 0 + || application->GetMissionPlayer() == 0 + || application->GetCurrentMission() == 0) + { + return; + } // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Check if we have a Director on this machine, @@ -10062,6 +10087,12 @@ unsigned int* // void DPLRenderer::LoadNameBitmaps() { + // NULL-GUARD (2026-07-26, the #35-class sweep): same teardown window as + // SortAndReloadNameBitmaps above. + if (application == 0 || application->GetCurrentMission() == 0) + { + return; + } int player_count = application->GetCurrentMission()->GetPlayerCount(); for (int ii=0; iisimulationFlags & SegmentCopyMask) == 0 && (owner->simulationFlags & MasterHeatSinkFlag) != 0) { - SetPerformance(&Reservoir::CoolantSimulation); // this[7..9] = PTR 0050e6b4 -> @4aef78 + // + // NULL-GUARD (2026-07-27, the games-night load crash: [crash] + // btl4+0x4990d = this ctor inlined into CreateReservoirSubsystem, + // AV reading NULL+0x1d0 = the masterScale FILD below with link==0). + // The binary derefs the resolved master bank raw -- in a pod the + // linked AggregateHeatSink always resolves. In the port's MP a + // poisoned roster (observed trigger: a DUPLICATE-pilot egg from the + // seat-ghost bug -- one identity in two seats -- corrupting the ID + // registry) can make Resolve() return NULL during viewpoint-entity + // construction, and every pod loading that egg dies. Fail LOUD and + // degrade to the inactive-copy shape (the else branch) instead: + // a mech with an unscaled reservoir beats six crashed clients. + // HeatSink *link = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x59) + if (link == 0) + { + DEBUG_STREAM << "[spawn] FATAL-AVOIDED: Reservoir(subsystem " + << subsystem_ID << ") master heat-sink bank did NOT resolve " + << "-- poisoned roster? (duplicate pilot / ID collision). " + << "Reservoir runs UNSCALED as an inactive copy.\n" << std::flush; + simulationFlags |= 2; // the else-branch shape + Check_Fpu(); + return; + } + SetPerformance(&Reservoir::CoolantSimulation); // this[7..9] = PTR 0050e6b4 -> @4aef78 link->Attach(this); // (**(link+0x1d8+4))(link+0x1d8, this) // Gitea #7 decode correction: the binary loads `*(int *)(link+0x1d0)` // and FILDs it -- `(float10)*(int *)(iVar1 + 0x1d0)` @4af408 -- i.e.