VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
EngineHooks.cpp
Go to the documentation of this file.
2
5
9#include <eiface.h>
10#include <entity2/entitysystem.h>
11#include <icvar.h>
12#include <igamesystem.h>
13#include <inetchannelinfo.h>
14#include <interfaces/interfaces.h>
15#include <iserver.h>
16#include <string>
17#include <string_view>
18#include <tier1/bufferstring.h>
19#include <tier1/convar.h>
20#include <utility>
21
22// KHook needs a complete type for this by-reference hook parameter.
25
26namespace VoltMod
27{
28
29/** The engine passes null for an absent string. */
30static std::string_view Text(const char* text)
31{
32 return text != nullptr ? text : "";
33}
34
35/** Offer a command to the plugins, and keep it from the engine when one consumes it. */
36static HookResult<void> RunCommand(PluginRegistry& registry, std::string_view name, const CCommand& arguments, int slot)
37{
38 // ArgS is the whole line after the command name.
39 const bool consumed = registry.RaiseConsoleCommand(name, Text(arguments.ArgS()), slot);
41}
42
43/** Ask the plugins about a joining client, and refuse it with the first reason one gives. */
46{
47 const std::string reason = registry.RaiseClientConnecting(slot, static_cast<int64_t>(xuid), name);
48 if (reason.empty())
49 {
50 return {};
51 }
52 if (rejectReason != nullptr)
53 {
54 rejectReason->Insert(0, reason.c_str());
55 }
56 return HookResult<bool>::Block(false);
57}
58
59/** A client's address, or empty for a bot, which has no net channel. */
60static std::string_view ClientAddress(IVEngineServer2* server, CPlayerSlot slot)
61{
62 auto* channel = server->GetPlayerNetInfo(slot);
63 return channel != nullptr ? Text(channel->GetAddress()) : std::string_view{};
64}
65
66/** Hand every map's resource manifest to the plugins. The game rules system is in every session. */
68{
70 KHook::GetVtableIndex(&IGameSystem::OnBuildGameSessionManifest),
71 FindVirtualTable("server", "CGameRulesGameSystem"));
73 if (event && event->m_pResourceManifest)
74 {
75 registry.RaiseBuildGameSessionManifest(event->m_pResourceManifest);
76 }
77 };
78 return HookVirtual("CGameRulesGameSystem::BuildGameSessionManifest", build, nullptr, onBuildManifest);
79}
80
82 std::function<void()> beforeServerStartup, std::function<void(int64_t)> beforeClientConnected,
83 std::function<void(int64_t)> afterClientDisconnected)
84 : _registry(registry),
85 _beforeFrame(std::move(beforeFrame)),
86 _beforeServerStartup(std::move(beforeServerStartup)),
87 _beforeClientConnected(std::move(beforeClientConnected)),
88 _afterClientDisconnected(std::move(afterClientDisconnected))
89{
90 // Registers the host's own ConCommands, `volt` among them.
91 g_pCVar = engine.Cvar;
93
94 auto add = [this](Subscription hook) { _hooks.Add(std::move(hook)); };
95
96 const auto onFrame = [this](IServerGameDLL&, bool, bool, bool) {
97 _beforeFrame();
98 _registry.RaiseFrame();
99 };
101 ISource2WorldSession*, const char* mapName) {
102 const std::string_view map = Text(mapName);
103 Log::Info("Server startup: map '{}'.", map.empty() ? "<none>" : map);
104 _beforeServerStartup();
105 _registry.RaiseServerStartup(map);
106 DisconnectEveryone();
107 };
108 const auto onClientConnect = [this](IServerGameClients&, CPlayerSlot slot, const char* name, uint64 xuid,
109 const char*, bool, CBufferString* rejectReason) {
110 return AdmitClient(_registry, slot.Get(), xuid, Text(name), rejectReason);
111 };
112 const auto onClientConnected = [this](IServerGameClients&, CPlayerSlot slot, const char* name, uint64 xuid,
113 const char*, const char* address, bool) {
114 // Before the call: the address is gone after it.
115 ConnectClient(slot.Get(), xuid, Text(name), Text(address));
116 };
117 const auto onClientPutInServer = [this, server = engine.Engine](IServerGameClients&, CPlayerSlot slot,
118 const char* name, int, uint64 xuid) {
119 if (IsValidSlot(slot.Get()) && !_registry.IsConnected(slot.Get()))
120 {
121 ConnectClient(slot.Get(), xuid, Text(name), ClientAddress(server, slot));
122 }
123 };
125 const char*, uint64 xuid, const char*) {
126 _registry.RaiseClientDisconnected(slot.Get());
127 _afterClientDisconnected(static_cast<int64_t>(xuid));
128 };
130 _registry.RaiseClientFullyConnected(slot.Get());
131 };
133 _registry.RaiseClientSettingsChanged(slot.Get());
134 };
135 const auto onConCommand = [this](ICvar&, ConCommandRef command, const CCommandContext& context,
136 const CCommand& arguments) {
137 return RunCommand(_registry, Text(command.GetName()), arguments, context.GetPlayerSlot().Get());
138 };
140 return RunCommand(_registry, Text(arguments.Arg(0)), arguments, slot.Get());
141 };
144 const uint16*, int) { _registry.RaiseCheckTransmit(infoList, infoCount); };
145
146 add(HookInterface(&IServerGameDLL::GameFrame, engine.ServerGameDll, nullptr, onFrame));
147 add(HookInterface(&INetworkServerService::StartupServer, engine.NetworkServerService, nullptr, onStartupServer));
148 // Before the engine admits the player, so a refusal keeps them out with a reason they see.
149 add(HookInterface(&IServerGameClients::ClientConnect, engine.ServerGameClients, onClientConnect));
150 add(HookInterface(&IServerGameClients::OnClientConnected, engine.ServerGameClients, onClientConnected));
151 // Clients returning from a map change skip OnClientConnected.
152 add(HookInterface(&IServerGameClients::ClientPutInServer, engine.ServerGameClients, nullptr, onClientPutInServer));
153 // After the call, before the slot is reused.
154 add(HookInterface(&IServerGameClients::ClientDisconnect, engine.ServerGameClients, nullptr, onClientDisconnect));
155 add(HookInterface(&IServerGameClients::ClientFullyConnect, engine.ServerGameClients, nullptr, onClientFullyConnect));
156 add(HookInterface(&IServerGameClients::ClientSettingsChanged, engine.ServerGameClients, nullptr,
158 add(HookInterface(&ICvar::DispatchConCommand, engine.Cvar, onConCommand));
159 // Client commands that are not ConCommands, `vote` among them.
160 add(HookInterface(&IServerGameClients::ClientCommand, engine.ServerGameClients, onClientCommand));
161 // Filter the bit vectors after the game fills them.
162 add(HookInterface(&ISource2GameEntities::CheckTransmit, engine.GameEntities, nullptr, onCheckTransmit));
163
164 if (auto manifest = HookSessionManifest(_registry))
165 {
166 add(std::move(*manifest));
167 }
168 else
169 {
170 Log::Error("Precache is off: {}", manifest.error().Detail);
171 }
172
173 Log::Info("Engine hooks installed.");
174}
175
176void EngineHooks::ConnectClient(int slot, uint64_t xuid, std::string_view name, std::string_view address)
177{
178 _beforeClientConnected(static_cast<int64_t>(xuid));
179 _registry.RaiseClientConnected(slot, static_cast<int64_t>(xuid), name, address);
180}
181
182void EngineHooks::DisconnectEveryone()
183{
184 for (int slot = 0; slot < MaxPlayers; ++slot)
185 {
186 if (_registry.IsConnected(slot))
187 {
188 _registry.RaiseClientDisconnected(slot);
189 }
190 }
191}
192
193} // namespace VoltMod
EngineHooks(PluginRegistry &registry, const EngineInterfaces &engine, std::function< void()> beforeFrame, std::function< void()> beforeServerStartup, std::function< void(int64_t)> beforeClientConnected, std::function< void(int64_t)> afterClientDisconnected)
A hook before-handler's verdict.
static HookResult Block(Ret value)
The loaded plugins and the engine events raised on them, one per process.
void RaiseClientSettingsChanged(int slot)
void RaiseCheckTransmit(CCheckTransmitInfo **infoList, int infoCount)
void RaiseClientConnected(int slot, int64_t steamId, std::string_view name, std::string_view address)
void RaiseServerStartup(std::string_view mapName)
void RaiseClientFullyConnected(int slot)
void RaiseClientDisconnected(int slot)
bool IsConnected(int slot) const
Owns one registration and releases it on destruction.
void Add(Subscription subscription)
void Error(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:97
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
@ Text
Not selectable; a caption or a summary line.
static HookResult< bool > AdmitClient(PluginRegistry &registry, int slot, uint64_t xuid, std::string_view name, CBufferString *rejectReason)
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static HookResult< void > RunCommand(PluginRegistry &registry, std::string_view name, const CCommand &arguments, int slot)
constexpr bool IsValidSlot(int slot)
Definition Slot.hpp:13
static std::string_view ClientAddress(IVEngineServer2 *server, CPlayerSlot slot)
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 Result< Subscription > HookSessionManifest(PluginRegistry &registry)
constexpr int MaxPlayers
Definition Slot.hpp:7
void * FindVirtualTable(std::string_view moduleName, std::string_view className)
std::expected< T, Error > Result
Definition Result.hpp:64