#pragma semicolon 1 #pragma newdecls required #include #define PLATFORM_LINE_LENGTH 1024 #define NORMAL_LINE_LENGTH 256 #define PLUGIN_CONFIG "CSGO_ParticleSystemFix.games" #define PLUGIN_LOGFILE "logs/CSGO_ParticleSystemFix.log" /** * @section List of operation systems. **/ enum EngineOS { OS_Unknown, OS_Windows, OS_Linux }; /** * @section Struct of operation types for server arrays. **/ enum struct ServerData { /* Internal Particles */ ArrayList Particles; /* OS */ EngineOS Platform; /* Gamedata */ Handle Config; } /** * @endsection **/ ServerData gServerData; /** * Variables to store SDK calls handlers. **/ Handle hSDKCallDestructorParticleDictionary; Handle hSDKCallTableDeleteAllStrings; /** * Variables to store virtual SDK offsets. **/ Address pParticleSystemDictionary; int ParticleSystem_Count; char g_sLogPath[PLATFORM_MAX_PATH]; public Plugin myinfo = { name = "[CS:GO] Particle System Fix", description = "", author = "gubka && PŠΣ™ SHUFEN", version = "1.0", url = "https://forums.alliedmods.net/showthread.php?t=313951 && https://possession.jp" }; public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { RegPluginLibrary("CSGO_ParticleSystemFix"); CreateNative("UncacheAllParticleSystems", Native_UncacheAllParticleSystems); return APLRes_Success; } public int Native_UncacheAllParticleSystems(Handle plugin, int numParams) { ParticlesOnPurge(); } /** * @brief Particles module init function. **/ public void OnPluginStart() { BuildPath(Path_SM, g_sLogPath, sizeof(g_sLogPath), PLUGIN_LOGFILE); if (GetEngineVersion() != Engine_CSGO) { LogToFileEx(g_sLogPath, "[System Init] Engine error: This plugin only works on Counter-Strike: Global Offensive."); return; } gServerData.Config = LoadGameConfigFile(PLUGIN_CONFIG); /*_________________________________________________________________________________________________________________________________________*/ // Starts the preparation of an SDK call StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(gServerData.Config, SDKConf_Signature, "CParticleSystemDictionary::~CParticleSystemDictionary"); // Validate call if ((hSDKCallDestructorParticleDictionary = EndPrepSDKCall()) == null) { // Log failure LogToFileEx(g_sLogPath, "[GameData Validation] Failed to load SDK call \"CParticleSystemDictionary::~CParticleSystemDictionary\". Update signature in \"%s\"", PLUGIN_CONFIG); return; } /*_________________________________________________________________________________________________________________________________________*/ // Starts the preparation of an SDK call StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(gServerData.Config, SDKConf_Signature, "CNetworkStringTable::DeleteAllStrings"); // Validate call if ((hSDKCallTableDeleteAllStrings = EndPrepSDKCall()) == null) { // Log failure LogToFileEx(g_sLogPath, "[GameData Validation] Failed to load SDK call \"CNetworkStringTable::DeleteAllStrings\". Update signature in \"%s\"", PLUGIN_CONFIG); return; } /*_________________________________________________________________________________________________________________________________________*/ // Load other offsets fnInitGameConfAddress(gServerData.Config, pParticleSystemDictionary, "m_pParticleSystemDictionary"); fnInitGameConfOffset(gServerData.Config, ParticleSystem_Count, "CParticleSystemDictionary::Count"); /*_________________________________________________________________________________________________________________________________________*/ //fnInitGameConfOffset(gServerData.Config, view_as(gServerData.Platform), "CServer::OS"); LogToFileEx(g_sLogPath, "[System Init] Loaded \"Particle System Fix\""); } public void OnMapStart() { // Precache CS:GO internal particles ParticlesOnLoad(); // Load map particles LoadMapExtraFiles(); } public void OnMapEnd() { ParticlesOnPurge(); } /** * @brief Particles module load function. **/ void ParticlesOnLoad() { // Initialize buffer char static char sBuffer[PLATFORM_LINE_LENGTH]; // Validate that particles wasn't precache yet bool bSave = LockStringTables(false); Address pTable = CNetworkStringTableContainer_FindTable("ParticleEffectNames"); int iCount = LoadFromAddress(pParticleSystemDictionary + view_as
(ParticleSystem_Count), NumberType_Int16); if (pTable != Address_Null && !iCount) { /// Validate that table is exist and it empty // Opens the file File hFile = OpenFile("particles/particles_manifest.txt", "rt", true); // If doesn't exist stop if (hFile == null) { LogToFileEx(g_sLogPath, "[Config Validation] Error opening file: \"particles/particles_manifest.txt\""); return; } // Read lines in the file while (hFile.ReadLine(sBuffer, sizeof(sBuffer))) { // Checks if string has correct quotes int iQuotes = CountCharInString(sBuffer, '"'); if (iQuotes == 4) { // Trim string TrimString(sBuffer); // Copy value string strcopy(sBuffer, sizeof(sBuffer), sBuffer[strlen("\"file\"")]); // Trim string TrimString(sBuffer); // Strips a quote pair off a string StripQuotes(sBuffer); // Precache model int i = 0; if (sBuffer[i] == '!') i++; PrecacheGeneric(sBuffer[i], true); SDKCall(hSDKCallTableDeleteAllStrings, pTable); /// HACK~HACK /// Clear tables after each file because some of them contains /// huge amount of particles and we work around the limit } } delete hFile; } // Initialize the table index static int tableIndex = INVALID_STRING_TABLE; // Validate table if (tableIndex == INVALID_STRING_TABLE) { // Searches for a string table tableIndex = FindStringTable("ParticleEffectNames"); } // If array hasn't been created, then create if (gServerData.Particles == null) { // Initialize a particle list array gServerData.Particles = CreateArray(NORMAL_LINE_LENGTH); // i = table string iCount = GetStringTableNumStrings(tableIndex); for (int i = 0; i < iCount; i++) { // Gets the string at a given index ReadStringTable(tableIndex, i, sBuffer, sizeof(sBuffer)); // Push data into array gServerData.Particles.PushString(sBuffer); } } else { // i = particle name iCount = gServerData.Particles.Length; for (int i = 0; i < iCount; i++) { // Gets the string at a given index gServerData.Particles.GetString(i, sBuffer, sizeof(sBuffer)); // Push data into table AddToStringTable(tableIndex, sBuffer); } } /* // Refresh tables pTable = CNetworkStringTableContainer_FindTable("ExtraParticleFilesTable"); if (pTable != Address_Null) { SDKCall(hSDKCallTableDeleteAllStrings, pTable); } pTable = CNetworkStringTableContainer_FindTable("genericprecache"); if (pTable != Address_Null) { SDKCall(hSDKCallTableDeleteAllStrings, pTable); } */ LockStringTables(bSave); } void LoadMapExtraFiles() { char sMapName[PLATFORM_MAX_PATH]; GetCurrentMap(sMapName, sizeof(sMapName)); LoadPerMapParticleManifest(sMapName); } void LoadPerMapParticleManifest(const char[] sMapName) { KeyValues hKv = GetParticleManifestKv(sMapName); if (hKv == null) { return; } ParseParticleManifestKv(hKv, true, true); delete hKv; } char sDirExt[][][] = { { "particles/", "maps/", "particles/", "maps/" }, { "_manifest_override.txt", "_particles_override.txt", "_manifest.txt", "_particles.txt" } }; KeyValues GetParticleManifestKv(const char[] sMapName) { char sFileName[PLATFORM_MAX_PATH]; for (int i = 0; i < sizeof(sDirExt[]); i++) { Format(sFileName, sizeof(sFileName), "%s%s%s", sDirExt[0][i], sMapName, sDirExt[1][i]); if (FileExists(sFileName, true)) { if (FileExists(sFileName, false)) AddFileToDownloadsTable(sFileName); return CreateManifestKv(sFileName); } } return null; } KeyValues CreateManifestKv(const char[] sPath) { KeyValues hKv = CreateKeyValues("particles_manifest"); //hKv.SetEscapeSequences(true); if (hKv.ImportFromFile(sPath)) { return hKv; } return null; } void ParseParticleManifestKv(KeyValues hKv, bool perMap = false, bool awayPreloads = false) { char sPCF[PLATFORM_MAX_PATH]; if (hKv.GotoFirstSubKey(false)) do { hKv.GetString(NULL_STRING, sPCF, sizeof(sPCF)); if (sPCF[0] == '\0') continue; bool preload = FindCharInString(sPCF, '!') == 0; if (preload) strcopy(sPCF, sizeof(sPCF), sPCF[1]); if (FileExists(sPCF, true)) { if (perMap && FileExists(sPCF, false)) AddFileToDownloadsTable(sPCF); PrecacheGeneric(sPCF, awayPreloads || preload); } } while (hKv.GotoNextKey(false)); } /** * @brief Particles module purge function. **/ void ParticlesOnPurge() { // @link https://github.com/VSES/SourceEngine2007/blob/43a5c90a5ada1e69ca044595383be67f40b33c61/src_main/particles/particles.cpp#L81 SDKCall(hSDKCallDestructorParticleDictionary, pParticleSystemDictionary); /*_________________________________________________________________________________________________________________________________________*/ // Clear particles in the effect table bool bSave = LockStringTables(false); Address pTable = CNetworkStringTableContainer_FindTable("ParticleEffectNames"); if (pTable != Address_Null) { SDKCall(hSDKCallTableDeleteAllStrings, pTable); } // Clear particles in the extra effect table pTable = CNetworkStringTableContainer_FindTable("ExtraParticleFilesTable"); if (pTable != Address_Null) { SDKCall(hSDKCallTableDeleteAllStrings, pTable); } // Clear particles in the generic precache table pTable = CNetworkStringTableContainer_FindTable("genericprecache"); if (pTable != Address_Null) { SDKCall(hSDKCallTableDeleteAllStrings, pTable); } LockStringTables(bSave); } stock Address CNetworkStringTableContainer_FindTable(const char[] tableName) { Address pNetworkstringtable = GetNetworkStringTableAddr(); if (pNetworkstringtable == Address_Null) return Address_Null; static Handle hFindTable = INVALID_HANDLE; if (hFindTable == INVALID_HANDLE) { if (gServerData.Config == INVALID_HANDLE) return Address_Null; StartPrepSDKCall(SDKCall_Raw); if (!PrepSDKCall_SetFromConf(gServerData.Config, SDKConf_Virtual, "CNetworkStringTableContainer::FindTable")) { LogToFileEx(g_sLogPath, "[Find Table] Cant find the method CNetworkStringTableContainer::FindTable."); return Address_Null; } PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); // tableName PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); hFindTable = EndPrepSDKCall(); if (hFindTable == INVALID_HANDLE) { LogToFileEx(g_sLogPath, "[Find Table] Method CNetworkStringTableContainer::FindTable was not loaded right."); return Address_Null; } } return SDKCall(hFindTable, pNetworkstringtable, tableName); } stock Address GetNetworkStringTableAddr() { static Address pEngineServerStringTable = Address_Null; if (pEngineServerStringTable == Address_Null) { if (gServerData.Config == null) return Address_Null; char sInterfaceName[64]; if (!GameConfGetKeyValue(gServerData.Config, "VEngineServerStringTable", sInterfaceName, sizeof(sInterfaceName))) strcopy(sInterfaceName, sizeof(sInterfaceName), "VEngineServerStringTable001"); pEngineServerStringTable = CreateEngineInterface(sInterfaceName); } return pEngineServerStringTable; } stock Address CreateEngineInterface(const char[] sInterfaceKey, Address ptr = Address_Null) { static Handle hCreateInterface = null; if (hCreateInterface == null) { if (gServerData.Config == null) return Address_Null; StartPrepSDKCall(SDKCall_Static); if (!PrepSDKCall_SetFromConf(gServerData.Config, SDKConf_Signature, "CreateInterface")) { LogToFileEx(g_sLogPath, "[Create Engine Interface] Failed to get CreateInterface"); return Address_Null; } PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain, VDECODE_FLAG_ALLOWNULL); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); hCreateInterface = EndPrepSDKCall(); if (hCreateInterface == null) { LogToFileEx(g_sLogPath, "[Create Engine Interface] Function CreateInterface was not loaded right."); return Address_Null; } } if (gServerData.Config == null) return Address_Null; char sInterfaceName[64]; if (!GameConfGetKeyValue(gServerData.Config, sInterfaceKey, sInterfaceName, sizeof(sInterfaceName))) strcopy(sInterfaceName, sizeof(sInterfaceName), sInterfaceKey); Address addr = SDKCall(hCreateInterface, sInterfaceName, ptr); if (addr == Address_Null) { LogToFileEx(g_sLogPath, "[Create Engine Interface] Failed to get pointer to interface %s(%s)", sInterfaceKey, sInterfaceName); return Address_Null; } return addr; } /** * @brief Finds the amount of all occurrences of a character in a string. * * @param sBuffer Input string buffer. * @param cSymbol The character to search for. * @return The amount of characters in the string, or -1 if the characters were not found. */ int CountCharInString(char[] sBuffer, char cSymbol) { // Initialize index int iCount; // i = char index int iLen = strlen(sBuffer); for (int i = 0; i < iLen; i++) { // Validate char if (sBuffer[i] == cSymbol) { // Increment amount iCount++; } } // Return amount return iCount ? iCount : -1; } /** * @brief Returns an offset value from a given config. * * @param gameConf The game config handle. * @param iOffset An offset, or -1 on failure. * @param sKey Key to retrieve from the offset section. **/ stock void fnInitGameConfOffset(Handle gameConf, int &iOffset, char[] sKey) { // Validate offset if ((iOffset = GameConfGetOffset(gameConf, sKey)) == -1) { LogToFileEx(g_sLogPath, "[GameData Validation] Failed to get offset: \"%s\"", sKey); } } /** * @brief Returns an address value from a given config. * * @param gameConf The game config handle. * @param pAddress An address, or null on failure. * @param sKey Key to retrieve from the address section. **/ stock void fnInitGameConfAddress(Handle gameConf, Address &pAddress, char[] sKey) { // Validate address if ((pAddress = GameConfGetAddress(gameConf, sKey)) == Address_Null) { LogToFileEx(g_sLogPath, "[GameData Validation] Failed to get address: \"%s\"", sKey); } }