468 lines
14 KiB
SourcePawn
468 lines
14 KiB
SourcePawn
#pragma semicolon 1
|
|
#pragma newdecls required
|
|
|
|
#include <sdktools>
|
|
|
|
#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<int>(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<Address>(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);
|
|
}
|
|
} |