VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
SchemaCheck.cpp
Go to the documentation of this file.
2
7#include "Schema/Layout.hpp"
8
10#include <VoltMod/Core/Log.hpp>
13#include <entity2/entityclass.h>
14#include <entity2/entitysystem.h>
15#include <filesystem>
16#include <format>
17#include <fstream>
18#include <interfaces/interfaces.h>
19#include <schemasystem/schemasystem.h>
20#include <string>
21#include <string_view>
22#include <vector>
23
24namespace VoltMod::Schema
25{
26
27/** Output consumed by `voltmod framework schemagen`. */
28static constexpr std::string_view DumpPath = "addons/voltmod/gamedata/dumps/schema.json";
29
30/** The build stamp near the top of the dump at @p path, or empty. */
31static std::string DumpedBuild(const std::filesystem::path& path)
32{
33 std::ifstream file(path, std::ios::binary);
34 if (!file.is_open())
35 {
36 return {};
37 }
38
39 std::string head(256, '\0');
40 file.read(head.data(), static_cast<std::streamsize>(head.size()));
41 head.resize(static_cast<size_t>(file.gcount()));
42
43 constexpr std::string_view key = "\"build\":";
44 const size_t at = head.find(key);
45 if (at == std::string::npos)
46 {
47 return {};
48 }
49
50 const size_t open = head.find('"', at + key.size());
51 const size_t close = open == std::string::npos ? std::string::npos : head.find('"', open + 1);
52 return close == std::string::npos ? std::string() : head.substr(open + 1, close - open - 1);
53}
54
56{
57 if (!schema || !schema->SchemaSystemIsReady())
58 {
59 return nullptr;
60 }
61 const std::string moduleName = PlatformModuleName("server");
62 return schema->FindTypeScopeForModule(moduleName.c_str());
63}
64
66{
67 static bool attempted = false; // one try per process; the build stamp prevents repeats
68 if (attempted || !entities)
69 {
70 return;
71 }
72
73 CSchemaSystemTypeScope* server = ServerScope(schema);
74 const std::filesystem::path output = ResolvePath(DumpPath);
75 if (!server || DumpedBuild(output) == GameBuild())
76 {
77 return;
78 }
79
80 // One serializer database covers every networked class.
81 const CEntityClass* entityClass = entities->FindClassByName("CBaseEntity");
82 const CNetworkSerializerClassInfo* serializer = entityClass ? entityClass->m_NetworkSerializerInfo : nullptr;
83 if (!serializer || !serializer->m_pDatabase)
84 {
85 return;
86 }
87
88 attempted = true;
89 const Status written =
90 WriteDumpFile(schema->GlobalTypeScope(), server, *serializer->m_pDatabase, DumpPath, GameBuild());
91 if (!written)
92 {
93 Log::Warn("Schema: no dump written to {}: {}", output.string(), written.error().Detail);
94 return;
95 }
96 Log::Info("Schema: dumped game build {} to {}.", GameBuild(), output.string());
97}
98
100{
101 CSchemaSystemTypeScope* server = ServerScope(schema);
102 if (!server)
103 {
104 return std::unexpected(Error::NotReady("the server schema scope is not ready"));
105 }
106 CSchemaSystemTypeScope* global = schema->GlobalTypeScope();
107
108 // Report all mismatches so one shifted class does not hide the rest.
109 std::vector<std::string> drift;
110 for (const FieldLayout& want : GeneratedLayout())
111 {
112 const std::string className(want.Class);
113 CSchemaClassInfo* live = server->FindDeclaredClass(className.c_str()).Get();
114 if (!live && global)
115 {
116 live = global->FindDeclaredClass(className.c_str()).Get();
117 }
118
119 const SchemaClassFieldData_t* found = live ? FindField(live, want.Field) : nullptr;
120 if (!found)
121 {
122 drift.push_back(std::format("{}::{}: gone", want.Class, want.Field));
123 continue;
124 }
125
126 if (found->m_nSingleInheritanceOffset != want.Offset)
127 {
128 drift.push_back(std::format("{}::{}: offset {} -> {}", want.Class, want.Field, want.Offset,
129 found->m_nSingleInheritanceOffset));
130 continue;
131 }
132
133 int size = 0;
134 uint8_t alignment = 0;
135 if (found->m_pType && found->m_pType->GetSizeAndAlignment(size, alignment) && size != want.Size)
136 {
137 drift.push_back(std::format("{}::{}: size {} -> {}", want.Class, want.Field, want.Size, size));
138 }
139 }
140
141 if (drift.empty())
142 {
143 return {};
144 }
145
146 std::string message = std::format(
147 "schema drift (accessors generated from game build {}, server is {}); "
148 "load a plugin into a running map to write the dump, then regenerate "
149 "with voltmod framework schemagen:",
151 for (const std::string& line : drift)
152 {
153 message += std::format("\n {}", line);
154 }
155 return std::unexpected(Error::Invalid(message));
156}
157
158} // namespace VoltMod::Schema
159
160namespace VoltMod
161{
162
163static constexpr std::string_view EntitySystemOffset = "GameEntitySystem";
164
166 : _registry(registry), _schema(schema), _resources(resources)
167{
168 // Where the entity system sits inside the resource service, which the dump needs.
169 if (IPluginGameData* gameData = registry.GameData())
170 {
172 _entitySystemOffset = entry.Found ? entry.Value : -1;
173 }
174
175 Check();
176}
177
179{
180 // The first map is the next chance when the schema scope was not there at host load.
181 if (!_schemaLoaded)
182 {
183 Check();
184 }
185 Schema::WriteSchemaDump(_schema, Entities());
186}
187
188void SchemaCheck::Check()
189{
190 const Status verified = Schema::VerifySchemaLayout(_schema);
191 _schemaLoaded = verified || verified.error().Code != ErrorCode::NotReady;
192 if (!verified)
193 {
194 Log::Error("Schema: {}", verified.error().Detail);
195 }
196 else
197 {
198 Log::Info("Schema: the generated layout matches game build {}.", Schema::GeneratedFromBuild());
199 }
200
201 _registry.SetSchemaLayout(Schema::GeneratedLayoutStamp(), verified.has_value());
202}
203
204CGameEntitySystem* SchemaCheck::Entities() const
205{
206 if (_resources == nullptr || _entitySystemOffset < 0)
207 {
208 return nullptr;
209 }
210 return ReadAt<CGameEntitySystem*>(_resources, _entitySystemOffset);
211}
212
213} // namespace VoltMod
Typed read/write of engine fields at a base pointer + byte offset.
The loaded plugins and the engine events raised on them, one per process.
void SetSchemaLayout(uint64_t stamp, bool verified)
SchemaCheck(PluginRegistry &registry, ISchemaSystem *schema, IGameResourceService *resources)
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
void Warn(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:88
static CSchemaSystemTypeScope * ServerScope(ISchemaSystem *schema)
const SchemaClassFieldData_t * FindField(const CSchemaClassInfo *klass, std::string_view field)
Status VerifySchemaLayout(ISchemaSystem *schema)
static std::string DumpedBuild(const std::filesystem::path &path)
static constexpr std::string_view DumpPath
std::string_view GeneratedFromBuild()
Definition Layout.cpp:117
Status WriteDumpFile(CSchemaSystemTypeScope *global, CSchemaSystemTypeScope *server, const CNetworkSerializerCodeGenDatabase &network, std::string_view path, std::string_view gameBuild)
uint64_t GeneratedLayoutStamp()
A hash of everything GeneratedLayout holds.
Definition Layout.cpp:122
std::span< const FieldLayout > GeneratedLayout()
Definition Layout.cpp:112
void WriteSchemaDump(ISchemaSystem *schema, CGameEntitySystem *entities)
std::filesystem::path ResolvePath(std::string_view relativePath)
a relative path against the base directory. If the path is already absolute, returns it as-is.
Definition Paths.cpp:14
static constexpr std::string_view EntitySystemOffset
@ NotReady
It exists but the subsystem behind it is not usable yet.
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
std::string_view GameBuild()
Definition GameBuild.cpp:10
std::expected< void, Error > Status
Definition Result.hpp:67
std::string PlatformModuleName(std::string_view moduleName)
static Error Invalid(std::string detail)
Definition Result.hpp:51
static Error NotReady(std::string detail)
Definition Result.hpp:50