VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
Loader.cpp
Go to the documentation of this file.
1#include "Loader/Loader.hpp"
2
6
7#include <cstring>
8#include <eiface.h>
9#include <filesystem>
10#include <fstream>
11#include <iterator>
12#include <khook.hpp>
13#include <khook/memory.hpp>
14#include <string>
15#include <system_error>
16#include <tier0/dbg.h>
17#include <tier0/icommandline.h>
18#include <utility>
19
20namespace VoltMod
21{
22
24static std::string g_gameDir;
26static HostStopFn g_stopHost = nullptr;
27
28// What the swapped slots held. Never written back: Metamod restores its own slots, and ours would undo that.
30static void (*g_disconnect)(void* config) = nullptr;
31static InitReturnVal_t (*g_init)(void* server) = nullptr;
32static void (*g_shutdown)(void* server) = nullptr;
33
34template <class Fn>
35static Fn SwapSlot(void* object, int index, Fn replacement)
36{
37 void** slot = *static_cast<void***>(object) + index;
38 KHook::Memory::SetAccess(slot, sizeof(void*),
39 KHook::Memory::Flags::READ | KHook::Memory::Flags::WRITE | KHook::Memory::Flags::EXECUTE);
40 const Fn original = reinterpret_cast<Fn>(*slot);
41 *slot = reinterpret_cast<void*>(replacement);
42 return original;
43}
44
45/** `csgo/addons/voltmod/bin/<platform>/` holds this module, so `csgo` is four directories up. */
46static std::filesystem::path GameDir()
47{
48 std::filesystem::path dir = OwnPath().parent_path();
49 for (int level = 0; level < 4; ++level)
50 {
51 dir = dir.parent_path();
52 }
53 return dir;
54}
55
56static std::string ReadFile(const std::filesystem::path& path)
57{
58 std::ifstream file(path, std::ios::binary);
59 return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
60}
61
62/** The first server library on the `Game` paths: Metamod's loader where it is installed, else the game's. */
64{
65 // Answering nothing makes the engine load `server` itself, so a game client never runs VoltMod.
66 if (!CommandLine()->HasParm("-dedicated"))
67 {
68 return nullptr;
69 }
70
71 const std::filesystem::path gameDir = GameDir();
72 const std::filesystem::path gameinfo = gameDir / "gameinfo.gi";
73 const std::string text = ReadFile(gameinfo);
74 if (text.empty())
75 {
76 Warning("[VoltMod] Could not read %s, so VoltMod is not loaded.\n", gameinfo.string().c_str());
77 return nullptr;
78 }
79
80 for (const std::string& searchPath : GameSearchPaths(text))
81 {
82 const std::filesystem::path server = gameDir.parent_path() / searchPath / "bin" / PlatformDir / ServerFile;
83 std::error_code missing;
84 if (!std::filesystem::is_regular_file(server, missing))
85 {
86 continue;
87 }
88
89 void* module = OpenModule(server);
90 auto* factory = module ? reinterpret_cast<InterfaceFactory>(FindExport(module, "CreateInterface")) : nullptr;
91 if (factory == nullptr)
92 {
93 Warning("[VoltMod] Could not load %s: %s\n", server.string().c_str(), LastError().c_str());
94 continue;
95 }
96
97 Msg("[VoltMod] Loader started; forwarding the game server to %s\n", server.string().c_str());
98 g_gameDir = gameDir.string();
99 g_start.GameDir = g_gameDir.c_str();
100 return factory;
101 }
102
103 Warning("[VoltMod] No Game path in %s holds a server library, so VoltMod is not loaded.\n",
104 gameinfo.string().c_str());
105 return nullptr;
106}
107
109{
111 if (next == nullptr)
112 {
113 // The engine may unload this module now; KHook's threads must be joined first.
114 KHook::Shutdown();
115 }
116 return next;
117}
118
119static void StartHost()
120{
121 if (g_start.EngineFactory == nullptr || g_start.ServerFactory == nullptr)
122 {
123 Warning("[VoltMod] The engine and game factories were not found, so VoltMod is not loaded.\n");
124 return;
125 }
126
127 const std::filesystem::path path = OwnPath().parent_path() / HostFile;
128 void* host = OpenModule(path);
129 if (host == nullptr)
130 {
131 Warning("[VoltMod] Could not load %s: %s\n", path.string().c_str(), LastError().c_str());
132 return;
133 }
134
135 auto* start = reinterpret_cast<HostStartFn>(FindExport(host, HostStartName));
136 auto* stop = reinterpret_cast<HostStopFn>(FindExport(host, HostStopName));
137 if (start == nullptr || stop == nullptr)
138 {
139 Warning("[VoltMod] Could not load %s: %s\n", path.string().c_str(), LastError().c_str());
140 return;
141 }
142
143 // The server still runs without VoltMod, as it would after a failed Metamod plugin.
144 char error[512] = "";
145 if (!start(&g_start, error, sizeof(error)))
146 {
147 Warning("[VoltMod] The host did not start: %s\n", error);
148 return;
149 }
151}
152
158
159static InitReturnVal_t OnInit(void* server)
160{
161 // First, so Metamod's plugins start after VoltMod and hook on top of it.
162 StartHost();
163 return g_init(server);
164}
165
166static void StopHost()
167{
168 if (g_stopHost != nullptr)
169 {
170 std::exchange(g_stopHost, nullptr)();
171 }
172}
173
174static void OnShutdown(void* server)
175{
176 // Here, not at Disconnect: by then the engine has shut down and a plugin's console command crashes it.
177 StopHost();
178 g_shutdown(server);
179}
180
181static void OnDisconnect(void* config)
182{
183 StopHost();
184 // Safe: this slot was swapped, not hooked through KHook.
185 KHook::Shutdown();
187}
188
189static void Patch(const char* name, void* object)
190{
191 if (std::strcmp(name, INTERFACEVERSION_SERVERCONFIG) == 0 && g_connect == nullptr)
192 {
193 g_connect = SwapSlot(object, KHook::GetVtableIndex(&ISource2ServerConfig::Connect), &OnConnect);
194 g_disconnect = SwapSlot(object, KHook::GetVtableIndex(&ISource2ServerConfig::Disconnect), &OnDisconnect);
195 }
196 else if (std::strcmp(name, INTERFACEVERSION_SERVERGAMEDLL) == 0 && g_init == nullptr)
197 {
198 // The game's own factory: asking Metamod's loader for this would patch Init again.
199 g_start.ServerFactory = FactoryAt(*static_cast<void**>(object));
200 g_init = SwapSlot(object, KHook::GetVtableIndex(&ISource2Server::Init), &OnInit);
201 g_shutdown = SwapSlot(object, KHook::GetVtableIndex(&ISource2Server::Shutdown), &OnShutdown);
202 }
203}
204
205} // namespace VoltMod
206
207void* CreateInterface(const char* name, int* returnCode)
208{
210 if (next == nullptr)
211 {
212 if (returnCode != nullptr)
213 {
214 *returnCode = IFACE_FAILED;
215 }
216 return nullptr;
217 }
218
219 void* object = next(name, returnCode);
220 if (object != nullptr)
221 {
222 VoltMod::Patch(name, object);
223 }
224 return object;
225}
void * CreateInterface(const char *name, int *returnCode)
Definition Loader.cpp:207
The loader's KHook, which every module's hooks go through.
static void StartHost()
Definition Loader.cpp:119
static bool OnConnect(void *config, InterfaceFactory engineFactory)
Definition Loader.cpp:153
constexpr std::string_view HostFile
Definition Loader.hpp:18
static void OnDisconnect(void *config)
Definition Loader.cpp:181
static HostStopFn g_stopHost
Definition Loader.cpp:26
static InitReturnVal_t(* g_init)(void *server)
Definition Loader.cpp:31
static std::string LastError()
constexpr std::string_view PlatformDir
Definition Loader.hpp:17
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
void *(*)(const char *name, int *returnCode) InterfaceFactory
constexpr const char * HostStopName
static InterfaceFactory LoadNextServer()
Definition Loader.cpp:63
static void OnShutdown(void *server)
Definition Loader.cpp:174
std::filesystem::path OwnPath()
constexpr const char * HostStartName
static void(* g_disconnect)(void *config)
Definition Loader.cpp:30
static void(* g_shutdown)(void *server)
Definition Loader.cpp:32
static LoaderHandoff g_start
Definition Loader.cpp:25
static std::filesystem::path GameDir()
Definition Loader.cpp:46
std::vector< std::string > GameSearchPaths(std::string_view gameinfo)
The Game values under SearchPaths in a gameinfo.gi, in file order.
void(*)() HostStopFn
InterfaceFactory FactoryAt(const void *address)
void * OpenModule(const std::filesystem::path &path)
static KHookForwarder g_dispatcher
Definition Loader.cpp:23
static bool(* g_connect)(void *config, InterfaceFactory engineFactory)
Definition Loader.cpp:29
static std::string g_gameDir
Definition Loader.cpp:24
static void StopHost()
Definition Loader.cpp:166
static void * FindExport(void *handle, const char *name)
static InterfaceFactory FindNextFactory()
Definition Loader.cpp:108
constexpr std::string_view ServerFile
Definition Loader.hpp:22
bool(*)(const LoaderHandoff *start, char *error, size_t errorSize) HostStartFn
static InitReturnVal_t OnInit(void *server)
Definition Loader.cpp:159
static Fn SwapSlot(void *object, int index, Fn replacement)
Definition Loader.cpp:35
InterfaceFactory EngineFactory
const char * GameDir
the absolute csgo directory
InterfaceFactory ServerFactory
the game's own server library, never a loader in front of it
KHook::IKHook * HookDispatcher
the loader's KHook, which every module's hooks go through