34 using namespace wowlib::formats::wmo::group::chunks;
47 =welder::weld_as(
"WMOGroupBody"),
50 The MOGP container payload, abstract over the client version. Construct a
51 concrete version with WMOGroupBody.for_version(expansion).)")
65 =welder::weld_as(
"WMOGroup"),
68 One WMO group file, abstract over the client version. A group file holds
69 the 3D model data (geometry, render batches, collision, liquid) for one
70 unit of a world map object. Construct a concrete version with
71 WMOGroup.for_version(expansion); the per-version WMOGroup* classes are
72 subclasses. See https://wowdev.wiki/WMO.)")
89 =welder::mark::no_reassign,
91 R
"(Triangle-strip batch overrides (MORB, Cata+); same count as
99 =welder::doc(R
"(Tangent arrays (MOTA, Cata+); offset-based layout, kept
107 =welder::mark::no_reassign,
108 =welder::doc(
"Shadow batches (MOBS, Cata+).")]]
118 =welder::mark::no_reassign,
119 =welder::doc(R
"(Ambient color override (MDAL, MoP+); a single color in
120 practice, replacing the header ambient. wowdev dates it
121 to WoD but flags that unverified — a 5.4.8 corpus sweep
122 found 421 of these, while sampled Cata and WotLK groups
133 =welder::mark::no_reassign,
134 =welder::doc(R
"(Terrain-cutting planes (MOPL, WoD+); requires the
135 can_cut_terrain flag, at most 32.)")]]
145 =welder::doc(R
"(Prepass batches (MOPB, Legion+); undocumented 24-byte
146 records, kept opaque.)")]]
154 R
"(Spot lights (MOLS, Legion+); undocumented 56-byte records,
162 =welder::mark::no_reassign,
163 =welder::doc(
"Point lights (MOLP, Legion+).")]]
173 =welder::mark::no_reassign,
175 R
"(Spot-light sets (MLSS, 8.1+): (first, count) ranges into MOLS
183 =welder::mark::no_reassign,
185 R
"(Point-light sets (MLSP, 8.1+): (first, count) ranges into
186 MOLP per doodad set.)")]]
193 =welder::mark::no_reassign,
194 =welder::doc(R
"(Point-light animation sets (MLSK, 8.1+): (first, count)
195 ranges into MOP2.)")]]
202 =welder::mark::no_reassign,
203 =welder::doc(
"Animated point lights (MOP2, 8.1+).")]]
213 =welder::mark::no_reassign,
215 "Particulate volume references (MPVR, 8.3+) into the root's MPVD.")]]
227 =welder::mark::no_reassign,
229 R
"(32-bit triangle vertex indices (MOVX, ~9.0+; the large-mesh
230 MOVI replacement).)")]]
238 =welder::mark::no_reassign,
240 "Ambient volume references (MAVR, 9.0+) into the root's MAVD.")]]
248 =welder::mark::no_reassign,
249 =welder::doc(
"Box volume references (MBVR, 9.0+) into the root's MBVD.")
258 =welder::mark::no_reassign,
260 R
"(Fog volume references (MFVR, 9.0+) into the root's MFOG and
269 =welder::mark::no_reassign,
271 "New-light references (MNLR, 9.0+) into the root's MNLD.")]
273 std::vector<std::uint16_t> newLightRefs;
283 =welder::mark::no_reassign,
285 R
"(Query face start (MOGX, 10.0+): the base subtracted from a
286 polygon index into MOQG. A single value in practice.)")]
288 std::vector<std::uint32_t> queryFaceStart;
294 =welder::mark::no_reassign,
296 "Per-triangle material info v2 (MPY2, 10.0+; replaces MOPY).")]]
297 std::vector<Poly2> polys2;
303 =welder::mark::no_reassign,
305 R
"(Per-polygon ground types (MOQG, 10.0+), indexed by polygon
306 index minus the MOGX base.)")]]
307 std::vector<std::uint32_t> queryFaces;
318 =welder::mark::no_reassign,
320 R
"(Second vertex-color-like weights (MOC2, 10.0+), used by the
321 parallax and shader-23 materials.)")]]
322 std::vector<CImVector> vertexColors2;
342 template <ClientVersion V>
346 The MOGP payload for one client version: the group header and the geometry
347 subchunks (triangles, vertices, normals, batches, BSP, liquid, lights).
348 See https://wowdev.wiki/WMO.)")
364 =welder::doc(
"The group header leading the MOGP payload.")]]
374 =welder::mark::no_reassign,
375 =welder::doc(
"Per-triangle material info (MOPY).")]]
376 std::vector<SMOPoly>
polys;
383 =welder::mark::no_reassign,
384 =welder::doc(
"Triangle vertex indices (MOVI), three per triangle.")]]
385 std::vector<std::uint16_t>
indices;
390 =welder::mark::no_reassign,
391 =welder::doc(
"Vertices (MOVT).")]]
398 =welder::mark::no_reassign,
399 =welder::doc(
"Normals (MONR).")]]
407 =welder::mark::only(welder::lang::py),
409 R
"(Texture-coordinate sets (MOTV), up to four; the active count
410 is driven by the group flags (has_two_motv, has_three_motv).
411 Dragonflight groups ship a fourth set with no known flag
412 (983 roots' groups in the 10.2.7 fleet client — e.g.
413 Stormwind 8sw_portalroom01 group 2); it engages the
414 fourth slot here. Binds as a list of the filled sets,
421 =welder::mark::no_reassign,
422 =welder::doc(
"Render batches (MOBA).")]]
423 std::vector<SMOBatch<V>>
batches;
429 =welder::mark::no_reassign,
430 =welder::doc(
"Light references into the root's MOLT (MOLR).")]]
437 =welder::mark::no_reassign,
438 =welder::doc(
"Doodad references into the root's MODD (MODR).")]]
444 =welder::mark::no_reassign,
445 =welder::doc(
"Collision BSP nodes (MOBN).")]]
451 =welder::mark::no_reassign,
452 =welder::doc(
"BSP face indices (MOBR).")]]
460 =welder::mark::only(welder::lang::py),
462 R
"(Vertex-color layers (MOCV), up to two; the active count is
463 driven by the group flags (has_vertex_colors, has_two_mocv).
464 Binds as a list of the filled layers, by value.)")]]
471 R
"(Liquid data (MLIQ): a vertex grid and tile-flag grid with a
472 base position and material id.)")]]
478 =welder::mark::no_reassign,
479 =welder::doc(
"Triangle-strip indices (MORI).")]]
493 [[=welder::doc(
"The number of filled texture-coordinate sets (MOTV), "
498 [[=welder::return_policy(welder::rv::reference_internal),
499 =welder::doc(
"One filled texture-coordinate set (MOTV), as a live "
502 "the set's coordinates, live; None/null when set is out of range")]]
503 std::vector<C2Vector>* texcoordSet(
504 std::size_t set [[=welder::doc(
"the filled-set index")]]) {
505 return set < texcoords.
size() ? &texcoords[set] :
nullptr;
509 [[=welder::doc(
"Replace one filled texture-coordinate set (MOTV)."),
510 =welder::returns(
"nothing; errors when set is out of range")]]
512 std::size_t set [[=welder::doc(
"the filled-set index")]],
513 std::vector<C2Vector> coords
514 [[=welder::doc(
"the coordinates, one per vertex")]]) {
515 if (set >= texcoords.
size())
518 "texcoord set {} out of range ({} filled)",
519 set, texcoords.
size()));
520 texcoords[set] = std::move(coords);
525 [[=welder::doc(
"Fill the next free texture-coordinate slot (MOTV)."),
526 =welder::returns(
"nothing; errors when all four slots are filled")]]
528 std::vector<C2Vector> coords
529 [[=welder::doc(
"the coordinates, one per vertex")]]) {
533 "all 4 texcoord slots are filled");
534 *slot = std::move(coords);
538 [[=welder::doc(
"Empty every texture-coordinate slot (MOTV).")]]
539 void clearTexcoordSets() { texcoords.
clear(); }
541 [[=welder::doc(
"The number of filled vertex-color layers (MOCV), "
546 [[=welder::return_policy(welder::rv::reference_internal),
547 =welder::doc(
"One filled vertex-color layer (MOCV), as a live "
550 "the layer's colors, live; None/null when layer is out of range")]]
551 std::vector<CImVector>* vertexColorLayer(
552 std::size_t layer [[=welder::doc(
"the filled-layer index")]]) {
553 return layer < vertexColors.
size() ? &vertexColors[layer] :
nullptr;
557 [[=welder::doc(
"Replace one filled vertex-color layer (MOCV)."),
558 =welder::returns(
"nothing; errors when layer is out of range")]]
560 std::size_t layer [[=welder::doc(
"the filled-layer index")]],
561 std::vector<CImVector> colors
562 [[=welder::doc(
"the colors, one per vertex")]]) {
563 if (layer >= vertexColors.
size())
566 "vertex-color layer {} out of range ({} filled)",
567 layer, vertexColors.
size()));
568 vertexColors[layer] = std::move(colors);
573 [[=welder::doc(
"Fill the next free vertex-color layer slot (MOCV)."),
574 =welder::returns(
"nothing; errors when both layers are filled")]]
576 std::vector<CImVector> colors
577 [[=welder::doc(
"the colors, one per vertex")]]) {
578 auto* slot = vertexColors.
push();
581 "both vertex-color layers are filled");
582 *slot = std::move(colors);
586 [[=welder::doc(
"Empty every vertex-color layer slot (MOCV).")]]
587 void clearVertexColorLayers() { vertexColors.
clear(); }
596 static constexpr std::array ChunkOrder = {
638 [[=welder::mark::exclude]]
639 std::size_t activeIndexCount()
const {
640 if constexpr (
requires { this->largeIndices; })
641 if (!this->largeIndices.empty())
642 return this->largeIndices.size();
643 return indices.size();
652 [[=welder::mark::exclude]]
653 void validateExtra(ValidationReport& report)
const {
654 const std::size_t indexCount = activeIndexCount();
658 const auto perTriangle = [&](
const auto& records,
659 std::string_view what) {
660 if (!records.empty() && records.size() * 3 != indexCount)
663 "count {} x 3 != the {} active triangle indices",
664 records.size(), indexCount));
666 perTriangle(polys,
"polys");
667 if constexpr (
requires { this->polys2; })
668 perTriangle(this->polys2,
"polys2");
672 for (std::size_t i = 0; i < batches.size(); ++i) {
673 const auto& batch = batches[i];
674 if (batch.startIndex + batch.count > indexCount)
675 report.
addError(std::format(
"batches[{}]", i),
677 "index range [{}, {}) overruns the {} indices",
679 batch.startIndex + batch.count,
681 if (batch.minIndex > batch.maxIndex)
682 report.
addError(std::format(
"batches[{}]", i),
683 std::format(
"min_index {} > max_index {}",
686 else if (!vertices.empty() && batch.maxIndex >= vertices.size())
687 report.
addError(std::format(
"batches[{}]", i),
689 "max_index {} out of range: {} vertices",
690 batch.maxIndex, vertices.size()));
694 const std::size_t declaredBatches =
static_cast<std::size_t
>(header.
696 + header.intBatchCount + header.extBatchCount;
697 if (declaredBatches != batches.size())
699 std::format(
"batch counts {}+{}+{} != {} batches",
700 header.transBatchCount,
701 header.intBatchCount,
702 header.extBatchCount, batches.size()));
705 const std::size_t faceCount = indexCount / 3;
707 bspFaceIndices, faceCount,
"bspFaceIndices",
709 for (std::size_t i = 0; i < bspNodes.size(); ++i) {
710 const auto& node = bspNodes[i];
711 for (
const std::int16_t child : {node.negChild, node.posChild})
712 if (child != -1 &&
static_cast<std::size_t
>(child) >= bspNodes.
714 report.
addError(std::format(
"bsp_nodes[{}]", i),
715 std::format(
"child {} out of range: {} nodes",
718 if (node.faceStart + node.nFaces > bspFaceIndices.size())
719 report.
addError(std::format(
"bsp_nodes[{}]", i),
721 "face range [{}, {}) overruns the {} face indices",
722 node.faceStart, node.faceStart + node.nFaces,
723 bspFaceIndices.size()));
734 std::string_view what,
736 if (
hasFlag(header.flags, flag) && have < want)
737 report.
add(severity, std::string{what},
739 "group flag {:#x} is set but only {} of {} {} present",
740 std::to_underlying(flag), have, want, what));
755 "present but the has_vertex_colors group flag is clear");
759 if (!liquid.empty()) {
760 const auto expect = [&](
const auto& grid,
762 std::string_view what) {
763 const std::size_t cells =
764 dim.x < 0 || dim.y < 0
766 :
static_cast<std::size_t
>(dim.x) *
static_cast<std::size_t
>(dim
768 if (grid.size() != cells)
769 report.
addError(std::format(
"liquid.{}", what),
770 std::format(
"count {} != {}x{} grid",
771 grid.size(), dim.x, dim.y));
773 expect(liquid.vertices, liquid.vertsDim,
"vertices");
774 expect(liquid.tiles, liquid.tilesDim,
"tiles");
775 if (liquid.vertsDim.x != liquid.tilesDim.x + 1
776 || liquid.vertsDim.y != liquid.tilesDim.y + 1)
779 "vertex grid {}x{} is not one larger per axis than the "
781 liquid.vertsDim.x, liquid.vertsDim.y,
782 liquid.tilesDim.x, liquid.tilesDim.y));
785 "present but the has_liquid group flag is clear");
789 if constexpr (
requires { this->terrainCutPlanes; }) {
790 if (this->terrainCutPlanes.size() > 32)
793 "{} planes exceed the client's cap of 32",
794 this->terrainCutPlanes.size()));
795 if (!this->terrainCutPlanes.empty()
798 "present but the can_cut_terrain flag2 is clear");
802 if constexpr (
requires { this->pointLightSets; }) {
803 const auto setRanges = [&](
const auto& sets,
805 std::string_view member,
806 std::string_view what) {
807 for (std::size_t i = 0; i < sets.size(); ++i)
808 if (sets[i].offset + sets[i].count > target.size())
809 report.
addError(std::format(
"{}[{}]", member, i),
811 "range [{}, {}) overruns the {} {}",
813 sets[i].offset + sets[i].count,
814 target.size(), what));
816 setRanges(this->pointLightSets, this->pointLights,
819 setRanges(this->pointLightAnimSets, this->pointLightAnims,
820 "pointLightAnimSets",
"animated point lights");
830 template <ClientVersion V>
834 One WMO group file for one client version: the format version and the MOGP
835 container holding the group's geometry (see WMOGroupBody). A group holds
836 the 3D model data for one unit of a world map object. See
837 https://wowdev.wiki/WMO.)")
838 ]] WMOGroup : ChunkedFile<WMOGroup<V>>, WMOGroupBase {
839 static constexpr ClientVersion Version = V;
844 =welder::doc(
"The WMO format version; 17 for every supported client.")]]
850 =welder::doc(
"The MOGP container: group header and geometry.")]]
861 template <ClientVersion V>
868 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...
FourCC chunk identifiers: compile-time conversion of the four-letter codes to the host integers chunk...
WMO group geometry: polys, batches, BSP (MOPY, MOBA, MOBN) (namespace wowlib::formats::wmo::group::ch...
WMO group lights and light sets (MOLP, MLSS/MLSP/MLSK, MOP2) (namespace wowlib::formats::wmo::group::...
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 DF_Alpha
dragonflight alpha, 10.0.0 build 46181.
constexpr ClientVersion Cata
Cataclysm (any 4.x client).
constexpr ClientVersion BfA_VisionsOfNzoth_33775
BfA, Visions of N'Zoth, 8.3.0 build 33775 (WMO MPVR).
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 MoP
Mists of Pandaria (any 5.x client).
constexpr ClientVersion WoD
Warlords of Draenor (any 6.x client).
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.
The version-agnostic base of every WMOGroupBody<V> (welded as "WMOGroupBody").
10.0+ (Dragonflight) group-body chunks: the query surface and the second vertex-color weights.
8.1+ group-body light-set chunks.
std::vector< LightSet > spotLightSets
std::vector< LightSet > pointLightAnimSets
std::vector< PointLightAnim > pointLightAnims
Animated point lights (MOP2, 8.1+).
std::vector< LightSet > pointLightSets
std::vector< std::uint16_t > particulateRefs
Particulate volume references (MPVR, 8.3+) into the root's MPVD.
9.0+ group-body chunks (large-mesh indices and the volume/light refs).
std::vector< std::uint16_t > boxVolumeRefs
Box volume references (MBVR, 9.0+) into the root's MBVD.
std::vector< std::uint32_t > largeIndices
std::vector< std::uint16_t > ambientVolumeRefs
Ambient volume references (MAVR, 9.0+) into the root's MAVD.
std::vector< std::uint16_t > fogVolumeRefs
cata+ (4.0) group-body chunks.
std::vector< ShadowBatch > shadowBatches
Shadow batches (MOBS, Cata+).
std::vector< RenderBatchOverride > batchOverrides
legion+ (7.0.1) group-body chunks.
std::vector< PointLight > pointLights
Point lights (MOLP, Legion+).
MoP+ (5.0) group-body chunks.
std::vector< CArgb > ambientColorOverride
WoD+ (6.0) group-body chunks.
std::vector< C4Plane > terrainCutPlanes
The MOGP container payload for one client version.
Result< void > appendTexcoordSet(std::vector< C2Vector > coords)
std::vector< C2Vector > * texcoordSet(std::size_t set)
std::vector< std::uint16_t > lightRefs
Result< void > appendVertexColorLayer(std::vector< CImVector > colors)
std::vector< std::uint16_t > bspFaceIndices
Result< void > setTexcoordSet(std::size_t set, std::vector< C2Vector > coords)
Repeated< std::vector< C2Vector >, 4 > texcoords
SMOGroupHeader< V > header
std::vector< SMOPoly > polys
std::vector< std::uint16_t > indices
std::size_t vertexColorLayerCount() const
std::vector< SMOBatch< V > > batches
std::vector< C3Vector > vertices
std::vector< C3Vector > normals
Repeated< std::vector< CImVector >, 2 > vertexColors
static constexpr std::array ChunkOrder
std::vector< CAaBspNode > bspNodes
std::vector< CImVector > * vertexColorLayer(std::size_t layer)
static constexpr ClientVersion Version
void validateExtra(ValidationReport &report) const
Validation hook (see detail::validateEntity): the group contracts the annotations cannot express — re...
Result< void > setVertexColorLayer(std::size_t layer, std::vector< CImVector > colors)
std::size_t texcoordSetCount() const
std::vector< std::uint16_t > doodadRefs
void clearVertexColorLayers()
std::size_t activeIndexCount() const
The number of triangle vertex indices the group's batches and BSP reference: MOVX (the large-mesh rep...
std::vector< std::uint16_t > transBatchIndices
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 group liquid (MLIQ) (namespace wowlib::formats::wmo::group::chunks).