diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..120c689 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..96ddc2e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,89 @@ +name: CI + +on: [push, pull_request, workflow_dispatch] + +jobs: + build: + name: "Build" + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04] + include: + - os: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Build sourcemod plugin + uses: maxime1907/action-sourceknight@v1 + with: + cmd: build + + - name: Create package + run: | + mkdir -p /tmp/package + cp -R .sourceknight/package/* /tmp/package + cp -R addons/sourcemod/configs /tmp/package/common/addons/sourcemod/ + + - name: Upload build archive for test runners + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }} + path: /tmp/package + + tag: + name: Tag + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' + + - uses: dev-drprasad/delete-tag-and-release@v0.2.1 + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' + with: + delete_release: true + tag_name: latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: rickstaa/action-create-tag@v1 + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' + with: + tag: "latest" + github_token: ${{ secrets.GITHUB_TOKEN }} + + release: + name: Release + if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' + needs: [build, tag] + runs-on: ubuntu-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + + - name: Versioning + run: | + version="latest" + if [[ "${{ github.ref_type }}" == 'tag' ]]; then + version=`echo $GITHUB_REF | sed "s/refs\/tags\///"`; + fi + echo "RELEASE_VERSION=$version" >> $GITHUB_ENV + + - name: Package + run: | + ls -Rall + if [ -d "./Linux/" ]; then + cd ./Linux/ + tar -czf ../${{ github.event.repository.name }}-${{ env.RELEASE_VERSION }}.tar.gz -T <(\ls -1) + cd - + fi + + - name: Release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: '*.tar.gz' + tag: ${{ env.RELEASE_VERSION }} + file_glob: true + overwrite: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40f37f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +build/ +release/ + +.DS_Store +.vscode + +*.smx +plugins/ +.sourceknight +.venv diff --git a/addons/sourcemod/configs/stripper/global_filters.cfg b/addons/sourcemod/configs/stripper/global_filters.cfg new file mode 100644 index 0000000..4b732d5 --- /dev/null +++ b/addons/sourcemod/configs/stripper/global_filters.cfg @@ -0,0 +1,86 @@ +;; Changes ammo to 8000 on every map-spawned weapon +;modify: +;{ +; match: +; { +; "classname" "/weapon_.*/" +; } +; replace: +; { +; "ammo" "8000" +; } +;} + +;; Remove game_end +;filter: +;{ +; "classname" "game_end" +;} + +;;Cool Music Entities +;add: +;{ +; "origin" "0 0 0" +; "targetname" "GlobalAmbientGenericStripper" +; "spawnflags" "49" +; "radius" "1250" +; "pitchstart" "100" +; "pitch" "100" +; "message" "music/hl1_song10.mp3" +; "health" "10" +; "classname" "ambient_generic" +;} + +;add: +;{ +; "origin" "0 0 0" +; "targetname" "GlobalAmbientGenericStripper2" +; "spawnflags" "49" +; "radius" "1250" +; "pitchstart" "100" +; "pitch" "100" +; "message" "music/hl2_song23_suitsong3.mp3" +; "health" "10" +; "classname" "ambient_generic" +;} + +;add: +;{ +; "origin" "0 0 0" +; "targetname" "GlobalAmbientGenericStripper2" +; "spawnflags" "49" +; "radius" "1250" +; "pitchstart" "100" +; "pitch" "100" +; "message" "music/hl2_song23_suitsong3.mp3" +; "health" "10" +; "classname" "ambient_generic" +;} + +;add: +;{ +; "origin" "0 0 0" +; "targetname" "GlobalLogicTimerStripper" +; "RefireTime" "0.1" +; "spawnflags" "0" +; "StartDisabled" "1" +; "UseRandomTime" "0" +; "classname" "logic_timer" +;} + +;add: +;{ +; "origin" "0 0 0" +; "targetname" "GlobalGameTextStripper" +; "x" "-1" +; "y" ".20" +; "channel" "4" +; "message" "Default Message" +; "color" "0 255 255" +; "color2" "0 240 240" +; "fadein" ".1" +; "fadeout" ".1" +; "holdtime" "3" +; "spawnflags" "1" +; "classname" "game_text" +;} diff --git a/addons/sourcemod/configs/stripper/maps/example_map.cfg b/addons/sourcemod/configs/stripper/maps/example_map.cfg new file mode 100644 index 0000000..3aff964 --- /dev/null +++ b/addons/sourcemod/configs/stripper/maps/example_map.cfg @@ -0,0 +1,35 @@ +;;EXAMPLE - remove all physics props +;remove: +;{ +; "classname" "/prop_physics.*/" +;} + +;;EXAMPLE - add the hostage to the map +;add: +;{ +; "origin" "1376 3168 -112" +; "HostageType" "0" +; "classname" "hostage_entity" +} + +;;EXAMPLE - replace all garbage cans with a hostage +;modify: +;{ +; match: +; { +; "model" "models/props_junk/garbage_metalcan002a.mdl" +; "classname" "prop_physics_multiplayer" +; } +; replace: +; { +; "classname" "hostage_entity" +; } +; delete: +; { +; "model" "models/props_junk/garbage_metalcan002a.mdl" +; } +; insert: +; { +; "scale" "0.99" +; } +;} diff --git a/stripper.sp b/addons/sourcemod/scripting/Stripper.sp similarity index 73% rename from stripper.sp rename to addons/sourcemod/scripting/Stripper.sp index 36820c3..6f662c9 100644 --- a/stripper.sp +++ b/addons/sourcemod/scripting/Stripper.sp @@ -1,495 +1,575 @@ -#pragma semicolon 1 -#pragma newdecls required - -#include -#include - -public Plugin myinfo = -{ - name = "Stripper:Source (SP edition)", - version = "1.3.1", - description = "Stripper:Source functionality in a Sourcemod plugin", - author = "tilgep, Stripper:Source by BAILOPAN", - url = "https://forums.alliedmods.net/showthread.php?t=339448" -} - -enum Mode -{ - Mode_None, - Mode_Filter, - Mode_Add, - Mode_Modify, -} - -enum SubMode -{ - SubMode_None, - SubMode_Match, - SubMode_Replace, - SubMode_Delete, - SubMode_Insert, -} - -enum struct Property -{ - char key[PLATFORM_MAX_PATH]; - char val[PLATFORM_MAX_PATH]; - bool regex; -} - -/* Stripper block struct */ -enum struct Block -{ - Mode mode; - SubMode submode; - ArrayList match; // Filter/Modify - ArrayList replace; // Modify - ArrayList del; // Modify - ArrayList insert; // Add/Modify - bool hasClassname; // Ensures that an add block has a classname set - - void Init() - { - this.mode = Mode_None; - this.submode = SubMode_None; - this.match = CreateArray(sizeof(Property)); - this.replace = CreateArray(sizeof(Property)); - this.del = CreateArray(sizeof(Property)); - this.insert = CreateArray(sizeof(Property)); - } - - void Clear() - { - this.hasClassname = false; - this.mode = Mode_None; - this.submode = SubMode_None; - this.match.Clear(); - this.replace.Clear(); - this.del.Clear(); - this.insert.Clear(); - } -} - -char file[PLATFORM_MAX_PATH]; -ConVar fileLowercase; -Block prop; // Global current stripper block -int section; - -public void OnPluginStart() -{ - prop.Init(); - - RegAdminCmd("stripper_dump", Command_Dump, ADMFLAG_ROOT, "Writes all of the map entity properties to a file in configs/stripper/dumps/"); - - fileLowercase = CreateConVar("stripper_file_lowercase", "0", "Whether to load map config filenames as lower case", _, true, 0.0, true, 1.0); - AutoExecConfig(true, "stripper"); -} - -public Action Command_Dump(int client, int args) -{ - char buf1[PLATFORM_MAX_PATH], buf2[PLATFORM_MAX_PATH], path[PLATFORM_MAX_PATH]; - int num = -1; - - GetCurrentMap(buf1, PLATFORM_MAX_PATH); - - BuildPath(Path_SM, buf2, PLATFORM_MAX_PATH, "configs/stripper/dumps"); - - if(!DirExists(buf2)) CreateDirectory(buf2, FPERM_O_READ|FPERM_O_EXEC|FPERM_G_READ|FPERM_G_EXEC|FPERM_U_READ|FPERM_U_WRITE|FPERM_U_EXEC); - - do - { - num++; - // Use same format as original stripper - Format(path, PLATFORM_MAX_PATH, "%s/%s.%04d.cfg", buf2, buf1, num); - } - while(FileExists(path)); - - File fi = OpenFile(path, "w"); - if(fi == null) - { - LogError("Failed to create dump file \"%s\"", path); - return Plugin_Handled; - } - - EntityLumpEntry ent; - - for(int i = 0; i < EntityLump.Length(); i++) - { - ent = EntityLump.Get(i); - - fi.WriteLine("{"); - - for(int j = 0; j < ent.Length; j++) - { - ent.Get(j, buf1, PLATFORM_MAX_PATH, buf2, PLATFORM_MAX_PATH); - fi.WriteLine("\"%s\" \"%s\"", buf1, buf2); - } - - fi.WriteLine("}"); - - delete ent; - } - - delete fi; - - ReplyToCommand(client, "[SM] Dumped entities to '%s'", path); - return Plugin_Handled; -} - -public void OnMapInit(const char[] mapName) -{ - // Parse global filters - BuildPath(Path_SM, file, sizeof(file), "configs/stripper/global_filters.cfg"); - - ParseFile(); - - // Now parse map config - strcopy(file, sizeof(file), mapName); - - if(fileLowercase.BoolValue) - { - for(int i = 0; file[i]; i++) - file[i] = CharToLower(file[i]); - } - - BuildPath(Path_SM, file, sizeof(file), "configs/stripper/maps/%s.cfg", file); - - ParseFile(); -} - -/** - * Parses a stripper config file - * - * @param path Path to parse from - */ -public void ParseFile() -{ - int line, col; - section = 0; - - prop.Clear(); - - SMCParser parser = SMC_CreateParser(); - SMC_SetReaders(parser, Config_NewSection, Config_KeyValue, Config_EndSection); - - SMCError result = SMC_ParseFile(parser, file, line, col); - delete parser; - - if(result != SMCError_Okay && result != SMCError_StreamOpen) - { - if(result == SMCError_StreamOpen) - { - LogMessage("Failed to open stripper config \"%s\"", file); - } - else - { - char error[128]; - SMC_GetErrorString(result, error, sizeof(error)); - LogError("%s on line %d, col %d of %s", error, line, col, file); - } - } -} - -public SMCResult Config_NewSection(SMCParser smc, const char[] name, bool opt_quotes) -{ - section++; - if(!strcmp(name, "filter:", false) || !strcmp(name, "remove:", false)) - { - if(prop.mode != Mode_None) - { - LogError("Found 'filter' block while inside another block at section %d in file '%s'", section, file); - } - - prop.Clear(); - prop.mode = Mode_Filter; - } - else if(!strcmp(name, "add:", false)) - { - if(prop.mode != Mode_None) - { - LogError("Found 'add' block while inside another block at section %d in file '%s'", section, file); - } - - prop.Clear(); - prop.mode = Mode_Add; - } - else if(!strcmp(name, "modify:", false)) - { - if(prop.mode != Mode_None) - { - LogError("Found 'modify' block while inside another block at section %d in file '%s'", section, file); - } - - prop.Clear(); - prop.mode = Mode_Modify; - } - else if(prop.mode == Mode_Modify) - { - if(!strcmp(name, "match:", false)) prop.submode = SubMode_Match; - else if(!strcmp(name, "replace:", false)) prop.submode = SubMode_Replace; - else if(!strcmp(name, "delete:", false)) prop.submode = SubMode_Delete; - else if(!strcmp(name, "insert:", false)) prop.submode = SubMode_Insert; - else - { - LogError("Found invalid section '%s' in modify block at section %d in file '%s'", name, section, file); - } - } - else - { - LogError("Found invalid section name '%s' at section %d in file '%s'", name, section, file); - } - - return SMCParse_Continue; -} - -public SMCResult Config_KeyValue(SMCParser smc, const char[] key, const char[] value, bool key_quotes, bool value_quotes) -{ - Property kv; - strcopy(kv.key, PLATFORM_MAX_PATH, key); - strcopy(kv.val, PLATFORM_MAX_PATH, value); - kv.regex = FormatRegex(kv.val, strlen(value)); - - switch(prop.mode) - { - case Mode_None: return SMCParse_Continue; - case Mode_Filter: prop.match.PushArray(kv); - case Mode_Add: - { - // Adding an entity without a classname will crash the server (shortest classname is "gib") - if(StrEqual(key, "classname", false) && strlen(value) > 2) prop.hasClassname = true; - - prop.insert.PushArray(kv); - } - case Mode_Modify: - { - switch(prop.submode) - { - case SubMode_Match: prop.match.PushArray(kv); - case SubMode_Replace: prop.replace.PushArray(kv); - case SubMode_Delete: prop.del.PushArray(kv); - case SubMode_Insert: prop.insert.PushArray(kv); - } - } - } - - return SMCParse_Continue; -} - -public SMCResult Config_EndSection(SMCParser smc) -{ - switch(prop.mode) - { - case Mode_Filter: - { - if(prop.match.Length > 0) RunRemoveFilter(); - - prop.mode = Mode_None; - } - case Mode_Add: - { - if(prop.insert.Length > 0) - { - if(prop.hasClassname) RunAddFilter(); - else LogError("Add block with no classname found at section %d in file '%s'", section, file); - } - - prop.mode = Mode_None; - } - case Mode_Modify: - { - // Exiting a modify sub-block - if(prop.submode != SubMode_None) - { - prop.submode = SubMode_None; - return SMCParse_Continue; - } - - // Must have something to match for modify blocks - if(prop.match.Length > 0) RunModifyFilter(); - - prop.mode = Mode_None; - } - } - return SMCParse_Continue; -} - -public void RunRemoveFilter() -{ - /* prop.match holds what we want - * we know it has at least 1 entry here - */ - - char val2[PLATFORM_MAX_PATH]; - Property kv; - EntityLumpEntry entry; - for(int i, matches, j, index; i < EntityLump.Length(); i++) - { - matches = 0; - entry = EntityLump.Get(i); - - for(j = 0; j < prop.match.Length; j++) - { - prop.match.GetArray(j, kv, sizeof(kv)); - - index = entry.GetNextKey(kv.key, val2, sizeof(val2)); - while(index != -1) - { - if(EntPropsMatch(kv.val, val2, kv.regex)) - { - matches++; - break; - } - - index = entry.GetNextKey(kv.key, val2, sizeof(val2), index); - } - } - - if(matches == prop.match.Length) - { - EntityLump.Erase(i); - i--; - } - delete entry; - } -} - -public void RunAddFilter() -{ - /* prop.insert holds what we want - * we know it has at least 1 entry here - */ - - int index = EntityLump.Append(); - EntityLumpEntry entry = EntityLump.Get(index); - - Property kv; - for(int i; i < prop.insert.Length; i++) - { - prop.insert.GetArray(i, kv, sizeof(kv)); - entry.Append(kv.key, kv.val); - } - - delete entry; -} - -public void RunModifyFilter() -{ - /* prop.match holds at least 1 entry here - * others may not have anything - */ - - // Nothing to do if these are all empty - if(prop.replace.Length == 0 && prop.del.Length == 0 && prop.insert.Length == 0) - { - return; - } - - char val2[PLATFORM_MAX_PATH]; - - Property kv; - EntityLumpEntry entry; - for(int i, matches, j, index; i < EntityLump.Length(); i++) - { - matches = 0; - entry = EntityLump.Get(i); - - /* Check matches */ - for(j = 0; j < prop.match.Length; j++) - { - prop.match.GetArray(j, kv, sizeof(kv)); - - index = entry.GetNextKey(kv.key, val2, sizeof(val2)); - while(index != -1) - { - if(EntPropsMatch(kv.val, val2, kv.regex)) - { - matches++; - break; - } - - index = entry.GetNextKey(kv.key, val2, sizeof(val2), index); - } - } - - if(matches < prop.match.Length) - { - delete entry; - continue; - } - - /* This entry matches, perform any changes */ - - /* First do deletions */ - if(prop.del.Length > 0) - { - for(j = 0; j < prop.del.Length; j++) - { - prop.del.GetArray(j, kv, sizeof(kv)); - - index = entry.GetNextKey(kv.key, val2, sizeof(val2)); - while(index != -1) - { - if(EntPropsMatch(kv.val, val2, kv.regex)) - { - entry.Erase(index); - index--; - } - index = entry.GetNextKey(kv.key, val2, sizeof(val2), index); - } - } - } - - /* do replacements */ - if(prop.replace.Length > 0) - { - for(j = 0; j < prop.replace.Length; j++) - { - prop.replace.GetArray(j, kv, sizeof(kv)); - - index = entry.GetNextKey(kv.key, val2, sizeof(val2)); - while(index != -1) - { - entry.Update(index, NULL_STRING, kv.val); - index = entry.GetNextKey(kv.key, val2, sizeof(val2), index); - } - } - } - - /* do insertions */ - if(prop.insert.Length > 0) - { - for(j = 0; j < prop.insert.Length; j++) - { - prop.insert.GetArray(j, kv, sizeof(kv)); - entry.Append(kv.key, kv.val); - } - } - - delete entry; - } -} - -/** - * Checks if 2 values match - * - * @param val1 First value - * @param val2 Second value - * @param isRegex True if val1 should be treated as a regex pattern, false if not - * @return True if match, false otherwise - * - */ -stock bool EntPropsMatch(const char[] val1, const char[] val2, bool isRegex) -{ - return isRegex ? SimpleRegexMatch(val2, val1) > 0 : !strcmp(val1, val2); -} - -stock bool FormatRegex(char[] pattern, int len) -{ - if(pattern[0] == '/' && pattern[len-1] == '/') - { - strcopy(pattern, len-1, pattern[1]); - return true; - } - - return false; -} +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include + +public Plugin myinfo = +{ + name = "Stripper:Source (SP edition)", + version = "1.3.3", + description = "Stripper:Source functionality in a Sourcemod plugin", + author = "Original Author: BAILOPAN. Ported to SM by: tilgep. Edited by: Lerrdy, .Rushaway", + url = "https://forums.alliedmods.net/showthread.php?t=339448" +} + +enum Mode +{ + Mode_None, + Mode_Filter, + Mode_Add, + Mode_Modify, +} + +enum SubMode +{ + SubMode_None, + SubMode_Match, + SubMode_Replace, + SubMode_Delete, + SubMode_Insert, +} + +enum struct Property +{ + char key[PLATFORM_MAX_PATH]; + char val[PLATFORM_MAX_PATH]; + bool regex; +} + +/* Stripper block struct */ +enum struct Block +{ + Mode mode; + SubMode submode; + ArrayList match; // Filter/Modify + ArrayList replace; // Modify + ArrayList del; // Modify + ArrayList insert; // Add/Modify + bool hasClassname; // Ensures that an add block has a classname set + + void Init() + { + this.mode = Mode_None; + this.submode = SubMode_None; + this.match = CreateArray(sizeof(Property)); + this.replace = CreateArray(sizeof(Property)); + this.del = CreateArray(sizeof(Property)); + this.insert = CreateArray(sizeof(Property)); + } + + void Clear() + { + this.hasClassname = false; + this.mode = Mode_None; + this.submode = SubMode_None; + this.match.Clear(); + this.replace.Clear(); + this.del.Clear(); + this.insert.Clear(); + } +} + +char file[PLATFORM_MAX_PATH]; +char g_sLogPath[PLATFORM_MAX_PATH]; +bool g_bConfigLoaded = false; +bool g_bConfigError = false; +Handle g_hFwd_OnErrorLogged = INVALID_HANDLE; +ConVar fileLowercase; +Block prop; // Global current stripper block +int section; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNative("Stripper_LogError", Native_Log); + g_hFwd_OnErrorLogged = CreateGlobalForward("Stripper_OnErrorLogged", ET_Ignore, Param_String); + + RegPluginLibrary("Stripper"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + prop.Init(); + + RegAdminCmd("stripper_dump", Command_Dump, ADMFLAG_ROOT, "Writes all of the map entity properties to a file in configs/stripper/dumps/"); + RegAdminCmd("sm_stripper", Command_Stripper, ADMFLAG_GENERIC, "Prints out if the current map has a loaded stripper file"); + + fileLowercase = CreateConVar("stripper_file_lowercase", "0", "Whether to load map config filenames as lower case", _, true, 0.0, true, 1.0); + AutoExecConfig(true, "stripper"); +} + +public Action Command_Stripper(int client, int args) +{ + bool bAccess = CheckCommandAccess(client, "sm_stripper", ADMFLAG_ROOT); + if (g_bConfigLoaded) + { + ReplyToCommand(client, "[Strippper] The current map has a loaded stripper config."); + if(bAccess) ReplyToCommand(client, "[Strippper] Actual cfg: %s", file); + } + else if (g_bConfigError) + { + ReplyToCommand(client, "[Strippper] The current map has a loaded stripper config but it contains error(s)"); + if(bAccess) ReplyToCommand(client, "[Strippper] Check (%s)", file); + } + else + { + ReplyToCommand(client, "[Strippper] The current map did not load a stripper config."); + if(bAccess) ReplyToCommand(client, "[Strippper] No file found: (%s)", file); + } + + return Plugin_Handled; +} + +public Action Command_Dump(int client, int args) +{ + char buf1[PLATFORM_MAX_PATH], buf2[PLATFORM_MAX_PATH], path[PLATFORM_MAX_PATH]; + int num = -1; + + GetCurrentMap(buf1, PLATFORM_MAX_PATH); + + BuildPath(Path_SM, buf2, PLATFORM_MAX_PATH, "logs/stripper/dumps"); + + if(!DirExists(buf2)) CreateDirectory(buf2, 0o666); + + do + { + num++; + // Use same format as original stripper + Format(path, PLATFORM_MAX_PATH, "%s/%s.%04d.cfg", buf2, buf1, num); + } + while(FileExists(path)); + + File fi = OpenFile(path, "w"); + if(fi == null) + { + Stripper_LogError("Failed to create dump file \"%s\"", path); + return Plugin_Handled; + } + + EntityLumpEntry ent; + + for(int i = 0; i < EntityLump.Length(); i++) + { + ent = EntityLump.Get(i); + + fi.WriteLine("{"); + + for(int j = 0; j < ent.Length; j++) + { + ent.Get(j, buf1, PLATFORM_MAX_PATH, buf2, PLATFORM_MAX_PATH); + fi.WriteLine("\"%s\" \"%s\"", buf1, buf2); + } + + fi.WriteLine("}"); + + delete ent; + } + + delete fi; + + ReplyToCommand(client, "[SM] Dumped entities to '%s'", path); + return Plugin_Handled; +} + +public void OnMapInit(const char[] mapName) +{ + // Path used for logging. + BuildPath(Path_SM, g_sLogPath, sizeof(g_sLogPath), "logs/stripper/maps/%s.log", mapName); + + g_bConfigLoaded = false; + g_bConfigError = false; + + // Parse global filters + BuildPath(Path_SM, file, sizeof(file), "configs/stripper/global_filters.cfg"); + ParseFile(false); + + // Now parse map config + BuildPath(Path_SM, file, sizeof(file), "configs/stripper/maps/%s.cfg", mapName); + + if(!ParseFile(true) && fileLowercase.BoolValue) + { + strcopy(file, sizeof(file), mapName); + for(int i = 0; file[i]; i++) + file[i] = CharToLower(file[i]); + + BuildPath(Path_SM, file, sizeof(file), "configs/stripper/maps/%s.cfg", file); + ParseFile(true); + } +} + +/** + * Parses a stripper config file + * + * @param path Path to parse from + * @return True if successful, false otherwise + */ +public bool ParseFile(bool mapconfig) +{ + int line, col; + section = 0; + + prop.Clear(); + + SMCParser parser = SMC_CreateParser(); + SMC_SetReaders(parser, Config_NewSection, Config_KeyValue, Config_EndSection); + + SMCError result = SMC_ParseFile(parser, file, line, col); + delete parser; + + if (result == SMCError_Okay) + { + if (mapconfig) + g_bConfigLoaded = true; + + return true; + } + + if(result != SMCError_Okay && result != SMCError_StreamOpen) + { + if(result == SMCError_StreamOpen) + { + g_bConfigLoaded = false; + LogMessage("Failed to open stripper config \"%s\"", file); + } + else + { + char error[128]; + g_bConfigError = true; + SMC_GetErrorString(result, error, sizeof(error)); + Stripper_LogError("%s on line %d, col %d of %s", error, line, col, file); + } + } + + return false; +} + +public SMCResult Config_NewSection(SMCParser smc, const char[] name, bool opt_quotes) +{ + section++; + if(!strcmp(name, "filter:", false) || !strcmp(name, "remove:", false)) + { + if(prop.mode != Mode_None) + { + g_bConfigError = true; + Stripper_LogError("Found 'filter' block while inside another block at section %d in file '%s'", section, file); + } + + prop.Clear(); + prop.mode = Mode_Filter; + } + else if(!strcmp(name, "add:", false)) + { + if(prop.mode != Mode_None) + { + g_bConfigError = true; + Stripper_LogError("Found 'add' block while inside another block at section %d in file '%s'", section, file); + } + + prop.Clear(); + prop.mode = Mode_Add; + } + else if(!strcmp(name, "modify:", false)) + { + if(prop.mode != Mode_None) + { + g_bConfigError = true; + Stripper_LogError("Found 'modify' block while inside another block at section %d in file '%s'", section, file); + } + + prop.Clear(); + prop.mode = Mode_Modify; + } + else if(prop.mode == Mode_Modify) + { + if(!strcmp(name, "match:", false)) prop.submode = SubMode_Match; + else if(!strcmp(name, "replace:", false)) prop.submode = SubMode_Replace; + else if(!strcmp(name, "delete:", false)) prop.submode = SubMode_Delete; + else if(!strcmp(name, "insert:", false)) prop.submode = SubMode_Insert; + else + { + g_bConfigError = true; + Stripper_LogError("Found invalid section '%s' in modify block at section %d in file '%s'", name, section, file); + } + } + else + { + g_bConfigError = true; + Stripper_LogError("Found invalid section name '%s' at section %d in file '%s'", name, section, file); + } + + return SMCParse_Continue; +} + +public SMCResult Config_KeyValue(SMCParser smc, const char[] key, const char[] value, bool key_quotes, bool value_quotes) +{ + Property kv; + strcopy(kv.key, PLATFORM_MAX_PATH, key); + strcopy(kv.val, PLATFORM_MAX_PATH, value); + kv.regex = FormatRegex(kv.val, strlen(value)); + + switch(prop.mode) + { + case Mode_None: return SMCParse_Continue; + case Mode_Filter: prop.match.PushArray(kv); + case Mode_Add: + { + // Adding an entity without a classname will crash the server (shortest classname is "gib") + if(strcmp(key, "classname", false) == 0 && strlen(value) > 2) prop.hasClassname = true; + + prop.insert.PushArray(kv); + } + case Mode_Modify: + { + switch(prop.submode) + { + case SubMode_Match: prop.match.PushArray(kv); + case SubMode_Replace: prop.replace.PushArray(kv); + case SubMode_Delete: prop.del.PushArray(kv); + case SubMode_Insert: prop.insert.PushArray(kv); + } + } + } + + return SMCParse_Continue; +} + +public SMCResult Config_EndSection(SMCParser smc) +{ + switch(prop.mode) + { + case Mode_Filter: + { + if(prop.match.Length > 0) RunRemoveFilter(); + + prop.mode = Mode_None; + } + case Mode_Add: + { + if(prop.insert.Length > 0) + { + if(prop.hasClassname) + RunAddFilter(); + else + { + g_bConfigError = true; + Stripper_LogError("Add block with no classname found at section %d in file '%s'", section, file); + } + } + + prop.mode = Mode_None; + } + case Mode_Modify: + { + // Exiting a modify sub-block + if(prop.submode != SubMode_None) + { + prop.submode = SubMode_None; + return SMCParse_Continue; + } + + // Must have something to match for modify blocks + if(prop.match.Length > 0) RunModifyFilter(); + + prop.mode = Mode_None; + } + } + return SMCParse_Continue; +} + +public void RunRemoveFilter() +{ + /* prop.match holds what we want + * we know it has at least 1 entry here + */ + + char val2[PLATFORM_MAX_PATH]; + Property kv; + EntityLumpEntry entry; + for(int i, matches, j, index; i < EntityLump.Length(); i++) + { + matches = 0; + entry = EntityLump.Get(i); + + for(j = 0; j < prop.match.Length; j++) + { + prop.match.GetArray(j, kv, sizeof(kv)); + + index = entry.GetNextKey(kv.key, val2, sizeof(val2)); + while(index != -1) + { + if(EntPropsMatch(kv.val, val2, kv.regex)) + { + matches++; + break; + } + + index = entry.GetNextKey(kv.key, val2, sizeof(val2), index); + } + } + + if(matches == prop.match.Length) + { + EntityLump.Erase(i); + i--; + } + delete entry; + } +} + +public void RunAddFilter() +{ + /* prop.insert holds what we want + * we know it has at least 1 entry here + */ + + int index = EntityLump.Append(); + EntityLumpEntry entry = EntityLump.Get(index); + + Property kv; + for(int i; i < prop.insert.Length; i++) + { + prop.insert.GetArray(i, kv, sizeof(kv)); + entry.Append(kv.key, kv.val); + } + + delete entry; +} + +public void RunModifyFilter() +{ + /* prop.match holds at least 1 entry here + * others may not have anything + */ + + // Nothing to do if these are all empty + if(prop.replace.Length == 0 && prop.del.Length == 0 && prop.insert.Length == 0) + { + return; + } + + char val2[PLATFORM_MAX_PATH]; + + Property kv; + EntityLumpEntry entry; + for(int i, matches, j, index; i < EntityLump.Length(); i++) + { + matches = 0; + entry = EntityLump.Get(i); + + /* Check matches */ + for(j = 0; j < prop.match.Length; j++) + { + prop.match.GetArray(j, kv, sizeof(kv)); + + index = entry.GetNextKey(kv.key, val2, sizeof(val2)); + while(index != -1) + { + if(EntPropsMatch(kv.val, val2, kv.regex)) + { + matches++; + break; + } + + index = entry.GetNextKey(kv.key, val2, sizeof(val2), index); + } + } + + if(matches < prop.match.Length) + { + delete entry; + continue; + } + + /* This entry matches, perform any changes */ + + /* First do deletions */ + if(prop.del.Length > 0) + { + for(j = 0; j < prop.del.Length; j++) + { + prop.del.GetArray(j, kv, sizeof(kv)); + + index = entry.GetNextKey(kv.key, val2, sizeof(val2)); + while(index != -1) + { + if(EntPropsMatch(kv.val, val2, kv.regex)) + { + entry.Erase(index); + index--; + } + index = entry.GetNextKey(kv.key, val2, sizeof(val2), index); + } + } + } + + /* do replacements */ + if(prop.replace.Length > 0) + { + for(j = 0; j < prop.replace.Length; j++) + { + prop.replace.GetArray(j, kv, sizeof(kv)); + + index = entry.GetNextKey(kv.key, val2, sizeof(val2)); + while(index != -1) + { + entry.Update(index, NULL_STRING, kv.val); + index = entry.GetNextKey(kv.key, val2, sizeof(val2), index); + } + } + } + + /* do insertions */ + if(prop.insert.Length > 0) + { + for(j = 0; j < prop.insert.Length; j++) + { + prop.insert.GetArray(j, kv, sizeof(kv)); + entry.Append(kv.key, kv.val); + } + } + + delete entry; + } +} + +/** + * Checks if 2 values match + * + * @param val1 First value + * @param val2 Second value + * @param isRegex True if val1 should be treated as a regex pattern, false if not + * @return True if match, false otherwise + * + */ +stock bool EntPropsMatch(const char[] val1, const char[] val2, bool isRegex) +{ + return isRegex ? SimpleRegexMatch(val2, val1) > 0 : !strcmp(val1, val2); +} + +stock bool FormatRegex(char[] pattern, int len) +{ + if(pattern[0] == '/' && pattern[len-1] == '/') + { + strcopy(pattern, len-1, pattern[1]); + return true; + } + + return false; +} + +// native Stripper_LogError(const char[] format, any...); +public int Native_Log(Handle plugin, int numParams) +{ + char sBuffer[2048]; + FormatNativeString(0, 1, 2, sizeof(sBuffer), _, sBuffer); + LogToFileEx(g_sLogPath, "%s", sBuffer); + + // Start forward call + Call_StartForward(g_hFwd_OnErrorLogged); + Call_PushString(sBuffer); + Call_Finish(); + + return 1; +} diff --git a/addons/sourcemod/scripting/include/Stripper.inc b/addons/sourcemod/scripting/include/Stripper.inc new file mode 100644 index 0000000..d322cc1 --- /dev/null +++ b/addons/sourcemod/scripting/include/Stripper.inc @@ -0,0 +1,39 @@ +#if defined _stripper_included +#endinput +#endif +#define _stripper_included + +/********************************************************* + * Log a message into logs/stripper/maps/.log + * + * @param format Message to log + * @noreturn + *********************************************************/ +native void Stripper_LogError(const char[] format, any ...); + +/** + * Called right after stripper loged a error. + * + * @param sBuffer Buffer to store the log message in. + * @param maxlen Size of the log buffer. + * @noreturn + */ +forward void Stripper_OnErrorLogged(char[] sBuffer, int maxlen); + +public SharedPlugin __pl_stripper = +{ + name = "Stripper", + file = "Stripper.smx", + #if defined REQUIRE_PLUGIN + required = 1 + #else + required = 0 + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_stripper_SetNTVOptional() +{ + MarkNativeAsOptional("Stripper_Log"); +} +#endif diff --git a/sourceknight.yaml b/sourceknight.yaml new file mode 100644 index 0000000..5773010 --- /dev/null +++ b/sourceknight.yaml @@ -0,0 +1,15 @@ +project: + sourceknight: 0.2 + name: Stripper + dependencies: + - name: sourcemod + type: tar + version: 1.11.0-git6934 + location: https://sm.alliedmods.net/smdrop/1.11/sourcemod-1.11.0-git6934-linux.tar.gz + unpack: + - source: /addons + dest: /addons + root: / + output: /addons/sourcemod/plugins + targets: + - Stripper