VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
CommandRouter.cpp
Go to the documentation of this file.
2
4
8#include <algorithm>
9#include <cctype>
10#include <utility>
11
12namespace VoltMod
13{
14
15/** The line a failed handler replies with: its own localized line, else its key, else the log text. */
16static std::string FailureLine(const Error& error, const Translations& translations, int slot)
17{
18 if (!error.Text.empty())
19 {
20 return error.Text;
21 }
22 if (!error.Key.empty())
23 {
24 return translations.Get(error.Key, slot);
25 }
26 return error.Detail;
27}
28
29/** Translation key for an argument kind's usage placeholder. */
30static std::string UsagePlaceholderKey(ArgKind kind)
31{
32 std::string key = "cmd.usage.";
33 const std::string_view name = Name(kind);
34 if (!name.empty())
35 {
36 key += static_cast<char>(std::tolower(static_cast<unsigned char>(name.front())));
37 key += name.substr(1);
38 }
39 return key;
40}
41
43{
44 const std::string name = Strings::ToLower(def.Name);
45
46 if (name.empty())
47 {
48 Log::Error("A command was registered with no name - ignoring it.");
49 return nullptr;
50 }
51 if (_commands.contains(name) || _aliases.contains(name))
52 {
53 Log::Error("Command '{}' is already registered - ignoring the second registration.", def.Name);
54 return nullptr;
55 }
56 if (_host && !_host->RegisterCommand(name))
57 {
58 Log::Error("Command '{}' is held by another plugin - ignoring this registration.", def.Name);
59 return nullptr;
60 }
61
62 // Index aliases once so lookup is deterministic.
63 for (const std::string& alias : def.Aliases)
64 {
65 std::string key = Strings::ToLower(alias);
66 if (key.empty() || key == name)
67 {
68 continue;
69 }
70 if (_commands.contains(key))
71 {
72 Log::Error("Command '{}' asks for alias '{}', which is already a command name - skipping the alias.",
73 def.Name, alias);
74 continue;
75 }
76 if (auto it = _aliases.find(key); it != _aliases.end())
77 {
78 Log::Error("Command '{}' asks for alias '{}', already taken by '{}' - skipping the alias.", def.Name, alias,
79 it->second);
80 continue;
81 }
82 if (_host && !_host->RegisterCommand(key))
83 {
84 Log::Error("Command '{}' asks for alias '{}', which another plugin owns - skipping the alias.", def.Name,
85 alias);
86 continue;
87 }
88 _aliases.emplace(std::move(key), name);
89 }
90
91 CommandDefinition& stored = _commands[name];
92 stored = std::move(def);
93 return &stored;
94}
95
97{
98 _commands.clear();
99 _aliases.clear();
100}
101
102const CommandDefinition* CommandRouter::Find(std::string_view name) const
103{
104 const std::string key = Strings::ToLower(std::string(name));
105
106 if (auto it = _commands.find(key); it != _commands.end())
107 {
108 return &it->second;
109 }
110
111 if (auto alias = _aliases.find(key); alias != _aliases.end())
112 {
113 if (auto it = _commands.find(alias->second); it != _commands.end())
114 {
115 return &it->second;
116 }
117 }
118
119 return nullptr;
120}
121
122bool CommandRouter::IsForeign(std::string_view name) const
123{
124 return _host && !Find(name) && _host->IsCommandRegistered(Strings::ToLower(std::string(name)));
125}
126
127size_t CommandRouter::RequiredArgs(const CommandDefinition& def)
128{
129 return static_cast<size_t>(
130 std::count_if(def.Args.begin(), def.Args.end(), [](const ArgDesc& arg) { return !arg.Optional; }));
131}
132
133bool CommandRouter::HasRest(const CommandDefinition& def)
134{
135 return std::any_of(def.Args.begin(), def.Args.end(), [](const ArgDesc& arg) { return arg.Kind == ArgKind::Rest; });
136}
137
138Tokens CommandRouter::UsageTokens(const CommandDefinition& def, int slot, Origin origin) const
139{
140 // Only chat usage includes a prefix.
141 std::string prefix = origin == Origin::Chat ? std::string(CommandSyntax::ChatPrefix) : std::string{};
142
143 std::string args;
144 for (const ArgDesc& arg : def.Args)
145 {
146 if (!args.empty())
147 {
148 args += ' ';
149 }
150 const std::string placeholder = _translations.Get(UsagePlaceholderKey(arg.Kind), slot);
151 args += arg.Optional ? '[' : '<';
152 args += placeholder;
153 args += arg.Optional ? ']' : '>';
154 }
155
156 std::string line = prefix + def.Name;
157 if (!args.empty())
158 {
159 line += ' ' + args;
160 }
161
162 return {{"prefix", prefix}, {"command", def.Name}, {"args", args}, {"usage", line}};
163}
164
166{
167 if (!def.UsageKey.empty())
168 {
169 return _translations.Get(def.UsageKey, slot);
170 }
171 return _translations.Get("cmd.usage", slot, UsageTokens(def, slot, origin));
172}
173
174void CommandRouter::Dispatch(const CommandDefinition& def, Player* caller, std::span<const std::string> tokens,
175 Origin origin, ArgBinder& binder, const std::function<void(const std::string&)>& say) const
176{
177 // Slot -1 selects the server language for console calls.
178 const int slot = caller ? caller->Slot() : -1;
179 const auto reply = [&say](const std::string& line) {
180 if (!line.empty())
181 {
182 say(line);
183 }
184 };
185
186 // Apply the permission gate to player callers; the console is the server.
187 if (caller && !def.PermissionName.empty())
188 {
189 auto authorized = _policy.Authorize(caller->Ref(), std::nullopt, def.PermissionName);
190 if (!authorized)
191 {
192 reply(_translations.Get(authorized.error().Key, slot));
193 return;
194 }
195 }
196
197 // Reject extra tokens unless the final argument consumes the remainder.
198 if (!HasRest(def) && tokens.size() > def.Args.size())
199 {
200 reply(_translations.Get("cmd.tooManyArgs", slot, UsageTokens(def, slot, origin)));
201 return;
202 }
203
204 if (tokens.size() < RequiredArgs(def))
205 {
207 return;
208 }
209
211 if (!bound)
212 {
213 reply(_translations.Get(bound.error().Key, slot, bound.error().Vars));
214 return;
215 }
216
217 if (!def.Invoke)
218 {
219 return;
220 }
221
222 const Caller who{.Player = caller, .Slot = slot, .Translations = _translations, .Send = say};
223 auto result = def.Invoke(who, *bound);
224 reply(result ? result->Text : FailureLine(result.error(), _translations, slot));
225}
226
227} // namespace VoltMod
How a chat line is spelled: the prefix, and splitting one into tokens.
Enumerator names without a hand-written switch.
The one thing command dispatch needs a running server for.
void Dispatch(const CommandDefinition &def, Player *caller, std::span< const std::string > tokens, Origin origin, ArgBinder &binder, const std::function< void(const std::string &)> &say) const
const CommandDefinition * Find(std::string_view name) const
const CommandDefinition * Add(CommandDefinition def)
std::string Usage(const CommandDefinition &def, int slot, Origin origin) const
bool IsForeign(std::string_view name) const
One connected player, owned by PlayerManager for the length of the connection.
Definition Player.hpp:23
int Slot() const noexcept
Definition Player.hpp:40
Result< Authorized > Authorize(PlayerRef caller, std::optional< PlayerRef > target, std::string_view permission) const
The single gate. Commands, target resolution and menu rows call exactly this.
Definition Policy.cpp:52
static std::string ToLower(std::string_view str)
Definition Strings.cpp:100
Localization system. Loads one JSON file per language; nested objects flatten into dotted keys (categ...
std::string Get(std::string_view key) const
constexpr std::string_view ChatPrefix
void Error(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:97
std::expected< std::vector< BoundArg >, ArgError > BindArgs(const CommandDefinition &def, std::span< const std::string > tokens, Player *caller, ArgBinder &binder)
static std::string FailureLine(const Error &error, const Translations &translations, int slot)
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static std::string UsagePlaceholderKey(ArgKind kind)
ArgKind
Which binder runs for one declared argument.
Definition Args.hpp:30
std::map< std::string, std::string > Tokens
Definition Strings.hpp:14
Who invoked the command, and how to answer them.
VoltMod::Player * Player
One command as the builder assembled it.
One failure: a code to branch on, text for the log, and an optional translation key.
Definition Result.hpp:42
virtual bool RegisterCommand(std::string_view name)=0
virtual bool IsCommandRegistered(std::string_view name) const =0