diff --git a/core/logic/smn_players.cpp b/core/logic/smn_players.cpp index 4fdb5304..9b73542c 100644 --- a/core/logic/smn_players.cpp +++ b/core/logic/smn_players.cpp @@ -1471,24 +1471,23 @@ static cell_t IsClientInKickQueue(IPluginContext *pContext, const cell_t *params return pPlayer->IsInKickQueue() ? 1 : 0; } +cmd_target_info_t g_ProcessTargetString_info; static cell_t ProcessTargetString(IPluginContext *pContext, const cell_t *params) { - cmd_target_info_t info; - - pContext->LocalToString(params[1], (char **) &info.pattern); - info.admin = params[2]; - pContext->LocalToPhysAddr(params[3], &info.targets); - info.max_targets = params[4]; - info.flags = params[5]; - pContext->LocalToString(params[6], &info.target_name); - info.target_name_maxlength = params[7]; + pContext->LocalToString(params[1], (char **) &g_ProcessTargetString_info.pattern); + g_ProcessTargetString_info.admin = params[2]; + pContext->LocalToPhysAddr(params[3], &g_ProcessTargetString_info.targets); + g_ProcessTargetString_info.max_targets = params[4]; + g_ProcessTargetString_info.flags = params[5]; + pContext->LocalToString(params[6], &g_ProcessTargetString_info.target_name); + g_ProcessTargetString_info.target_name_maxlength = params[7]; cell_t *tn_is_ml; pContext->LocalToPhysAddr(params[8], &tn_is_ml); - playerhelpers->ProcessCommandTarget(&info); + playerhelpers->ProcessCommandTarget(&g_ProcessTargetString_info); - if (info.target_name_style == COMMAND_TARGETNAME_ML) + if (g_ProcessTargetString_info.target_name_style == COMMAND_TARGETNAME_ML) { *tn_is_ml = 1; } @@ -1497,16 +1496,30 @@ static cell_t ProcessTargetString(IPluginContext *pContext, const cell_t *params *tn_is_ml = 0; } - if (info.num_targets == 0) + if (g_ProcessTargetString_info.num_targets == 0) { - return info.reason; + return g_ProcessTargetString_info.reason; } else { - return info.num_targets; + return g_ProcessTargetString_info.num_targets; } } +static cell_t GetLastProcessTargetString(IPluginContext *pContext, const cell_t *params) +{ + cell_t *admin, *flags; + + pContext->StringToLocalUTF8(params[1], params[2], g_ProcessTargetString_info.pattern, NULL); + pContext->LocalToPhysAddr(params[3], &admin); + pContext->LocalToPhysAddr(params[4], &flags); + + *admin = g_ProcessTargetString_info.admin; + *flags = g_ProcessTargetString_info.flags; + + return 0; +} + static cell_t FormatActivitySource(IPluginContext *pContext, const cell_t *params) { int value; @@ -1658,6 +1671,7 @@ REGISTER_NATIVES(playernatives) { "NotifyPostAdminCheck", NotifyPostAdminCheck }, { "IsClientInKickQueue", IsClientInKickQueue }, { "ProcessTargetString", ProcessTargetString }, + { "GetLastProcessTargetString", GetLastProcessTargetString }, { "FormatActivitySource", FormatActivitySource }, { "GetClientSerial", sm_GetClientSerial }, { "GetClientFromSerial", sm_GetClientFromSerial }, diff --git a/core/smn_console.cpp b/core/smn_console.cpp index 1621977a..63fe16e8 100644 --- a/core/smn_console.cpp +++ b/core/smn_console.cpp @@ -789,6 +789,16 @@ static cell_t sm_RegAdminCmd(IPluginContext *pContext, const cell_t *params) return 1; } +static cell_t sm_IsCommandCallback(IPluginContext *pContext, const cell_t *params) +{ + const ICommandArgs *pCmd = g_HL2.PeekCommandStack(); + + if (!pCmd) + return 0; + + return 1; +} + static cell_t sm_GetCmdArgs(IPluginContext *pContext, const cell_t *params) { const ICommandArgs *pCmd = g_HL2.PeekCommandStack(); @@ -1467,6 +1477,7 @@ REGISTER_NATIVES(consoleNatives) {"GetConVarDefault", GetConVarDefault}, {"RegServerCmd", sm_RegServerCmd}, {"RegConsoleCmd", sm_RegConsoleCmd}, + {"IsCommandCallback", sm_IsCommandCallback}, {"GetCmdArgString", sm_GetCmdArgString}, {"GetCmdArgs", sm_GetCmdArgs}, {"GetCmdArg", sm_GetCmdArg}, diff --git a/plugins/AMBuilder b/plugins/AMBuilder index bf9c3490..35d50df5 100644 --- a/plugins/AMBuilder +++ b/plugins/AMBuilder @@ -24,7 +24,8 @@ files = [ 'basecommands.sp', 'mapchooser.sp', 'randomcycle.sp', - 'sql-admin-manager.sp' + 'sql-admin-manager.sp', + 'DynamicTargeting.sp' ] spcomp_argv = [ diff --git a/plugins/DynamicTargeting.sp b/plugins/DynamicTargeting.sp new file mode 100644 index 00000000..b4ddff2c --- /dev/null +++ b/plugins/DynamicTargeting.sp @@ -0,0 +1,266 @@ +#pragma semicolon 1 +#define PLUGIN_VERSION "1.0" + +#include +#include + +#pragma newdecls required + +public Plugin myinfo = +{ + name = "Dynamic Targeting", + author = "BotoX", + description = "", + version = PLUGIN_VERSION, + url = "" +} + +char g_PlayerNames[MAXPLAYERS + 1][MAX_NAME_LENGTH]; +Handle g_PlayerData[MAXPLAYERS + 1]; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNative("AmbiguousMenu", Native_AmbiguousMenu); + RegPluginLibrary("DynamicTargeting"); + + return APLRes_Success; +} + +public void OnClientDisconnect(int client) +{ + if(g_PlayerData[client] != INVALID_HANDLE) + { + CloseHandle(g_PlayerData[client]); + g_PlayerData[client] = INVALID_HANDLE; + } +} + +int CreateAmbiguousMenu(int client, const char[] sCommand, const char[] sArgString, const char[] sPattern, int FilterFlags) +{ + Menu menu = new Menu(MenuHandler_AmbiguousMenu, MenuAction_Select|MenuAction_Cancel|MenuAction_End|MenuAction_DrawItem|MenuAction_DisplayItem); + menu.ExitButton = true; + + char sTitle[32 + MAX_TARGET_LENGTH]; + FormatEx(sTitle, sizeof(sTitle), "Target \"%s\" is ambiguous.", sPattern); + menu.SetTitle(sTitle); + + int Players = 0; + int[] aClients = new int[MaxClients + 1]; + + for(int i = 1; i <= MaxClients; i++) + { + if(!IsClientConnected(i) || i == client) + continue; + + if(FilterFlags & COMMAND_FILTER_NO_BOTS && IsFakeClient(i)) + continue; + + if(!(FilterFlags & COMMAND_FILTER_CONNECTED) && !IsClientInGame(i)) + continue; + + if(FilterFlags & COMMAND_FILTER_ALIVE && !IsPlayerAlive(i)) + continue; + + if(FilterFlags & COMMAND_FILTER_DEAD && IsPlayerAlive(i)) + continue; + + // insert player names into g_PlayerNames array + GetClientName(i, g_PlayerNames[i], sizeof(g_PlayerNames[])); + + if(StrContains(g_PlayerNames[i], sPattern, false) != -1) + aClients[Players++] = i; + } + + // sort aClients array by player name + SortCustom1D(aClients, Players, SortByPlayerName); + + // insert players sorted + char sUserId[12]; + char sDisp[MAX_NAME_LENGTH + 16]; + for(int i = 0; i < Players; i++) + { + IntToString(GetClientUserId(aClients[i]), sUserId, sizeof(sUserId)); + + FormatEx(sDisp, sizeof(sDisp), "%s (%s)", g_PlayerNames[aClients[i]], sUserId); + menu.AddItem(sUserId, sDisp); + } + + DataPack pack = new DataPack(); + pack.WriteString(sCommand); + pack.WriteString(sArgString); + pack.WriteString(sPattern); + pack.WriteCell(FilterFlags); + + if(g_PlayerData[client] != INVALID_HANDLE) + { + CloseHandle(g_PlayerData[client]); + g_PlayerData[client] = INVALID_HANDLE; + } + CancelClientMenu(client); + + g_PlayerData[client] = pack; + menu.Display(client, MENU_TIME_FOREVER); + + return 0; +} + +public int MenuHandler_AmbiguousMenu(Menu menu, MenuAction action, int param1, int param2) +{ + switch(action) + { + case MenuAction_End: + { + CloseHandle(menu); + } + case MenuAction_Cancel: + { + if(g_PlayerData[param1] != INVALID_HANDLE) + { + CloseHandle(g_PlayerData[param1]); + g_PlayerData[param1] = INVALID_HANDLE; + } + } + case MenuAction_Select: + { + int Style; + char sItem[32]; + char sDisp[MAX_NAME_LENGTH + 16]; + menu.GetItem(param2, sItem, sizeof(sItem), Style, sDisp, sizeof(sDisp)); + + int UserId = StringToInt(sItem); + int client = GetClientOfUserId(UserId); + if(!client) + { + PrintToChat(param1, "\x04[DynamicTargeting]\x01 Player no longer available."); + menu.DisplayAt(param1, GetMenuSelectionPosition(), MENU_TIME_FOREVER); + return 0; + } + + DataPack pack = view_as(g_PlayerData[param1]); + pack.Reset(); + + char sCommand[128]; + pack.ReadString(sCommand, sizeof(sCommand)); + + char sArgString[256]; + pack.ReadString(sArgString, sizeof(sArgString)); + + char sPattern[MAX_TARGET_LENGTH]; + pack.ReadString(sPattern, sizeof(sPattern)); + + int Result = ReCallAmbiguous(param1, client, sCommand, sArgString, sPattern); + + return Result; + } + case MenuAction_DrawItem: + { + int Style; + char sItem[32]; + menu.GetItem(param2, sItem, sizeof(sItem), Style); + + int UserId = StringToInt(sItem); + int client = GetClientOfUserId(UserId); + if(!client) // Player disconnected + return ITEMDRAW_DISABLED; + + return Style; + } + case MenuAction_DisplayItem: + { + int Style; + char sItem[32]; + char sDisp[MAX_NAME_LENGTH + 16]; + menu.GetItem(param2, sItem, sizeof(sItem), Style, sDisp, sizeof(sDisp)); + + if(!sItem[0]) + return 0; + + char sBuffer[MAX_NAME_LENGTH + 16]; + int UserId = StringToInt(sItem); + int client = GetClientOfUserId(UserId); + if(!client) // Player disconnected + return 0; + + GetClientName(client, g_PlayerNames[client], sizeof(g_PlayerNames[])); + FormatEx(sBuffer, sizeof(sBuffer), "%s (%d)", g_PlayerNames[client], UserId); + + if(!StrEqual(sDisp, sBuffer)) + return RedrawMenuItem(sBuffer); + + return 0; + } + } + + return 0; +} + +int ReCallAmbiguous(int client, int newClient, const char[] sCommand, const char[] sArgString, const char[] sPattern) +{ + char sTarget[16]; + FormatEx(sTarget, sizeof(sTarget), "#%d", GetClientUserId(newClient)); + + char sNewArgString[256]; + strcopy(sNewArgString, sizeof(sNewArgString), sArgString); + + char sPart[256]; + int CurrentIndex = 0; + int NextIndex = 0; + + while(NextIndex != -1 && CurrentIndex < sizeof(sNewArgString)) + { + NextIndex = BreakString(sNewArgString[CurrentIndex], sPart, sizeof(sPart)); + + if(StrEqual(sPart, sPattern)) + { + ReplaceStringEx(sNewArgString[CurrentIndex], sizeof(sNewArgString) - CurrentIndex, sPart, sTarget); + break; + } + + CurrentIndex += NextIndex; + } + + FakeClientCommandEx(client, "%s %s", sCommand, sNewArgString); + + return 0; +} + +public int Native_AmbiguousMenu(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + + if(client > MaxClients || client <= 0) + { + ThrowNativeError(SP_ERROR_NATIVE, "Client is not valid."); + return -1; + } + + if(!IsClientInGame(client)) + { + ThrowNativeError(SP_ERROR_NATIVE, "Client is not in-game."); + return -1; + } + + if(IsFakeClient(client)) + { + ThrowNativeError(SP_ERROR_NATIVE, "Client is fake-client."); + return -1; + } + + char sCommand[128]; + GetNativeString(2, sCommand, sizeof(sCommand)); + + char sArgString[256]; + GetNativeString(3, sArgString, sizeof(sArgString)); + + char sPattern[MAX_TARGET_LENGTH]; + GetNativeString(4, sPattern, sizeof(sPattern)); + + int FilterFlags = GetNativeCell(5); + + return CreateAmbiguousMenu(client, sCommand, sArgString, sPattern, FilterFlags); +} + +public int SortByPlayerName(int elem1, int elem2, const int[] array, Handle hndl) +{ + return strcmp(g_PlayerNames[elem1], g_PlayerNames[elem2], false); +} diff --git a/plugins/include/DynamicTargeting.inc b/plugins/include/DynamicTargeting.inc new file mode 100644 index 00000000..14dd0abc --- /dev/null +++ b/plugins/include/DynamicTargeting.inc @@ -0,0 +1,24 @@ +#if defined _DynamicTargeting_Included + #endinput +#endif +#define _DynamicTargeting_Included + +native int AmbiguousMenu(int client, char[] sCommand, char[] sArgString, char[] sPattern, int FilterFlags); + +public SharedPlugin __pl_DynamicTargeting = +{ + name = "DynamicTargeting", + file = "DynamicTargeting.smx", +#if defined REQUIRE_PLUGIN + required = 1, +#else + required = 0, +#endif +}; + +#if !defined REQUIRE_PLUGIN +public __pl_DynamicTargeting_SetNTVOptional() +{ + MarkNativeAsOptional("AmbiguousMenu"); +} +#endif diff --git a/plugins/include/commandfilters.inc b/plugins/include/commandfilters.inc index 9a658a92..48c564d2 100644 --- a/plugins/include/commandfilters.inc +++ b/plugins/include/commandfilters.inc @@ -84,6 +84,25 @@ native int ProcessTargetString(const char[] pattern, int tn_maxlength, bool &tn_is_ml); + +/** + * Retrieves arguments that were passed to the last ProcessTargetString call. + * + * @param pattern Buffer to store the pattern. + * @param p_maxlen Maximum length of the pattern buffer. + * @param admin OUTPUT: Admin performing the action, or 0 if the server. + * @param filter_flags OUTPUT: Filter flags. + * @noreturn + */ +native void GetLastProcessTargetString(char[] pattern, + int p_maxlen, + int &admin, + int &filter_flags); + +#undef REQUIRE_PLUGIN +#include +#define REQUIRE_PLUGIN + /** * Replies to a client with a given message describing a targetting * failure reason. @@ -93,7 +112,7 @@ native int ProcessTargetString(const char[] pattern, * @param client Client index, or 0 for server. * @param reason COMMAND_TARGET reason. */ -stock void ReplyToTargetError(int client, int reason) +stock void ReplyToTargetError(int client, int reason, bool dynamic=true) { switch (reason) { @@ -128,6 +147,34 @@ stock void ReplyToTargetError(int client, int reason) case COMMAND_TARGET_AMBIGUOUS: { ReplyToCommand(client, "[SM] %t", "More than one client matched"); + + if(dynamic && + GetFeatureStatus(FeatureType_Native, "GetLastProcessTargetString") == FeatureStatus_Available && + LibraryExists("DynamicTargeting")) + { + if(GetFeatureStatus(FeatureType_Native, "IsCommandCallback") == FeatureStatus_Available && + !IsCommandCallback()) + { + return; + } + + char sCommand[128]; + GetCmdArg(0, sCommand, sizeof(sCommand)); + + char sArgString[256]; + GetCmdArgString(sArgString, sizeof(sArgString)); + + char pattern[MAX_TARGET_LENGTH]; + int admin; + int filter_flags; + + GetLastProcessTargetString(pattern, sizeof(pattern), admin, filter_flags); + + if(!admin || !IsClientInGame(admin) || IsFakeClient(admin)) + return; + + AmbiguousMenu(admin, sCommand, sArgString, pattern, filter_flags); + } } } } diff --git a/plugins/include/console.inc b/plugins/include/console.inc index ccbcbf1a..4487d380 100644 --- a/plugins/include/console.inc +++ b/plugins/include/console.inc @@ -402,7 +402,14 @@ native void RegAdminCmd(const char[] cmd, const char[] description="", const char[] group="", int flags=0); - + +/** + * Returns whether there is a command callback available. + * + * @return True if called from inside a command callback. + */ +native bool IsCommandCallback(); + /** * Returns the number of arguments from the current console or server command. * @note Unlike the HL2 engine call, this does not include the command itself. diff --git a/plugins/include/core.inc b/plugins/include/core.inc index b6aaf6e1..bc1377cc 100644 --- a/plugins/include/core.inc +++ b/plugins/include/core.inc @@ -311,6 +311,9 @@ public void __ext_core_SetNTVOptional() MarkNativeAsOptional("Protobuf.ReadRepeatedMessage"); MarkNativeAsOptional("Protobuf.AddMessage"); + MarkNativeAsOptional("IsCommandCallback"); + MarkNativeAsOptional("GetLastProcessTargetString"); + VerifyCoreVersion(); } diff --git a/plugins/include/sourcemod.inc b/plugins/include/sourcemod.inc index 01dc5456..243ac2d8 100644 --- a/plugins/include/sourcemod.inc +++ b/plugins/include/sourcemod.inc @@ -47,6 +47,92 @@ struct Plugin public const char[] url; /**< Plugin URL */ }; +/** + * Returns whether a library exists. This function should be considered + * expensive; it should only be called on plugin to determine availability + * of resources. Use OnLibraryAdded()/OnLibraryRemoved() to detect changes + * in optional resources. + * + * @param name Library name of a plugin or extension. + * @return True if exists, false otherwise. + */ +native bool LibraryExists(const char[] name); + +/** + * Feature types. + */ +enum FeatureType +{ + /** + * A native function call. + */ + FeatureType_Native, + + /** + * A named capability. This is distinctly different from checking for a + * native, because the underlying functionality could be enabled on-demand + * to improve loading time. Thus a native may appear to exist, but it might + * be part of a set of features that are not compatible with the current game + * or version of SourceMod. + */ + FeatureType_Capability +}; + +/** + * Feature statuses. + */ +enum FeatureStatus +{ + /** + * Feature is available for use. + */ + FeatureStatus_Available, + + /** + * Feature is not available. + */ + FeatureStatus_Unavailable, + + /** + * Feature is not known at all. + */ + FeatureStatus_Unknown +}; + +/** + * Returns whether "GetFeatureStatus" will work. Using this native + * or this function will not cause SourceMod to fail loading on older versions, + * however, GetFeatureStatus will only work if this function returns true. + * + * @return True if GetFeatureStatus will work, false otherwise. + */ +stock bool CanTestFeatures() +{ + return LibraryExists("__CanTestFeatures__"); +} + +/** + * Returns whether a feature exists, and if so, whether it is usable. + * + * @param type Feature type. + * @param name Feature name. + * @return Feature status. + */ +native FeatureStatus GetFeatureStatus(FeatureType type, const char[] name); + +/** + * Requires that a given feature is available. If it is not, SetFailState() + * is called with the given message. + * + * @param type Feature type. + * @param name Feature name. + * @param fmt Message format string, or empty to use default. + * @param ... Message format parameters, if any. + */ +native void RequireFeature(FeatureType type, const char[] name, + const char[] fmt="", any ...); + + #include #include #include @@ -448,17 +534,6 @@ native void AutoExecConfig(bool autoCreate=true, const char[] name="", const cha */ native void RegPluginLibrary(const char[] name); -/** - * Returns whether a library exists. This function should be considered - * expensive; it should only be called on plugin to determine availability - * of resources. Use OnLibraryAdded()/OnLibraryRemoved() to detect changes - * in optional resources. - * - * @param name Library name of a plugin or extension. - * @return True if exists, false otherwise. - */ -native bool LibraryExists(const char[] name); - /** * Returns the status of an extension, by filename. * @@ -582,80 +657,6 @@ forward bool OnClientFloodCheck(int client); */ forward void OnClientFloodResult(int client, bool blocked); -/** - * Feature types. - */ -enum FeatureType -{ - /** - * A native function call. - */ - FeatureType_Native, - - /** - * A named capability. This is distinctly different from checking for a - * native, because the underlying functionality could be enabled on-demand - * to improve loading time. Thus a native may appear to exist, but it might - * be part of a set of features that are not compatible with the current game - * or version of SourceMod. - */ - FeatureType_Capability -}; - -/** - * Feature statuses. - */ -enum FeatureStatus -{ - /** - * Feature is available for use. - */ - FeatureStatus_Available, - - /** - * Feature is not available. - */ - FeatureStatus_Unavailable, - - /** - * Feature is not known at all. - */ - FeatureStatus_Unknown -}; - -/** - * Returns whether "GetFeatureStatus" will work. Using this native - * or this function will not cause SourceMod to fail loading on older versions, - * however, GetFeatureStatus will only work if this function returns true. - * - * @return True if GetFeatureStatus will work, false otherwise. - */ -stock bool CanTestFeatures() -{ - return LibraryExists("__CanTestFeatures__"); -} - -/** - * Returns whether a feature exists, and if so, whether it is usable. - * - * @param type Feature type. - * @param name Feature name. - * @return Feature status. - */ -native FeatureStatus GetFeatureStatus(FeatureType type, const char[] name); - -/** - * Requires that a given feature is available. If it is not, SetFailState() - * is called with the given message. - * - * @param type Feature type. - * @param name Feature name. - * @param fmt Message format string, or empty to use default. - * @param ... Message format parameters, if any. - */ -native void RequireFeature(FeatureType type, const char[] name, - const char[] fmt="", any ...); - /** * Represents how many bytes we can read from an address with one load */ diff --git a/tools/buildbot/PackageScript b/tools/buildbot/PackageScript index 88a075e3..2f1765d6 100644 --- a/tools/buildbot/PackageScript +++ b/tools/buildbot/PackageScript @@ -324,6 +324,7 @@ CopyFiles('plugins', 'addons/sourcemod/scripting', 'rockthevote.sp', 'sounds.sp', 'sql-admin-manager.sp', + 'DynamicTargeting.sp', ] ) CopyFiles('plugins/include', 'addons/sourcemod/scripting/include', @@ -394,6 +395,7 @@ CopyFiles('plugins/include', 'addons/sourcemod/scripting/include', 'usermessages.inc', 'vector.inc', 'version.inc', + 'DynamicTargeting.inc', ] ) CopyFiles('translations', 'addons/sourcemod/translations',