44 =welder::weld_as(
"WMO"),
47 A whole world map object, abstract over the client version — the root
48 file and all its group files as one entity. Construct the concrete
49 version with WMO.for_version(expansion), then read()/write(); the
50 per-version WMO* classes are subclasses. See https://wowdev.wiki/WMO.)")
68 template <ClientVersion V>
72 A whole world map object for one client version: the root file and all
73 its group files as one entity. Group files are located by GFID (Legion+
74 clients) or the "{root}_NNN.wmo" naming convention. An entity read from
75 a client and left unmodified rewrites byte-for-byte. See
76 https://wowdev.wiki/WMO.)")
80 [[=welder::doc(
"The root file contents.")]]
83 [[=welder::doc(
"The group files, in group order."), =
84 welder::mark::no_reassign]]
85 std::vector<WMOGroup<V>>
groups;
96 =welder::doc(
"Load the WMO and all its group files from a client "
97 "filesystem, replacing this entity's contents.")]]
99 const FileKey& key [[=welder::doc(
"the root file identity (path and/or FileDataID)")]]);
108 [[=welder::mark::exclude]]
109 Result<void> read(std::span<const std::byte> rootData, std::span<
const std::span<const std::byte>> groupDatas);
113 "Serialize and store the WMO (root and every group) through "
114 "the filesystem's project overlay; group file names are "
115 "derived from the root key, which must resolve to a path.")]]
117 const FileKey& key [[=welder::doc(
"the root file identity; must resolve to a path")]])
const;
125 Check the logical integrity contracts this object must satisfy to LOAD
126 in the client — across the root file AND every group file — which
127 write() deliberately never enforces. Call it before writing when you
128 want to know the files will load. An object read from a client and left
129 unmodified reports no errors; warnings mark states real client files
131 =welder::returns(R"(every violated contract, each with its member path
132 ("root..." / "groups[i]..."))")]]
137 "Validate and raise on the first error instead of returning "
138 "a report — the assert-style face of validate()."),
139 =welder::returns(
"nothing; raises when validate() finds any error")]]
151 static std::string _groupPath(std::string_view rootPath, std::size_t index);
159 _checkMver(std::uint32_t mver, std::string_view which);
166 template <ClientVersion V>
177 template <ClientVersion V>
178 std::string detail::WMO<V>::_groupPath(std::string_view rootPath, std::size_t index) {
179 std::string_view stem = rootPath;
180 if (stem.ends_with(
".wmo")) stem.remove_suffix(4);
181 return std::format(
"{}_{:03}.wmo", stem, index);
184 template <ClientVersion V>
186 ValidationReport report;
188 const std::size_t mark = report.size();
190 report.prefixFrom(mark,
"root");
192 for (std::size_t i = 0; i < groups.size(); ++i) {
193 const std::size_t mark = report.size();
195 report.prefixFrom(mark, std::format(
"groups[{}]", i));
199 if (
root.groupInfos.size() != groups.size())
201 std::format(
"count {} != {} group files held",
root.groupInfos.size(), groups.size()));
205 if constexpr (
requires {
root.groupFdids; })
206 if (!groups.empty() && !
root.groupFdids.empty()) {
207 if (
root.groupFdids.size() < groups.size())
208 report.addError(
"root.group_fdids",
209 std::format(
"count {} < {} group files held",
root.groupFdids.size(), groups.size()));
210 else if (
root.groupFdids.size() % groups.size() != 0)
211 report.addWarning(
"root.group_fdids",
213 "count {} is not a whole multiple of the {} groups " "(LOD tables repeat per level)",
214 root.groupFdids.size(), groups.size()));
217 constexpr std::size_t maxReported = 8;
218 const std::size_t materialCount =
root.materials.size();
219 for (std::size_t i = 0; i < groups.size(); ++i) {
220 const auto& body = groups[i].body;
221 const std::string prefix = std::format(
"groups[{}].body", i);
225 template for (
constexpr auto m : BodyMembers) {
229 static_assert(target != std::meta::info{},
"indexes_in_root names no member of the root entity");
230 constexpr const char* ident = std::define_static_string(std::meta::identifier_of(m));
232 std::format(
"{}.{}", prefix, ident), ir->view(), report);
237 if (body.header.portalCount > 0 && body.header.portalStart + body.header.portalCount >
root.portalRefs.size())
238 report.addError(std::format(
"{}.header", prefix),
239 std::format(
"portal range [{}, {}) overruns the {} portal references",
240 body.header.portalStart, body.header.portalStart + body.header.portalCount,
241 root.portalRefs.size()));
245 std::size_t badPolys = 0;
246 for (std::size_t j = 0; j < body.polys.size(); ++j)
247 if (body.polys[j].materialId != 0xFF && body.polys[j].materialId >= materialCount)
248 if (++badPolys <= maxReported)
249 report.addError(std::format(
"{}.polys[{}]", prefix, j),
250 std::format(
"material {} out of range: {} materials", body.polys[j].materialId,
252 if (badPolys > maxReported)
253 report.addError(std::format(
"{}.polys", prefix),
254 std::format(
"... and {} more unresolvable materials", badPolys - maxReported));
255 for (std::size_t j = 0; j < body.batches.size(); ++j) {
256 const auto& batch = body.batches[j];
257 std::size_t material = batch.materialId;
258 if constexpr (
requires { batch.materialIdLarge; })
259 if ((batch.flags & 0x2) != 0) material = batch.materialIdLarge;
260 if (material >= materialCount)
261 report.addError(std::format(
"{}.batches[{}]", prefix, j),
262 std::format(
"material {} out of range: {} materials", material, materialCount));
273 template <ClientVersion V>
275 return validate().toResult();
278 template <ClientVersion V>
279 Result<void> detail::WMO<V>::_checkMver(std::uint32_t mver, std::string_view which) {
282 std::format(
"{} MVER is {}, expected {}", which, mver,
WmoVersionV17));
286 template <ClientVersion V>
288 std::span<
const std::span<const std::byte>> groupDatas) {
292 if (
auto r =
root.read(rootData); !r)
return std::unexpected{r.error()};
293 if (
auto r = _checkMver(root.
mver,
"root"); !r)
return std::unexpected{r.error()};
295 groups.reserve(groupDatas.size());
296 for (std::size_t i = 0; i < groupDatas.size(); ++i) {
298 if (
auto r = group.read(groupDatas[i]); !r)
299 return makeError(r.error().code, std::format(
"group {}: {}", i, r.error().message), r.error().nativeError);
300 if (
auto r = _checkMver(group.mver, std::format(
"group {}", i)); !r)
return std::unexpected{r.error()};
301 groups.push_back(std::move(group));
306 template <ClientVersion V>
308 const auto rootData = fs.readFile(key);
309 if (!rootData)
return std::unexpected{rootData.error()};
314 if (
auto r =
root.read(*rootData); !r)
return std::unexpected{r.error()};
315 if (
auto r = _checkMver(
root.mver,
"root"); !r)
return std::unexpected{r.error()};
319 const std::size_t nGroups =
root.groupInfos.size();
324 if constexpr (
requires {
root.groupFdids; }) byFdid =
root.groupFdids.size() >= nGroups;
326 std::string rootPath;
331 "group files need the root path (no GFID chunk and the root " "key has no resolvable path)");
332 rootPath = *resolved.
path;
335 groups.reserve(nGroups);
336 for (std::size_t i = 0; i < nGroups; ++i) {
338 if constexpr (
requires {
root.groupFdids; })
340 return FileKey{_groupPath(rootPath, i)};
342 const auto groupData = fs.readFile(groupKey);
344 return makeError(groupData.error().code, std::format(
"group {}: {}", i, groupData.error().message),
345 groupData.error().nativeError);
348 if (
auto r =
group.read(*groupData); !r)
349 return makeError(r.error().code, std::format(
"group {}: {}", i, r.error().message), r.error().nativeError);
350 if (
auto r = _checkMver(
group.mver, std::format(
"group {}", i)); !r)
return std::unexpected{r.error()};
351 groups.push_back(std::move(
group));
356 template <ClientVersion V>
358 const FileKey resolved = fs.resolve(key);
363 if (
root.groupInfos.size() != groups.size())
366 "the MOGI group-info table holds {} records but {} group "
367 "files are baked in — every group needs its info record",
root.groupInfos.size(),
370 const auto rootData =
root.write();
371 if (!rootData)
return std::unexpected{rootData.error()};
372 if (
auto r = fs.addFile(*resolved.path, *rootData); !r)
return std::unexpected{r.error()};
374 for (std::size_t i = 0; i < groups.size(); ++i) {
375 const auto groupData = groups[i].write();
376 if (!groupData)
return std::unexpected{groupData.error()};
377 if (
auto r = fs.addFile(_groupPath(*resolved.path, i), *groupData); !r)
378 return makeError(r.error().code, std::format(
"group {}: {}", i, r.error().message), r.error().nativeError);
Client version identity, the flavor axis that separates a client's CONTENT version from the engine ge...
The error-handling vocabulary: ErrorCode, Error and the Result<T> alias every fallible wowlib operati...
File identity types: the strong FileDataID and the FileKey a read request travels as.
The runtime facade over the static compositions — the primary welder binding surface of the fs layer.
The WMO group-file entities (namespace wowlib::formats::wmo::group): the MOGP container body (geometr...
Binding-language identities welder's core does not name.
#define WOWLIB_CS_FAMILY_SURFACE
The C# rod's family-surface opt-in, spellable in every build.
constexpr welder::lang Cs
C#/.NET — the welder-csharp rod's identity (user-range slot 0), respelled for wowlib's annotation sit...
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
@ InvalidEntityState
An entity's members disagree (e.g.
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
@ FormatVersionMismatch
The file's version chunk disagrees with the requested version.
std::optional< std::string > path
The canonical client-internal path, if known.
The WMO root-file entity (namespace wowlib::formats::wmo::root): header, materials,...