From 6dcfe3a82599343e4d889463dbe215c5ab0a86ac Mon Sep 17 00:00:00 2001
From: e2ectronic <3356207189@qq.com>
Date: Sat, 27 May 2023 08:14:06 +0800
Subject: [PATCH 1/2] Implement skins search

for #273
---
 addons/sourcemod/scripting/weapons.sp         |  15 ++
 addons/sourcemod/scripting/weapons/config.sp  |  57 +++++
 .../sourcemod/scripting/weapons/forwards.sp   |   1 +
 addons/sourcemod/scripting/weapons/globals.sp |   6 +
 addons/sourcemod/scripting/weapons/helpers.sp |  31 +++
 addons/sourcemod/scripting/weapons/menus.sp   | 213 ++++++++++++++++++
 .../translations/chi/weapons.phrases.txt      |  12 +
 .../translations/weapons.phrases.txt          |  12 +
 8 files changed, 347 insertions(+)

diff --git a/addons/sourcemod/scripting/weapons.sp b/addons/sourcemod/scripting/weapons.sp
index 0697c0ac..7ab18128 100644
--- a/addons/sourcemod/scripting/weapons.sp
+++ b/addons/sourcemod/scripting/weapons.sp
@@ -23,6 +23,7 @@
 #include <weapons>
 #undef REQUIRE_PLUGIN
 #include <updater>
+#include <regex>
 
 #pragma semicolon 1
 #pragma newdecls required
@@ -88,6 +89,7 @@ public void OnPluginStart()
 	g_Cvar_EnableNameTag 			= CreateConVar("sm_weapons_enable_nametag", 		"1", 				"Enable/Disable name tag options");
 	g_Cvar_EnableStatTrak 			= CreateConVar("sm_weapons_enable_stattrak", 		"1", 				"Enable/Disable StatTrak options");
 	g_Cvar_EnableSeed				= CreateConVar("sm_weapons_enable_seed",			"1",				"Enable/Disable Seed options");
+	g_Cvar_EnableSearch             = CreateConVar("sm_weapons_enable_search",          "1",                "Enable/Disable Search Function");
 	g_Cvar_FloatIncrementSize 		= CreateConVar("sm_weapons_float_increment_size", 	"0.05", 			"Increase/Decrease by value for weapon float");
 	g_Cvar_EnableWeaponOverwrite 	= CreateConVar("sm_weapons_enable_overwrite", 		"1", 				"Enable/Disable players overwriting other players' weapons (picked up from the ground) by using !ws command");
 	g_Cvar_GracePeriod 			= CreateConVar("sm_weapons_grace_period", 			"0", 				"Grace period in terms of seconds counted after round start for allowing the use of !ws command. 0 means no restrictions");
@@ -202,6 +204,18 @@ public Action CommandWeaponSkins(int client, int args)
 		int menuTime;
 		if((menuTime = GetRemainingGracePeriodSeconds(client)) >= 0)
 		{
+			if (args > 0) {
+				
+				char searchSkinName[32];
+				GetCmdArgString(searchSkinName, sizeof(searchSkinName));
+				
+				Menu resultMenu = SearchSkins(client, searchSkinName);
+				menuPlayerSearchTemp[client] = resultMenu;
+				resultMenu.Display(client, menuTime);
+				
+				return Plugin_Handled;
+			}
+			
 			CreateMainMenu(client).Display(client, menuTime);
 		}
 		else
@@ -209,6 +223,7 @@ public Action CommandWeaponSkins(int client, int args)
 			PrintToChat(client, " %s \x02%t", g_ChatPrefix, "GracePeriod", g_iGracePeriod);
 		}
 	}
+	
 	return Plugin_Handled;
 }
 
diff --git a/addons/sourcemod/scripting/weapons/config.sp b/addons/sourcemod/scripting/weapons/config.sp
index 790fc0a5..442156b8 100644
--- a/addons/sourcemod/scripting/weapons/config.sp
+++ b/addons/sourcemod/scripting/weapons/config.sp
@@ -55,6 +55,9 @@ public void ReadConfig()
 			CloseHandle(kv);
 		}
 		
