VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
Hook.hpp
Go to the documentation of this file.
1#pragma once
2
9#include <format>
10#include <functional>
11#include <memory>
12#include <string_view>
13#include <type_traits>
14#include <utility>
15
16namespace VoltMod
17{
18namespace Internal
19{
20
21inline KHook::Action ToKHookAction(HookAction action)
22{
23 switch (action)
24 {
26 return KHook::Action::Override;
28 return KHook::Action::Supersede;
29 default:
30 return KHook::Action::Ignore;
31 }
32}
33
34/** KHook ignores the default value carried by an Allow result. */
35template <class Ret>
36KHook::Return<Ret> ToKHook(HookResult<Ret> result)
37{
38 return {ToKHookAction(result.Action()), std::move(result).Value()};
39}
40
41inline KHook::Return<void> ToKHook(HookResult<void> result)
42{
43 return {ToKHookAction(result.Action())};
44}
45
46/**
47 * KHook storage and the before/after handlers it invokes.
48 *
49 * The object is heap allocated and non-polymorphic because KHook stores its address and invokes
50 * RunBefore and RunAfter through raw member addresses.
51 *
52 * @tparam HookKind KHook::Virtual or KHook::Member.
53 */
54template <template <class, class, class...> class HookKind, class Object, class Ret, class... Args>
56{
57public:
58 /** Either handler may be `nullptr`, but not both. */
59 template <class BeforeHandler, class AfterHandler>
61 : _before(WrapBefore(std::forward<BeforeHandler>(before))), _after(std::forward<AfterHandler>(after))
62 {
63 static_assert(!std::is_polymorphic_v<InstalledHook>, "KHook dispatches through a raw member address");
64 static_assert(
65 !std::is_null_pointer_v<std::decay_t<BeforeHandler>> || !std::is_null_pointer_v<std::decay_t<AfterHandler>>,
66 "a hook with neither handler would install nothing");
67
68 // KHook requires the context before Configure is called.
69 Hook.AddContext(this, &InstalledHook::RunBefore, &InstalledHook::RunAfter);
70 }
71
72 InstalledHook(const InstalledHook&) = delete;
74
75private:
76 using Before = std::move_only_function<HookResult<Ret>(Object&, Args...)>;
77 using After = std::move_only_function<void(Object&, Args...)>;
78
79 /** Adapt a void before-handler to an Allow result. */
80 template <class Handler>
81 static Before WrapBefore(Handler&& handler)
82 {
83 if constexpr (std::is_null_pointer_v<std::decay_t<Handler>>)
84 {
85 return {};
86 }
87 else if constexpr (std::is_void_v<std::invoke_result_t<std::decay_t<Handler>&, Object&, Args...>>)
88 {
89 return [handler = std::forward<Handler>(handler)](Object& self, Args... args) mutable {
90 handler(self, std::forward<Args>(args)...);
91 return HookResult<Ret>{};
92 };
93 }
94 else
95 {
96 return std::forward<Handler>(handler);
97 }
98 }
99
100 KHook::Return<Ret> RunBefore(Object* self, Args... args)
101 {
102 return ToKHook(_before ? _before(*self, std::forward<Args>(args)...) : HookResult<Ret>{});
103 }
104
105 KHook::Return<Ret> RunAfter(Object* self, Args... args)
106 {
107 if (_after)
108 {
109 _after(*self, std::forward<Args>(args)...);
110 }
111 return ToKHook(HookResult<Ret>{});
112 }
113
114 Before _before;
115 After _after;
116
117public:
118 HookKind<Object, Ret, Args...> Hook; // Destroyed before the handlers it invokes.
119};
120
121/** Return a subscription that removes the hook when destroyed. */
122template <class Hook>
123Subscription ToSubscription(std::unique_ptr<Hook> hook)
124{
125 return Subscription([hook = std::move(hook)]() mutable { hook.reset(); });
126}
127
128} // namespace Internal
129
130/**
131 * @brief Hook one virtual method of an engine interface for a subscription's lifetime.
132 *
133 * Only @p instance is hooked, even when other objects share its vtable. Pass `nullptr` for an
134 * unused handler. A before-handler that only observes returns nothing.
135 *
136 * @code
137 * hooks.Add(VoltMod::HookInterface(&IServerGameDLL::GameFrame, gi.ServerGameDLL, nullptr,
138 * [this](IServerGameDLL&, bool, bool, bool) { Tick(); }));
139 * @endcode
140 */
141template <class Iface, class Ret, class... Args, class Before, class After = std::nullptr_t>
143 After&& after = nullptr)
144{
145 using Installed = Internal::InstalledHook<KHook::Virtual, Iface, Ret, Args...>;
146 auto hook = std::make_unique<Installed>(std::forward<Before>(before), std::forward<After>(after));
147 hook->Hook.Configure(method);
148 if (instance)
149 {
150 hook->Hook.Add(instance);
151 }
152
153 return Internal::ToSubscription(std::move(hook));
154}
155
156/**
157 * @brief Hook a gamedata-bound virtual function on objects sharing its class vtable.
158 *
159 * The hook catches calls through that table only, even when its slot code is shared by other
160 * classes.
161 *
162 * @param name Names the hook in the log and in any error.
163 * @param function Gamedata-resolved slot and class table. Its first parameter is the object.
164 * @param before Handler invoked before the engine call, or nullptr.
165 * @param after Handler invoked after the engine call, or nullptr.
166 */
167template <class Object, class Ret, class... Args, class Before, class After = std::nullptr_t>
168[[nodiscard]] Result<Subscription> HookVirtual(std::string_view name, const VirtualFn<Ret(Object*, Args...)>& function,
169 Before&& before, After&& after = nullptr)
170{
171 if (!function)
172 {
173 return std::unexpected(Error::Unsupported(std::format("the {} vtable slot did not bind", name)));
174 }
175
176 using Installed = Internal::InstalledHook<KHook::Virtual, Object, Ret, Args...>;
177 auto hook = std::make_unique<Installed>(std::forward<Before>(before), std::forward<After>(after));
178 hook->Hook.Configure(function.Index());
179
180 // The class table is the only object KHook can use for a global hook.
181 void* asObject = function.Table();
182 hook->Hook.AddGlobal(reinterpret_cast<Object*>(&asObject));
183
184 Log::Info("{} hook installed (vtable index {}).", name, function.Index());
185 return Internal::ToSubscription(std::move(hook));
186}
187
188/**
189 * @brief Hook a signature-bound function at its entry.
190 *
191 * Use this for functions without a vtable slot. The hook affects every caller of the matched
192 * code, including callers from other classes that share it.
193 *
194 * @param name Names the hook in the log and in any error.
195 * @param function The binding; its first parameter is the object the function runs on.
196 * @param before Handler invoked before the engine call, or nullptr.
197 * @param after Handler invoked after the engine call, or nullptr.
198 */
199template <class Object, class Ret, class... Args, class Before, class After = std::nullptr_t>
200[[nodiscard]] Result<Subscription> HookFunction(std::string_view name, const Fn<Ret(Object*, Args...)>& function,
201 Before&& before, After&& after = nullptr)
202{
203 if (!function)
204 {
205 return std::unexpected(Error::Unsupported(std::format("the {} signature did not bind", name)));
206 }
207
208 using Installed = Internal::InstalledHook<KHook::Member, Object, Ret, Args...>;
209 auto hook = std::make_unique<Installed>(std::forward<Before>(before), std::forward<After>(after));
210 hook->Hook.Configure(static_cast<const void*>(function.Ptr()));
211
212 Log::Info("{} hook installed.", name);
213 return Internal::ToSubscription(std::move(hook));
214}
215
216/** The engine's own implementation of @p method, bypassing every hook on the slot. */
217template <class Iface, class Ret, class... Args, class... Passed>
218Ret CallOriginal(Ret (Iface::*method)(Args...), Iface* instance, Passed&&... args)
219{
220 return KHook::CallOriginal(method, instance, std::forward<Passed>(args)...);
221}
222
223} // namespace VoltMod
A hook before-handler's verdict.
InstalledHook(BeforeHandler &&before, AfterHandler &&after)
Definition Hook.hpp:60
HookKind< Object, Ret, Args... > Hook
Definition Hook.hpp:118
InstalledHook(const InstalledHook &)=delete
InstalledHook & operator=(const InstalledHook &)=delete
Owns one registration and releases it on destruction.
KHook::Action ToKHookAction(HookAction action)
Definition Hook.hpp:21
Subscription ToSubscription(std::unique_ptr< Hook > hook)
Definition Hook.hpp:123
KHook::Return< Ret > ToKHook(HookResult< Ret > result)
Definition Hook.hpp:36
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
Ret CallOriginal(Ret(Iface::*method)(Args...), Iface *instance, Passed &&... args)
Definition Hook.hpp:218
Result< Subscription > HookFunction(std::string_view name, const Fn< Ret(Object *, Args...)> &function, Before &&before, After &&after=nullptr)
Hook a signature-bound function at its entry.
Definition Hook.hpp:200
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
@ Replace
Let it run, but return this value instead of its own.
@ Block
Skip it entirely and return this value.
Subscription HookInterface(Ret(Iface::*method)(Args...), Iface *instance, Before &&before, After &&after=nullptr)
Hook one virtual method of an engine interface for a subscription's lifetime.
Definition Hook.hpp:142
static InstalledScan Installed()
std::expected< T, Error > Result
Definition Result.hpp:64
static Error Unsupported(std::string detail)
Definition Result.hpp:57