39 using namespace wowlib::formats::wmo::root::chunks;
52 =welder::weld_as(
"WMORoot"),
55 A WMO root file, abstract over the client version. The root file lists the
56 object's shared data (materials, doodads, portals, lights, fog and the
57 group table); the geometry lives in the separate group files. Construct a
58 concrete version with WMORoot.for_version(expansion); the per-version
59 WMORoot* classes are subclasses. See https://wowdev.wiki/WMO.)")
75 =welder::mark::no_reassign,
76 =welder::doc(R
"(Group file FileDataIDs (GFID, Legion+), in group order;
77 repeated per LOD level for LOD WMOs.)")]]
88 =welder::mark::no_reassign,
90 R
"(Texture-coordinate translation animations (MOUV, 7.3+), one
106 R
"(Texture filenames (MOTX); the material texture fields index
107 into this block. Pre-8.1 it is the primary reference; in
108 8.1/8.2 its presence marks the fallback mode (MOMT texture
109 fields are MOTX offsets, not FileDataIDs). Removed at 8.3,
110 when the client dropped name-based texture loading.)")]]
118 R
"(Doodad (M2) filenames (MODN); MODD entries index into this
119 block. Pre-8.1 primary, 8.1/8.2 fallback (see textures).
120 Removed at 8.3 alongside MOTX; MODI (doodad FileDataIDs)
131 =welder::mark::no_reassign,
132 =welder::doc(
"Skybox FileDataID (MOSI, 8.1+).")]]
139 =welder::mark::no_reassign,
141 "Doodad FileDataIDs (MODI, 8.1+; replaces doodad_names).")]]
152 =welder::mark::no_reassign,
154 R
"(Per-doodad color multipliers (MDDI, 8.3+), applied to the
163 R
"(Particulate volume data (MPVD, 8.3+); undocumented layout,
171 =welder::mark::no_reassign,
173 R
"(Global ambient volumes (MAVG, 8.3+); position and radii are
174 zero, selected by doodad set.)")]]
181 =welder::mark::no_reassign,
183 R
"(Ambient volumes (MAVD, 8.3+), overriding the header ambient
184 color inside their radius.)")]]
191 =welder::mark::no_reassign,
193 R
"(Box ambient volumes (MBVD, 8.3+); read only when MAVG/MAVD is
205 =welder::mark::no_reassign,
206 =welder::doc(
"Fog extra data (MFED, 9.0+); same count as MFOG.")]]
214 =welder::mark::no_reassign,
216 R
"(Group info v2 (MGI2, 9.0+); same count as MOGI, overrides LOD
224 =welder::mark::no_reassign,
225 =welder::doc(R
"(Dynamic lights (MNLD, 9.0+): torch fires, window light
226 projection and the like.)")]]
233 =welder::doc(R
"(Detail (ground-effect) doodad layers (MDDL, 9.0+);
234 variable-length RLE layout, kept opaque.)")]]
244 =welder::mark::no_reassign,
246 R
"(Directional-gradient light extensions (MOLV, 9.1+); entries
247 reference lights by index.)")]]
258 R
"(M3 materials (MOM3, 11.0+); when present, MOMT is ignored. An
259 m3SI blob, kept opaque.)")]]
269 =welder::mark::no_reassign,
270 =welder::doc(
"Portal extra data (MOPE, 11.1+).")]]
291 template <ClientVersion V>
295 A WMO root file for one client version. The root lists the object's shared
296 data — materials and textures, doodad (M2) sets and placements, portals,
297 lights, fog, culling volumes and the per-group table — while the 3D
298 geometry lives in the separate group files (see WMOGroup). An instance read
299 from a client file rewrites byte-for-byte until modified. See
300 https://wowdev.wiki/WMO.)")
318 =welder::doc(
"The WMO format version; 17 for every supported client.")]]
323 =welder::doc(
"The root header (MOHD).")]]
329 =welder::mark::no_reassign,
330 =welder::doc(
"Materials (MOMT).")]]
331 std::vector<SMOMaterial> materials;
336 =welder::doc(
"Group names (MOGN), referenced by byte offset.")]]
342 =welder::mark::no_reassign,
343 =welder::doc(
"Per-group info (MOGI).")]]
344 std::vector<SMOGroupInfo> groupInfos;
350 R
"(Skybox filename (MOSB); raw bytes — files pad it to 4-byte
351 alignment. skybox_fdid (MOSI, 8.1+) functionally replaces
352 it, but the chunk never left the corpus: a 9.2.7 client
353 survey (2026-08-08) found MOSB in 5404 of 8148 roots —
354 always empty, never alongside MOSI — so it stays on every
355 version as the no-skybox marker exporters still emit.)")
362 =welder::mark::no_reassign,
363 =welder::doc(
"Portal vertices (MOPV).")]]
364 std::vector<C3Vector> portalVertices;
369 =welder::mark::no_reassign,
370 =welder::doc(
"Portals (MOPT).")]]
371 std::vector<SMOPortal> portals;
376 =welder::mark::no_reassign,
377 =welder::doc(
"Portal references from groups (MOPR).")]]
378 std::vector<SMOPortalRef> portalRefs;
383 =welder::mark::no_reassign,
384 =welder::doc(
"Visible block vertices (MOVV).")]]
385 std::vector<C3Vector> visibleBlockVertices;
390 =welder::mark::no_reassign,
391 =welder::doc(
"Visible blocks (MOVB).")]]
392 std::vector<SMOVisibleBlock> visibleBlocks;
397 =welder::mark::no_reassign,
398 =welder::doc(
"Lights (MOLT).")]]
399 std::vector<SMOLight> lights;
404 =welder::mark::no_reassign,
405 =welder::doc(
"Doodad sets (MODS).")]]
406 std::vector<SMODoodadSet> doodadSets;
411 =welder::mark::no_reassign,
412 =welder::doc(
"Doodad placements (MODD).")]]
413 std::vector<SMODoodadDef> doodadDefs;
418 =welder::mark::no_reassign,
419 =welder::doc(
"Fog volumes (MFOG).")]]
420 std::vector<SMOFog> fogs;
425 =welder::mark::no_reassign,
426 =welder::doc(
"Convex volume planes (MCVP); transports mostly.")]]
427 std::vector<C4Plane> convexVolumePlanes;
432 =welder::doc(
"MOMX (11.x, undocumented purpose); preserved opaque.")]]
441 static constexpr std::array ChunkOrder = {
486 [[=welder::mark::exclude]]
487 void patchChunk(std::uint32_t fourccMagic, std::span<std::byte> payload)
const {
488 if (fourccMagic !=
fourcc(
"MOHD") || payload.size() <
sizeof(
SMOHeader))
return;
490 std::memcpy(&h, payload.data(),
sizeof h);
491 h.
nGroups =
static_cast<std::uint32_t
>(groupInfos.size());
492 h.
nPortals =
static_cast<std::uint32_t
>(portals.size());
493 h.
nDoodadSets =
static_cast<std::uint32_t
>(doodadSets.size());
494 std::memcpy(payload.data(), &h,
sizeof h);
507 [[=welder::mark::exclude]]
510 for (std::size_t i = 0; i < doodadSets.size(); ++i)
511 if (doodadSets[i].startIndex + doodadSets[i].count > doodadDefs.size())
512 report.
addError(std::format(
"doodad_sets[{}]", i),
513 std::format(
"range [{}, {}) overruns the {} doodad placements", doodadSets[i].startIndex,
514 doodadSets[i].startIndex + doodadSets[i].count, doodadDefs.size()));
519 constexpr std::size_t maxReported = 8;
520 std::size_t badNames = 0;
521 for (std::size_t i = 0; i < doodadDefs.size(); ++i) {
522 const std::uint32_t name = doodadDefs[i].nameIndex();
523 bool resolvable =
false;
524 std::string_view space;
525 std::size_t bound = 0;
526 if constexpr (
requires { this->doodadNames; })
527 if (!this->doodadNames.empty()) {
528 resolvable = name < this->doodadNames.size();
529 space =
"doodad_names blob byte";
530 bound = this->doodadNames.size();
532 if constexpr (
requires { this->doodadFdids; })
534 resolvable = name < this->doodadFdids.size();
535 space =
"doodad FileDataID";
536 bound = this->doodadFdids.size();
540 space =
"doodad name source (none engaged)";
542 if (!resolvable && ++badNames <= maxReported)
543 report.
addError(std::format(
"doodadDefs[{}]", i),
544 std::format(
"name reference {} does not resolve: {} count {}", name, space, bound));
546 if (badNames > maxReported)
547 report.
addError(
"doodadDefs", std::format(
548 "... and {} more unresolvable name " "references", badNames - maxReported));
551 for (std::size_t i = 0; i < portals.size(); ++i)
552 if (portals[i].startVertex + portals[i].count > portalVertices.size())
553 report.
addError(std::format(
"portals[{}]", i),
554 std::format(
"vertex range [{}, {}) overruns the {} portal vertices",
555 portals[i].startVertex, portals[i].startVertex + portals[i].count,
556 portalVertices.size()));
559 for (std::size_t i = 0; i < portalRefs.size(); ++i) {
560 if (portalRefs[i].portalIndex >= portals.size())
561 report.
addError(std::format(
"portal_refs[{}]", i),
562 std::format(
"portal_index {} out of range: {} portals", portalRefs[i].portalIndex,
564 if (portalRefs[i].groupIndex >= groupInfos.size())
565 report.
addError(std::format(
"portal_refs[{}]", i),
566 std::format(
"group_index {} out of range: {} groups", portalRefs[i].groupIndex,
571 for (std::size_t i = 0; i < visibleBlocks.size(); ++i)
572 if (visibleBlocks[i].firstVertex + visibleBlocks[i].count > visibleBlockVertices.size())
573 report.
addError(std::format(
"visible_blocks[{}]", i), std::format(
574 "vertex range [{}, {}) overruns the {} visible block " "vertices",
575 visibleBlocks[i].firstVertex, visibleBlocks[i].firstVertex + visibleBlocks[i].count,
576 visibleBlockVertices.size()));
579 for (std::size_t i = 0; i < groupInfos.size(); ++i)
580 if (groupInfos[i].nameOffset >= 0 &&
static_cast<std::size_t
>(groupInfos[i].nameOffset) >= groupNames.
582 report.
addError(std::format(
"groupInfos[{}]", i),
583 std::format(
"name offset {} out of range: {} blob bytes", groupInfos[i].nameOffset,
591 if constexpr (
requires { this->textures; })
592 if (!this->textures.empty())
593 for (std::size_t i = 0; i < materials.size(); ++i)
594 if (materials[i].texture1 >= this->textures.size())
595 report.
addError(std::format(
"materials[{}]", i),
596 std::format(
"texture_1 offset {} out of range: {} blob bytes", materials[i].texture1,
597 this->textures.size()));
600 if constexpr (
requires { this->lightExtensions; })
601 for (std::size_t i = 0; i < this->lightExtensions.size(); ++i)
602 if (
const auto index = this->lightExtensions[i].lightIndex; index < 0 || static_cast<std::size_t>(index)
604 report.
addError(std::format(
"light_extensions[{}]", i),
605 std::format(
"light_index {} out of range: {} lights", index, lights.size()));
614 template <ClientVersion V>
The chunk framework, vocabulary and engine in one header.
Named ClientVersion constants for the exact client builds format features appeared (or vanished) at —...
Client version identity, the flavor axis that separates a client's CONTENT version from the engine ge...
WMO doodad sets and placements (MODS, MODD) (namespace wowlib::formats::wmo::root::chunks).
WMO fog and ambient volumes (MFOG, MAVD/MAVG/MBVD) (namespace wowlib::formats::wmo::root::chunks).
FourCC chunk identifiers: compile-time conversion of the four-letter codes to the host integers chunk...
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 ClientVersion Legion_Alpha
legion alpha, 7.0.1 build 20740 — the first chunked-format build (the .m2 MD21 container,...
constexpr ClientVersion TWW_Alpha
The War Within alpha, 11.0.0 build 54210.
constexpr ClientVersion Legion_ShadowsOfArgus_24473
legion, Shadows of Argus PTR, 7.3.0 build 24473.
constexpr ClientVersion BfA_VisionsOfNzoth_32044
BfA, Visions of N'Zoth PTR, 8.3.0 build 32044.
constexpr ClientVersion SL_Alpha_33978
shadowlands alpha, 9.0.1 build 33978 (WFV3/EDGF/NERF/DBOC; PFDC is dated here by wowdev but reached l...
constexpr ClientVersion BfA_TidesOfVengeance
BfA, Tides of Vengeance PTR, 8.1.0 build 27826 (RPID/GPID, WMO MOSI/MODI).
constexpr ClientVersion SL_ChainsOfDomination
shadowlands, Chains of Domination PTR, 9.1.0 build 39015.
constexpr ClientVersion TWW_Undermined
TWW, Undermine(d) PTR, 11.1.0 build 58221.
WMO lights and their extensions (MOLT, MOLV, MNLD) (namespace wowlib::formats::wmo::root::chunks).
StringBlock — the decoded representation of a chunk of zero-terminated strings (MOTX,...
WMO group info, portals and visible blocks (namespace wowlib::formats::wmo::root::chunks).
The binary-level math and color primitives shared across WoW file formats (wowdev....
The conditional-base mechanism that gives a versioned chunked entity exactly the fields its client ve...
WMO version pivots that change a binary-struct layout (not just chunk presence).
WMO materials and UV animation (MOMT, MOUV) (namespace wowlib::formats::wmo::root::chunks).