VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
CommandBuilder.hpp
Go to the documentation of this file.
1#pragma once
2
8#include <cstdint>
9#include <functional>
10#include <span>
11#include <string>
12#include <string_view>
13#include <type_traits>
14#include <utility>
15#include <vector>
16
17namespace VoltMod
18{
19
20/**
21 * @brief What a command handler answers with.
22 *
23 * An empty @ref Text sends no reply.
24 */
25struct Reply
26{
27 std::string Text;
28
29 /** Handled, with nothing more to say. */
30 static Reply Silent() { return {}; }
31};
32
33/**
34 * @brief Who invoked the command, and how to answer them.
35 *
36 * A handler's first parameter. @ref Player is null and @ref Slot is -1 for console commands;
37 * reply helpers use the caller's language.
38 */
39struct Caller
40{
41 /** The player who typed the command, or null for console commands. */
43 /** @ref Player's slot, or -1 for the console - which is also the server-language slot. */
44 int Slot = -1;
45 /** The plugin's translations. `Get(key)` with no slot resolves the server language. */
47 /** Sends a reply to chat or the console for the duration of the handler call. */
48 std::function<void(const std::string&)> Send;
49
50 /** True when the server ran the command: the server console, rcon or a cfg file. */
51 bool IsServer() const { return Player == nullptr; }
52
53 /** Return localized @p key with `{token}` substitution. */
54 std::string Text(std::string_view key, Tokens tokens = {}) const;
55
56 /** Succeed, replying with @p key localized for this caller. */
57 Result<Reply> Ok(std::string_view key, Tokens tokens = {}) const;
58
59 /**
60 * Fail, replying with @p key localized for this caller.
61 *
62 * The error keeps @p key in `Key` and the localized line in `Text`. Errors from other
63 * services carry only a key and are localized by dispatch.
64 */
65 Result<Reply> Fail(std::string_view key, Tokens tokens = {}) const;
66
67 /** Send one extra localized line; finish a multi-line reply with `Ok` or `Reply::Silent`. */
68 void Say(std::string_view key, Tokens tokens = {}) const;
69
70 /** Send @p line verbatim: an already-formatted row that has no translation of its own. */
71 void SayRaw(std::string_view line) const;
72};
73
74/** Who may run a command, and so where it can be typed. */
76{
77 /** Players, from chat. The default. */
78 Players,
79 /** Players from chat or their own console, and the server from its console, rcon and cfg files. */
81 /** The server only, from its console, rcon and cfg files. Players are ignored. */
83};
84
85/**
86 * @brief One command as the builder assembled it.
87 *
88 * The engine-free command definition built by @ref CommandBuilder and installed by
89 * @ref CommandManager.
90 */
92{
93 std::string Name;
94 std::vector<std::string> Aliases;
95 std::string Description;
96 /** Empty means no permission check. Never checked when the server runs the command. */
97 std::string PermissionName;
98 /** Translation key for the whole usage line; empty derives one from @ref Args. */
99 std::string UsageKey;
101 std::vector<ArgDesc> Args;
102 /** The type-erased handler: unpacks @p bound back into the parameter list it was written
103 * with. Built by @ref CommandBuilder::Run. */
104 std::function<Result<Reply>(const Caller&, std::span<const BoundArg>)> Invoke;
105};
106
107/** Marker holding the parameter types recovered from a handler's signature. */
108template <class... A>
110{};
111
112/** A callable with exactly one `operator()` to name: a plain lambda or functor, but not a generic
113 * one (`auto` parameters) and not one carrying overloads. */
114template <class F>
115concept HasOneCallOperator = requires { &std::remove_reference_t<F>::operator(); };
116
117/**
118 * @brief The parameter list of a handler, after the leading @ref Caller.
119 *
120 * @ref CommandBuilder::Run deduces `A...` from the callable, so the signature is the argument
121 * specification. The specializations below cover the shapes a handler is written in - a lambda, a
122 * functor, a function - and anything else lands on the primary and says so.
123 */
124template <class F>
126{
127 static_assert(false,
128 "A command handler takes (Caller, Args::...) and returns Result<Reply> or nothing. A generic lambda "
129 "cannot be one: its parameter list is the argument specification, so the types have to be "
130 "written out.");
131};
132
133/** A lambda or functor, through the one `operator()` it has. */
134template <HasOneCallOperator F>
135struct CommandHandlerArgs<F> : CommandHandlerArgs<decltype(&std::remove_reference_t<F>::operator())>
136{};
137
138/** The one place the list is named; every shape below reduces to this. */
139template <class R, class... A>
141{
142 using List = CommandArgList<A...>;
143};
144
145/** @{ A lambda's `operator()`, and a function pointer. `Run` decays the callable first, so a
146 * function passed by name arrives here as a pointer. */
147template <class C, class R, class... A>
148struct CommandHandlerArgs<R (C::*)(Caller, A...) const> : CommandHandlerArgs<R(Caller, A...)>
149{};
150
151template <class C, class R, class... A>
152struct CommandHandlerArgs<R (C::*)(Caller, A...) const noexcept> : CommandHandlerArgs<R(Caller, A...)>
153{};
154
155template <class C, class R, class... A>
156struct CommandHandlerArgs<R (C::*)(Caller, A...)> : CommandHandlerArgs<R(Caller, A...)>
157{};
158
159template <class C, class R, class... A>
160struct CommandHandlerArgs<R (C::*)(Caller, A...) noexcept> : CommandHandlerArgs<R(Caller, A...)>
161{};
162
163template <class R, class... A>
164struct CommandHandlerArgs<R (*)(Caller, A...)> : CommandHandlerArgs<R(Caller, A...)>
165{};
166
167template <class R, class... A>
169{};
170/** @} */
171
172/**
173 * @brief Fluent command registration, returned by @ref CommandManager::Add.
174 *
175 * @code
176 * commands.Add("ban")
177 * .Describe("Ban a player.")
178 * .Alias("b")
179 * .Permission("admin.ban")
180 * .Run([&app](Caller c, Args::Target t, Args::Duration d, Args::Opt<Args::Rest> why) {
181 * std::string name = t->Name(); // capture first: a ban drops the target
182 * std::string reason = why.ValueOr(c.Translations.Get("reason.bannedByAdmin"));
183 * if (!app.Ban(*c.Player, *t.Value, reason, d.Value))
184 * return c.Fail("cmd.banFailed");
185 * return c.Ok("cmd.banSuccess", {{"name", name}});
186 * });
187 * @endcode
188 *
189 * @ref Run installs the command. The manager owns it for the plugin load cycle.
190 */
192{
193public:
194 /** How @ref Run hands the finished definition back to the manager that made this builder. */
195 using Installer = std::function<void(CommandDefinition)>;
196
197 CommandBuilder(Installer install, std::string_view name) : _install(std::move(install))
198 {
199 _def.Name = std::string(name);
200 }
201
202 /** Another name for the same command. Collisions are refused and logged at registration. */
203 CommandBuilder& Alias(std::string_view alias)
204 {
205 _def.Aliases.emplace_back(alias);
206 return *this;
207 }
208
209 /** Operator-facing description; also the console command's help text. */
210 CommandBuilder& Describe(std::string_view text)
211 {
212 _def.Description = std::string(text);
213 return *this;
214 }
215
216 /** Gate on `Policy::Authorize`, which denies while no plugin publishes `IPermissions`. */
218 {
219 _def.PermissionName = std::string(permission);
220 return *this;
221 }
222
223 /** Also register a tier1 ConCommand, so players can type it in their console and the server
224 * can run it from its console, rcon, cfg files and `ExecuteServerCommand`. */
226 {
228 return *this;
229 }
230
231 /** Register only the ConCommand, for an operator command players cannot run. */
233 {
235 return *this;
236 }
237
238 /** Translation key for the whole usage line, replacing the one derived from the argument
239 * types. */
240 CommandBuilder& UsageKey(std::string_view key)
241 {
242 _def.UsageKey = std::string(key);
243 return *this;
244 }
245
246 /**
247 * Install the command. The handler takes a @ref Caller and one `Args::` value per argument;
248 * that list defines arity, parsing, and usage. A handler that returns nothing succeeds silently.
249 *
250 * The command is unregistered when @ref CommandManager is destroyed.
251 */
252 template <class F>
253 void Run(F&& handler)
254 {
255 Bind(std::forward<F>(handler), typename CommandHandlerArgs<std::decay_t<F>>::List{});
256 }
257
258private:
259 template <class F, class... A>
260 void Bind(F&& handler, CommandArgList<A...>)
261 {
262 static_assert((CommandArg<A> && ...), "Every command handler parameter after Caller must be an Args:: type.");
263 static_assert(OptionalsTrail<A...>(), "Only trailing command arguments may be Args::Opt.");
264 static_assert(RestIsLast<A...>(), "Args::Rest must be the last command argument.");
265
266 std::function<Result<Reply>(Caller, A...)> fn;
267 if constexpr (std::is_void_v<std::invoke_result_t<std::decay_t<F>&, Caller, A...>>)
268 {
269 fn = [h = std::forward<F>(handler)](Caller c, A... args) mutable -> Result<Reply> {
270 h(c, std::move(args)...);
271 return Reply::Silent();
272 };
273 }
274 else
275 {
276 fn = std::forward<F>(handler);
277 }
278
279 _def.Args = DescribeArgs<A...>();
280 _def.Invoke = [fn = std::move(fn)](const Caller& caller, std::span<const BoundArg> bound) {
281 return Unpack(fn, caller, bound, std::index_sequence_for<A...>{});
282 };
283 _install(std::move(_def));
284 }
285
286 /** The trampoline: one `std::get` per declared argument, in descriptor order. */
287 template <class... A, std::size_t... I>
288 static Result<Reply> Unpack(const std::function<Result<Reply>(Caller, A...)>& fn, const Caller& caller,
289 std::span<const BoundArg> bound, std::index_sequence<I...>)
290 {
291 return fn(caller, ArgUnpack<A>::From(bound[I])...);
292 }
293
294 Installer _install;
295 CommandDefinition _def;
296};
297
298} // namespace VoltMod
Fluent command registration, returned by CommandManager::Add.
std::function< void(CommandDefinition)> Installer
CommandBuilder & Anywhere()
CommandBuilder & Alias(std::string_view alias)
CommandBuilder & UsageKey(std::string_view key)
CommandBuilder & Describe(std::string_view text)
CommandBuilder(Installer install, std::string_view name)
CommandBuilder & Permission(std::string_view permission)
CommandBuilder & ServerOnly()
One connected player, owned by PlayerManager for the length of the connection.
Definition Player.hpp:23
Localization system. Loads one JSON file per language; nested objects flatten into dotted keys (categ...
@ Text
Not selectable; a caption or a summary line.
consteval bool RestIsLast()
Definition Args.hpp:260
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
std::vector< ArgDesc > DescribeArgs()
Definition Args.hpp:280
std::map< std::string, std::string > Tokens
Definition Strings.hpp:14
consteval bool OptionalsTrail()
Definition Args.hpp:239
std::expected< T, Error > Result
Definition Result.hpp:64
static T From(const BoundArg &bound)
Definition Args.hpp:289
Who invoked the command, and how to answer them.
void SayRaw(std::string_view line) const
Result< Reply > Ok(std::string_view key, Tokens tokens={}) const
std::function< void(const std::string &)> Send
VoltMod::Translations & Translations
Result< Reply > Fail(std::string_view key, Tokens tokens={}) const
bool IsServer() const
void Say(std::string_view key, Tokens tokens={}) const
One command as the builder assembled it.
std::function< Result< Reply >(const Caller &, std::span< const BoundArg >)> Invoke
std::vector< std::string > Aliases
std::vector< ArgDesc > Args
The parameter list of a handler, after the leading Caller.
What a command handler answers with.
static Reply Silent()