VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
ClientConVars.cpp
Go to the documentation of this file.
3
13#include <cstdint>
14#include <engine/igameeventsystem.h>
15#include <inetchannel.h>
16#include <netmessages.pb.h>
17#include <networksystem/inetworkmessages.h>
18#include <networksystem/netmessage.h>
19#include <utility>
20
21namespace VoltMod
22{
23
24static_assert(std::to_underlying(ClientConVarStatus::Answered) == eQueryCvarValueStatus_ValueIntact);
25static_assert(std::to_underlying(ClientConVarStatus::NotFound) == eQueryCvarValueStatus_CvarNotFound);
26static_assert(std::to_underlying(ClientConVarStatus::NotAConVar) == eQueryCvarValueStatus_NotACvar);
27static_assert(std::to_underlying(ClientConVarStatus::Protected) == eQueryCvarValueStatus_CvarProtected);
28
30 : _interfaces(interfaces), _bindings(bindings), _pending(std::make_unique<PendingConVarQueries>())
31{
32 // A slot transition invalidates requests for its previous occupant.
33 _slotListener = slots.Changed += [this](int slot) { _pending->Clear(slot); };
34
35 if (Status installed = Install(); !installed)
36 {
37 _failure = installed.error();
38 }
39}
40
42
44{
45 if (_hook)
46 {
47 return {};
48 }
49 return std::unexpected(_failure);
50}
51
52Status ClientConVars::Install()
53{
54 if (!_interfaces.Engine || !_interfaces.NetworkMessages || !_interfaces.GameEventSystem)
55 {
56 return std::unexpected(Error::NotReady("engine interfaces unavailable"));
57 }
58
59 if (!_bindings.ClientSlot)
60 {
61 return std::unexpected(Error::Unsupported("the ClientSlot offset did not bind"));
62 }
63
65 _interfaces.NetworkMessages->FindNetworkMessagePartial("CSVCMsg_GetCvarValue");
66 if (!getCvarValue)
67 {
68 return std::unexpected(Error::Engine("the engine does not provide CSVCMsg_GetCvarValue"));
69 }
70
71 const auto onResponse = [this](EngineClient& client, const CNetMessage* message) {
72 OnRespondCvarValue(client, *message);
73 };
74 auto hook = HookVirtual("Client convar response", _bindings.ProcessRespondCvarValue, nullptr, onResponse);
75 if (!hook)
76 {
77 return std::unexpected(hook.error());
78 }
79
80 _hook = std::move(*hook);
81 _getCvarValue = getCvarValue;
82 Log::Info("Client convar queries enabled (slot offset {}).", _bindings.ClientSlot.Value());
83 return {};
84}
85
87{
88 if (!_hook || !IsValidSlot(slot) || cvarName.empty() || !callback)
89 {
90 return false;
91 }
92
93 const double now = Time::MonotonicSeconds();
94 _pending->Prune(slot, now);
95
96 // Share one request among callbacks for the same convar.
97 if (PendingConVarQuery* inFlight = _pending->Find(slot, cvarName))
98 {
99 inFlight->Callback = std::move(callback);
100 return true;
101 }
102
103 if (_pending->Full(slot))
104 {
105 return false;
106 }
107
108 const int cookie = _pending->NextCookie(slot);
109 if (cookie < 0 || !Send(slot, cvarName, cookie))
110 {
111 return false;
112 }
113
114 _pending->Add(slot, cookie, std::string(cvarName), std::move(callback), now);
115 return true;
116}
117
119{
120 return _pending->Count(slot);
121}
122
124{
125 _pending->ClearAll();
126}
127
128bool ClientConVars::Send(int slot, std::string_view cvarName, int cookie)
129{
130 // Bots and empty slots have no network channel.
131 if (!_interfaces.Engine->GetPlayerNetInfo(CPlayerSlot(slot)))
132 {
133 return false;
134 }
135
136 CNetMessage* message = _getCvarValue->AllocateMessage();
137 if (!message)
138 {
139 return false;
140 }
141
142 auto* request = message->ToPB<CSVCMsg_GetCvarValue>();
143 if (!request)
144 {
145 _interfaces.NetworkMessages->DeallocateNetMessageAbstract(_getCvarValue, message);
146 return false;
147 }
148
149 request->set_cookie(cookie);
150 request->set_cvar_name(std::string(cvarName));
151
152 SingleRecipientFilter filter(slot);
153 _interfaces.GameEventSystem->PostEventAbstract(-1, false, &filter, _getCvarValue, request, 0);
154 _interfaces.NetworkMessages->DeallocateNetMessageAbstract(_getCvarValue, message);
155 return true;
156}
157
158void ClientConVars::OnRespondCvarValue(const EngineClient& client, const CNetMessage& message)
159{
160 // Gamedata supplies the SDK-missing client slot offset; -1 means it did not bind.
161 const int slot = ClientSlot(_bindings, &client);
162 const auto& msg = *message.ToPB<CCLCMsg_RespondCvarValue>();
163
164 if (!IsValidSlot(slot) || !msg.has_cookie() || !msg.has_status_code() || !msg.has_name())
165 {
166 return;
167 }
168
169 // Validate every client-controlled field before dispatch.
170 const int status = msg.status_code();
172 {
173 return;
174 }
175
176 std::string_view value;
177 if (status == std::to_underlying(ClientConVarStatus::Answered))
178 {
179 if (!msg.has_value() || msg.value().find('\0') != std::string::npos)
180 {
181 return;
182 }
183 value = msg.value();
184 }
185
186 // Remove before callbacks may query the same convar again.
187 auto query = _pending->Take(slot, msg.cookie(), msg.name());
188 if (query && query->Callback)
189 {
190 query->Callback(slot, static_cast<ClientConVarStatus>(status), msg.name(), value);
191 }
192}
193
194} // namespace VoltMod
bool Query(int slot, std::string_view cvarName, QueryCallback callback)
std::function< void(int slot, ClientConVarStatus status, std::string_view name, std::string_view value)> QueryCallback
ClientConVars(Interfaces &interfaces, const Bindings &bindings, SlotEvents &slots)
size_t PendingCount(int slot) const
constexpr int Value() const noexcept
Bookkeeping for ClientConVars: per-slot, cookie-keyed queries awaiting an answer.
"The occupant of this slot changed" - raised by PlayerManager, consumed below it.
static double MonotonicSeconds()
Definition Durations.hpp:29
void Info(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:79
Result< Subscription > HookVirtual(std::string_view name, const VirtualFn< Ret(Object *, Args...)> &function, Before &&before, After &&after=nullptr)
Hook a gamedata-bound virtual function on objects sharing its class vtable.
Definition Hook.hpp:168
int ClientSlot(const Bindings &bindings, const void *client)
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
constexpr bool IsValidSlot(int slot)
Definition Slot.hpp:13
ClientConVarStatus
How the client answered a convar query (CCLCMsg_RespondCvarValue::status_code).
@ NotFound
The client has no convar by that name.
@ Protected
The convar is marked protected; the client withholds its value.
@ Answered
The client reported the convar's current value.
@ NotAConVar
The name exists on the client but is a command, not a convar.
std::expected< void, Error > Status
Definition Result.hpp:67
@ Send
point the client at AddonDecision::Id and wait for its reconnect
VirtualFn< bool(EngineClient *, const CNetMessage *)> ProcessRespondCvarValue
Definition Bindings.hpp:90
Offset< int > ClientSlot
Definition Bindings.hpp:97
static Error Engine(std::string detail)
Definition Result.hpp:58
static Error Unsupported(std::string detail)
Definition Result.hpp:57
static Error NotReady(std::string detail)
Definition Result.hpp:50
Centralized holder for all HL2SDK interface pointers.
IVEngineServer2 * Engine
IGameEventSystem * GameEventSystem
INetworkMessages * NetworkMessages
One outstanding client convar query, identified by the cookie sent to the client.