1
0

Merge pull request #2000 from mc-server/CommandHandling

Command handling
This commit is contained in:
Mattes D 2015-05-11 10:39:45 +02:00
commit 7d66162e3c
16 changed files with 173 additions and 73 deletions

View File

@ -68,6 +68,7 @@ cPluginManager.AddHook(cPluginManager.HOOK_CHAT, OnChatMessage);
CallPlugin = { Params = "PluginName, FunctionName, [FunctionArgs...]", Return = "[FunctionRets]", Notes = "(STATIC) Calls the specified function in the specified plugin, passing all the given arguments to it. If it succeeds, it returns all the values returned by that function. If it fails, returns no value at all. Note that only strings, numbers, bools, nils and classes can be used for parameters and return values; tables and functions cannot be copied across plugins." },
DoWithPlugin = { Params = "PluginName, CallbackFn", Return = "bool", Notes = "(STATIC) Calls the CallbackFn for the specified plugin, if found. A plugin can be found even if it is currently unloaded, disabled or errored, the callback should check the plugin status. If the plugin is not found, this function returns false, otherwise it returns the bool value that the callback has returned. The CallbackFn has the following signature: <pre class=\"prettyprint lang-lua\">function ({{cPlugin|Plugin}})</pre>" },
ExecuteCommand = { Params = "{{cPlayer|Player}}, CommandStr", Return = "{{cPluginManager#CommandResult|CommandResult}}", Notes = "Executes the command as if given by the specified Player. Checks permissions." },
ExecuteConsoleCommand = { Params = "CommandStr", Return = "bool, string", Notes = "Executes the console command as if given by the admin on the console. If the command is successfully executed, returns true and the text that would be output to the console normally. On error it returns false and an error message." },
FindPlugins = { Params = "", Return = "", Notes = "<b>OBSOLETE</b>, use RefreshPluginList() instead"},
ForceExecuteCommand = { Params = "{{cPlayer|Player}}, CommandStr", Return = "{{cPluginManager#CommandResult|CommandResult}}", Notes = "Same as ExecuteCommand, but doesn't check permissions" },
ForEachCommand = { Params = "CallbackFn", Return = "bool", Notes = "Calls the CallbackFn function for each command that has been bound using BindCommand(). The CallbackFn has the following signature: <pre class=\"prettyprint lang-lua\">function(Command, Permission, HelpString)</pre> If the callback returns true, the enumeration is aborted and this API function returns false; if it returns false or no value, the enumeration continues with the next command, and the API function returns true." },

View File

