fix: harden warmup timer calculation and lifecycle - #37
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Timer-handle clearing in OnWarmupTimer should be guarded by if (timer == g_hWarmupTimer) to prevent a stale timer instance from clobbering the handle of a newly created timer.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens TeamManager’s warmup timer subsystem by preventing server ConVars from being mutated during warmup initialization, ensuring dynamic warmup duration is recalculated safely per map, and making the repeating warmup timer lifecycle deterministic (tracked, killed/replaced, and cleared correctly).
Changes:
- Compute warmup duration from local ConVar copies and store the resolved value in a plugin-owned
g_iWarmupTimeinstead of writing back to ConVars. - Track the repeating warmup timer via
g_hWarmupTimer, kill any existing timer before starting a new one, and clear the handle when warmup stops / map ends. - Initialize warmup from
OnPluginStart()to support mid-map plugin loads.
File summaries
| File | Description |
|---|---|
| addons/sourcemod/scripting/TeamManager.sp | Reworks warmup duration calculation and adds explicit timer-handle tracking/cleanup to avoid ConVar mutation, stale values, and duplicate timers. |
Review details
Suppressed comments (1)
addons/sourcemod/scripting/TeamManager.sp:228
- Same handle-safety issue when the warmup ends:
g_hWarmupTimershould only be cleared if the timer instance being stopped is the one currently tracked, so a previous/stale timer can’t clobber a newly created handle.
if (g_iWarmup >= iTime)
{
g_hWarmupTimer = INVALID_HANDLE;
EndWarmUp();
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Stop overwriting operator ConVars (sm_warmuptime, sm_warmuptime_dynamic, sm_warmup_dynamic_ratio) from InitWarmup(). The clamping/derivation now works on local copies, so a single bad map no longer permanently changes the server configuration. - Reset the resolved warmup duration on every InitWarmup() call. The old g_iDynamicWarmupTime kept a stale value across maps when dynamic warmup was toggled. - Track the repeating warmup timer in a handle and kill it before starting a new one. Toggling sm_warmup (or any other re-init) could previously leave two OnWarmupTimer timers running at once, double-counting the countdown. The handle is cleared in OnMapEnd and whenever the timer stops itself. - Run InitWarmup() at the end of OnPluginStart so a mid-map plugin load (where OnMapStart never fires) still starts the warmup system. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OnWarmupTimer() cleared g_hWarmupTimer unconditionally in both of its early-return paths. If a stale callback from an old timer fired after InitWarmup() had already replaced it with a newer one, it would wipe out the handle of the still-running timer, reintroducing the duplicate-timer bug the handle tracking was meant to fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9df5271 to
6346140
Compare
SourceMod already synthesizes an OnMapStart forward call for plugins loaded after the map has started, so mid-map plugin loads are not actually inert without special-casing. The extra InitWarmup() call in OnPluginStart ran before the map state was reliable and just forced a premature create-then-kill cycle on the warmup timer ahead of the real OnMapStart-driven init. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audit finding — warmup timer subsystem
1. ConVars were being overwritten from
InitWarmup()InitWarmup()clamped and derived values by writing them back into theoperator's ConVars:
After a single map with an unusual size, the server config was silently and
permanently changed until the next
exec. The clamping/derivation now runs onlocal copies and the result is stored in a plugin-owned
g_iWarmupTime.2. Stale dynamic warmup duration across maps
g_iDynamicWarmupTimewas never reset at the top ofInitWarmup(), so a mapwhere dynamic warmup was disabled could reuse the value computed for a previous
map. It is now reset every call.
3. Duplicate
OnWarmupTimertimersThe repeating timer was created without keeping its handle.
InitWarmup()runsfrom
OnMapStartand from thesm_warmupchange hook; when it ran againwhile a timer was still alive (e.g.
sm_warmup 0; sm_warmup 1in one frame),g_bWarmupwastrueagain by the next tick and the old timer kept runningnext to the new one — the countdown then ticked twice per second and the HUD
text printed twice. The timer handle is now tracked and killed before a new one
is started, cleared in
OnMapEnd, and cleared whenever the timer stops itself.Build is validated by CI (
spcomp1.12.x). No API/behaviour change forconsumers of the natives/forward.
🤖 Generated with Claude Code