VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
Plugin.cpp
Go to the documentation of this file.
2#include "Schema/Layout.hpp"
3
11#include <VoltMod/Runtime.hpp>
12#include <eiface.h>
13#include <engine/igameeventsystem.h>
14#include <exception>
15#include <format>
16#include <icvar.h>
17#include <interfaces/interfaces.h>
18#include <memory>
19#include <networksystem/inetworkmessages.h>
20#include <schemasystem/schemasystem.h>
21#include <string>
22#include <string_view>
23#include <tier1/convar.h>
24
25namespace VoltMod::Internal
26{
27
32
33bool PluginModule::Attach(IPluginContext& host, char* error, size_t errorSize) noexcept
34{
35 try
36 {
37 const bool loaded = AttachImpl(host, error, errorSize);
38 if (!loaded)
39 {
40 _host = nullptr;
41 }
42 return loaded;
43 }
44 catch (const std::exception& exception)
45 {
47 Shutdown();
48 _host = nullptr;
49 return false;
50 }
51 catch (...)
52 {
53 Strings::CopyToBuffer(error, errorSize, "plugin threw a non-standard exception during load");
54 Shutdown();
55 _host = nullptr;
56 return false;
57 }
58}
59
60using InterfaceLookup = void* (IPluginContext::*)(const char* version) const;
61
62template <class T>
63static Status Resolve(T*& field, const IPluginContext& host, InterfaceLookup lookup, const char* version)
64{
65 field = static_cast<T*>((host.*lookup)(version));
66 if (!field)
67 {
68 return std::unexpected(Error::NotReady(std::format("Could not find interface: {}", version)));
69 }
70 return {};
71}
72
73/** Resolve the engine interfaces and bind the host's gamedata. Fails when an interface is missing; a
74 * gamedata failure is kept in `GameData`, and the services that need it say so. */
76{
77 auto unsafe = std::make_unique<UnsafeServices>();
78 auto& gi = unsafe->Interfaces;
79
82 const Status resolved[] = {
83 Resolve(gi.ServerGameDLL, host, server, INTERFACEVERSION_SERVERGAMEDLL),
84 Resolve(gi.ServerGameClients, host, server, INTERFACEVERSION_SERVERGAMECLIENTS),
85 Resolve(gi.NetworkServerService, host, engine, NETWORKSERVERSERVICE_INTERFACE_VERSION),
86 Resolve(gi.GameEntities, host, server, INTERFACEVERSION_SERVERGAMEENTS),
88 Resolve(gi.GameEventSystem, host, engine, GAMEEVENTSYSTEM_INTERFACE_VERSION),
89 Resolve(gi.NetworkMessages, host, engine, NETWORKMESSAGES_INTERFACE_VERSION),
90 Resolve(gi.SchemaSystem, host, engine, SCHEMASYSTEM_INTERFACE_VERSION),
93 };
94 for (const Status& status : resolved)
95 {
96 if (!status)
97 {
98 return std::unexpected(status.error());
99 }
100 }
101
102 // Register pending tier1 ConCommands before the engine invokes ServerCommand instances.
103 g_pCVar = gi.CVar;
105
106 // The host read and scanned gamedata once for the process; this only takes the numbers.
107 if (IPluginGameData* gameData = host.GameData())
108 {
109 unsafe->GameData = unsafe->Bindings.Bind(
110 [gameData](GameDataSection sections, std::string_view name) { return gameData->Lookup(sections, name); });
111 }
112 else
113 {
114 unsafe->GameData = std::unexpected(Error::Engine("the host has no gamedata"));
115 }
116 return unsafe;
117}
118
119/** The host's one schema check covers this plugin only when both baked the same layout. */
121{
123 {
124 return std::unexpected(Error::Invalid(
125 std::format("this plugin was built against another schema layout (plugin {:016X}, host {:016X}); "
126 "rebuild it against this VoltMod",
128 }
129 if (!host.SchemaVerified())
130 {
131 return std::unexpected(Error::Invalid("the host found schema drift; its log names every field"));
132 }
133 return {};
134}
135
136bool PluginModule::AttachImpl(IPluginContext& host, char* error, size_t errorSize)
137{
139 _host = &host;
140
141 // The host owns the console and prefixes this plugin's log tag; services log while they are built.
142 Log::SetMinimumLevel(static_cast<LogLevel>(host.MinLogLevel()));
144 [&host](LogLevel level, std::string_view message) { host.WriteLog(static_cast<uint8_t>(level), message); });
145 SetBaseDir(host.BaseDir());
146
147 if (Status schema = CheckSchema(host); !schema)
148 {
149 return Refuse(std::format("SchemaLayout: {}", schema.error().Detail), error, errorSize);
150 }
151
152 auto unsafe = OpenUnsafe(host);
153 if (!unsafe)
154 {
155 return Refuse(unsafe.error().Detail, error, errorSize);
156 }
157 _unsafe = std::move(*unsafe);
158 _languages = std::make_unique<HostPlayerLanguages>(host.Languages());
159 _runtime = std::make_unique<Runtime>(host, *_unsafe, *_languages);
160
161 // Members that load settings record a required step, so a failure refuses the plugin before Load.
162 _plugin = _factory(*_runtime);
163 if (std::string reason = _runtime->LoadReport.AbortReason(); !reason.empty())
164 {
165 return Refuse(reason, error, errorSize);
166 }
167
168 SubscribeHostEvents();
169
170 if (!_plugin->Load())
171 {
172 std::string reason = _runtime->LoadReport.AbortReason();
173 return Refuse(reason.empty() ? "Load returned false" : reason, error, errorSize);
174 }
175
176 Log::Info("{}", _runtime->LoadReport.Summary());
177 return true;
178}
179
180bool PluginModule::Refuse(std::string_view reason, char* error, size_t errorSize)
181{
182 if (_runtime)
183 {
184 Log::Info("{}", _runtime->LoadReport.Summary());
185 }
187 Shutdown();
188 return false;
189}
190
192{
193 Shutdown();
194 _host = nullptr;
195}
196
197// Stop commands before destroying their captured state, then drop host events and services.
198void PluginModule::Shutdown() noexcept
199{
200 if (_runtime)
201 {
202 _runtime->Commands.RemoveAll();
203 }
204 _plugin.reset();
205 _hostEvents.Clear();
206 _runtime.reset();
207 _languages.reset();
208 _unsafe.reset();
209}
210
212{
213 try
214 {
215 _status = _runtime ? _runtime->Status.BuildJson() : std::string("{}");
216 return _status.c_str();
217 }
218 catch (...)
219 {
220 return "{}";
221 }
222}
223
224void PluginModule::SubscribeHostEvents()
225{
226 IPluginEvents& events = _host->Events();
227 auto keep = [&](uint64_t token) { _hostEvents.Add(HostSubscription(events, token)); };
228
229 keep(events.OnFrame(&HostCallback<&PluginModule::OnFrame>::Call, this));
230 keep(events.OnServerStartup(&HostCallback<&PluginModule::OnServerStartup>::Call, this));
231 keep(events.OnClientConnecting(&HostCallback<&PluginModule::OnClientConnecting>::Call, this));
232 keep(events.OnClientConnected(&HostCallback<&PluginModule::OnClientConnected>::Call, this));
233 keep(events.OnClientDisconnected(&HostCallback<&PluginModule::OnClientDisconnected>::Call, this));
234 keep(events.OnClientFullyConnected(&HostCallback<&PluginModule::OnClientFullyConnected>::Call, this));
235 keep(events.OnClientSettingsChanged(&HostCallback<&PluginModule::OnClientSettingsChanged>::Call, this));
236 keep(events.OnConsoleCommand(&HostCallback<&PluginModule::OnConsoleCommand>::Call, this));
237 keep(events.OnCheckTransmit(&HostCallback<&PluginModule::OnCheckTransmit>::Call, this));
238 keep(events.OnBuildGameSessionManifest(&HostCallback<&PluginModule::OnBuildGameSessionManifest>::Call, this));
239}
240
241void PluginModule::OnFrame()
242{
243 // `volt log` changes this; reading it once a frame lets the log helpers skip silenced lines.
244 Log::SetMinimumLevel(static_cast<LogLevel>(_host->MinLogLevel()));
245 _runtime->OnGameFrame();
246}
247
248void PluginModule::OnServerStartup(std::string_view mapName)
249{
250 Log::Info("Server startup: map '{}'.", mapName.empty() ? std::string_view("<none>") : mapName);
251 _runtime->Map.SetCurrent(std::string(mapName));
252 _runtime->Entities.OnServerStartup();
253 _runtime->GameEvents.OnServerStartup();
254 _runtime->ClientConVars.OnServerStartup();
255 _runtime->Map.Started.Raise(mapName);
256}
257
258bool PluginModule::OnClientConnecting(int slot, int64_t steamId, std::string_view name, char* reason, size_t reasonSize)
259{
260 ConnectRequest request{.Slot = slot, .SteamId = steamId, .Name = name};
261 _runtime->Players.Connecting.Raise(request);
262 if (request.Rejected)
263 {
265 }
266 return request.Rejected;
267}
268
269void PluginModule::OnClientConnected(int slot, int64_t steamId, std::string_view name, std::string_view address)
270{
271 _runtime->Players.Add(slot, steamId, std::string(name), std::string(address));
272}
273
274void PluginModule::OnClientDisconnected(int slot)
275{
276 _runtime->Players.Remove(slot);
277}
278
279void PluginModule::OnClientFullyConnected(int slot)
280{
281 _runtime->Players.OnClientFullyConnected(slot);
282}
283
284void PluginModule::OnClientSettingsChanged(int slot)
285{
286 _runtime->Players.OnClientSettingsChanged(slot);
287}
288
289void PluginModule::OnCheckTransmit(CCheckTransmitInfo** infoList, int infoCount)
290{
291 _runtime->Visibility.OnCheckTransmit(infoList, infoCount);
292}
293
294void PluginModule::OnBuildGameSessionManifest(IEntityResourceManifest* manifest)
295{
296 _runtime->Precache.WriteTo(*manifest);
297}
298
299} // namespace VoltMod::Internal
const char * StatusJson() noexcept
Definition Plugin.cpp:211
bool Attach(IPluginContext &host, char *error, size_t errorSize) noexcept
Definition Plugin.cpp:33
static void CopyToBuffer(char *buffer, std::size_t size, std::string_view text) noexcept
Definition Strings.cpp:257
void Add(Subscription subscription)
KHook::IKHook * __exported__khook
Definition Host.cpp:30
static Status CheckSchema(const IPluginContext &host)
Definition Plugin.cpp:120
static Status Resolve(T *&field, const IPluginContext &host, InterfaceLookup lookup, const char *version)
Definition Plugin.cpp:63
static Result< std::unique_ptr< UnsafeServices > > OpenUnsafe(IPluginContext &host)
Definition Plugin.cpp:75
Subscription HostSubscription(IPluginEvents &events, uint64_t token)
void *(IPluginContext::*)(const char *version) const InterfaceLookup
Definition Plugin.cpp:60
void Info(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:79
void SetHandler(Handler handler)
Definition Logger.cpp:26
void SetMinimumLevel(LogLevel level)
Definition Logger.cpp:37
uint64_t GeneratedLayoutStamp()
A hash of everything GeneratedLayout holds.
Definition Layout.cpp:122
void SetBaseDir(const std::filesystem::path &baseDir)
Set the base directory for path resolution (the host's csgo directory). Must be called during initial...
Definition Paths.cpp:9
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
LogLevel
Definition Log.hpp:13
std::expected< void, Error > Status
Definition Result.hpp:67
std::expected< T, Error > Result
Definition Result.hpp:64
static Error Invalid(std::string detail)
Definition Result.hpp:51
static Error Engine(std::string detail)
Definition Result.hpp:58
static Error NotReady(std::string detail)
Definition Result.hpp:50
virtual IPluginGameData * GameData() const =0
virtual uint64_t SchemaLayoutStamp() const =0
virtual IPluginEvents & Events()=0
virtual KHook::IKHook * HookDispatcher() const =0
virtual bool SchemaVerified() const =0
virtual void * ServerInterface(const char *version) const =0
virtual void WriteLog(uint8_t level, std::string_view text)=0
virtual std::string_view BaseDir() const =0
virtual uint8_t MinLogLevel() const =0
virtual void * EngineInterface(const char *version) const =0
virtual IPluginLanguages & Languages()=0