Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/core/timer_system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,19 @@ void TimerSystem::OnLevelEnd()
m_has_map_ticked = false;
}

void TimerSystem::OnStartupServer()
void TimerSystem::OnStartupServer(bool levelShutdown)
{
if (m_has_map_ticked)
if (levelShutdown && m_has_map_ticked)
{
CALL_GLOBAL_LISTENER(OnLevelEnd());

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

// Reset is UNCONDITIONAL -- universal_time math in OnGameFrame depends on
// m_has_map_ticked being false on the first frame of every new session,
// whether that session came in via a genuine LevelShutdown or a workshop
// ss_dead reload.
m_has_map_ticked = false;
m_has_map_simulated = false;
}
Expand Down
9 changes: 8 additions & 1 deletion src/core/timer_system.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,14 @@ class TimerSystem : public GlobalClass
void OnShutdown() override;
void OnLevelEnd() override;
void OnGameFrame(bool simulating);
void OnStartupServer();
// levelShutdown=true on a genuine OnLevelShutdown -> OnStartupServer sequence
// (real changelevel/shutdown). levelShutdown=false on workshop ss_dead reload
// cycles that fire Hook_StartupServer without OnLevelShutdown -- in that case
// we must NOT fire OnLevelEnd (PlayerManager disconnects still-connected
// players -> stale .NET callbacks -> SEGV on the next DispatchConCommand
// hook), but we MUST still reset the per-map tick state for universal_time
// math to stay correct across the cycle.
void OnStartupServer(bool levelShutdown);
double CalculateNextThink(double last_think_time, float interval);
void RunFrame();
void RemoveMapChangeTimers();
Expand Down
21 changes: 19 additions & 2 deletions src/mm_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,29 @@ bool CounterStrikeSharpMMPlugin::Load(PluginId id, ISmmAPI* ismm, char* error, s
return true;
}

static bool s_bLevelShutdownOccurred = false;

void CounterStrikeSharpMMPlugin::Hook_StartupServer(const GameSessionConfiguration_t& config, ISource2WorldSession*, const char*)
{
globals::entitySystem = interfaces::pGameResourceServiceServer->GetGameEntitySystem();
// Remove before adding to prevent double-registration when workshop addon changes
// trigger a second StartupServer within the same map session (ss_dead cycle).
globals::entitySystem->RemoveListenerEntity(&globals::entityManager.entityListener);
globals::entitySystem->AddListenerEntity(&globals::entityManager.entityListener);

globals::timerSystem.OnStartupServer();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this now no longer fire on first startup since the bool is false by default?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

// Workshop ss_dead reload cycles fire Hook_StartupServer without a
// preceding OnLevelShutdown. We pass that distinction down so that:
// levelShutdown=true -> fires OnLevelEnd (PlayerManager etc.) and
// resets timer tick state. Genuine changelevel.
// levelShutdown=false -> ONLY resets timer tick state. No OnLevelEnd,
// which is what avoids the PlayerManager
// disconnect -> stale .NET callbacks -> SEGV
// chain on ss_dead reloads.
// Tick-state reset must be unconditional so universal_time math in
// OnGameFrame doesn't desync across the cycle (otherwise pending one-off
// timers stall arbitrarily long).
globals::timerSystem.OnStartupServer(s_bLevelShutdownOccurred);
s_bLevelShutdownOccurred = false;

on_activate_callback->ScriptContext().Reset();
on_activate_callback->ScriptContext().Push(globals::getGlobalVars()->mapname.ToCStr());
Expand Down Expand Up @@ -299,7 +316,7 @@ int CounterStrikeSharpMMPlugin::Hook_LoadEventsFromFile(const char* filename, bo
RETURN_META_VALUE(MRES_IGNORED, 0);
}

void CounterStrikeSharpMMPlugin::OnLevelShutdown() {}
void CounterStrikeSharpMMPlugin::OnLevelShutdown() { s_bLevelShutdownOccurred = true; }

bool CounterStrikeSharpMMPlugin::Pause(char* error, size_t maxlen) { return true; }

Expand Down