VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
GameDataScanner.cpp
Go to the documentation of this file.
2
5
7#include <algorithm>
8#include <cstdint>
9#include <format>
10#include <ranges>
11
12namespace VoltMod
13{
14
15static std::unexpected<Error> Unbound(std::string reason)
16{
17 return std::unexpected(Error::NotFound(std::move(reason)));
18}
19
20static uint64_t Rva(const Image& module, const void* address)
21{
22 return static_cast<uint64_t>(static_cast<const uint8_t*>(address) - module.Base);
23}
24
25static Result<ScanResult> Scan(std::string_view moduleName, const std::string& pattern)
26{
27 if (pattern.empty())
28 {
29 return Unbound("empty pattern");
30 }
31
33 if (!match.Module.Base)
34 {
35 return Unbound(std::format("module '{}' is not loaded", moduleName));
36 }
37 if (!match.Address)
38 {
39 return Unbound("pattern not found");
40 }
41 if (!match.Unique)
42 {
43 return Unbound("pattern matched more than once");
44 }
45 return match;
46}
47
48/** A non-negative number from this platform's column, or why there is none. */
49template <class TEntry>
50static Result<int> ColumnValue(const TEntry& entry, std::string_view what)
51{
52 const auto& value = PlatformColumn(entry);
53 if (!value)
54 {
55 return Unbound(std::format("no {} {}", PlatformName, what));
56 }
57 if (*value < 0)
58 {
59 return Unbound(std::format("{} {} is negative", what, *value));
60 }
61 return *value;
62}
63
64static const void* OriginalSlot(void* table, int index, const OriginalSlotLookup& originalOf)
65{
66 void** slots = static_cast<void**>(table);
67 const void* original = originalOf ? originalOf(slots, index) : nullptr;
68 return original ? original : slots[index];
69}
70
73 : _file(file), _originalOf(originalOf), _scriptOf(scriptOf)
74{
75 const auto index = [this](const auto& section, GameDataSection kind, std::string_view name) {
76 for (const auto& key : section | std::views::keys)
77 {
78 GameDataEntry& entry = _entries[key];
79 entry.Sections.push_back(name);
80 // A key in two sections binds from neither, so it has no kind to resolve from.
81 entry.Kind = entry.Sections.size() == 1 ? kind : GameDataSection{};
82 }
83 };
84 index(file.functions, GameDataSection::Function, "functions");
85 index(file.globals, GameDataSection::Global, "globals");
86 index(file.vtables, GameDataSection::VTable, "vtables");
87 index(file.offsets, GameDataSection::Offset, "offsets");
88
89 for (auto& [key, entry] : _entries)
90 {
91 if (entry.Sections.size() > 1)
92 {
93 Fail(key, entry,
94 Error::NotFound(std::format("in both '{}' and '{}'", entry.Sections[0], entry.Sections[1])));
95 }
96 }
97}
98
100{
101 for (auto& [key, entry] : _entries)
102 {
103 if (!entry.Reason.empty())
104 {
105 continue;
106 }
107 if (const Result<Bound> bound = Locate(entry.Kind, key))
108 {
109 entry.Address = bound->Address;
110 entry.Value = bound->Value;
111 }
112 else
113 {
114 Fail(key, entry, bound.error());
115 }
116 }
117}
118
119const GameDataEntry* GameDataScanner::Find(std::string_view key) const
120{
121 const auto it = _entries.find(key);
122 return it != _entries.end() ? &it->second : nullptr;
123}
124
125void GameDataScanner::LogSummary(std::string_view path) const
126{
127 Log::Info("GameData: {} (server {}, verified {}): {} functions, {} globals, {} vtables, {} offsets.", path,
128 _file.build.server, _file.build.verified, _file.functions.size(), _file.globals.size(),
129 _file.vtables.size(), _file.offsets.size());
130
131 // Patterns, RTTI and VScript are checked at load; hand-written numbers are not.
132 if (_file.build.server != GameBuild())
133 {
134 const auto indices =
135 std::ranges::count_if(_file.vtables, [](const auto& each) { return each.second.Script.empty(); });
136 const auto numbered =
137 std::ranges::count_if(_file.offsets, [](const auto& each) { return each.second.Base.empty(); });
138 Log::Warn("GameData: verified on server {}, running {}: {} vtable indices and {} offsets are unchecked.",
140 }
141}
142
143Result<GameDataScanner::Bound> GameDataScanner::Locate(GameDataSection kind, const std::string& key)
144{
145 const auto address = [](void* found) { return Bound{.Address = found}; };
146 switch (kind)
147 {
149 return FindFunction(key).transform(address);
151 return FindGlobal(key).transform(address);
153 return FindSlot(key).transform(
154 [](VirtualSlot slot) { return Bound{.Address = slot.Table, .Value = slot.Index}; });
156 return FindOffset(key).transform([](int value) { return Bound{.Value = value}; });
157 }
158 return Unbound("in no section");
159}
160
161void GameDataScanner::Fail(std::string_view key, GameDataEntry& entry, const Error& error)
162{
163 entry.Reason = error.Detail;
164 _failures.push_back(std::format("{}: {}", key, entry.Reason));
165}
166
167Result<void*> GameDataScanner::FindFunction(const std::string& key)
168{
169 const GameDataDocument::Function& entry = _file.functions.at(key);
170 if (!entry.Script.empty())
171 {
172 return FindScriptFunction(key, entry);
173 }
174
175 const auto& pattern = PlatformColumn(entry);
176 if (!pattern)
177 {
178 return Unbound(std::format("no {} pattern", PlatformName));
179 }
180
181 const auto match = Scan(entry.Module, *pattern);
182 if (!match)
183 {
184 return std::unexpected(match.error());
185 }
186
187 _record.Functions.emplace(key, GameDataRecord::Location{entry.Module, Rva(match->Module, match->Address)});
188 return match->Address;
189}
190
191Result<void*> GameDataScanner::FindScriptFunction(const std::string& key, const GameDataDocument::Function& entry)
192{
193 const auto module = _symbols.Module(entry.Module);
194 if (!module)
195 {
196 return std::unexpected(module.error());
197 }
198 const auto table = _symbols.Table(entry.Module, entry.Class);
199 if (!table)
200 {
201 return std::unexpected(table.error());
202 }
203 const auto target = FindScript(*table, entry.Script);
204 if (!target)
205 {
206 return std::unexpected(target.error());
207 }
208 if (!(*module)->Contains(target->Address))
209 {
210 return Unbound(std::format("VScript '{}' is virtual or outside '{}'", entry.Script, entry.Module));
211 }
212
213 _record.Functions.emplace(key, GameDataRecord::Location{entry.Module, Rva(**module, target->Address)});
214 return target->Address;
215}
216
217Result<void*> GameDataScanner::FindGlobal(const std::string& key)
218{
219 const GameDataDocument::Global& entry = _file.globals.at(key);
220 const auto& column = PlatformColumn(entry);
221 if (!column)
222 {
223 return Unbound(std::format("no {} pattern", PlatformName));
224 }
225
226 const auto match = Scan(entry.Module, column->pattern);
227 if (!match)
228 {
229 return std::unexpected(match.error());
230 }
231
232 const uintptr_t target =
233 ResolveRelativeAddress(match->Module, reinterpret_cast<uintptr_t>(match->Address), column->rel32At);
234 auto* global = reinterpret_cast<void*>(target);
235 if (!target || !match->Module.Contains(global) || !IsReadableAddress(global, sizeof(void*)))
236 {
237 return Unbound(
238 std::format("the rel32 at +{} does not point at readable memory in '{}'", column->rel32At, entry.Module));
239 }
240
241 _record.Globals.emplace(key, GameDataRecord::Location{entry.Module, Rva(match->Module, global)});
242 return global;
243}
244
245Result<VirtualSlot> GameDataScanner::FindSlot(const std::string& key)
246{
247 const GameDataDocument::VTable& entry = _file.vtables.at(key);
248 // A `script` entry's index needs the class table, so it is read once the table is found.
249 Result<int> index = entry.Script.empty() ? ColumnValue(entry, "index") : -1;
250 if (!index)
251 {
252 return std::unexpected(index.error());
253 }
254
255 const auto module = _symbols.Module(entry.Module);
256 if (!module)
257 {
258 return std::unexpected(module.error());
259 }
260 const auto table = _symbols.Table(entry.Module, entry.Class, entry.Base);
261 if (!table)
262 {
263 return std::unexpected(table.error());
264 }
265 if (!entry.Script.empty())
266 {
267 index = ScriptSlot(*table, entry.Script);
268 if (!index)
269 {
270 return std::unexpected(index.error());
271 }
272 }
273
274 // A short table may end before the index, so check the slot is readable first.
275 void** slot = static_cast<void**>(*table) + *index;
276 const void* code = IsReadableAddress(slot, sizeof(void*)) ? OriginalSlot(*table, *index, _originalOf) : nullptr;
278 {
279 const std::string& owner = entry.Base.empty() ? entry.Class : entry.Base;
280 return Unbound(std::format("{}::[{}] does not hold code", owner, *index));
281 }
282
283 // A hook trampoline can live outside the module, so it has no module-relative address.
284 const Image& loaded = **module;
285 const uint64_t codeRva = loaded.Contains(code) ? Rva(loaded, code) : 0;
286 _record.VTables.emplace(key, GameDataRecord::Slot{entry.Module, Rva(loaded, *table), *index, codeRva});
287 return VirtualSlot{.Index = *index, .Table = *table};
288}
289
290Result<ScriptTarget> GameDataScanner::FindScript(void* table, const std::string& name) const
291{
292 if (!_scriptOf)
293 {
294 return Unbound("no VScript lookup");
295 }
296 return _scriptOf(table, name);
297}
298
299Result<int> GameDataScanner::ScriptSlot(void* table, const std::string& name) const
300{
301 const auto target = FindScript(table, name);
302 if (!target)
303 {
304 return std::unexpected(target.error());
305 }
306 if (target->Index < 0)
307 {
308 return Unbound(std::format("VScript '{}' is not virtual", name));
309 }
310 return target->Index;
311}
312
313Result<int> GameDataScanner::FindOffset(const std::string& key)
314{
315 const GameDataDocument::Offset& entry = _file.offsets.at(key);
316 Result<int> offset = entry.Base.empty() ? ColumnValue(entry, "offset") : FindBaseOffset(entry);
317 if (offset)
318 {
319 _record.Offsets.emplace(key, *offset);
320 }
321 return offset;
322}
323
324Result<int> GameDataScanner::FindBaseOffset(const GameDataDocument::Offset& entry)
325{
326 if (entry.Class.empty())
327 {
328 return Unbound(std::format("base '{}' names no class", entry.Base));
329 }
330 return _symbols.Base(entry.Module, entry.Class, entry.Base).transform([](const BaseSubobject& base) {
331 return base.Offset;
332 });
333}
334
335} // namespace VoltMod
GameDataScanner(const GameDataDocument &file, const OriginalSlotLookup &originalOf, const ScriptBindingLookup &scriptOf)
void LogSummary(std::string_view path) const
const GameDataEntry * Find(std::string_view key) const
Result< BaseSubobject > Base(const std::string &moduleName, const std::string &className, const std::string &baseName)
Result< void * > Table(const std::string &moduleName, const std::string &className, const std::string &baseName={})
void Info(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:79
void Warn(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:88
ScanResult FindPatternEx(std::string_view moduleName, const std::string &pattern)
std::function< Result< ScriptTarget >(void *classTable, std::string_view name)> ScriptBindingLookup
static const void * OriginalSlot(void *table, int index, const OriginalSlotLookup &originalOf)
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
bool IsExecutableAddress(const void *address)
bool IsReadableAddress(const void *address, size_t bytes)
const auto & PlatformColumn(const TEntry &entry)
uintptr_t ResolveRelativeAddress(const Image &module, uintptr_t matchAddress, int ripOffset, int ripSize)
static std::unexpected< Error > Unbound(std::string reason)
std::string_view GameBuild()
Definition GameBuild.cpp:10
std::function< const void *(void **vtable, int index)> OriginalSlotLookup
static uint64_t Rva(const Image &module, const void *address)
static Result< int > ColumnValue(const TEntry &entry, std::string_view what)
std::expected< T, Error > Result
Definition Result.hpp:64
constexpr std::string_view PlatformName
static Result< ScanResult > Scan(std::string_view moduleName, const std::string &pattern)
static Error NotFound(std::string detail)
Definition Result.hpp:49
std::string server
steam.inf ServerVersion the entries were verified on.
std::string verified
YYYY-MM-DD of the last full review.
std::map< std::string, Function > functions
std::map< std::string, Global > globals
std::map< std::string, VTable > vtables
std::map< std::string, Offset > offsets
std::map< std::string, Location > Functions
std::map< std::string, Location > Globals
std::map< std::string, int > Offsets
std::map< std::string, Slot > VTables