+		delete g_smSkinMenuMap[langCounter];
+		g_smSkinMenuMap[langCounter] = new StringMap();
+		
 		for (int k = 0; k < sizeof(g_WeaponClasses); k++)
 		{
 			if(menuWeapons[langCounter][k] != null)
@@ -70,6 +73,7 @@ public void ReadConfig()
 		
 		int counter = 0;
 		char weaponTemp[20];
+		
 		do {
 			char name[64];
 			char index[5];
@@ -79,14 +83,67 @@ public void ReadConfig()
 			KvGetString(kv, "classes", classes, sizeof(classes));
 			KvGetString(kv, "index", index, sizeof(index));
 			
+			Menu menuSkins[MAX_SKIN];
+			
 			for (int k = 0; k < sizeof(g_WeaponClasses); k++)
 			{
 				Format(weaponTemp, sizeof(weaponTemp), "%s;", g_WeaponClasses[k]);
 				if(StrContains(classes, weaponTemp) > -1)
 				{
 					menuWeapons[langCounter][k].AddItem(index, name);
+					
+					if (g_bEnableSearch)
+					{
+						if (menuSkins[counter] == null)
+						{
+							menuSkins[counter] = new Menu(SkinsMenuHandler, MENU_ACTIONS_DEFAULT | MenuAction_DisplayItem);
+							menuSkins[counter].SetTitle(name);
+							menuSkins[counter].AddItem("-1", "Apply all");
+							menuSkins[counter].AddItem("-2", "Apply current");
+							menuSkins[counter].ExitBackButton = true;
+						}
+						
+						char weaponName[32];
+						Format(weaponName, sizeof(weaponName), "%T (%s)", g_WeaponClasses[k], LANG_SERVER, index);
+						char weaponIndexStr[32];
+						Format(weaponIndexStr, sizeof(weaponIndexStr), "%d", k);
+						menuSkins[counter].AddItem(weaponIndexStr, weaponName);
+					}
+					
 				}
 			}
+			
+			if (g_bEnableSearch)
+			{
+				for (int j = 0; j < MAX_SKIN; j++)
+				{
+					Menu currentMenu = menuSkins[j];
+					if (currentMenu == null)continue;
+					
+					char currentMenuName[32];
+					currentMenu.GetTitle(currentMenuName, sizeof(currentMenuName));
+					if (g_smSkinMenuMap[langCounter].ContainsKey(name))
+					{
+						Menu fatherMenu;
+						g_smSkinMenuMap[langCounter].GetValue(name, fatherMenu);
+						for (int l = 2; l < currentMenu.ItemCount; l++)
+						{
+							char info[32];
+							char display[64];
+							int a;
+							currentMenu.GetItem(l, info, sizeof(info), a, display, sizeof(display));
+							fatherMenu.AddItem(info, display);
+						}
+						
+						delete currentMenu;
+					}
+					else
+					{
+						g_smSkinMenuMap[langCounter].SetValue(name, currentMenu);
+					}
+				}
+			}
+			
 			counter++;
 		} while (KvGotoNextKey(kv));
 		
diff --git a/addons/sourcemod/scripting/weapons/forwards.sp b/addons/sourcemod/scripting/weapons/forwards.sp
index e390240e..aebc068d 100644
--- a/addons/sourcemod/scripting/weapons/forwards.sp
+++ b/addons/sourcemod/scripting/weapons/forwards.sp
@@ -44,6 +44,7 @@ public void OnConfigsExecuted()
 	g_bEnableNameTag = g_Cvar_EnableNameTag.BoolValue;
 	g_bEnableStatTrak = g_Cvar_EnableStatTrak.BoolValue;
 	g_bEnableSeed = g_Cvar_EnableSeed.BoolValue;
+	g_bEnableSearch = g_Cvar_EnableSearch.BoolValue;
 	g_fFloatIncrementSize = g_Cvar_FloatIncrementSize.FloatValue;
 	g_iFloatIncrementPercentage = RoundFloat(g_fFloatIncrementSize * 100.0);
 	g_bOverwriteEnabled = g_Cvar_EnableWeaponOverwrite.BoolValue;
diff --git a/addons/sourcemod/scripting/weapons/globals.sp b/addons/sourcemod/scripting/weapons/globals.sp
index c8b300c1..b8deacb3 100644
--- a/addons/sourcemod/scripting/weapons/globals.sp
+++ b/addons/sourcemod/scripting/weapons/globals.sp
@@ -40,6 +40,7 @@ int g_iKnifeIndices[] = {
 };
 
 const int MAX_LANG = 40;
+const int MAX_SKIN = 1255;
 
 Database db = null;
 
@@ -74,6 +75,9 @@ bool g_bEnableStatTrak;
 ConVar g_Cvar_EnableSeed;
 bool g_bEnableSeed;
 
+ConVar g_Cvar_EnableSearch;
+bool g_bEnableSearch;
+
 ConVar g_Cvar_EnableWeaponOverwrite;
 bool g_bOverwriteEnabled;
 
@@ -120,10 +124,12 @@ char g_MigrationWeapons[][] = {
 char g_Language[MAX_LANG][32];
 int g_iClientLanguage[MAXPLAYERS+1];
 Menu menuWeapons[MAX_LANG][sizeof(g_WeaponClasses)];
+Menu menuPlayerSearchTemp[MAXPLAYERS+1];
 
 StringMap g_smWeaponIndex;
 StringMap g_smWeaponDefIndex;
 StringMap g_smLanguageIndex;
+StringMap g_smSkinMenuMap[MAX_LANG];
 
 GlobalForward g_hOnKnifeSelect_Pre;
 GlobalForward g_hOnKnifeSelect_Post;
diff --git a/addons/sourcemod/scripting/weapons/helpers.sp b/addons/sourcemod/scripting/weapons/helpers.sp
index ce937ba9..f2630495 100644
--- a/addons/sourcemod/scripting/weapons/helpers.sp
+++ b/addons/sourcemod/scripting/weapons/helpers.sp
@@ -262,3 +262,34 @@ bool IsWarmUpPeriod()
 {
 	return view_as<bool>(GameRules_GetProp("m_bWarmupPeriod"));
 }
+
+int GetSkinIdFromSkinMenuDisplay(char display[32])
+{
+	Regex regex = CompileRegex(".+ \\((.+)\\)");
+	regex.Match(display);
+	
+	char skinIdStr[32];
+	regex.GetSubString(1, skinIdStr, sizeof(skinIdStr));
+	return StringToInt(skinIdStr);
+}
+
+Menu SearchSkins(int client, char skinName[32])
+{
+	StringMapSnapshot snapshot = g_smSkinMenuMap[g_iClientLanguage[client]].Snapshot();
+	menuPlayerSearchTemp[client] = null;
+	Menu result = new Menu(SearchMenuHandler, MENU_ACTIONS_DEFAULT);
+	
+	for (int i = 0; i < snapshot.Length; i++)
+	{
+		char name[32]
+		snapshot.GetKey(i, name, sizeof(name));
+		
+		// if current menu is the menu we searched for
+		if (StrContains(skinName, name, false) > -1 || StrContains(name, skinName, false) > -1)
+		{
+			result.AddItem(name, name);
+		}
+	}
+	
+	return result;
+}
\ No newline at end of file
diff --git a/addons/sourcemod/scripting/weapons/menus.sp b/addons/sourcemod/scripting/weapons/menus.sp
index f7908551..4b337832 100644
--- a/addons/sourcemod/scripting/weapons/menus.sp
+++ b/addons/sourcemod/scripting/weapons/menus.sp
@@ -1017,3 +1017,216 @@ public int LanguageMenuHandler(Menu menu, MenuAction action, int client, int sel
 		}
 	}
 }
