diff --git a/docs/control-socket.md b/docs/control-socket.md index 03d54635c3..9d4441b59b 100644 --- a/docs/control-socket.md +++ b/docs/control-socket.md @@ -76,6 +76,7 @@ Error codes: `bad_request`, `unknown_command`, `wrong_scene`, `busy`, | `hover-ui` (`x`, `y`) | move the pointer to a window pixel with no button, as physical motion: RmlUi hover and the game's own pointer (native hover, tooltips) both follow it | | `drag-ui` (`x`, `y`, `to_x`, `to_y`, `button`) | press at one window pixel, move to the other over eight frames with the button held, release there | | `type` (`text`, `enter`) | deliver committed UTF-8 to the focused field; optional boolean `enter` submits on a later frame | +| `console` (`text`) | run one `$` developer command (`$fps 60`, …) as if typed into the chat box, without opening it, so nothing typed shows in the frame; `bad_request` when the text is not a `$` command the client knows, or its arguments are missing or malformed | | `ui` | the open windows by name — `inventory`, `inventory_extension`, `character`, `trade`, `storage`, `storage_extension`, `mix`, `npc_shop`, `lucky_item`, `chat_input`, `party`, `command`, `my_shop`, `purchase_shop`, `npc_quest` (the quest dialog of Sebina, Marlon and Devin), and `message_box` while a dialog waits for Enter or Esc — and the window pixels of the named elements that are shown: `trade.confirm`, `trade.zen`, `inventory.repair`, `inventory.my_shop`, `npc_shop.repair`, `npc_shop.repair_all`, `my_shop.title`, `my_shop.open`, `my_shop.close`, `command.trade`, `command.purchase`, `command.party`, `npc_quest.answer.0` and on (the rows of the quest dialog's answers), `npc_quest.complete`, `npc_quest.close`, `character.stat.strength` (`agility`, `vitality`, `energy`, `command`: the character window's "+" buttons, while there are level-up points) | | `slot-pixel` (`grid`, `slot`) | the window pixel of a slot's square: `inventory` and `equipment` (the slot numbers `state` reports), `trade`, `trade_partner`, `storage`, `mix`, `npc_shop`, and the personal shops `my_shop` and `purchase_shop` (slots 204 and up, as `state` and the server number them); `not_open` while that window is closed, `bad_request` for a slot the grid does not have | | `login` (`account`, `password`, `server`) | server selection, credentials, character list | diff --git a/src/source/App/Control/ControlCommands.h b/src/source/App/Control/ControlCommands.h index 081ecda113..043a26f407 100644 --- a/src/source/App/Control/ControlCommands.h +++ b/src/source/App/Control/ControlCommands.h @@ -35,6 +35,9 @@ std::string ClickUi(const Request& request, std::unique_ptr& act); std::string HoverUi(const Request& request, std::unique_ptr& act); std::string DragUi(const Request& request, std::unique_ptr& act); std::string Type(const Request& request, std::unique_ptr& act); +// Runs one `$` developer command as if typed into the chat box, without +// opening it, so nothing typed shows up in the frame. +std::string Console(const Request& request, std::unique_ptr& act); // Ui family: where the open item windows are. std::string Ui(const Request& request, std::unique_ptr& act); diff --git a/src/source/App/Control/ControlCommandsUi.cpp b/src/source/App/Control/ControlCommandsUi.cpp index 4f350d9549..6ccfc4124f 100644 --- a/src/source/App/Control/ControlCommandsUi.cpp +++ b/src/source/App/Control/ControlCommandsUi.cpp @@ -1,6 +1,8 @@ #include "stdafx.h" #include "App/Control/ControlCommands.h" +#include "Core/Text/Utf8.h" +#include "Core/Utilities/Log/muConsoleDebug.h" #include "UI/Core/WindowAccess.h" #include "UI/Core/WindowSystem.h" #include "UI/Dialogs/GenericConfirmDialog.h" @@ -13,6 +15,7 @@ #include "json.hpp" +#include #include #include #include @@ -290,4 +293,32 @@ std::string SlotPixel(const Request& request, std::unique_ptr&) } return SquarePixel(request, **square, slot); } + +std::string Console(const Request& request, std::unique_ptr& /*act*/) +{ + std::string text; + if (!request.GetString("text", text) || text.empty() || text.front() != '$') + { + return EncodeError(request.EncodedId(), ErrorCode::BadRequest, "`text` is a `$` command, e.g. \"$fps 60\""); + } + bool known = false; + try + { + known = g_ConsoleDebug->CheckCommand(Core::Text::FromUtf8(text)); + } + catch (const std::exception&) + { + // A command that parses its arguments with std::stof/substr throws on a missing or + // malformed one ("$fps" without a number); refuse it instead of ending the client. + return EncodeError(request.EncodedId(), ErrorCode::BadRequest, + "`" + text + "` is missing an argument or has a malformed one"); + } + if (!known) + { + return EncodeError(request.EncodedId(), ErrorCode::BadRequest, "the client has no `$` command `" + text + "`"); + } + nlohmann::json result; + result["text"] = text; + return EncodeResult(request.EncodedId(), result.dump()); +} } // namespace App::Control::Commands diff --git a/src/source/App/Control/ControlDispatcher.cpp b/src/source/App/Control/ControlDispatcher.cpp index 52585f84ba..8940c37435 100644 --- a/src/source/App/Control/ControlDispatcher.cpp +++ b/src/source/App/Control/ControlDispatcher.cpp @@ -39,6 +39,7 @@ const std::vector& CommandTable() {"screenshot", SceneRequirement::Any, &Commands::Screenshot}, {"hotkey", SceneRequirement::Any, &Commands::Hotkey}, {"type", SceneRequirement::Any, &Commands::Type}, + {"console", SceneRequirement::Any, &Commands::Console}, {"click-ui", SceneRequirement::Any, &Commands::ClickUi}, {"hover-ui", SceneRequirement::Any, &Commands::HoverUi}, {"drag-ui", SceneRequirement::Any, &Commands::DragUi}, diff --git a/src/source/App/Control/ControlProtocol.cpp b/src/source/App/Control/ControlProtocol.cpp index 6c94d85b39..cc673c70c6 100644 --- a/src/source/App/Control/ControlProtocol.cpp +++ b/src/source/App/Control/ControlProtocol.cpp @@ -193,10 +193,10 @@ const std::vector& CommandNames() // the whole client into that test. The dispatcher checks itself against // this list instead (ControlDispatcher.cpp, CommandTable). static const std::vector names = { - "ping", "scene", "state", "nearby", "events", "wait-for", "screenshot", "login", "select-char", "logout", - "quit", "move", "warp", "teleport", "attack", "skill", "pickup", "use", "equip", "say", - "whisper", "party", "halt", "hotkey", "click-ui", "drag-ui", "hover-ui", "type", "ui", "slot-pixel", - "trade", + "ping", "scene", "state", "nearby", "events", "wait-for", "screenshot", "login", + "select-char", "logout", "quit", "move", "warp", "teleport", "attack", "skill", + "pickup", "use", "equip", "say", "whisper", "party", "halt", "hotkey", + "click-ui", "drag-ui", "hover-ui", "type", "ui", "slot-pixel", "trade", "console", }; return names; } diff --git a/tests/network/test_control_protocol.cpp b/tests/network/test_control_protocol.cpp index 9854be91be..88d1df843a 100644 --- a/tests/network/test_control_protocol.cpp +++ b/tests/network/test_control_protocol.cpp @@ -83,10 +83,10 @@ TEST_CASE("Control protocol treats a blank line as no request [network][control- TEST_CASE("Control protocol serves the documented command vocabulary [network][control-protocol]") { const std::vector expected = { - "ping", "scene", "state", "nearby", "events", "wait-for", "screenshot", "login", "select-char", "logout", - "quit", "move", "warp", "teleport", "attack", "skill", "pickup", "use", "equip", "say", - "whisper", "party", "halt", "hotkey", "click-ui", "drag-ui", "hover-ui", "type", "ui", "slot-pixel", - "trade", + "ping", "scene", "state", "nearby", "events", "wait-for", "screenshot", "login", + "select-char", "logout", "quit", "move", "warp", "teleport", "attack", "skill", + "pickup", "use", "equip", "say", "whisper", "party", "halt", "hotkey", + "click-ui", "drag-ui", "hover-ui", "type", "ui", "slot-pixel", "trade", "console", }; for (const std::string& command : expected)