@ -7,7 +7,8 @@ return
Desc = [[
A plugin may implement an OnChat() function and register it as a Hook to process chat messages from
the players. The function is then called for every in-game message sent from any player. Note that
commands are handled separately using a command framework API.
registered in-game commands are not sent through this hook. Use the
{{OnExecuteCommand|HOOK_EXECUTE_COMMAND}} to intercept registered in-game commands.
]],
Params = {
{ Name = "Player", Type = "{{cPlayer}}", Notes = "The player who sent the message" },

View File

@ -2,7 +2,10 @@ return
{
HOOK_EXECUTE_COMMAND =
{
CalledWhen = "A player executes an in-game command, or the admin issues a console command. Note that built-in console commands are exempt to this hook - they are always performed and the hook is not called.",
CalledWhen = [[
A player executes an in-game command, or the admin issues a console command. Note that built-in
console commands are exempt to this hook - they are always performed and the hook is not called.
]],
DefaultFnName = "OnExecuteCommand", -- also used as pagename
Desc = [[
A plugin may implement a callback for this hook to intercept both in-game commands executed by the
@ -11,17 +14,25 @@ return
server.</p>
<p>
If the command is in-game, the first parameter to the hook function is the {{cPlayer|player}} who's
executing the command. If the command comes from the server console, the first parameter is nil.
executing the command. If the command comes from the server console, the first parameter is nil.</p>
<p>
The server calls this hook even for unregistered (unknown) console commands. However, it doesn't call
the hook for unregistered in-game commands, simply because there's no way to distinguish between a
command and a chat message. If a plugin needs to intercept unknown in-game commands, it should use the
{{OnChat|HOOK_CHAT}} hook.
]],
Params =
{
{ Name = "Player", Type = "{{cPlayer}}", Notes = "For in-game commands, the player who has sent the message. For console commands, nil" },
{ Name = "Command", Type = "table of strings", Notes = "The command and its parameters, broken into a table by spaces" },
{ Name = "CommandSplit", Type = "array-table of strings", Notes = "The command and its parameters, broken into a table by spaces" },
{ Name = "EntireCommand", Type = "string", Notes = "The entire command as a single string" },
},
Returns = [[
If the plugin returns true, the command will be blocked and none of the remaining hook handlers will
be called. If the plugin returns false, MCServer calls all the remaining hook handlers and finally
the command will be executed.
If the plugin returns false, MCServer calls all the remaining hook handlers and finally the command
will be executed. If the plugin returns true, the none of the remaining hook handlers will be called.
In this case the plugin can return a second value, specifying whether what the command result should
be set to, one of the {{cPluginManager#CommandResult|CommandResult}} constants. If not
provided, the value defaults to crBlocked.
]],
}, -- HOOK_EXECUTE_COMMAND
}

View File

@ -285,7 +285,7 @@ local function WriteHtmlHook(a_Hook, a_HookNav)
for _, param in ipairs(a_Hook.Params) do
f:write("<tr><td>", param.Name, "</td><td>", LinkifyString(param.Type, HookName), "</td><td>", LinkifyString(param.Notes, HookName), "</td></tr>\n");
end
f:write("</table>\n<p>" .. (a_Hook.Returns or "") .. "</p>\n\n");
f:write("</table>\n<p>" .. LinkifyString(a_Hook.Returns or "", HookName) .. "</p>\n\n");
f:write([[<hr /><h1>Code examples</h1><h2>Registering the callback</h2>]]);
f:write("<pre class=\"prettyprint lang-lua\">\n");
f:write([[cPluginManager:AddHook(cPluginManager.]] .. a_Hook.Name .. ", My" .. a_Hook.DefaultFnName .. [[);]]);

View File

@ -43,21 +43,21 @@ end
--- This is a generic command callback used for handling multicommands' parent commands
-- For example, if there are "/gal save" and "/gal load" commands, this callback handles the "/gal" command
-- It is used for both console and in-game commands; the console version has a_Player set to nil
local function MultiCommandHandler(a_Split, a_Player, a_CmdString, a_CmdInfo, a_Level)
local Verb = a_Split[a_Level + 1];
local function MultiCommandHandler(a_Split, a_Player, a_CmdString, a_CmdInfo, a_Level, a_EntireCommand)
local Verb = a_Split[a_Level + 1]
if (Verb == nil) then
-- No verb was specified. If there is a handler for the upper level command, call it:
if (a_CmdInfo.Handler ~= nil) then
return a_CmdInfo.Handler(a_Split, a_Player);
return a_CmdInfo.Handler(a_Split, a_Player, a_EntireCommand)
end
-- Let the player know they need to give a subcommand:
assert(type(a_CmdInfo.Subcommands) == "table", "Info.lua error: There is no handler for command \"" .. a_CmdString .. "\" and there are no subcommands defined at level " .. a_Level)
ListSubcommands(a_Player, a_CmdInfo.Subcommands, a_CmdString);
return true;
ListSubcommands(a_Player, a_CmdInfo.Subcommands, a_CmdString)
return true
end
-- A verb was specified, look it up in the subcommands table:
local Subcommand = a_CmdInfo.Subcommands[Verb];
local Subcommand = a_CmdInfo.Subcommands[Verb]
if (Subcommand == nil) then
if (a_Level > 1) then
-- This is a true subcommand, display the message and make MCS think the command was handled
@ -67,7 +67,7 @@ local function MultiCommandHandler(a_Split, a_Player, a_CmdString, a_CmdInfo, a_
else
a_Player:SendMessage("The " .. a_CmdString .. " command doesn't support verb " .. Verb)
end
return true;
return true
end
-- This is a top-level command, let MCS handle the unknown message
return false;
@ -76,22 +76,22 @@ local function MultiCommandHandler(a_Split, a_Player, a_CmdString, a_CmdInfo, a_
-- Check the permission:
if (a_Player ~= nil) then
if not(a_Player:HasPermission(Subcommand.Permission or "")) then
a_Player:SendMessage("You don't have permission to execute this command");
return true;
a_Player:SendMessage("You don't have permission to execute this command")
return true
end
end
-- If the handler is not valid, check the next sublevel:
if (Subcommand.Handler == nil) then
if (Subcommand.Subcommands == nil) then
LOG("Cannot find handler for command " .. a_CmdString .. " " .. Verb);
return false;
LOG("Cannot find handler for command " .. a_CmdString .. " " .. Verb)
return false
end
return MultiCommandHandler(a_Split, a_Player, a_CmdString .. " " .. Verb, Subcommand, a_Level + 1);
return MultiCommandHandler(a_Split, a_Player, a_CmdString .. " " .. Verb, Subcommand, a_Level + 1, a_EntireCommand)
end
-- Execute:
return Subcommand.Handler(a_Split, a_Player);
return Subcommand.Handler(a_Split, a_Player, a_EntireCommand)
end
@ -104,39 +104,39 @@ function RegisterPluginInfoCommands()
-- The a_Prefix param already contains the space after the previous command
-- a_Level is the depth of the subcommands being registered, with 1 being the top level command
local function RegisterSubcommands(a_Prefix, a_Subcommands, a_Level)
assert(a_Subcommands ~= nil);
assert(a_Subcommands ~= nil)
-- A table that will hold aliases to subcommands temporarily, during subcommand iteration
local AliasTable = {}
-- Iterate through the subcommands, register them, and accumulate aliases:
for cmd, info in pairs(a_Subcommands) do
local CmdName = a_Prefix .. cmd;
local Handler = info.Handler;
local CmdName = a_Prefix .. cmd
local Handler = info.Handler
-- Provide a special handler for multicommands:
if (info.Subcommands ~= nil) then
Handler = function(a_Split, a_Player)
return MultiCommandHandler(a_Split, a_Player, CmdName, info, a_Level);
Handler = function(a_Split, a_Player, a_EntireCommand)
return MultiCommandHandler(a_Split, a_Player, CmdName, info, a_Level, a_EntireCommand)
end
end
if (Handler == nil) then
LOGWARNING(g_PluginInfo.Name .. ": Invalid handler for command " .. CmdName .. ", command will not be registered.");
LOGWARNING(g_PluginInfo.Name .. ": Invalid handler for command " .. CmdName .. ", command will not be registered.")
else
local HelpString;
local HelpString
if (info.HelpString ~= nil) then
HelpString = " - " .. info.HelpString;
HelpString = " - " .. info.HelpString
else
HelpString = "";
HelpString = ""
end
cPluginManager.BindCommand(CmdName, info.Permission or "", Handler, HelpString);
cPluginManager.BindCommand(CmdName, info.Permission or "", Handler, HelpString)
-- Register all aliases for the command:
if (info.Alias ~= nil) then
if (type(info.Alias) == "string") then
info.Alias = {info.Alias};
info.Alias = {info.Alias}
end
for idx, alias in ipairs(info.Alias) do
cPluginManager.BindCommand(a_Prefix .. alias, info.Permission or "", Handler, HelpString);
cPluginManager.BindCommand(a_Prefix .. alias, info.Permission or "", Handler, HelpString)
-- Also copy the alias's info table as a separate subcommand,
-- so that MultiCommandHandler() handles it properly. Need to off-load into a separate table
-- than the one we're currently iterating and join after the iterating.
@ -147,7 +147,7 @@ function RegisterPluginInfoCommands()
-- Recursively register any subcommands:
if (info.Subcommands ~= nil) then
RegisterSubcommands(a_Prefix .. cmd .. " ", info.Subcommands, a_Level + 1);
RegisterSubcommands(a_Prefix .. cmd .. " ", info.Subcommands, a_Level + 1)
end
end -- for cmd, info - a_Subcommands[]
@ -159,7 +159,7 @@ function RegisterPluginInfoCommands()
end
-- Loop through all commands in the plugin info, register each:
RegisterSubcommands("", g_PluginInfo.Commands, 1);
RegisterSubcommands("", g_PluginInfo.Commands, 1)
end
@ -171,26 +171,26 @@ function RegisterPluginInfoConsoleCommands()
-- A sub-function that registers all subcommands of a single command, using the command's Subcommands table
-- The a_Prefix param already contains the space after the previous command
local function RegisterSubcommands(a_Prefix, a_Subcommands, a_Level)
assert(a_Subcommands ~= nil);
assert(a_Subcommands ~= nil)
for cmd, info in pairs(a_Subcommands) do
local CmdName = a_Prefix .. cmd;
local CmdName = a_Prefix .. cmd
local Handler = info.Handler
if (Handler == nil) then
Handler = function(a_Split)
return MultiCommandHandler(a_Split, nil, CmdName, info, a_Level);
Handler = function(a_Split, a_EntireCommand)
return MultiCommandHandler(a_Split, nil, CmdName, info, a_Level, a_EntireCommand)
end
end
cPluginManager.BindConsoleCommand(CmdName, Handler, info.HelpString or "");
cPluginManager.BindConsoleCommand(CmdName, Handler, info.HelpString or "")
-- Recursively register any subcommands:
if (info.Subcommands ~= nil) then
RegisterSubcommands(a_Prefix .. cmd .. " ", info.Subcommands, a_Level + 1);
RegisterSubcommands(a_Prefix .. cmd .. " ", info.Subcommands, a_Level + 1)
end
end
end
-- Loop through all commands in the plugin info, register each:
RegisterSubcommands("", g_PluginInfo.ConsoleCommands, 1);
RegisterSubcommands("", g_PluginInfo.ConsoleCommands, 1)
end

View File

@ -958,6 +958,18 @@ void cLuaState::GetStackValue(int a_StackPos, bool & a_ReturnedVal)
void cLuaState::GetStackValue(int a_StackPos, cPluginManager::CommandResult & a_Result)
{
if (lua_isnumber(m_LuaState, a_StackPos))
{
a_Result = static_cast<cPluginManager::CommandResult>(static_cast<int>((tolua_tonumber(m_LuaState, a_StackPos, a_Result))));
}
}
void cLuaState::GetStackValue(int a_StackPos, cRef & a_Ref)
{
a_Ref.RefStack(*this, a_StackPos);

View File

@ -32,6 +32,7 @@ extern "C"
#include "../Vector3.h"
#include "../Defines.h"
#include "PluginManager.h"
@ -57,7 +58,6 @@ class cPickup;
class cPlayer;
class cPlugin;
class cPluginLua;
class cPluginManager;
class cProjectileEntity;
class cRoot;
class cScoreboard;
@ -249,6 +249,7 @@ public:
void GetStackValue(int a_StackPos, AString & a_Value);
void GetStackValue(int a_StackPos, BLOCKTYPE & a_Value);
void GetStackValue(int a_StackPos, bool & a_Value);
void GetStackValue(int a_StackPos, cPluginManager::CommandResult & a_Result);
void GetStackValue(int a_StackPos, cRef & a_Ref);
void GetStackValue(int a_StackPos, double & a_Value);
void GetStackValue(int a_StackPos, float & a_ReturnedVal);

View File

@ -5,6 +5,7 @@
#undef TOLUA_TEMPLATE_BIND
#include <sstream>
#include <iomanip>
#include <array>
#include "tolua++/include/tolua++.h"
#include "polarssl/md5.h"
#include "polarssl/sha1.h"
@ -33,9 +34,10 @@
#include "../CompositeChat.h"
#include "../StringCompression.h"
#include "../Broadcaster.h"
#include "../CommandOutput.h"
#include <array>
// Better error reporting for Lua
@ -2000,6 +2002,40 @@ static int tolua_cPluginManager_CallPlugin(lua_State * tolua_S)
static int tolua_cPluginManager_ExecuteConsoleCommand(lua_State * tolua_S)
{
/*
Function signature:
cPluginManager:ExecuteConsoleCommand(EntireCommandStr) -> OutputString
*/
// Check params:
cLuaState L(tolua_S);
if (
!L.CheckParamUserTable(1, "cPluginManager") ||
!L.CheckParamString(2) ||
!L.CheckParamEnd(3)
)
{
return 0;
}
// Get the params:
AString Command;
L.GetStackValues(2, Command);
auto Split = StringSplit(Command, " ");
// Store the command output in a string:
cStringAccumCommandOutputCallback CommandOutput;
L.Push(cPluginManager::Get()->ExecuteConsoleCommand(Split, CommandOutput, Command));
L.Push(CommandOutput.GetAccum());
return 2;
}
static int tolua_cPluginManager_FindPlugins(lua_State * tolua_S)
{
// API function no longer exists:
@ -3906,6 +3942,7 @@ void ManualBindings::Bind(lua_State * tolua_S)
tolua_function(tolua_S, "BindConsoleCommand", tolua_cPluginManager_BindConsoleCommand);
tolua_function(tolua_S, "CallPlugin", tolua_cPluginManager_CallPlugin);
tolua_function(tolua_S, "DoWithPlugin", tolua_StaticDoWith<cPluginManager, cPlugin, &cPluginManager::DoWithPlugin>);
tolua_function(tolua_S, "ExecuteConsoleCommand", tolua_cPluginManager_ExecuteConsoleCommand);
tolua_function(tolua_S, "FindPlugins", tolua_cPluginManager_FindPlugins);
tolua_function(tolua_S, "ForEachCommand", tolua_cPluginManager_ForEachCommand);
tolua_function(tolua_S, "ForEachConsoleCommand", tolua_cPluginManager_ForEachConsoleCommand);

View File

@ -56,7 +56,7 @@ public:
virtual bool OnDisconnect (cClientHandle & a_Client, const AString & a_Reason) = 0;
virtual bool OnEntityAddEffect (cEntity & a_Entity, int a_EffectType, int a_EffectDurationTicks, int a_EffectIntensity, double a_DistanceModifier) = 0;
virtual bool OnEntityTeleport (cEntity & a_Entity, const Vector3d & a_OldPosition, const Vector3d & a_NewPosition) = 0;
virtual bool OnExecuteCommand (cPlayer * a_Player, const AStringVector & a_Split) = 0;
virtual bool OnExecuteCommand (cPlayer * a_Player, const AStringVector & a_Split, const AString & a_EntireCommand, cPluginManager::CommandResult & a_Result) = 0;
virtual bool OnExploded (cWorld & a_World, double a_ExplosionSize, bool a_CanCauseFire, double a_X, double a_Y, double a_Z, eExplosionSource a_Source, void * a_SourceData) = 0;
virtual bool OnExploding (cWorld & a_World, double & a_ExplosionSize, bool & a_CanCauseFire, double a_X, double a_Y, double a_Z, eExplosionSource a_Source, void * a_SourceData) = 0;
virtual bool OnHandshake (cClientHandle & a_Client, const AString & a_Username) = 0;

View File

@ -534,7 +534,7 @@ bool cPluginLua::OnEntityAddEffect(cEntity & a_Entity, int a_EffectType, int a_E
bool cPluginLua::OnExecuteCommand(cPlayer * a_Player, const AStringVector & a_Split)
bool cPluginLua::OnExecuteCommand(cPlayer * a_Player, const AStringVector & a_Split, const AString & a_EntireCommand, cPluginManager::CommandResult & a_Result)
{
cCSLock Lock(m_CriticalSection);
if (!m_LuaState.IsValid())
@ -545,7 +545,7 @@ bool cPluginLua::OnExecuteCommand(cPlayer * a_Player, const AStringVector & a_Sp
cLuaRefs & Refs = m_HookMap[cPluginManager::HOOK_EXECUTE_COMMAND];
for (cLuaRefs::iterator itr = Refs.begin(), end = Refs.end(); itr != end; ++itr)
{
m_LuaState.Call((int)(**itr), a_Player, a_Split, cLuaState::Return, res);
m_LuaState.Call((int)(**itr), a_Player, a_Split, a_EntireCommand, cLuaState::Return, res, a_Result);
if (res)
{
return true;

View File

@ -115,7 +115,7 @@ public:
virtual bool OnCraftingNoRecipe (cPlayer & a_Player, cCraftingGrid & a_Grid, cCraftingRecipe & a_Recipe) override;
virtual bool OnDisconnect (cClientHandle & a_Client, const AString & a_Reason) override;
virtual bool OnEntityAddEffect (cEntity & a_Entity, int a_EffectType, int a_EffectDurationTicks, int a_EffectIntensity, double a_DistanceModifier) override;
virtual bool OnExecuteCommand (cPlayer * a_Player, const AStringVector & a_Split) override;
virtual bool OnExecuteCommand (cPlayer * a_Player, const AStringVector & a_Split, const AString & a_EntireCommand, cPluginManager::CommandResult & a_Result) override;
virtual bool OnExploded (cWorld & a_World, double a_ExplosionSize, bool a_CanCauseFire, double a_X, double a_Y, double a_Z, eExplosionSource a_Source, void * a_SourceData) override;
virtual bool OnExploding (cWorld & a_World, double & a_ExplosionSize, bool & a_CanCauseFire, double a_X, double a_Y, double a_Z, eExplosionSource a_Source, void * a_SourceData) override;
virtual bool OnHandshake (cClientHandle & a_Client, const AString & a_Username) override;

View File

@ -525,14 +525,14 @@ bool cPluginManager::CallHookEntityTeleport(cEntity & a_Entity, const Vector3d &
bool cPluginManager::CallHookExecuteCommand(cPlayer * a_Player, const AStringVector & a_Split)
bool cPluginManager::CallHookExecuteCommand(cPlayer * a_Player, const AStringVector & a_Split, const AString & a_EntireCommand, CommandResult & a_Result)
{
FIND_HOOK(HOOK_EXECUTE_COMMAND);
VERIFY_HOOK;
for (PluginList::iterator itr = Plugins->second.begin(); itr != Plugins->second.end(); ++itr)
{
if ((*itr)->OnExecuteCommand(a_Player, a_Split))
if ((*itr)->OnExecuteCommand(a_Player, a_Split, a_EntireCommand, a_Result))
{
return true;
}
@ -1449,10 +1449,14 @@ cPluginManager::CommandResult cPluginManager::HandleCommand(cPlayer & a_Player,
}
// Ask plugins first if a command is okay to execute the command:
if (CallHookExecuteCommand(&a_Player, Split))
CommandResult Result = crBlocked;
if (CallHookExecuteCommand(&a_Player, Split, a_Command, Result))
{
if (Result == crBlocked)
{
LOGINFO("Player %s tried executing command \"%s\" that was stopped by the HOOK_EXECUTE_COMMAND hook", a_Player.GetName().c_str(), Split[0].c_str());
return crBlocked;
}
return Result;
}
if (
@ -1750,7 +1754,10 @@ bool cPluginManager::ExecuteConsoleCommand(const AStringVector & a_Split, cComma
if (cmd == m_ConsoleCommands.end())
{
// Command not found
return false;
// Still notify the plugins (so that plugins such as Aliases can intercept unknown commands).
CommandResult res = crBlocked;
CallHookExecuteCommand(nullptr, a_Split, a_Command, res);
return (res == crExecuted);
}
if (cmd->second.m_Plugin == nullptr)
@ -1760,10 +1767,10 @@ bool cPluginManager::ExecuteConsoleCommand(const AStringVector & a_Split, cComma
}
// Ask plugins first if a command is okay to execute the console command:
if (CallHookExecuteCommand(nullptr, a_Split))
CommandResult res = crBlocked;
if (CallHookExecuteCommand(nullptr, a_Split, a_Command, res))
{
a_Output.Out("Command \"%s\" was stopped by the HOOK_EXECUTE_COMMAND hook", a_Split[0].c_str());
return false;
return (res == crExecuted);
}
return cmd->second.m_Plugin->HandleConsoleCommand(a_Split, a_Output, a_Command);

View File

@ -200,7 +200,7 @@ public:
bool CallHookDisconnect (cClientHandle & a_Client, const AString & a_Reason);
bool CallHookEntityAddEffect (cEntity & a_Entity, int a_EffectType, int a_EffectDurationTicks, int a_EffectIntensity, double a_DistanceModifier);
bool CallHookEntityTeleport (cEntity & a_Entity, const Vector3d & a_OldPosition, const Vector3d & a_NewPosition);
bool CallHookExecuteCommand (cPlayer * a_Player, const AStringVector & a_Split); // If a_Player == nullptr, it is a console cmd
bool CallHookExecuteCommand (cPlayer * a_Player, const AStringVector & a_Split, const AString & a_EntireCommand, CommandResult & a_Result); // If a_Player == nullptr, it is a console cmd
bool CallHookExploded (cWorld & a_World, double a_ExplosionSize, bool a_CanCauseFire, double a_X, double a_Y, double a_Z, eExplosionSource a_Source, void * a_SourceData);
bool CallHookExploding (cWorld & a_World, double & a_ExplosionSize, bool & a_CanCauseFire, double a_X, double a_Y, double a_Z, eExplosionSource a_Source, void * a_SourceData);
bool CallHookHandshake (cClientHandle & a_ClientHandle, const AString & a_Username);
@ -299,7 +299,9 @@ public:
/** Returns true if the console command is in the command map */
bool IsConsoleCommandBound(const AString & a_Command); // tolua_export
/** Executes the command split into a_Split, as if it was given on the console. Returns true if executed. Output is sent to the a_Output callback */
/** Executes the command split into a_Split, as if it was given on the console.
Returns true if executed. Output is sent to the a_Output callback
Exported in ManualBindings.cpp with a different signature. */
bool ExecuteConsoleCommand(const AStringVector & a_Split, cCommandOutputCallback & a_Output, const AString & a_Command);
/** Appends all commands beginning with a_Text (case-insensitive) into a_Results.

View File

@ -29,29 +29,32 @@ void cCommandOutputCallback::Out(const char * a_Fmt, ...)
////////////////////////////////////////////////////////////////////////////////
// cLogCommandOutputCallback:
// cStringAccumCommandOutputCallback:
void cLogCommandOutputCallback::Out(const AString & a_Text)
void cStringAccumCommandOutputCallback::Out(const AString & a_Text)
{
m_Buffer.append(a_Text);
m_Accum.append(a_Text);
}
////////////////////////////////////////////////////////////////////////////////
// cLogCommandOutputCallback:
void cLogCommandOutputCallback::Finished(void)
{
// Log each line separately:
size_t len = m_Buffer.length();
size_t len = m_Accum.length();
size_t last = 0;
for (size_t i = 0; i < len; i++)
{
switch (m_Buffer[i])
switch (m_Accum[i])
{
case '\n':
{
LOG("%s", m_Buffer.substr(last, i - last).c_str());
LOG("%s", m_Accum.substr(last, i - last).c_str());
last = i + 1;
break;
}
@ -59,11 +62,11 @@ void cLogCommandOutputCallback::Finished(void)
} // for i - m_Buffer[]
if (last < len)
{
LOG("%s", m_Buffer.substr(last).c_str());
LOG("%s", m_Accum.substr(last).c_str());
}
// Clear the buffer for the next command output:
m_Buffer.clear();
m_Accum.clear();
}

View File

@ -47,18 +47,36 @@ class cNullCommandOutputCallback :
/// Sends all command output to a log, line by line, when the command finishes processing
class cLogCommandOutputCallback :
/** Accumulates all command output into a string. */
class cStringAccumCommandOutputCallback:
public cCommandOutputCallback
{
typedef cCommandOutputCallback super;
public:
// cCommandOutputCallback overrides:
virtual void Out(const AString & a_Text) override;
virtual void Finished(void) override;
virtual void Finished(void) override {}
/** Returns the accumulated command output in a string. */
const AString & GetAccum(void) const { return m_Accum; }
protected:
/// Output is stored here until the command finishes processing
AString m_Buffer;
/** Output is stored here until the command finishes processing */
AString m_Accum;
} ;
/// Sends all command output to a log, line by line, when the command finishes processing
class cLogCommandOutputCallback :
public cStringAccumCommandOutputCallback
{
public:
// cStringAccumCommandOutputCallback overrides:
virtual void Finished(void) override;
} ;

View File

@ -150,6 +150,13 @@ AStringVector StringSplitWithQuotes(const AString & str, const AString & delim)
while ((cutAt = str.find_first_of(delim, Prev)) != str.npos)
{
if (cutAt == Prev)
{
// Empty string due to multiple whitespace / whitespace at the beginning of the input
// Just skip it
Prev = Prev + 1;
continue;
}
AString current = str.substr(Prev, cutAt - Prev);
if ((current.front() == '"') || (current.front() == '\''))
{