fix: guard Hook_StartupServer against spurious second call (HLTV wakeup / ss_dead)#1314
fix: guard Hook_StartupServer against spurious second call (HLTV wakeup / ss_dead)#1314stephanebruckert wants to merge 2 commits into
Conversation
| globals::entitySystem->RemoveListenerEntity(&globals::entityManager.entityListener); | ||
| globals::entitySystem->AddListenerEntity(&globals::entityManager.entityListener); | ||
|
|
||
| globals::timerSystem.OnStartupServer(); |
There was a problem hiding this comment.
Will this now no longer fire on first startup since the bool is false by default?
There was a problem hiding this comment.
It still fires on first startup, but via OnLevelInit rather than Hook_StartupServer, so the flag being false by default is intentional. Hook_StartupServer only triggers after the initial load (on genuine changelevel or the spurious HLTV/ss_dead calls this fix guards against).
There was a problem hiding this comment.
Updating this since the code has changed. While testing the original gated version on a live server, I hit a mid-match MatchZy failure. Tracing it, I found the if (s_bLevelShutdownOccurred) { OnStartupServer(); } gate was also skipping the m_has_map_ticked reset, which broke universal_time math in OnGameFrame and made MatchZy's AutoStart timer fire late mid-match.
So the shape is different now. OnStartupServer(bool levelShutdown) gets called every time, and the bool just controls whether OnLevelEnd fires. On first startup the bool is false, so OnLevelEnd doesn't fire (which was your concern). The function still runs, but the only thing it does is reset m_has_map_ticked / m_has_map_simulated to false, and they're already false at that point, so it's a no-op in practice.
|
Hey @roflmuffin I'm investigating a regression, please consider this PR as draft for the time being. This needs much more testing |
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>
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>
… server Servers running with HLTV/tv_broadcast crash when waking from hibernation: the first player to connect triggers an HLTV network startup that fires a second StartupServer call, causing CSS to synthetic-disconnect all tracked players → stale .NET callbacks → SEGV. This also manifests as a crash on map change when HLTV is active (reported as MatchZy roflmuffin#380). INetworkServerService::StartupServer fires more than once per session: once on genuine changelevel, and again when HLTV starts its own network session as the first player wakes a hibernating server (ss_dead cycle). The second call triggered OnStartupServer() → OnLevelEnd() → PlayerManager synthetic-disconnects all CSS-tracked players → stale .NET callbacks → SEGV on the next DispatchConCommand hook. Fix: introduce s_bLevelShutdownOccurred, set by OnLevelShutdown() (which fires only on genuine ILoopMode::LoopShutdown, never during ss_dead). Hook_StartupServer now calls OnStartupServer() only when that flag is set, ignoring the spurious second invocation. Also call RemoveListenerEntity before AddListenerEntity to prevent double-registration on the same entity listener during the ss_dead cycle. Reproducer: server in hibernation, first player connects → HLTV wakeup fires second StartupServer → crash. Confirmed fixed empirically. Fixes roflmuffin#946. Also resolves MatchZy issue roflmuffin/MatchZy#380.
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>
852bb78 to
202d0ee
Compare
|
Investigation is done, this is ready for review again. Details on the code shape change are in the inline thread above, full bug and fix story in the PR description. Validated on CS2 1.41.6.2 across 40+ map transitions: If anyone wants to try the fix on their own server before merge, prebuilt binaries are at https://github.com/2hours-gg/CounterStrikeSharp/releases/tag/v1.0.368-2hours.2. I also have unit tests for the |
Problem
Hook_StartupServercan fire twice for the same session when CS2internally reloads without deactivating the loop mode. The second call
fires
OnLevelEnd→ PlayerManager disconnects connected players in CSSstate → stale .NET callbacks → SEGV on the next
DispatchConCommand.Two known triggers:
host_workshop_map) — CS2 goes through aninternal
ss_deadreload to fetch the addon and firesHook_StartupServera second time.server, HLTV starts its own session and triggers a second
Hook_StartupServer.Fix
Track whether the call follows a genuine
OnLevelShutdown(set in theOnLevelShutdownhook).OnStartupServertakes abool levelShutdown:true→ fireOnLevelEndand reset per-map tick state (genuine path)false→ reset tick state only, noOnLevelEnd(the spurious cycle)Tick-state reset is unconditional —
OnGameFrame'suniversal_timemath needs it false on the first frame of every session regardless of
which path started it.
Testing
Verified on CS2 1.41.6.2 across 40+ map transitions:
host_workshop_mapcycles,
changelevelwithtv_broadcaston, HLTV wakeup fromhibernation, MatchZy loaded throughout. No crashes.
Related
Fixes #946 and #1239.
Also resolves the following MatchZy issues (same root cause, no MatchZy changes needed):