Repository navigation
Conversation
OnMapEnd walked the whole download folder and moved the first 52 sprays in one call (89 ms), and each TimerNext tick moved 52 more through Valve filesystem calls (57 ms). - OnMapEnd now only starts the scan and does no file work - One SprayScan job walks, checks or moves, then removes emptied folders, with at most SCAN_BUDGET (2 ms) of work per 0.1 s tick - Scan file calls use the plain filesystem instead of Valve FS - Read the queued files by index instead of Erase(0) - Create the backup folder that sprays are actually moved into - sm_spray_test falls back to the server console if the admin leaves Fixes #36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Backup-directory creation still uses Valve FS, contrary to the performance objective.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Refactors spray-folder maintenance into incremental timer-based scans to reduce server-thread stalls.
Changes:
- Adds budgeted asynchronous scanning, validation, moving, and cleanup.
- Fixes backup-folder targeting and disconnected-admin output.
- Updates Copilot guidance for the new scanning architecture.
| File | Description |
|---|---|
FixSprayExploit.sp |
Implements incremental spray scanning and cleanup. |
.github/copilot-instructions.md |
Documents the updated workflow. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The moves rename into the backup folder through plain paths under the game folder, so create it the same way instead of under Valve's write path. The scan root is already open at that point, so the non-recursive CreateDirectory is enough. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cmer81
left a comment
There was a problem hiding this comment.
Reviewed and built on SourceMod 1.13.0.7427 (base and head compile, no new warnings).
The incremental scan looks correct: directories are removed leaf-first (visited is filled parent-before-child and popped in reverse, RemoveDir refuses non-empty folders), the scan root is no longer a removal candidate, the active guard prevents overlapping scans and all handles are freed at the end, and a disconnected admin falls back to the server console.
Performance: OnMapEnd now only queues the scan, and each tick does at most ~2 ms of work with the overshoot bounded to a single file operation (the clock is checked before every operation). Replacing Erase(0) by an index also removes the O(n^2) cost on large queues. The trade-off is a throughput cap (~2% duty cycle), which is acceptable here. This is from reading the code, I have not profiled it on a live server.
One thing to fix before merging: the behaviour changes but PLUGIN_VERSION is still "2.28", please bump it (e.g. 2.29).
Minor, non-blocking: the final sm_spray_test message always goes to the console now (the original reply source is not kept), which is fine.

Fixes #36
@cmer81 could you review this? It removes the 89 ms
OnMapEndand 57 msTimerNexthitches by spreading the spray folder work over timer ticks.What changed
OnMapEnddoes no file work. It only starts a background scan.SprayScanreplacesRecursiveFiles,RecursiveSearchDirs,TimerNextandDeleteEmptyDirs. It runs on a 0.1 s repeating timer (TimerScan) and stops afterSCAN_BUDGET(2 ms) each tick, then continues where it left off. It runs in three steps:sm_spray_test) or move (map end, plugin start) the queued sprays.RemoveDirfails on folders that still hold files, so only empty ones go.FileCheckalready reads sprays this way.Erase(0).<root>/backup_sprays, the folder sprays are actually moved into. On CS:S, the old code createddownload/user_custom/backup_spraysinstead.sm_spray_testprints to the server console if the admin leaves before it finishes, instead of throwing an error.OnPlayerRunCmdis unchanged. Outside impulse 202 it does one integer comparison, and removing it would reopen the jingle exploit.Behaviour changes
SCAN_BUDGETto speed it up.Testing
TimerScanstays around 2 ms andOnMapEnddrops out of the profilerdownload/backup_spraysfills up and emptieduser_custom/xxfolders are removedsm_spray_testfinishes and prints its summary🤖 Generated with Claude Code