VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
ArgBinding.cpp
Go to the documentation of this file.
2
4#include <utility>
5
6namespace VoltMod
7{
8
9/** The reply key and tokens for a target that did not resolve. */
10static ArgError TargetKey(const TargetFailure& failure, const std::string& token)
11{
12 switch (failure.Error)
13 {
15 return {.Key = "target.immune", .Vars = {{"token", token}}};
18 return {.Key = "target.ambiguous", .Vars = {{"token", token}, {"count", std::to_string(failure.Count)}}};
20 return {.Key = "target.dead", .Vars = {{"token", token}}};
22 return {.Key = "target.bot", .Vars = {{"token", token}}};
24 default:
25 return {.Key = "target.noMatch", .Vars = {{"token", token}}};
26 }
27}
28
29/** One token to the single player it names, or the key explaining why it named none. */
30static std::expected<Args::Target, ArgError> BindTarget(ArgBinder& binder, const std::string& token, Player* caller)
31{
32 auto resolved = binder.Resolve(token, caller, TargetRules{});
33 if (!resolved)
34 {
35 return std::unexpected(TargetKey(resolved.error(), token));
36 }
37 return Args::Target{.Value = resolved->front()};
38}
39
40/** One token to every player it names. `AllowMultiple` is what separates this from BindTarget. */
41static std::expected<Args::Targets, ArgError> BindTargets(ArgBinder& binder, const std::string& token, Player* caller)
42{
43 auto resolved = binder.Resolve(token, caller, TargetRules{.AllowMultiple = true});
44 if (!resolved)
45 {
46 return std::unexpected(TargetKey(resolved.error(), token));
47 }
48 return Args::Targets{.Value = std::move(*resolved)};
49}
50
51/**
52 * One token to a player if anybody online answers to it, otherwise to a bare SteamID64.
53 *
54 * The fallback is only for NoMatch: falling back on Immune would turn a refused target into an
55 * offline one and act on it anyway, and on Ambiguous it would pick nobody's id.
56 */
57static std::expected<Args::PlayerOrSteamId, ArgError> BindPlayerOrSteamId(ArgBinder& binder, const std::string& token,
59{
60 auto resolved = binder.Resolve(token, caller, TargetRules{});
61 if (resolved)
62 {
63 Player* player = resolved->front();
64 return Args::PlayerOrSteamId{.Online = player, .SteamId = player->SteamId()};
65 }
66
68 {
69 if (auto id = ParseInt64(token))
70 {
71 return Args::PlayerOrSteamId{.Online = nullptr, .SteamId = *id};
72 }
73 }
74
75 return std::unexpected(TargetKey(resolved.error(), token));
76}
77
78static std::expected<Args::Duration, ArgError> BindDuration(const std::string& token)
79{
80 const int seconds = ParseDuration(token);
81 if (seconds < 0)
82 {
83 return std::unexpected(ArgError{.Key = "cmd.badDuration", .Vars = {{"token", token}}});
84 }
85
86 // ParseDuration reads a bare number as seconds; a command duration reads it as minutes.
87 const int64_t value = Strings::IsNumeric(token) ? static_cast<int64_t>(seconds) * 60 : seconds;
88 return Args::Duration{.Value = std::chrono::seconds{value}};
89}
90
91static std::expected<Args::SteamId, ArgError> BindSteamId(const std::string& token)
92{
93 auto id = ParseInt64(token);
94 if (!id || !Strings::IsNumeric(token))
95 {
96 return std::unexpected(ArgError{.Key = "cmd.badSteamId", .Vars = {{"token", token}}});
97 }
98 return Args::SteamId{.Value = *id};
99}
100
101static std::expected<Args::Int, ArgError> BindInt(const std::string& token)
102{
103 auto value = ParseInt64(token);
104 if (!value || !std::in_range<int>(*value))
105 {
106 return std::unexpected(ArgError{.Key = "cmd.badNumber", .Vars = {{"token", token}}});
107 }
108 return Args::Int{.Value = static_cast<int>(*value)};
109}
110
111static std::expected<Args::U64, ArgError> BindU64(const std::string& token)
112{
113 auto value = ParseUInt64(token);
114 if (!value)
115 {
116 return std::unexpected(ArgError{.Key = "cmd.badNumber", .Vars = {{"token", token}}});
117 }
118 return Args::U64{.Value = *value};
119}
120
121/** Store @p parsed, or return its error. Each binder above returns its own argument type. */
122template <class T>
123static std::optional<ArgError> Store(std::expected<T, ArgError> parsed, std::vector<BoundArg>& bound)
124{
125 if (!parsed)
126 {
127 return std::move(parsed.error());
128 }
129 bound.emplace_back(std::move(*parsed));
130 return std::nullopt;
131}
132
133std::expected<std::vector<BoundArg>, ArgError> BindArgs(const CommandDefinition& def,
134 std::span<const std::string> tokens, Player* caller,
136{
137 using enum ArgKind;
138
139 std::vector<BoundArg> bound;
140 bound.reserve(def.Args.size());
141 size_t i = 0;
142
143 for (const ArgDesc& arg : def.Args)
144 {
145 if (i >= tokens.size())
146 {
147 // Arity was checked first, so only an optional argument can be missing here.
148 bound.emplace_back(std::monostate{});
149 continue;
150 }
151
152 const std::string& token = tokens[i];
153 std::optional<ArgError> failed;
154
155 switch (arg.Kind)
156 {
157 case Target:
159 break;
160 case Targets:
162 break;
163 case PlayerOrSteamId:
165 break;
166 case Duration:
168 break;
169 case SteamId:
171 break;
172 case Int:
174 break;
175 case U64:
177 break;
178 case Word:
179 bound.emplace_back(Args::Word{.Value = token});
180 break;
181 case Rest:
182 {
183 // A Rest swallows the remainder, so it is the last argument by construction.
184 std::vector<std::string> rest(tokens.begin() + static_cast<std::ptrdiff_t>(i), tokens.end());
185 bound.emplace_back(Args::Rest{.Value = Strings::Join(rest, " ")});
186 i = tokens.size();
187 continue;
188 }
189 }
190
191 if (failed)
192 {
193 return std::unexpected(std::move(*failed));
194 }
195 ++i;
196 }
197
198 return bound;
199}
200
201} // namespace VoltMod
The one thing command dispatch needs a running server for.
One connected player, owned by PlayerManager for the length of the connection.
Definition Player.hpp:23
int64_t SteamId() const noexcept
Definition Player.hpp:41
Static utilities for converting between SteamID formats (64-bit, SteamID2, SteamID3).
Definition SteamId.hpp:12
static std::string Join(const std::vector< std::string > &parts, std::string_view delimiter)
Definition Strings.cpp:114
static bool IsNumeric(std::string_view str)
Definition Strings.cpp:248
static std::expected< Args::Int, ArgError > BindInt(const std::string &token)
static std::expected< Args::Target, ArgError > BindTarget(ArgBinder &binder, const std::string &token, Player *caller)
static std::expected< Args::Duration, ArgError > BindDuration(const std::string &token)
static std::expected< Args::SteamId, ArgError > BindSteamId(const std::string &token)
std::optional< int64_t > ParseInt64(std::string_view text)
Definition Strings.cpp:13
int ParseDuration(std::string_view text)
Definition Strings.cpp:35
std::expected< std::vector< BoundArg >, ArgError > BindArgs(const CommandDefinition &def, std::span< const std::string > tokens, Player *caller, ArgBinder &binder)
static std::optional< ArgError > Store(std::expected< T, ArgError > parsed, std::vector< BoundArg > &bound)
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static std::expected< Args::PlayerOrSteamId, ArgError > BindPlayerOrSteamId(ArgBinder &binder, const std::string &token, Player *caller)
static std::expected< Args::Targets, ArgError > BindTargets(ArgBinder &binder, const std::string &token, Player *caller)
std::optional< uint64_t > ParseUInt64(std::string_view text)
Definition Strings.cpp:24
@ Ambiguous
a name fragment matched more than one player
@ MultiNotAllowed
a multi-selector was used where the command takes a single target
@ Immune
matches existed, but the policy blocked all of them
ArgKind
Which binder runs for one declared argument.
Definition Args.hpp:30
static ArgError TargetKey(const TargetFailure &failure, const std::string &token)
static std::expected< Args::U64, ArgError > BindU64(const std::string &token)
A binding failure, as the key and tokens the reply is built from.
std::string Key
std::chrono::seconds Value
Definition Args.hpp:86
std::string Value
Definition Args.hpp:135
std::vector< Player * > Value
Definition Args.hpp:78
uint64_t Value
Definition Args.hpp:120
std::string Value
Definition Args.hpp:127
One command as the builder assembled it.
bool AllowMultiple
permit selectors (@all/@t/...) that match more than one player
Definition Targeting.hpp:18