VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
ButtonPresses.cpp
Go to the documentation of this file.
2
5
11#include <format>
12#include <inetchannel.h>
13#include <networksystem/inetworkmessages.h>
14#include <networksystem/netmessage.h>
15#include <string_view>
16#include <utility>
17
18namespace VoltMod
19{
20
21// The SDK does not define CS_UM_CustomHudClicked, so decode it as a generic user message.
22static constexpr std::string_view UserMessageName = "CSVCMsg_UserMessage";
23static constexpr int32_t CustomHudClickType = 390;
24static constexpr int64_t WarningIntervalSeconds = 10;
25
27 : _registry(registry), _bindings(bindings), _warnings(WarningIntervalSeconds)
28{
29 if (!engine.NetworkMessages)
30 {
31 return;
32 }
33 if (Status installed = Install(engine); !installed)
34 {
35 Log::Warn("Button presses: {}; presses will not arrive.", installed.error().Detail);
36 }
37}
38
39Status ButtonPresses::Install(const EngineInterfaces& engine)
40{
41 if (!_bindings.ClientMessageFilter)
42 {
43 return std::unexpected(Error::Unsupported("the FilterMessage client offset did not bind"));
44 }
45 if (auto* message = engine.NetworkMessages->FindNetworkMessagePartial(std::string(UserMessageName).c_str()))
46 {
47 _messageId = message->GetNetMessageInfo()->m_MessageId;
48 }
49 if (_messageId < 0)
50 {
51 return std::unexpected(Error::Engine(std::format("the engine does not know {}", UserMessageName)));
52 }
53
55 INetChannel*) { Queue(message, filter); };
56 auto hook = HookVirtual("Custom HUD button presses", _bindings.FilterMessage, onMessage);
57 if (!hook)
58 {
59 return std::unexpected(hook.error());
60 }
61 _hook = std::move(*hook);
62 return {};
63}
64
66{
67 // Swapped out first: a handler may cause another press to queue.
68 for (const Press& press : std::exchange(_queued, {}))
69 {
70 _registry.RaiseButtonPress(press.Slot, press.ButtonId);
71 }
72}
73
74const ButtonPresses::MessageFields& ButtonPresses::FieldsOf(const ProtoMessage& proto)
75{
76 static const MessageFields fields = [&proto] {
77 const MessageFields resolved{.Type = ProtoField(proto, "msg_type"), .Data = ProtoField(proto, "msg_data")};
78 if (!resolved)
79 {
80 Log::Warn("Button presses: {} has no 'msg_type'/'msg_data' field; ignoring presses.", UserMessageName);
81 }
82 return resolved;
83 }();
84 return fields;
85}
86
87void ButtonPresses::Queue(const CNetMessage* message, const INetworkMessageProcessingPreFilter& filter)
88{
89 // Every inbound message passes here; the id rules out nearly all before any parsing.
90 INetworkMessageInternal* info = message ? message->GetNetMessage() : nullptr;
91 if (!info || info->GetNetMessageInfo()->m_MessageId != _messageId)
92 {
93 return;
94 }
95
96 const ProtoMessage* proto = message->ToPB<ProtoMessage>();
97 if (!proto)
98 {
99 return;
100 }
101 const MessageFields& fields = FieldsOf(*proto);
102 if (!fields)
103 {
104 return;
105 }
106
107 const auto* reflection = proto->GetReflection();
108 if (reflection->GetInt32(*proto, fields.Type) != CustomHudClickType)
109 {
110 return;
111 }
112
113 // The sending connection's slot, so a spectator's press stays theirs.
114 const int slot = ClientSlot(_bindings, FilterClient(_bindings, filter));
115 if (!IsValidSlot(slot))
116 {
117 return;
118 }
119
120 auto payload = ButtonPressMessage::Parse(reflection->GetString(*proto, fields.Data));
121 if (!payload)
122 {
123 WarnMalformed(slot, payload.error().Detail);
124 return;
125 }
126 if (payload->ButtonId.find('\0') != std::string::npos)
127 {
128 return;
129 }
130
131 _queued.push_back({.Slot = slot, .ButtonId = std::move(payload->ButtonId)});
132}
133
134void ButtonPresses::WarnMalformed(int slot, std::string_view detail)
135{
136 if (!_warnings.TryStart(slot, static_cast<int64_t>(Time::MonotonicSeconds())))
137 {
138 return;
139 }
140 Log::Warn("Button presses: a press from slot {} did not parse ({}).", slot, detail);
141}
142
143} // namespace VoltMod
ButtonPresses(const Bindings &bindings, const EngineInterfaces &engine, PluginRegistry &registry)
bool TryStart(const TKey &key, int64_t nowSec, int64_t seconds)
Definition Cooldown.hpp:24
The loaded plugins and the engine events raised on them, one per process.
void RaiseButtonPress(int slot, std::string_view buttonId)
static double MonotonicSeconds()
Definition Durations.hpp:29
void Warn(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:88
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
static constexpr int32_t CustomHudClickType
google::protobuf::Message ProtoMessage
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
static constexpr std::string_view UserMessageName
std::expected< void, Error > Status
Definition Result.hpp:67
const void * FilterClient(const Bindings &bindings, const INetworkMessageProcessingPreFilter &filter)
static constexpr int64_t WarningIntervalSeconds
const ProtoFieldDescriptor * ProtoField(const ProtoMessage &message, std::string_view name)
VirtualFn< bool(INetworkMessageProcessingPreFilter *, const CNetMessage *, INetChannel *)> FilterMessage
Definition Bindings.hpp:65
Offset< void > ClientMessageFilter
Definition Bindings.hpp:101
static Result< ButtonPressMessage > Parse(std::string_view bytes)
static Error Engine(std::string detail)
Definition Result.hpp:58
static Error Unsupported(std::string detail)
Definition Result.hpp:57