/** * [CSRD] SRCDS Data Dumper * * Uses SourceMod dumping commands to dump useful information for plugin developers. */ #pragma semicolon 1 #include #tryinclude #include #pragma newdecls required #include #include #include #define PLUGIN_VERSION "1.5.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; Handle g_SDKCallGetTFConditionName, g_SDKCallGetTFConditionNamePtr; public APLRes AskPluginLoad2(Handle hPluginSelf, bool bLateLoaded, char[] error, int err_max) { if (bLateLoaded) { // skip dump on late load because it's too late to cache flags for 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() { Handle hGameConf = LoadGameConfigFile("csrd.data_dump"); if (!hGameConf) { SetFailState("Failed to load gamedata (csrd.data_dump)."); } // we call and fetch the pointer to work around alliedmodders/sourcemod#874 // we could read the pointer directly, but ain't got time for that StartPrepSDKCall(SDKCall_Static); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "GetTFConditionName()"); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); g_SDKCallGetTFConditionNamePtr = EndPrepSDKCall(); StartPrepSDKCall(SDKCall_Static); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "GetTFConditionName()"); PrepSDKCall_SetReturnInfo(SDKType_String, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); g_SDKCallGetTFConditionName = EndPrepSDKCall(); delete hGameConf; 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) || IsNewServerVersion()) { 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) || IsNewServerVersion())) { DataPack pack = new DataPack(); pack.WriteString(path); PerformDataDump(OnDataDumpFinished, pack); } else { LogError("Failed to change to map %s", LOW_ENTITY_MAP); } } } } bool IsNewServerVersion() { char updatePath[PLATFORM_MAX_PATH]; BuildPath(Path_SM, updatePath, sizeof(updatePath), "%s/%s", REDUMP_OUTPUT_DIRECTORY, ".server-version"); if (FileExists(updatePath)) { char versionString[64]; File versionFile = OpenFile(updatePath, "r"); versionFile.ReadLine(versionString, sizeof(versionString)); delete versionFile; return GetNetworkPatchVersion() > StringToInt(versionString); } return true; } 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); DumpTFCondNames("%s/%d.tf_conds.txt", outputDirectory, version); /** * Spawns all entities, server will crash on map change -- make sure there are 700-ish * free edicts. * No more class dump; we'll just generate it with `grep -P '^\w+ - \w+$'` on datamaps. */ ServerCommand("sm_dump_datamaps %s/%d.datamaps.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; if (FileExists(path)) { 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; } void DumpTFCondNames(const char[] format, any ...) { char outputPath[PLATFORM_MAX_PATH]; VFormat(outputPath, sizeof(outputPath), format, 2); File outputFile = OpenFile(outputPath, "w"); outputFile.WriteLine("\"%s\",\"%s\"", "Condition Index", "Name"); char condName[64]; int cond; while (SDKCall(g_SDKCallGetTFConditionNamePtr, cond) && SDKCall(g_SDKCallGetTFConditionName, condName, sizeof(condName), cond)) { outputFile.WriteLine("\"%d\",\"%s\"", cond, condName); cond++; } 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; }