/** * [CSRD] SRCDS Data Dumper * * Uses SourceMod dumping commands to dump useful information for plugin developers. */ #pragma semicolon 1 #include #tryinclude #pragma newdecls required #include #include #include #define PLUGIN_VERSION "1.2.0" public Plugin myinfo = { name = "[CSRD] SRCDS Automatic Data Dumper", author = "nosoop", description = "Automatically dumps useful engine data for plugin stuff.", #if defined _steamtools_included version = PLUGIN_VERSION, #else version = PLUGIN_VERSION ... "-no-steamtools", #endif url = "https://git.csrd.science/" } #define REDUMP_MARKER_FILE "data/.force-data-dump" #define REDUMP_OUTPUT_DIRECTORY "data/datadump" // Use a map with as few entities as possible to ensure enough free edicts #define LOW_ENTITY_MAP "itemtest" enum DumpAction { DumpAction_None = 0, // no checks are performed DumpAction_UpdateCheck, // check if dump indicator is present (TODO change method?) DumpAction_PendingDump // pending dump on current map }; DumpAction g_DumpAction; StringMap g_ConCommandFlagCache; public APLRes AskPluginLoad2(Handle hPluginSelf, bool bLateLoaded, char[] error, int err_max) { if (bLateLoaded) { // skip dump on late load because muh consistency g_DumpAction = DumpAction_None; } else { g_DumpAction = DumpAction_UpdateCheck; /** * cache flags early in case a plugin overrides flags * https://github.com/nosoop/SM-ConVarConfigs does this when all plugins are loaded */ g_ConCommandFlagCache = new StringMap(); char commandName[128]; bool bIsCommand; int flags; Handle hConCommandIter = FindFirstConCommand(commandName, sizeof(commandName), bIsCommand, flags); do { g_ConCommandFlagCache.SetValue(commandName, flags); } while ( FindNextConCommand(hConCommandIter, commandName, sizeof(commandName), bIsCommand, flags) ); delete hConCommandIter; } return APLRes_Success; } public void OnPluginStart() { RegAdminCmd("csrd_force_data_dump", ForceDataDump, ADMFLAG_ROOT); } public void OnMapStart() { char path[PLATFORM_MAX_PATH]; BuildPath(Path_SM, path, sizeof(path), "%s", REDUMP_MARKER_FILE); switch (g_DumpAction) { /** * Stage 1: Check if the marker file exists; change map if necessary. * This is performed on first map load. */ case DumpAction_UpdateCheck: { if (FileExists(path, false)) { g_DumpAction = DumpAction_PendingDump; ForceChangeLevel(LOW_ENTITY_MAP, "Starting server data dump"); } else { g_DumpAction = DumpAction_None; // we're not dumping, so there's no point in keeping the cache delete g_ConCommandFlagCache; g_ConCommandFlagCache = null; } } /** * Stage 2: Validate map change and ensure dump is pending; start dumping if so. */ case DumpAction_PendingDump: { char currentMap[PLATFORM_MAX_PATH]; GetCurrentMap(currentMap, sizeof(currentMap)); if (StrEqual(LOW_ENTITY_MAP, currentMap) && FileExists(path, false)) { DataPack pack = new DataPack(); pack.WriteString(path); PerformDataDump(OnDataDumpFinished, pack); } else { LogError("Failed to change to map %s", LOW_ENTITY_MAP); } } } } void PerformDataDump(RequestFrameCallback postDumpCallback = INVALID_FUNCTION, any data = 0) { char outputDirectory[PLATFORM_MAX_PATH]; BuildPath(Path_SM, outputDirectory, sizeof(outputDirectory), REDUMP_OUTPUT_DIRECTORY); CreateDirectories(outputDirectory, 0b111101000); // u+rwx,g+rx int version = GetNetworkPatchVersion(); // does not spawn entities ServerCommand("sm_dump_netprops %s/%d.netprops.txt", outputDirectory, version); DumpServerCommands("%s/%d.commands.txt", outputDirectory, version); DumpServerConVars("%s/%d.convars.txt", outputDirectory, version); DumpUserMessageNames("%s/%d.usermessages.txt", outputDirectory, version); /** * spawns all entities, server will crash on map change -- make sure there are 1400-ish * free edicts */ ServerCommand("sm_dump_datamaps %s/%d.datamaps.txt", outputDirectory, version); // TODO replace this with just filtering the appropriate lines from datamaps? // match expr /^\w+ - \w+$/ ServerCommand("sm_dump_classes %s/%d.classes.txt", outputDirectory, version); ServerCommand("sm_dump_teprops %s/%d.tempents.txt", outputDirectory, version); // Defer the rest of the actions by a frame to ensure everything is finished processing. DataPack pack = new DataPack(); pack.WriteFunction(postDumpCallback); pack.WriteCell(data); RequestFrame(PerformDataDump_NextFrame, pack); } public void PerformDataDump_NextFrame(DataPack pack) { // Create and update a .server-version file to notify incron of updates char updatePath[PLATFORM_MAX_PATH]; BuildPath(Path_SM, updatePath, sizeof(updatePath), "%s/%s", REDUMP_OUTPUT_DIRECTORY, ".server-version"); File versionFile = OpenFile(updatePath, "w"); versionFile.WriteLine("%d", GetNetworkPatchVersion()); delete versionFile; pack.Reset(); Function postDumpCallback = pack.ReadFunction(); any data = pack.ReadCell(); delete pack; if (postDumpCallback != INVALID_FUNCTION) { Call_StartFunction(INVALID_HANDLE, postDumpCallback); Call_PushCell(data); Call_Finish(); } } /** * User-defined call when the dumping process is finished. */ public void OnDataDumpFinished(DataPack pack) { pack.Reset(); char path[PLATFORM_MAX_PATH]; pack.ReadString(path, sizeof(path)); delete pack; DeleteFile(path); LogMessage("Dumped server data for network patchversion %d", GetNetworkPatchVersion()); ServerCommand("quit"); } #if defined _steamtools_included /** * Steam master server has requested a restart; game version probably updated. * Create the file indicating a data dump should be performed. */ public Action Steam_RestartRequested() { PrepareRedump(); return Plugin_Continue; } #endif public Action ForceDataDump(int client, int argc) { PrepareRedump(); ReplyToCommand(client, "[SM] Prepared dump process. Restart server."); return Plugin_Continue; } /* Dump utilities */ void DumpServerCommands(const char[] format, any ...) { char outputPath[PLATFORM_MAX_PATH]; VFormat(outputPath, sizeof(outputPath), format, 2); File outputFile = OpenFile(outputPath, "w"); char commandName[128], commandDescription[512]; bool bIsCommand; int flags; outputFile.WriteLine("\"%s\",\"%s\",\"%s\"", "Names", "Flags", "Help Text"); Handle hConCommandIter = FindFirstConCommand(commandName, sizeof(commandName), bIsCommand, flags, commandDescription, sizeof(commandDescription)); do { if (bIsCommand) { if (g_ConCommandFlagCache) { g_ConCommandFlagCache.GetValue(commandName, flags); } ReplaceString(commandDescription, sizeof(commandDescription), "\"", "'"); ReplaceString(commandDescription, sizeof(commandDescription), "\n", " / "); outputFile.WriteLine("\"%s\",\"%s\",\"%s\"", commandName, GetCommandFlagString(flags), commandDescription); } } while ( FindNextConCommand(hConCommandIter, commandName, sizeof(commandName), bIsCommand, flags, commandDescription, sizeof(commandDescription)) ); delete hConCommandIter; delete outputFile; } void DumpServerConVars(const char[] format, any ...) { char outputPath[PLATFORM_MAX_PATH]; VFormat(outputPath, sizeof(outputPath), format, 2); File outputFile = OpenFile(outputPath, "w"); char commandName[128], commandDescription[512]; bool bIsCommand; int flags; outputFile.WriteLine("\"%s\",\"%s\",\"%s\",\"%s\"", "Names", "Defaults", "Flags", "Help Text"); Handle hConCommandIter = FindFirstConCommand(commandName, sizeof(commandName), bIsCommand, flags, commandDescription, sizeof(commandDescription)); do { if (!bIsCommand) { if (g_ConCommandFlagCache) { g_ConCommandFlagCache.GetValue(commandName, flags); } char defaultValue[128]; FindConVar(commandName).GetDefault(defaultValue, sizeof(defaultValue)); ReplaceString(commandDescription, sizeof(commandDescription), "\"", "'"); ReplaceString(commandDescription, sizeof(commandDescription), "\n", " / "); outputFile.WriteLine("\"%s\",\"%s\",\"%s\",\"%s\"", commandName, defaultValue, GetCommandFlagString(flags), commandDescription); } } while ( FindNextConCommand(hConCommandIter, commandName, sizeof(commandName), bIsCommand, flags, commandDescription, sizeof(commandDescription)) ); delete hConCommandIter; delete outputFile; } /** * Dumps user message names to a path created by the format string. */ void DumpUserMessageNames(const char[] format, any ...) { char outputPath[PLATFORM_MAX_PATH]; VFormat(outputPath, sizeof(outputPath), format, 2); File outputFile = OpenFile(outputPath, "w"); outputFile.WriteLine("\"%s\",\"%s\"", "Index", "Name"); char umName[128]; for (int um = 0; GetUserMessageName(view_as(um), umName, sizeof(umName)); um++) { outputFile.WriteLine("\"%d\",\"%s\"", um, umName); } delete outputFile; } /** * Converts bitflags from a ConCommandBase to a space-separated list of flag descriptions. */ char[] GetCommandFlagString(int flags) { // static array of flag descriptions, index mapped to corresponding bit static char s_flagStrings[][] = { "UNREGISTERED", "DEVELOPMENTONLY", "GAMEDLL", "CLIENTDLL", "HIDDEN", "PROTECTED", "SPONLY", "ARCHIVE", "NOTIFY", "USERINFO", "PRINTABLEONLY", "UNLOGGED", "NEVER_AS_STRING", "REPLICATED", "CHEAT", "SS", // split screen "DEMO", "DONTRECORD", "PLUGIN_OR_SS_ADDED", // split screen; same value as FCVAR_PLUGIN "RELEASE", "RELOAD_MATERIALS", "RELOAD_TEXTURES", "NOT_CONNECTED", "MATERIAL_SYSTEM_THREAD", "ARCHIVE_GAMECONSOLE", "ACCESSIBLE_FROM_THREADS", "SERVER_CAN_EXECUTE", "SERVER_CANNOT_QUERY", "CLIENTCMD_CAN_EXECUTE" }; char buffer[512]; for (int i = 0; i < sizeof(s_flagStrings); i++) { int flagbit = (1 << i); if (flags & flagbit) { if (strlen(buffer)) { StrCat(buffer, sizeof(buffer), " "); } StrCat(buffer, sizeof(buffer), s_flagStrings[i]); } } return buffer; } /* Dump marker functrions */ void PrepareRedump() { char path[PLATFORM_MAX_PATH]; BuildPath(Path_SM, path, sizeof(path), "%s", REDUMP_MARKER_FILE); TouchFile(path); } void TouchFile(const char[] file) { File f = OpenFile(file, "w"); delete f; }