diff --git a/plugins/tf2item_cosmetics.smx b/plugins/tf2item_cosmetics.smx index 77d5c59..ccced8c 100644 Binary files a/plugins/tf2item_cosmetics.smx and b/plugins/tf2item_cosmetics.smx differ diff --git a/plugins/tf2item_weapons.smx b/plugins/tf2item_weapons.smx index 7b4b641..6b61731 100644 Binary files a/plugins/tf2item_weapons.smx and b/plugins/tf2item_weapons.smx differ diff --git a/scripting/tf2item_cosmetics.sp b/scripting/tf2item_cosmetics.sp index 3021a29..81b5e4d 100644 --- a/scripting/tf2item_cosmetics.sp +++ b/scripting/tf2item_cosmetics.sp @@ -3,7 +3,7 @@ #pragma semicolon 1 #pragma newdecls required -#define PLUGIN_VERSION "3.0.2" +#define PLUGIN_VERSION "3.1.0" public Plugin myinfo = { @@ -18,9 +18,6 @@ public Plugin myinfo = // GLOBAL DECLARES // ///////////////////// -// Tag -#define PGTAG "{mythical}[TF2Items]{white}" - // Global Regeneration SDKCall Handle (Used to update items the player has) Handle hRegen = INVALID_HANDLE; @@ -30,6 +27,19 @@ CosmeticsInfo pCosmetics[MAXPLAYERS + 1]; // Original Cosmetic Information for every player Cosmetic orgCosmetics[MAXPLAYERS + 1][3]; +// Global boolean to indicate a player is trying to do a search +bool bPlayerIsSearching[MAXPLAYERS + 1] = false; +// Global 2 cell array with item index and slot before the search +int searchInfo[MAXPLAYERS + 1][2]; +// Global timer Handle for the query timer +Handle gSearchTimer[MAXPLAYERS + 1] = INVALID_HANDLE; + +// Global Handle for the Preferences Cookie +Handle pPreferences = INVALID_HANDLE; + +// Global Late Loading Value +bool bLateLoad; + // Networkable Server Offsets (used for regen) int clipOff; int ammoOff; @@ -45,6 +55,8 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max { SetFailState("This plugin was made for use with Team Fortress 2 only."); } + + bLateLoad = late; } public void OnPluginStart() @@ -68,6 +80,25 @@ public void OnPluginStart() LoadTranslations("cosmetics.phrases.txt"); LoadTranslations("unusuals.phrases.txt"); // Translations! + + // Occupy memory + unusualNames = new ArrayList(64); + unusualIds = new ArrayList(); + + // Handle late loading + if (bLateLoad) { + OnConfigsExecuted(); + + // Register Preference Saving Cookie + pPreferences = CV_UseCookies.BoolValue ? RegClientCookie("tf2item_cosmetics_prefs", "Cosmetic override preferences set for this user.", CookieAccess_Private) : INVALID_HANDLE; + + for (int i = 1; i < MaxClients; i++) { + if (IsClientInGame(i) && !IsClientSourceTV(i) && !IsFakeClient(i)) + OnClientPostAdminCheck(i); + } + } else + // Register Preference Saving Cookie + pPreferences = CV_UseCookies.BoolValue ? RegClientCookie("tf2item_cosmetics_prefs", "Cosmetic override preferences set for this user.", CookieAccess_Private) : INVALID_HANDLE; } // Hook spawns if the ConVar is on @@ -76,9 +107,27 @@ public void OnMapStart() { HookRespawns(); } +// Clean memory on map change +public void OnMapEnd() { delete unusualNames; delete unusualIds; } + public void OnClientPostAdminCheck(int client) { - for (int i = 0; i < 3; i++) - pCosmetics[client].ResetFor(i); + bPlayerIsSearching[client] = false; + + delete gSearchTimer[client]; + + pCosmetics[client].ResetAll(); + + // If user still has access to these commands, get their cookie and set their prefs. + // If permissions have been revoked, or no prefs are saved, just set them null. + if ((CheckCommandAccess(client, "sm_cosmetics", ADMFLAG_RESERVATION) + || CheckCommandAccess(client, "sm_hats", ADMFLAG_RESERVATION) + || CheckCommandAccess(client, "sm_myhats", ADMFLAG_RESERVATION)) && pPreferences != INVALID_HANDLE) { + char cookie[520]; + GetClientCookie(client, pPreferences, cookie, sizeof(cookie)); + + if (strlen(cookie) > 0) + ParsePreferenceString(client, cookie); + } } /* Only utilized for testing @@ -212,6 +261,27 @@ public int EffectHdlr(Menu menu, MenuAction action, int client, int p2) { char sel[32]; GetMenuItem(menu, p2, sel, sizeof(sel)); + // Handle search option + if (sel[0] == 's') { + // Clear timer if it is still running. + delete gSearchTimer[client]; + + // Set boolean for searching to true, this is to intercept their next say command + bPlayerIsSearching[client] = true; + + // Assign information + searchInfo[client][0] = iItemDefinitionIndex; + searchInfo[client][1] = slot; + + CPrintToChat(client, "%s Write the {unusual}Unusual Effect{white} name you wish to search for in chat.", PGTAG); + CPrintToChat(client, "%s You have 15 seconds before the query expires.", PGTAG); + + // Create timer to forget about the function. + if (gSearchTimer[client] == INVALID_HANDLE) + gSearchTimer[client] = CreateTimer(15.0, ClearSearch, client); + return 0; + } + if (pCosmetics[client].iItemIndex[slot] != iItemDefinitionIndex) pCosmetics[client].ResetFor(slot); @@ -230,6 +300,69 @@ public int EffectHdlr(Menu menu, MenuAction action, int client, int p2) { return 0; } +// Handle effect searching +public Action OnClientSayCommand(int client, const char[] command, const char[] query) { + // Ignore chat messages if this is false. + if (!bPlayerIsSearching[client]) return Plugin_Continue; + + // Is the ArrayList available? + if (unusualNames == INVALID_HANDLE) return Plugin_Continue; + + // Find any match for this query. + // Player is no longer searching, deactivate the boolean! + bPlayerIsSearching[client] = false; + + // Create new menu with results for this query. + Menu results = new Menu(EffectHdlr); + results.SetTitle("Search results for %s", query); + + // Data embedding + char itemStr[32], slotStr[32]; + IntToString(searchInfo[client][0], itemStr, sizeof(itemStr)); + IntToString(searchInfo[client][1], slotStr, sizeof(slotStr)); + + results.AddItem(slotStr, "", ITEMDRAW_IGNORE); + results.AddItem(itemStr, "", ITEMDRAW_IGNORE); + + // Time to query! + int found = 0; + for (int i = 0; i < unusualNames.Length; i++) { + char name[64], idStr[32]; + unusualNames.GetString(i, name, sizeof(name)); + Format(idStr, sizeof(idStr), "%d", unusualIds.Get(i)); + + if (StrContains(name, query, false) != -1) + results.AddItem(idStr, name) && found++; + } + + // If no matches, just add empty string. + if (!found) + results.AddItem("-", "No Unusual Effects found for your query.", ITEMDRAW_DISABLED); + + // Display the menu! + results.ExitButton = true; + results.Display(client, MENU_TIME_FOREVER); + + return Plugin_Stop; +} + +// Search timer expiry +public Action ClearSearch(Handle timer, any client) { + // If the player boolean is false, no need to handle. + if (!bPlayerIsSearching[client]) { + delete gSearchTimer[client]; + return Plugin_Stop; + } + + bPlayerIsSearching[client] = false; + + CPrintToChat(client, "%s Your search query time has expired.", PGTAG); + + delete gSearchTimer[client]; + + return Plugin_Handled; +} + // // Spell Paints & Footprints Handlers ////////////////////////////////////// @@ -562,11 +695,20 @@ void OthersMenu(int client, const char[] name, int iItemDefinitionIndex, int slo // ForceChange() - Forces an SDKCall on the player to get the Unusual effects to be applied instantly. void ForceChange(int client, int slot) { + // Handle OnlySpawn if (CV_OnlySpawn.BoolValue && !bPlayerInSpawn[client]) { CPrintToChat(client, "%s You are not allowed to make changes outside of spawn!", PGTAG); return; } + // Save preferences at this instance + if (pPreferences != INVALID_HANDLE && CV_UseCookies.BoolValue) { + char prefs[520]; + PreferencesToString(client, prefs, sizeof(prefs)); + + SetClientCookie(client, pPreferences, prefs); + } + int ent = -1; while ((ent = FindEntityByClassname(ent, "tf_wearable")) != INVALID_ENT_REFERENCE) { if (client == GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity")) { @@ -649,4 +791,91 @@ public Action ForceTimer(Handle timer, any client) delete timer; return Plugin_Stop; +} + +// PreferencesToString() - Gets all settings on the user and stringifies them into a readable string for later parsing. +// +// Format: +// i,i,i|u,u,u|p,p,p|s,s,s|f,f,f|v,v,v +// +// Where: +// i = Item Indexes +// u = Unusual Effects +// p = Paint +// s = Spell Paint +// f = Footprints +// v = Voices From Below +void PreferencesToString(int client, char[] buffer, int size) { + // don't look + char prefs[520]; + FormatEx(prefs, sizeof(prefs), "%d,%d,%d|%d,%d,%d|%d,%d,%d|%.1f,%.1f,%.1f|%d,%d,%d|%d,%d,%d", + pCosmetics[client].iItemIndex[0], pCosmetics[client].iItemIndex[1], pCosmetics[client].iItemIndex[2], + pCosmetics[client].uEffects[0], pCosmetics[client].uEffects[1], pCosmetics[client].uEffects[2], + pCosmetics[client].cPaint[0], pCosmetics[client].cPaint[1], pCosmetics[client].cPaint[2], + pCosmetics[client].sPaint[0], pCosmetics[client].sPaint[1], pCosmetics[client].sPaint[2], + pCosmetics[client].sFoot[0], pCosmetics[client].sFoot[1], pCosmetics[client].sFoot[2], + pCosmetics[client].sVoices[0], pCosmetics[client].sVoices[1], pCosmetics[client].sVoices[2]); + + strcopy(buffer, size, prefs); +} + +// ParsePreferenceString() +// +// Parses a Preferences string and loads it for the client. This should only be called ONCE per client connection. +void ParsePreferenceString(int client, const char[] prefs) { + // please, don't kill me + char info[11][64]; + ExplodeString(prefs, "|", info, sizeof(info), sizeof(info[])); + + // Since we're parsing, let's validate! We don't want any bad data being passed. + + // Item Indexes (Validation: Do they exist in schema?) + char id[3][16]; + ExplodeString(info[0], ",", id, sizeof(id), sizeof(id[])); + + for (int i = 0; i < 3; i++) { + int tId = StringToInt(id[i]); + + if (TF2Econ_IsValidItemDefinition(tId)) + pCosmetics[client].iItemIndex[i] = tId; + } + + // Unusual Effects + char u[3][12]; + ExplodeString(info[1], ",", u, sizeof(u), sizeof(u[])); + + for (int i = 0; i < 3; i++) + pCosmetics[client].uEffects[i] = StringToInt(u[i]); + + // Paint Value (Validation: Is the hat paintable?) + char p[3][24]; + ExplodeString(info[2], ",", p, sizeof(p), sizeof(p[])); + + for (int i = 0; i < 3; i++) { + int tP = StringToInt(p[i]); + + if (IsHatPaintable(StringToInt(id[i]))) + pCosmetics[client].cPaint[i] = tP; + } + + // Spell Paint + char sp[3][24]; + ExplodeString(info[3], ",", sp, sizeof(sp), sizeof(sp[])); + + for (int i = 0; i < 3; i++) + pCosmetics[client].sPaint[i] = StringToInt(sp[i]); + + // Footprints + char f[3][24]; + ExplodeString(info[4], ",", f, sizeof(f), sizeof(f[])); + + for (int i = 0; i < 3; i++) + pCosmetics[client].sFoot[i] = StringToInt(f[i]); + + // Voices + char v[3][4]; + ExplodeString(info[5], ",", v, sizeof(v), sizeof(v[])); + + for (int i = 0; i < 3; i++) + pCosmetics[client].sVoices[i] = view_as(StringToInt(v[i])); } \ No newline at end of file diff --git a/scripting/tf2item_weapons.sp b/scripting/tf2item_weapons.sp index b1dda2e..734a21e 100644 --- a/scripting/tf2item_weapons.sp +++ b/scripting/tf2item_weapons.sp @@ -3,7 +3,7 @@ #pragma semicolon 1 #pragma newdecls required -#define PLUGIN_VERSION "3.0.2" +#define PLUGIN_VERSION "3.1.0" public Plugin myinfo = { @@ -27,6 +27,16 @@ WeaponsInfo pWeapons[MAXPLAYERS + 1]; // Original Weapon Information for every player Weapon orgWeapons[MAXPLAYERS + 1][3]; +// Global boolean to indicate a player is trying to do a search +bool bPlayerIsSearching[MAXPLAYERS + 1] = false; +// Global 2 cell array with item index and slot before the search +int searchInfo[MAXPLAYERS + 1][2]; +// Global timer Handle for the query timer +Handle gSearchTimer[MAXPLAYERS + 1] = INVALID_HANDLE; + +// Global Handle for the Preferences Cookie +Handle pPreferences = INVALID_HANDLE; + // Networkable Server Offsets (used for regen) int clipOff; int ammoOff; @@ -54,7 +64,7 @@ public void OnPluginStart() { RegAdminCmd("sm_weps", CMD_Weapons, ADMFLAG_RESERVATION, "Opens the Weapons Manager menu."); RegAdminCmd("sm_myweps", CMD_Weapons, ADMFLAG_RESERVATION, "Opens the Weapons Manager menu."); // Various ways of invoking the command. For user commodity ;) - //RegConsoleCmd("sm_my", CMD_Test); + // RegConsoleCmd("sm_my", CMD_Test); Handle hGameConf = LoadGameConfigFile("sm-tf2.games"); @@ -69,20 +79,31 @@ public void OnPluginStart() { LoadTranslations("weapons.phrases.txt"); // Translations! + // Initialize ArrayLists + wPaintNames = new ArrayList(64); + wPaintProtoDef = new ArrayList(); + // Hook onto player spawning / loadout reload (for special weapons to be given correctly) HookEvent("player_spawn", OnPlayerSpawn); HookEvent("post_inventory_application", OnPlayerSpawn); // Late loading reset if (bLateLoad) { + OnConfigsExecuted(); + + // Register Preference Saving Cookie + pPreferences = CV_UseCookies.BoolValue ? RegClientCookie("tf2item_weapons_prefs", "Weapon override preferences set for this user.", CookieAccess_Private) : INVALID_HANDLE; + for (int i = 1; i < MaxClients; i++) { if (IsClientInGame(i) && !IsClientSourceTV(i) && !IsFakeClient(i)) - pWeapons[i].ResetAll(); + OnClientPostAdminCheck(i); } - } + } else + // Register Preference Saving Cookie + pPreferences = CV_UseCookies.BoolValue ? RegClientCookie("tf2item_weapons_prefs", "Weapon override preferences set for this user.", CookieAccess_Private) : INVALID_HANDLE; } -/*public Action CMD_Test(int client, int args) { +/* public Action CMD_Test(int client, int args) { PrintToConsole(client, "Your Overrides:"); PrintToConsole(client, "Item Indexes: %d, %d, %d", pWeapons[client].iItemIndex[0], pWeapons[client].iItemIndex[1], pWeapons[client].iItemIndex[2]); PrintToConsole(client, "Unusual Weps: %d, %d, %d", pWeapons[client].uEffects[0], pWeapons[client].uEffects[1], pWeapons[client].uEffects[2]); @@ -94,8 +115,9 @@ public void OnPluginStart() { PrintToConsole(client, "Ks. Sheen: %d, %d, %d", pWeapons[client].kSheen[0], pWeapons[client].kSheen[1], pWeapons[client].kSheen[2]); PrintToConsole(client, "Ks. Streaker: %d, %d, %d", pWeapons[client].kStreaker[0], pWeapons[client].kStreaker[1], pWeapons[client].kStreaker[2]); PrintToConsole(client, "Spell Overr.: %b, %b, %b", pWeapons[client].sSpells[0], pWeapons[client].sSpells[1], pWeapons[client].sSpells[2]); + PrintToConsole(client, "Special Weapon: %d", pWeapons[client].Special); return Plugin_Handled; -}*/ +} */ public Action CMD_Weapons(int client, int args) { if (CV_OnlySpawn.BoolValue && !bPlayerInSpawn[client]) @@ -110,8 +132,27 @@ public void OnMapStart() { HookRespawns(); } +// Clear ArrayList memory space +public void OnMapEnd() { delete wPaintNames; delete wPaintProtoDef; } + public void OnClientPostAdminCheck(int client) { + bPlayerIsSearching[client] = false; + + delete gSearchTimer[client]; + pWeapons[client].ResetAll(true); + + // If user still has access to these commands, get their cookie and set their prefs. + // If permissions have been revoked, or no prefs are saved, just set them null. + if ((CheckCommandAccess(client, "sm_weapons", ADMFLAG_RESERVATION) + || CheckCommandAccess(client, "sm_weps", ADMFLAG_RESERVATION) + || CheckCommandAccess(client, "sm_myweps", ADMFLAG_RESERVATION)) && pPreferences != INVALID_HANDLE) { + char cookie[520]; + GetClientCookie(client, pPreferences, cookie, sizeof(cookie)); + + if (strlen(cookie) > 0) + ParsePreferenceString(client, cookie); + } } // @@ -250,6 +291,27 @@ public int wPaintProtoHdlr(Menu menu, MenuAction action, int client, int p2) { char sel[32]; GetMenuItem(menu, p2, sel, sizeof(sel)); + // Handle search option + if (sel[0] == 's') { + // Clear timer if it is still running. + delete gSearchTimer[client]; + + // Set boolean for searching to true, this is to intercept their next say command + bPlayerIsSearching[client] = true; + + // Assign information + searchInfo[client][0] = iItemDefinitionIndex; + searchInfo[client][1] = slot; + + CPrintToChat(client, "%s Write the {uncommon}War Paint{white} name you wish to search for in chat.", PGTAG); + CPrintToChat(client, "%s You have 15 seconds before the query expires.", PGTAG); + + // Create timer to forget about the function. + if (gSearchTimer[client] == INVALID_HANDLE) + gSearchTimer[client] = CreateTimer(15.0, ClearSearch, client); + return 0; + } + if (pWeapons[client].iItemIndex[slot] != iItemDefinitionIndex) pWeapons[client].ResetFor(slot); @@ -269,6 +331,69 @@ public int wPaintProtoHdlr(Menu menu, MenuAction action, int client, int p2) { return 0; } +// Handle War Paint searching +public Action OnClientSayCommand(int client, const char[] command, const char[] query) { + // Ignore chat messages if this is false. + if (!bPlayerIsSearching[client]) return Plugin_Continue; + + // Is the ArrayList available? + if (wPaintNames == INVALID_HANDLE) return Plugin_Continue; + + // Find any match for this query. + // Player is no longer searching, deactivate the boolean! + bPlayerIsSearching[client] = false; + + // Create new menu with results for this query. + Menu results = new Menu(wPaintProtoHdlr); + results.SetTitle("Search results for %s", query); + + // Data embedding + char itemStr[32], slotStr[32]; + IntToString(searchInfo[client][0], itemStr, sizeof(itemStr)); + IntToString(searchInfo[client][1], slotStr, sizeof(slotStr)); + + results.AddItem(itemStr, "", ITEMDRAW_IGNORE); + results.AddItem(slotStr, "", ITEMDRAW_IGNORE); + + // Time to query! + int found = 0; + for (int i = 0; i < wPaintNames.Length; i++) { + char name[64], idStr[32]; + wPaintNames.GetString(i, name, sizeof(name)); + Format(idStr, sizeof(idStr), "%d", wPaintProtoDef.Get(i)); + + if (StrContains(name, query, false) != -1) + results.AddItem(idStr, name) && found++; + } + + // If no matches, just add empty string. + if (!found) + results.AddItem("-", "No War Paints found for your query.", ITEMDRAW_DISABLED); + + // Display the menu! + results.ExitButton = true; + results.Display(client, MENU_TIME_FOREVER); + + return Plugin_Stop; +} + +// Search timer expiry +public Action ClearSearch(Handle timer, any client) { + // If the player boolean is false, no need to handle. + if (!bPlayerIsSearching[client]) { + delete gSearchTimer[client]; + return Plugin_Stop; + } + + bPlayerIsSearching[client] = false; + + CPrintToChat(client, "%s Your search query time has expired.", PGTAG); + + delete gSearchTimer[client]; + + return Plugin_Handled; +} + public int wWarPaintWearHdlr(Menu menu, MenuAction action, int client, int p2) { char idStr[12], slotStr[4]; GetMenuItem(menu, 0, idStr, sizeof(idStr)); @@ -709,9 +834,11 @@ Action ApplyChanges(Handle& hItem, int client, int iItemDefinitionIndex, char[] // Also, if they have a War Paint override, Australium will not be set. bool hasAussie = pWeapons[client].Aussie[slot] && !hasWarPaint, orgAussie = orgWeapons[client][slot].Aussie; - TF2Items_SetAttribute(hItem, 1, 2027, hasAussie ? float(hasAussie) : float(orgAussie)); - TF2Items_SetAttribute(hItem, 2, 2022, hasAussie ? float(hasAussie) : float(orgAussie)); - TF2Items_SetAttribute(hItem, 3, 542, isSpecial ? 0.0 : (hasAussie ? float(hasAussie) : float(orgAussie))); + float setAussie = hasAussie ? float(hasAussie) : float(orgAussie); + + TF2Items_SetAttribute(hItem, 1, 2027, hasWarPaint ? 0.0 : setAussie); + TF2Items_SetAttribute(hItem, 2, 2022, hasWarPaint ? 0.0 : setAussie); + TF2Items_SetAttribute(hItem, 3, 542, isSpecial ? 0.0 : (hasWarPaint ? 0.0 : setAussie)); // Has Festive Override? bool hasFestive = pWeapons[client].Festive[slot], orgFestive = orgWeapons[client][slot].Festive; @@ -877,6 +1004,14 @@ void ForceChange(int client, int slot) { return; } + // Save user preferences at this point. + if (pPreferences != INVALID_HANDLE && CV_UseCookies.BoolValue) { + char prefs[520]; + PreferencesToString(client, prefs, sizeof(prefs)); + + SetClientCookie(client, pPreferences, prefs); + } + // Get all SOC attribs he had so we check for No Override settings GetOriginalAttributes(client, slot); @@ -956,6 +1091,140 @@ public Action ForceTimer(Handle timer, DataPack data) return Plugin_Stop; } +// PreferencesToString() - Gets all settings on the user and stringifies them into a readable string for later parsing. +// +// Format: +// i,i,i|u,u,u|w,w,w|r,r,r|a,a,a|f,f,f|kt,kt,kt|ks,ks,ks|kr,kr,kr|s,s,s|p +// +// Where: +// i = Item Indexes +// u = Unusual Effects +// w = War Paint +// r = War Paint Wear +// a = Australium Preference +// f = Festivized Preference +// kt = Killstreak Type +// ks = Killstreak Sheen +// kr = Killstreaker +// s = Spell Bitfields +// p = Special Weapon +void PreferencesToString(int client, char[] buffer, int size) { + // don't look + char prefs[520]; + FormatEx(prefs, sizeof(prefs), "%d,%d,%d|%d,%d,%d|%d,%d,%d|%.1f,%.1f,%.1f|%d,%d,%d|%d,%d,%d|%d,%d,%d|%d,%d,%d|%d,%d,%d|%d,%d,%d|%d", + pWeapons[client].iItemIndex[0], pWeapons[client].iItemIndex[1], pWeapons[client].iItemIndex[2], + pWeapons[client].uEffects[0], pWeapons[client].uEffects[1], pWeapons[client].uEffects[2], + pWeapons[client].wPaint[0], pWeapons[client].wPaint[1], pWeapons[client].wPaint[2], + pWeapons[client].wWear[0], pWeapons[client].wWear[1], pWeapons[client].wWear[2], + pWeapons[client].Aussie[0], pWeapons[client].Aussie[1], pWeapons[client].Aussie[2], + pWeapons[client].Festive[0], pWeapons[client].Festive[1], pWeapons[client].Festive[2], + pWeapons[client].kType[0], pWeapons[client].kType[1], pWeapons[client].kType[2], + pWeapons[client].kSheen[0], pWeapons[client].kSheen[1], pWeapons[client].kSheen[2], + pWeapons[client].kStreaker[0], pWeapons[client].kStreaker[1], pWeapons[client].kStreaker[2], + pWeapons[client].sSpells[0], pWeapons[client].sSpells[1], pWeapons[client].sSpells[2], + pWeapons[client].Special); + + strcopy(buffer, size, prefs); +} + +// ParsePreferenceString() +// +// Parses a Preferences string and loads it for the client. This should only be called ONCE per client connection. +void ParsePreferenceString(int client, const char[] prefs) { + // please, don't kill me + char info[11][64]; + ExplodeString(prefs, "|", info, sizeof(info), sizeof(info[])); + + // Since we're parsing, let's validate! We don't want any bad data being passed. + + // Item Indexes (Validation: Do they exist in schema?) + char id[3][12]; + ExplodeString(info[0], ",", id, sizeof(id), sizeof(id[])); + + for (int i = 0; i < 3; i++) { + int tId = StringToInt(id[i]); + + if (TF2Econ_IsValidItemDefinition(tId)) + pWeapons[client].iItemIndex[i] = tId; + } + + // Unusual Effects + char u[3][12]; + ExplodeString(info[1], ",", u, sizeof(u), sizeof(u[])); + + for (int i = 0; i < 3; i++) + pWeapons[client].uEffects[i] = StringToInt(u[i]); + + // War Paints (Validation: Are they a listed translation? Can this weapon be War Painted?) + char w[3][12]; + ExplodeString(info[2], ",", w, sizeof(w), sizeof(w[])); + + for (int i = 0; i < 3; i++) { + int tW = StringToInt(w[i]); + + if (TranslationPhraseExists(w[i]) && CanBePainted(StringToInt(id[i]))) + pWeapons[client].wPaint[i] = tW; + } + + // War Paint Wear + char wear[3][12]; + ExplodeString(info[3], ",", wear, sizeof(wear), sizeof(wear[])); + + for (int i = 0; i < 3; i++) + pWeapons[client].wWear[i] = StringToFloat(wear[i]); + + // Australium (Validation: Can this weapon be australium?) + char a[3][4]; + ExplodeString(info[4], ",", a, sizeof(a), sizeof(a[])); + + for (int i = 0; i < 3; i++) { + int tId = StringToInt(id[i]); + if (CanBeAustralium(tId)) + pWeapons[client].Aussie[i] = view_as(StringToInt(a[i])); + } + + // Festivized (Validation: Can this weapon be festivized?) + char f[3][4]; + ExplodeString(info[5], ",", f, sizeof(f), sizeof(f[])); + + for (int i = 0; i < 3; i++) { + if (CanBeFestivized(StringToInt(id[i]))) + pWeapons[client].Festive[i] = view_as(StringToInt(f[i])); + } + + // Killstreak Type + char kT[3][12]; + ExplodeString(info[6], ",", kT, sizeof(kT), sizeof(kT[])); + + for (int i = 0; i < 3; i++) + pWeapons[client].kType[i] = StringToInt(kT[i]); + + // Killstreak Sheen + char kS[3][12]; + ExplodeString(info[7], ",", kS, sizeof(kS), sizeof(kS[])); + + for (int i = 0; i < 3; i++) + pWeapons[client].kSheen[i] = StringToInt(kS[i]); + + // Killstreaker + char kR[3][12]; + ExplodeString(info[8], ",", kR, sizeof(kR), sizeof(kR[])); + + for (int i = 0; i < 3; i++) + pWeapons[client].kStreaker[i] = StringToInt(kR[i]); + + // Spells + char s[3][12]; + ExplodeString(info[9], ",", s, sizeof(s), sizeof(s[])); + + for (int i = 0; i < 3; i++) + pWeapons[client].sSpells[i] = StringToInt(s[i]); + + // Special Weapon (it's alone, no commas) + int special = StringToInt(info[10]); + pWeapons[client].Special = special > 0 ? StringToInt(info[9]) : -1; +} + // mMainMenu - Main menu for all users. Allows them to select one of their weapons to begin modifying them. void mMainMenu(int client) { Menu menu = new Menu(mainHdlr); diff --git a/scripting/tf2items/cosmetics.sp b/scripting/tf2items/cosmetics.sp index 815ac1d..c4ce61b 100644 --- a/scripting/tf2items/cosmetics.sp +++ b/scripting/tf2items/cosmetics.sp @@ -169,6 +169,8 @@ int teamColors[7][2] = { {12073019, 5801378}, {4732984, 3686984}, {11049612, 8626083}, {3874595, 1581885}, {6637376, 2636109}, {8400928, 2452877}, {12807213, 12091445} }; +// Globals +ArrayList unusualNames, unusualIds; // Functions @@ -328,6 +330,9 @@ void EffectsMenu(int client, const char[] name, int iItemDefinitionIndex, int sl effMenu.AddItem(slotStr, "", ITEMDRAW_IGNORE); effMenu.AddItem(idStr, "", ITEMDRAW_IGNORE); + // Utilize chat to search for a specific Unusual effect. + effMenu.AddItem("search", "Search for an Unusual Effect..."); + // add unusual effects respectively AddUnusuals(effMenu, client); @@ -343,6 +348,10 @@ void AddUnusuals(Menu menu, int client) // Unusual Particle Effects ID List ArrayList unusuals = TF2Econ_GetParticleAttributeList(ParticleSet_CosmeticUnusualEffects); + // Clear global ArrayLists + unusualNames.Clear(); + unusualIds.Clear(); + for (int i; i < unusuals.Length; i++) { int id = unusuals.Get(i); @@ -351,9 +360,13 @@ void AddUnusuals(Menu menu, int client) Format(name, sizeof(name), "Cosmetic_Eff%d", id); Format(idStr, sizeof(idStr), "%d", id); - if (TranslationPhraseExists(name)) + if (TranslationPhraseExists(name)) { Format(name, sizeof(name), "%T", name, client); - else { + + // Push them into a global ArrayList (search feature) + unusualNames.PushString(name); + unusualIds.Push(id); + } else { char system[64]; TF2Econ_GetParticleAttributeSystemName(id, system, sizeof(system)); @@ -368,6 +381,9 @@ void AddUnusuals(Menu menu, int client) menu.AddItem(idStr, name); } + + // Clean memory! + delete unusuals; } // AddPaints(Menu menu) - Adds all of the possible paint colors to a menu. diff --git a/scripting/tf2items/tf2item_base.inc b/scripting/tf2items/tf2item_base.inc index a909f5c..584bc92 100644 --- a/scripting/tf2items/tf2item_base.inc +++ b/scripting/tf2items/tf2item_base.inc @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -12,6 +13,21 @@ /* * Change-Log * + * 3.1.0 - 02/11/21 + * General + * - Implemented search functionality for Unusual effects and War Paints. + * - Players can now select an option and type their query on chat to search for specific entries. + * - Players have 15 seconds before this query expires. + * - Implemented preference saving. + * - Added a new ConVar: tf2items_save_preferences + * - Controls if both plugins will utilize the Cookies system to keep user preferences. Requires plugin reload on change. + * - Player selections and overrides will be saved after making any change to their modifications. + * - Fixed #11 + * - Plugins late loading would throw an Invalid ConVar error due to it not being declared before hand on late loading. + * - Bug fixes and more memory management to prevent leaks. + * tf2item_weapons + * - Fixed setting War Paints on real Australium weapons not being displayed due to bad logic handling. + * * 3.0.2 - 14/10/21 * tf2item_weapons * - Fixed a wrongly initialized value for connecting players causing them to recieve a stock bat regardless of class. @@ -44,7 +60,7 @@ bool bPlayerInSpawn[MAXPLAYERS + 1] = false; // I am now officially a student of dark arts. Don't thank me, thank my master: Scag :) -stock ConVar CV_OnlySpawn, CV_LogMissingTranslations, +stock ConVar CV_OnlySpawn, CV_LogMissingTranslations, CV_UseCookies, CV_Cosmetics_Unusuals, CV_Cosmetics_Paint, CV_Cosmetics_Spells, CV_Cosmetics_ShowIDs, CV_Cosmetics_UParticles; @@ -55,7 +71,8 @@ public void OnConfigsExecuted() { CV_OnlySpawn = CreateConVar("tf2items_general_onlyspawn", "0", "Controls wether players can only modify their items inside spawn boundaries. Default is 1.", _, true, 0.0, true, 1.0); CV_LogMissingTranslations = CreateConVar("tf2items_cosmetics_show_missing_particles", "0", "Logs to the server whenever a parsed Unusual Particle/War Paint ID does not have an" ... " existing translation for their name. I recommend leaving this off, as it causes" ... - " too much console spam and should only be used to debug. Default is 0.", _, true, 0.0, true, 1.0); + " too much console spam and should only be used to debug. Default is 0.", _, true, 0.0, true, 1.0), + CV_UseCookies = CreateConVar("tf2items_save_preferences", "1", "Enables the use of Player Cookies to save selected preferences. Default is 1.", _, true, 0.0, false, 1.0); // Cosmetics Manager CV_Cosmetics_Unusuals = CreateConVar("tf2items_cosmetics_unusuals", "1", "Are unusual overrides enabled? Default is 1.", _, true, 0.0, true, 1.0); diff --git a/scripting/tf2items/weapons.smx b/scripting/tf2items/weapons.smx new file mode 100644 index 0000000..7b75a7f Binary files /dev/null and b/scripting/tf2items/weapons.smx differ diff --git a/scripting/tf2items/weapons.sp b/scripting/tf2items/weapons.sp index cfaed36..21ec61e 100644 --- a/scripting/tf2items/weapons.sp +++ b/scripting/tf2items/weapons.sp @@ -14,6 +14,9 @@ #define MAX_WEAPONS 3 #define INVALID_WEAPON_ENTITY -1 +// Global ArrayLists +ArrayList wPaintNames, wPaintProtoDef; + // Weapon // // Represents a single weapon instance for the user. @@ -177,19 +180,32 @@ void wWarPaintProtodef(int client, int iItemDefinitionIndex, int slot) { menu.AddItem(idStr, "", ITEMDRAW_IGNORE); menu.AddItem(slotStr, "", ITEMDRAW_IGNORE); + // Utilize chat to search for a specific War Paint kit + menu.AddItem("search", "Search for a War Paint..."); + menu.AddItem("-1", "No Override"); + // Clear search ArrayList values + wPaintNames.Clear(); + wPaintProtoDef.Clear(); + // Get all valid War Paints at the moment. ArrayList paints = TF2Econ_GetPaintKitDefinitionList(); for (int i = 0; i < paints.Length; i++) { + int protodef = paints.Get(i); + char pStr[12]; - IntToString(paints.Get(i), pStr, sizeof(pStr)); + IntToString(protodef, pStr, sizeof(pStr)); if (TranslationPhraseExists(pStr)) { char pName[64]; Format(pName, sizeof(pName), "%T", pStr, client); menu.AddItem(pStr, pName); + + // Save into ArrayList + wPaintNames.PushString(pName); + wPaintProtoDef.Push(protodef); } else if (CV_LogMissingTranslations.BoolValue) LogError("[TF2Weapons] Error while adding Paint Kit %s. Translation is missing. Paint Kit will not be added to the menu.", pStr); }