Added hack to make plugins open a menu with all possible targets on ReplyToTargetError COMMAND_TARGET_AMBIGUOUS.

Explanation:
There are two clients in the server, one named gene, the other one "Ene ~special characters~".
An admin issues "sm_slay Ene" and gets following error message: More than one client matched the given pattern.
What this hack will do is: Use GetCmdArg(0, ...); to get the command name "sm_slay".
Use GetCmdArgString(...); to get the arguments supplied to the command.
Use GetLastProcessTargetString(...); (which was implemented in this commit) to retrieve the arguments that were passed to the last ProcessTargetString call.
It will then pass this data to the DynamicTargeting plugin through its AmbiguousMenu native.
The plugin will open up a menu on the client and list all targets which match the pattern that was supplied to ProcessTargetString.
If the client selects a menu entry, FakeClientCommand will be used to re-execute the command with the correct target.

# Conflicts:
#	tools/buildbot/PackageScript
This commit is contained in:
Your Name 2023-08-30 22:03:26 +02:00
parent 688e274c30
commit d099dbb659
11 changed files with 393 additions and 20 deletions

View File

@ -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 },

View File

@ -827,6 +827,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();
@ -1505,6 +1515,7 @@ REGISTER_NATIVES(consoleNatives)
{"GetConVarDefault", GetConVarDefault},
{"RegServerCmd", sm_RegServerCmd},
{"RegConsoleCmd", sm_RegConsoleCmd},
{"IsCommandCallback", sm_IsCommandCallback},
{"GetCmdArgString", sm_GetCmdArgString},
{"GetCmdArgs", sm_GetCmdArgs},
{"GetCmdArg", sm_GetCmdArg},

View File

@ -24,7 +24,8 @@ files = [
'basecommands.sp',
'mapchooser.sp',
'randomcycle.sp',
'sql-admin-manager.sp'
'sql-admin-manager.sp',
'DynamicTargeting.sp'
]
spcomp_argv = [

266
plugins/DynamicTargeting.sp Normal file
View File

@ -0,0 +1,266 @@
#pragma semicolon 1
#define PLUGIN_VERSION "1.0"
#include <sourcemod>
#include <DynamicTargeting>
#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<DataPack>(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);
}

View File

@ -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

View File

@ -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 <DynamicTargeting>
#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);
}
}
}
}

View File

@ -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.

View File

@ -311,6 +311,9 @@ public void __ext_core_SetNTVOptional()
MarkNativeAsOptional("Protobuf.ReadRepeatedMessage");
MarkNativeAsOptional("Protobuf.AddMessage");
MarkNativeAsOptional("IsCommandCallback");
MarkNativeAsOptional("GetLastProcessTargetString");
VerifyCoreVersion();
}

View File

@ -73,7 +73,6 @@ struct Plugin
#include <halflife>
#include <adt>
#include <banning>
#include <commandfilters>
#include <nextmap>
#include <commandline>
#include <entitylump>
@ -767,6 +766,7 @@ methodmap FrameIterator < Handle {
public native void GetFilePath(char[] buffer, int maxlen);
}
#include <commandfilters>
#include <helpers>
#include <entity>
#include <entity_prop_stocks>

@ -1 +1 @@
Subproject commit fb8a4edaeaeea97b1c07f42b8a8ba97ce2c614cd
Subproject commit ef79d8d86a9a858828b352f0c77ca57ab6c266e3

@ -1 +1 @@
Subproject commit 29212225d6e1bbc7887c9807b06110552a7db1bd
Subproject commit f31a11eb06a622277efe85064693d7079da879aa