+
+public int SkinsMenuHandler(Menu menu, MenuAction action, int client, int selection) 
+{
+	switch(action)
+	{
+		case MenuAction_Select:
+		{
+			if(IsClientInGame(client))
+			{
+				if (selection == 0)
+				{
+					// Apply to all
+					for (int i = 2; i < menu.ItemCount; i++) {
+						char weaponTempIndexStr[32];
+						char display[32];
+						int a;
+						menu.GetItem(i, weaponTempIndexStr, sizeof(weaponTempIndexStr), a, display, sizeof(display));
+						int tempIndex = StringToInt(weaponTempIndexStr);
+						int skinId = GetSkinIdFromSkinMenuDisplay(display);
+						
+						g_iSkins[client][tempIndex] = skinId;
+						
+						char updateFields[256];
+						char weaponName[32];
+						RemoveWeaponPrefix(g_WeaponClasses[tempIndex], weaponName, sizeof(weaponName));
+						Format(updateFields, sizeof(updateFields), "%s = %d", weaponName, skinId);
+						UpdatePlayerData(client, updateFields);
+						
+						RefreshWeapon(client, tempIndex);
+					}
+					
+					DataPack pack;
+					CreateDataTimer(0.5, SkinsMenuTimer, pack);
+					pack.WriteCell(menu);
+					pack.WriteCell(GetClientUserId(client));
+					pack.WriteCell(GetMenuSelectionPosition());
+					
+					return 0;
+				}
+				else if (selection == 1)
+				{
+					// Apply to current
+					
+					int weaponEntity = GetEntPropEnt(client, Prop_Data, "m_hActiveWeapon", 0);
+					char weaponClass[32];
+					if (weaponEntity != -1 && GetWeaponClass(weaponEntity, weaponClass, sizeof(weaponClass)))
+					{
+						int index;
+						g_smWeaponIndex.GetValue(weaponClass, index);
+						
+						for (int i = 2; i < menu.ItemCount; i++) {
+						
+							char weaponTempIndexStr[32];
+							char display[32];
+							int a;
+							menu.GetItem(i, weaponTempIndexStr, sizeof(weaponTempIndexStr), a, display, sizeof(display));
+							
+							int tempIndex = StringToInt(weaponTempIndexStr);
+							
+							if (tempIndex == index)
+							{
+								int skinId = GetSkinIdFromSkinMenuDisplay(display);
+								
+								g_iSkins[client][tempIndex] = skinId;
+								
+								char updateFields[256];
+								char weaponName[32];
+								RemoveWeaponPrefix(g_WeaponClasses[tempIndex], weaponName, sizeof(weaponName));
+								Format(updateFields, sizeof(updateFields), "%s = %d", weaponName, skinId);
+								UpdatePlayerData(client, updateFields);
+								
+								RefreshWeapon(client, tempIndex);
+								
+								DataPack pack;
+								CreateDataTimer(0.5, SkinsMenuTimer, pack);
+								pack.WriteCell(menu);
+								pack.WriteCell(GetClientUserId(client));
+								pack.WriteCell(GetMenuSelectionPosition());
+								
+								return 0;
+							}
+						}
+						
+						// if didn't find
+						PrintToChat(client, " %s \x02%t", g_ChatPrefix, "SearchApplyCurrentFailed");
+						
+					}
+				}
+				else {
+					/*
+					TODO:
+					this enable player to apply selected skin to the weapon in player hand
+					the weapon may not originally have this skin,
+					since PR #271 added a cvar "sm_weapons_enable_all_skins",
+					so its necessary to add a check after #271 is merged
+					*/
+					char weaponIndexStr[32];
+					char display[32];
+					int a;
+					menu.GetItem(selection, weaponIndexStr, sizeof(weaponIndexStr), a, display, sizeof(display));
+					
+					int weaponEntity = GetEntPropEnt(client, Prop_Data, "m_hActiveWeapon", 0);
+					char weaponClass[32];
+					if (weaponEntity != -1 && GetWeaponClass(weaponEntity, weaponClass, sizeof(weaponClass)))
+					{
+						int index;
+						g_smWeaponIndex.GetValue(weaponClass, index);
+						int skinId = GetSkinIdFromSkinMenuDisplay(display);
+						
+						g_iSkins[client][index] = skinId;
+					
+						char updateFields[256];
+						char weaponName[32];
+						RemoveWeaponPrefix(g_WeaponClasses[index], weaponName, sizeof(weaponName));
+						Format(updateFields, sizeof(updateFields), "%s = %d", weaponName, skinId);
+						UpdatePlayerData(client, updateFields);
+						
+						RefreshWeapon(client, index);
+					}
+				}
+				
+				DataPack pack;
+				CreateDataTimer(0.5, SkinsMenuTimer, pack);
+				pack.WriteCell(menu);
+				pack.WriteCell(GetClientUserId(client));
+				pack.WriteCell(GetMenuSelectionPosition());
+				
+				return 0;
+			}
+		}
+		case MenuAction_DisplayItem:
+		{
+			if(IsClientInGame(client))
+			{
+				char info[32];
+				char display[32];
+				int a;
+				menu.GetItem(selection, info, sizeof(info), a, display, sizeof(display));
+				
+				if (StrEqual(info, "-1"))
+				{
+					Format(display, sizeof(display), "%T", "SearchApplyAll", client);
+					return RedrawMenuItem(display);
+				}
+				else if (StrEqual(info, "-2"))
+				{
+					Format(display, sizeof(display), "%T", "SearchApplyCurrent", client);
+					return RedrawMenuItem(display);
+				}
+				else
+				{
+					// translate weapon name
+					int skinId = GetSkinIdFromSkinMenuDisplay(display);
+					Format(display, sizeof(display), "%T", g_WeaponClasses[StringToInt(info)], client);
+					Format(display, sizeof(display), "%s (%d)", display, skinId);
+					return RedrawMenuItem(display);
+				}
+			}
+		}
+		case MenuAction_Cancel:
+		{
+			int menuTime;
+			if((menuTime = GetRemainingGracePeriodSeconds(client)) >= 0)
+			{
+				menuPlayerSearchTemp[client].Display(client, menuTime);
+			}
+		}
+	}
+	
+	return;
+}
+
+public Action SkinsMenuTimer(Handle timer, DataPack pack)
+{
+	ResetPack(pack);
+	Menu menu = pack.ReadCell();
+	int clientIndex = GetClientOfUserId(pack.ReadCell());
+	int menuSelectionPosition = pack.ReadCell();
+	
+	if(IsValidClient(clientIndex))
+	{
+		int menuTime;
+		if((menuTime = GetRemainingGracePeriodSeconds(clientIndex)) >= 0)
+		{
+			menu.DisplayAt(clientIndex, menuSelectionPosition, menuTime);
+		}
+	}
+}
+
+public int SearchMenuHandler(Menu menu, MenuAction menuaction, int client, int selection)
+{
+	switch (menuaction)
+	{
+		case MenuAction_Select:
+		{
+			if(IsClientInGame(client))
+			{
+				char subMenuName[32];
+				menu.GetItem(selection, subMenuName, sizeof(subMenuName));
+				
+				Menu subMenu;
+				g_smSkinMenuMap[g_iClientLanguage[client]].GetValue(subMenuName, subMenu);
+				
+				int menuTime;
+				if((menuTime = GetRemainingGracePeriodSeconds(client)) >= 0)
+				{
+					subMenu.Display(client, menuTime);
+				}
+			}
+		}
+	}
+	
+}
\ No newline at end of file
diff --git a/addons/sourcemod/translations/chi/weapons.phrases.txt b/addons/sourcemod/translations/chi/weapons.phrases.txt
index d5b54f45..2fa8da2a 100644
--- a/addons/sourcemod/translations/chi/weapons.phrases.txt
+++ b/addons/sourcemod/translations/chi/weapons.phrases.txt
@@ -56,6 +56,18 @@
 	{
 		"chi"	"名称标签已禁用"
 	}
