Skip to content

Commit 852bb78

Browse files
fix: reset timer tick state on workshop ss_dead reload cycles
Hook_StartupServer fires twice per workshop addon change: once via a genuine OnLevelShutdown -> OnStartupServer sequence, and again during the internal ss_dead reload cycle WITHOUT a preceding OnLevelShutdown. The previous gating (PR roflmuffin#1314) suppressed the entire second call to TimerSystem::OnStartupServer to avoid the PlayerManager-disconnect -> stale .NET callbacks -> SEGV chain. But that also skipped the m_has_map_ticked/m_has_map_simulated reset. On the first frame of the reloaded session, OnGameFrame computed universal_time delta against an unrelated last_ticked_time, producing a large positive jump. Per-map one-off timers (notably MatchZy's 1-second AddTimer(AutoStart)) then fired arbitrarily late mid-match -- reproducibly aborting live matches. Fix: OnStartupServer now takes a `levelShutdown` bool. The OnLevelEnd fan-out (and its disconnect side-effects) stays gated on it, but the tick-state reset is unconditional. mm_plugin tracks the genuine LevelShutdown via s_bLevelShutdownOccurred and passes it through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 15eab93 commit 852bb78

3 files changed

Lines changed: 27 additions & 15 deletions

File tree

src/core/timer_system.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,19 @@ void TimerSystem::OnLevelEnd()
8888
m_has_map_ticked = false;
8989
}
9090

91-
void TimerSystem::OnStartupServer()
91+
void TimerSystem::OnStartupServer(bool levelShutdown)
9292
{
93-
if (m_has_map_ticked)
93+
if (levelShutdown && m_has_map_ticked)
9494
{
9595
CALL_GLOBAL_LISTENER(OnLevelEnd());
9696

9797
CSSHARP_CORE_TRACE("name={0}", "LevelShutdown");
9898
}
9999

100+
// Reset is UNCONDITIONAL -- universal_time math in OnGameFrame depends on
101+
// m_has_map_ticked being false on the first frame of every new session,
102+
// whether that session came in via a genuine LevelShutdown or a workshop
103+
// ss_dead reload.
100104
m_has_map_ticked = false;
101105
m_has_map_simulated = false;
102106
}

src/core/timer_system.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,14 @@ class TimerSystem : public GlobalClass
7272
void OnShutdown() override;
7373
void OnLevelEnd() override;
7474
void OnGameFrame(bool simulating);
75-
void OnStartupServer();
75+
// levelShutdown=true on a genuine OnLevelShutdown -> OnStartupServer sequence
76+
// (real changelevel/shutdown). levelShutdown=false on workshop ss_dead reload
77+
// cycles that fire Hook_StartupServer without OnLevelShutdown -- in that case
78+
// we must NOT fire OnLevelEnd (PlayerManager disconnects still-connected
79+
// players -> stale .NET callbacks -> SEGV on the next DispatchConCommand
80+
// hook), but we MUST still reset the per-map tick state for universal_time
81+
// math to stay correct across the cycle.
82+
void OnStartupServer(bool levelShutdown);
7683
double CalculateNextThink(double last_think_time, float interval);
7784
void RunFrame();
7885
void RemoveMapChangeTimers();

src/mm_plugin.cpp

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -214,18 +214,19 @@ void CounterStrikeSharpMMPlugin::Hook_StartupServer(const GameSessionConfigurati
214214
globals::entitySystem->RemoveListenerEntity(&globals::entityManager.entityListener);
215215
globals::entitySystem->AddListenerEntity(&globals::entityManager.entityListener);
216216

217-
// Only fire OnLevelEnd lifecycle after a genuine loop-mode deactivation (OnLevelShutdown).
218-
// Workshop addon changes trigger a second StartupServer via an internal ss_dead cycle
219-
// without deactivating the loop mode; calling OnStartupServer here fires OnLevelEnd →
220-
// PlayerManager disconnects still-connected players in CSS state → stale .NET callbacks
221-
// → SEGV on the next DispatchConCommand hook.
222-
// OnLevelShutdown (via ILoopMode::LoopShutdown post-hook in metamod-source) fires only
223-
// on genuine changelevel/shutdown, not during the ss_dead reload cycle.
224-
if (s_bLevelShutdownOccurred)
225-
{
226-
s_bLevelShutdownOccurred = false;
227-
globals::timerSystem.OnStartupServer();
228-
}
217+
// Workshop ss_dead reload cycles fire Hook_StartupServer without a
218+
// preceding OnLevelShutdown. We pass that distinction down so that:
219+
// levelShutdown=true -> fires OnLevelEnd (PlayerManager etc.) and
220+
// resets timer tick state. Genuine changelevel.
221+
// levelShutdown=false -> ONLY resets timer tick state. No OnLevelEnd,
222+
// which is what avoids the PlayerManager
223+
// disconnect -> stale .NET callbacks -> SEGV
224+
// chain on ss_dead reloads.
225+
// Tick-state reset must be unconditional so universal_time math in
226+
// OnGameFrame doesn't desync across the cycle (otherwise pending one-off
227+
// timers stall arbitrarily long).
228+
globals::timerSystem.OnStartupServer(s_bLevelShutdownOccurred);
229+
s_bLevelShutdownOccurred = false;
229230

230231
on_activate_callback->ScriptContext().Reset();
231232
on_activate_callback->ScriptContext().Push(globals::getGlobalVars()->mapname.ToCStr());

0 commit comments

Comments
 (0)