From 183709306e9373f693a9a8a12db3bb21108e11f3 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Thu, 10 Sep 2026 22:52:24 +0200 Subject: [PATCH 1/3] fix: DeleteOutput target/input parsing, late load, config reload cache - RestoreLevel(): fix `DeleteOutput` entries that specify a target and input (e.g. "m_OnUser4 score10,ApplyScore"). FindCharInString() returns an offset relative to the `sValue[Target]` substring, but it was used as an absolute index, corrupting the output-name portion of the buffer. The parameter guard also re-tested `Input` instead of the parameter offset (copy/paste bug), so it never took the "no parameter" path. Only the output-only and output+target forms worked before. - Support late loads / `sm plugins reload`: AskPluginLoad2() received `late` but ignored it, and OnMapStart() (the only place the config loads) does not fire until the next map. Store the flag and load on OnPluginStart() when late. - Split config loading into LoadMapConfig() so `sm_savelevel_reload` no longer wipes the saved-level cache (OnMapStart() recreating g_PlayerLevels dropped levels of players who already disconnected). The reload command now reports whether a config was actually loaded instead of always claiming success. - GetLevel(): guard the `math` entry parser against a missing comma so a malformed config line skips that entry instead of triggering a negative array-index runtime error that aborts level detection. Co-Authored-By: Claude Sonnet 5 --- addons/sourcemod/scripting/SaveLevel.sp | 55 ++++++++++++++++++------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/addons/sourcemod/scripting/SaveLevel.sp b/addons/sourcemod/scripting/SaveLevel.sp index 3ef8542..c3f180b 100644 --- a/addons/sourcemod/scripting/SaveLevel.sp +++ b/addons/sourcemod/scripting/SaveLevel.sp @@ -11,6 +11,7 @@ StringMap g_PlayerLevels; KeyValues g_Config; KeyValues g_PropAltNames; +bool g_bLateLoad; #define PREFIX "{green}[SaveLevel]{default}" @@ -19,13 +20,14 @@ public Plugin myinfo = name = "SaveLevel", author = "BotoX", description = "Saves players level on maps when they disconnect and restore them on connect.", - version = "2.4.4", + version = "2.4.5", url = "" }; public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { RegPluginLibrary("SaveLevel"); + g_bLateLoad = late; return APLRes_Success; } @@ -39,6 +41,13 @@ public void OnPluginStart() RegServerCmd("sm_clearlevelcache", Command_ClearCache); RegAdminCmd("sm_level", Command_Level, ADMFLAG_GENERIC, "Set a players map level."); RegAdminCmd("sm_savelevel_reload", Command_ReloadConfig, ADMFLAG_CONFIG, "Reload the SaveLevel Map Config File."); + + // Support `sm plugins reload` / late loads: OnMapStart() won't fire until the next map otherwise. + if(g_bLateLoad) + { + g_bLateLoad = false; + OnMapStart(); + } } public void OnPluginEnd() @@ -52,10 +61,17 @@ public void OnPluginEnd() public void OnMapStart() { - if(g_Config) - delete g_Config; - if(g_PlayerLevels) - delete g_PlayerLevels; + // A new map means the cached levels no longer map to anything, so start fresh here. + // Config (re)loading lives in LoadMapConfig() so `sm_savelevel_reload` can keep the cache. + delete g_PlayerLevels; + g_PlayerLevels = new StringMap(); + + LoadMapConfig(); +} + +bool LoadMapConfig() +{ + delete g_Config; char sMapName[PLATFORM_MAX_PATH]; GetCurrentMap(sMapName, sizeof(sMapName)); @@ -71,7 +87,7 @@ public void OnMapStart() if(!FileExists(sConfigFile)) // Second attempt with Map name as lowercase { LogMessage("Could not find mapconfig: \"%s\"", sMapName); - return; + return false; } } @@ -82,7 +98,7 @@ public void OnMapStart() { delete g_Config; LogMessage("ImportFromFile() failed!"); - return; + return false; } g_Config.Rewind(); @@ -90,10 +106,10 @@ public void OnMapStart() { delete g_Config; LogMessage("GotoFirstSubKey() failed!"); - return; + return false; } - g_PlayerLevels = new StringMap(); + return true; } public void OnClientPostAdminCheck(int client) @@ -205,17 +221,20 @@ bool RestoreLevel(int client, const char[] sTarget, char[] sName = NULL_STRING, continue; } + // FindCharInString() returns an offset relative to sValue[Target], make it absolute. + Input += Target; sValue[Input] = 0; Input++; // Input (e.g. add) - int Parameter = Input + FindCharInString(sValue[Input], ','); - if(Input == -1) + int Parameter = FindCharInString(sValue[Input], ','); + if(Parameter == -1) { while((Index = FindOutput(client, sValue, 0, sValue[Target], sValue[Input])) != -1) DeleteOutput(client, sValue, Index); continue; } + Parameter += Input; sValue[Parameter] = 0; Parameter++; // Parameter (e.g. 1) @@ -368,9 +387,14 @@ bool GetLevel(int client, char[] sTargets, int TargetsLen, char[] sNames = NULL_ int Parameter; Input = FindCharInString(sValue[Target], ','); + if(Input == -1) // Malformed ",," entry, skip it. + continue; sValue[Input] = 0; Input++; - Parameter = Input + FindCharInString(sValue[Input], ','); + int ParameterOffset = FindCharInString(sValue[Input], ','); + if(ParameterOffset == -1) + continue; + Parameter = Input + ParameterOffset; sValue[Parameter] = 0; Parameter++; int Value = 0; @@ -455,8 +479,11 @@ public Action Command_ClearCache(int args) public Action Command_ReloadConfig(int client, int args) { - OnMapStart(); - CReplyToCommand(client, "%s Map config file has been reloaded.", PREFIX); + if(LoadMapConfig()) + CReplyToCommand(client, "%s Map config file has been reloaded.", PREFIX); + else + CReplyToCommand(client, "%s No valid map config found for the current map (check the server logs).", PREFIX); + return Plugin_Handled; } From 0f794e6ff47422c3a79bb17d6f13dba68fad6ed4 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Fri, 11 Sep 2026 09:48:17 +0200 Subject: [PATCH 2/3] Late load of SM already call OnMapStart Removed unused late load variable and related logic. --- addons/sourcemod/scripting/SaveLevel.sp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/addons/sourcemod/scripting/SaveLevel.sp b/addons/sourcemod/scripting/SaveLevel.sp index c3f180b..27973e7 100644 --- a/addons/sourcemod/scripting/SaveLevel.sp +++ b/addons/sourcemod/scripting/SaveLevel.sp @@ -11,7 +11,6 @@ StringMap g_PlayerLevels; KeyValues g_Config; KeyValues g_PropAltNames; -bool g_bLateLoad; #define PREFIX "{green}[SaveLevel]{default}" @@ -27,7 +26,6 @@ public Plugin myinfo = public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { RegPluginLibrary("SaveLevel"); - g_bLateLoad = late; return APLRes_Success; } @@ -41,13 +39,6 @@ public void OnPluginStart() RegServerCmd("sm_clearlevelcache", Command_ClearCache); RegAdminCmd("sm_level", Command_Level, ADMFLAG_GENERIC, "Set a players map level."); RegAdminCmd("sm_savelevel_reload", Command_ReloadConfig, ADMFLAG_CONFIG, "Reload the SaveLevel Map Config File."); - - // Support `sm plugins reload` / late loads: OnMapStart() won't fire until the next map otherwise. - if(g_bLateLoad) - { - g_bLateLoad = false; - OnMapStart(); - } } public void OnPluginEnd() From 20ae650f97faf15775eb687cd035e5a041b5bb3b Mon Sep 17 00:00:00 2001 From: Rushaway Date: Fri, 11 Sep 2026 11:25:37 +0200 Subject: [PATCH 3/3] fix: keep last known-good config on a failed sm_savelevel_reload LoadMapConfig() unconditionally deleted g_Config before attempting to load the replacement file. If the file was missing or failed to parse, g_Config was left null for the rest of the map even though Command_ReloadConfig() reported failure, silently disabling level saving/restoration. Load into a temporary KeyValues and only swap it into g_Config once it has been fully validated. A new `keepOnFailure` parameter distinguishes the two callers: OnMapStart() (false) still clears g_Config on failure, since the previous map's config is meaningless on a new map; the reload admin command (true) now leaves the active configuration untouched when the reload itself fails. Co-Authored-By: Claude Sonnet 5 --- addons/sourcemod/scripting/SaveLevel.sp | 35 +++++++++++++++++-------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/addons/sourcemod/scripting/SaveLevel.sp b/addons/sourcemod/scripting/SaveLevel.sp index 27973e7..9284794 100644 --- a/addons/sourcemod/scripting/SaveLevel.sp +++ b/addons/sourcemod/scripting/SaveLevel.sp @@ -60,10 +60,13 @@ public void OnMapStart() LoadMapConfig(); } -bool LoadMapConfig() +// `keepOnFailure` controls what happens to the currently active g_Config when loading fails: +// - OnMapStart() passes false: the previous map's config is meaningless on a new map, so it +// must be cleared even if the new map has no config of its own. +// - Command_ReloadConfig() passes true: a missing/malformed file on `sm_savelevel_reload` +// should leave the last known-good config (and level saving/restoring) running as-is. +bool LoadMapConfig(bool keepOnFailure = false) { - delete g_Config; - char sMapName[PLATFORM_MAX_PATH]; GetCurrentMap(sMapName, sizeof(sMapName)); @@ -78,28 +81,38 @@ bool LoadMapConfig() if(!FileExists(sConfigFile)) // Second attempt with Map name as lowercase { LogMessage("Could not find mapconfig: \"%s\"", sMapName); + if(!keepOnFailure) + delete g_Config; return false; } } LogMessage("Found mapconfig: \"%s\"", sConfigFile); - g_Config = new KeyValues("levels"); - if(!g_Config.ImportFromFile(sConfigFile)) + // Load into a temporary KeyValues and only swap it into g_Config once it has been fully + // validated, so a failed reload never leaves g_Config half-updated or unset. + KeyValues Config = new KeyValues("levels"); + if(!Config.ImportFromFile(sConfigFile)) { - delete g_Config; + delete Config; LogMessage("ImportFromFile() failed!"); + if(!keepOnFailure) + delete g_Config; return false; } - g_Config.Rewind(); + Config.Rewind(); - if(!g_Config.GotoFirstSubKey()) + if(!Config.GotoFirstSubKey()) { - delete g_Config; + delete Config; LogMessage("GotoFirstSubKey() failed!"); + if(!keepOnFailure) + delete g_Config; return false; } + delete g_Config; + g_Config = Config; return true; } @@ -470,10 +483,10 @@ public Action Command_ClearCache(int args) public Action Command_ReloadConfig(int client, int args) { - if(LoadMapConfig()) + if(LoadMapConfig(true)) CReplyToCommand(client, "%s Map config file has been reloaded.", PREFIX); else - CReplyToCommand(client, "%s No valid map config found for the current map (check the server logs).", PREFIX); + CReplyToCommand(client, "%s Failed to reload: no valid map config found for the current map (check the server logs). The active configuration is unchanged.", PREFIX); return Plugin_Handled; }