+	"SearchApplyAll"
+	{
+		"chi"	"应用所有皮肤"
+	}
+	"SearchApplyCurrent"
+	{
+		"chi"	"应用到当前武器"
+	}
+	"SearchApplyCurrentFailed"
+	{
+		"chi"	"您的武器没有该皮肤。"
+	}
 	"ChooseLanguage"
 	{
 		"chi"	"请选择皮肤名称显示语言:"
diff --git a/addons/sourcemod/translations/weapons.phrases.txt b/addons/sourcemod/translations/weapons.phrases.txt
index db22bf93..fcf9b7de 100644
--- a/addons/sourcemod/translations/weapons.phrases.txt
+++ b/addons/sourcemod/translations/weapons.phrases.txt
@@ -68,6 +68,18 @@
 	{
 		"en"	"Use: !nametag <tag>"
 	}
+	"SearchApplyAll"
+	{
+		"en"	"Apply to all"
+	}
+	"SearchApplyCurrent"
+	{
+		"en"	"Apply to current"
+	}
+	"SearchApplyCurrentFailed"
+	{
+		"en"	"Your weapon doesn't have this skin."
+	}
 	"ChooseLanguage"
 	{
 		"en"	"Select language for skin names:"

From 1b0249f26b60464de830f3dc59d4fe489b1bea90 Mon Sep 17 00:00:00 2001
From: e2ectronic <3356207189@qq.com>
Date: Sat, 27 May 2023 08:20:39 +0800
Subject: [PATCH 2/2] small fix

---
 addons/sourcemod/scripting/weapons.sp         | 21 ++++++++++++-------
 .../translations/chi/weapons.phrases.txt      |  4 ++++
 .../translations/weapons.phrases.txt          |  4 ++++
 3 files changed, 22 insertions(+), 7 deletions(-)

diff --git a/addons/sourcemod/scripting/weapons.sp b/addons/sourcemod/scripting/weapons.sp
index 7ab18128..b1fce137 100644
--- a/addons/sourcemod/scripting/weapons.sp
+++ b/addons/sourcemod/scripting/weapons.sp
@@ -206,14 +206,21 @@ public Action CommandWeaponSkins(int client, int args)
 		{
 			if (args > 0) {
 				
-				char searchSkinName[32];
-				GetCmdArgString(searchSkinName, sizeof(searchSkinName));
-				
-				Menu resultMenu = SearchSkins(client, searchSkinName);
-				menuPlayerSearchTemp[client] = resultMenu;
-				resultMenu.Display(client, menuTime);
-				
+				if (g_bEnableSearch)
+				{
+					char searchSkinName[32];
+					GetCmdArgString(searchSkinName, sizeof(searchSkinName));
+					
+					Menu resultMenu = SearchSkins(client, searchSkinName);
+					menuPlayerSearchTemp[client] = resultMenu;
+					resultMenu.Display(client, menuTime);
+				}
+				else
+				{
+					PrintToChat(client, " %s \x02%T", g_ChatPrefix, "SearchDisabled");
+				}
 				return Plugin_Handled;
+				
 			}
 			
 			CreateMainMenu(client).Display(client, menuTime);
diff --git a/addons/sourcemod/translations/chi/weapons.phrases.txt b/addons/sourcemod/translations/chi/weapons.phrases.txt
index 2fa8da2a..b90be3ae 100644
--- a/addons/sourcemod/translations/chi/weapons.phrases.txt
+++ b/addons/sourcemod/translations/chi/weapons.phrases.txt
@@ -56,6 +56,10 @@
 	{
 		"chi"	"名称标签已禁用"
 	}
+	"SearchDisabled"
+	{
+		"chi"	"搜索功能已禁用"
+	}
 	"SearchApplyAll"
 	{
 		"chi"	"应用所有皮肤"
diff --git a/addons/sourcemod/translations/weapons.phrases.txt b/addons/sourcemod/translations/weapons.phrases.txt
index fcf9b7de..c8ba327e 100644
--- a/addons/sourcemod/translations/weapons.phrases.txt
+++ b/addons/sourcemod/translations/weapons.phrases.txt
@@ -68,6 +68,10 @@
 	{
 		"en"	"Use: !nametag <tag>"
 	}
+	"SearchDisabled"
+	{
+		"en"	"Skin search is disabled at the moment."
+	}
 	"SearchApplyAll"
 	{
 		"en"	"Apply to all"