wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
group.hpp
Go to the documentation of this file.
1#pragma once
2
13
14#include <array>
15#include <cstdint>
16#include <format>
17#include <utility>
18#include <vector>
19
22#include <wowlib/core/lang.hpp>
32
34 using namespace wowlib::formats::wmo::group::chunks;
35
45 struct [[
46 =welder::weld,
47 =welder::weld_as("WMOGroupBody"),
49 =welder::doc(R"(
50 The MOGP container payload, abstract over the client version. Construct a
51 concrete version with WMOGroupBody.for_version(expansion).)")
52 ]] WMOGroupBodyBase {};
53
59 `x: WMOGroup` annotation, `WMOGroup.for_version(expansion)`). It has no role
60 in the C++ API, where you use the concrete WMOGroup<V> directly.
61
62 @see https://wowdev.wiki/WMO */
63 struct [[
64 =welder::weld,
65 =welder::weld_as("WMOGroup"),
67 =welder::doc(R"(
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.)")
73 ]] WMOGroupBase {};
74
75 namespace detail {
76 // --- version-range trait bases (unwelded) ---------------------------------
77 // One struct per availability range; members keep their chunk/since/doc/marks
78 // (read off the declaring class, so flattening preserves them). Members are in
79 // canonical order within a trait for readability; the serialization order is
80 // the entity's ChunkOrder table, not the flatten order.
81
83 struct GroupBodyCata {
84 [[
85 =chunk("MORB"),
89 =welder::mark::no_reassign,
90 =welder::doc(
91 R"(Triangle-strip batch overrides (MORB, Cata+); same count as
92 MOBA.)")]]
93 std::vector<RenderBatchOverride> batchOverrides;
95 [[
96 =chunk("MOTA"),
99 =welder::doc(R"(Tangent arrays (MOTA, Cata+); offset-based layout, kept
100 opaque.)")]]
103 [[
104 =chunk("MOBS"),
107 =welder::mark::no_reassign,
108 =welder::doc("Shadow batches (MOBS, Cata+).")]]
109 std::vector<ShadowBatch> shadowBatches;
110 };
111
114 [[
115 =chunk("MDAL"),
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
123 carry none.)")]]
124 std::vector<CArgb> ambientColorOverride;
125 };
126
127
128 struct GroupBodyWod {
129 [[
130 =chunk("MOPL"),
133 =welder::mark::no_reassign,
134 =welder::doc(R"(Terrain-cutting planes (MOPL, WoD+); requires the
135 can_cut_terrain flag, at most 32.)")]]
136 std::vector<C4Plane> terrainCutPlanes;
137 };
140 struct GroupBodyLegion {
141 [[
142 =chunk("MOPB"),
145 =welder::doc(R"(Prepass batches (MOPB, Legion+); undocumented 24-byte
146 records, kept opaque.)")]]
148
149 [[
150 =chunk("MOLS"),
153 =welder::doc(
154 R"(Spot lights (MOLS, Legion+); undocumented 56-byte records,
155 kept opaque.)")]]
157
158 [[
159 =chunk("MOLP"),
162 =welder::mark::no_reassign,
163 =welder::doc("Point lights (MOLP, Legion+).")]]
164 std::vector<PointLight> pointLights;
165 };
166
168 struct GroupBody81 {
169 [[
170 =chunk("MLSS"),
173 =welder::mark::no_reassign,
174 =welder::doc(
175 R"(Spot-light sets (MLSS, 8.1+): (first, count) ranges into MOLS
176 per doodad set.)")]]
177 std::vector<LightSet> spotLightSets;
178
179 [[
180 =chunk("MLSP"),
183 =welder::mark::no_reassign,
184 =welder::doc(
185 R"(Point-light sets (MLSP, 8.1+): (first, count) ranges into
186 MOLP per doodad set.)")]]
187 std::vector<LightSet> pointLightSets;
188
189 [[
190 =chunk("MLSK"),
193 =welder::mark::no_reassign,
194 =welder::doc(R"(Point-light animation sets (MLSK, 8.1+): (first, count)
195 ranges into MOP2.)")]]
196 std::vector<LightSet> pointLightAnimSets;
197
198 [[
199 =chunk("MOP2"),
202 =welder::mark::no_reassign,
203 =welder::doc("Animated point lights (MOP2, 8.1+).")]]
204 std::vector<PointLightAnim> pointLightAnims;
205 };
206
208 struct GroupBody83 {
209 [[
210 =chunk("MPVR"),
213 =welder::mark::no_reassign,
214 =welder::doc(
215 "Particulate volume references (MPVR, 8.3+) into the root's MPVD.")]]
216 std::vector<std::uint16_t> particulateRefs;
217 };
218
220 struct GroupBody90 {
221 [[
222 =chunk("MOVX"),
226 =formats::indexes("vertices"),
227 =welder::mark::no_reassign,
228 =welder::doc(
229 R"(32-bit triangle vertex indices (MOVX, ~9.0+; the large-mesh
230 MOVI replacement).)")]]
231 std::vector<std::uint32_t> largeIndices;
232
233 [[
234 =chunk("MAVR"),
237 =formats::indexesInRoot("ambientVolumes"),
238 =welder::mark::no_reassign,
239 =welder::doc(
240 "Ambient volume references (MAVR, 9.0+) into the root's MAVD.")]]
241 std::vector<std::uint16_t> ambientVolumeRefs;
242
243 [[
244 =chunk("MBVR"),
247 =formats::indexesInRoot("ambientBoxVolumes"),
248 =welder::mark::no_reassign,
249 =welder::doc("Box volume references (MBVR, 9.0+) into the root's MBVD.")
250 ]]
251 std::vector<std::uint16_t> boxVolumeRefs;
253 [[
254 =chunk("MFVR"),
257 =formats::indexesInRoot("fogs"),
258 =welder::mark::no_reassign,
259 =welder::doc(
260 R"(Fog volume references (MFVR, 9.0+) into the root's MFOG and
261 MFED.)")]]
262 std::vector<std::uint16_t> fogVolumeRefs;
264 [[
265 =chunk("MNLR"),
268 =formats::indexesInRoot("newLights"),
269 =welder::mark::no_reassign,
270 =welder::doc(
271 "New-light references (MNLR, 9.0+) into the root's MNLD.")]
273 std::vector<std::uint16_t> newLightRefs;
274 };
275
278 struct GroupBody100 {
279 [[
280 =chunk("MOGX"),
283 =welder::mark::no_reassign,
284 =welder::doc(
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;
289
290 [[
291 =chunk("MPY2"),
294 =welder::mark::no_reassign,
295 =welder::doc(
296 "Per-triangle material info v2 (MPY2, 10.0+; replaces MOPY).")]]
297 std::vector<Poly2> polys2;
298
299 [[
300 =chunk("MOQG"),
303 =welder::mark::no_reassign,
304 =welder::doc(
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;
308
309 // wowdev lists MOC2 without a version marker, but its consumers are the
310 // Parallax and DF-shader-23 materials and the wiki section only appeared
311 // 2022-10 (the Dragonflight beta window); corpus sweeps found ZERO MOC2
312 // across 9,346 3.3.5a and 27,191 9.2.7 group files, so it is gated 10.0+.
313 [[
314 =chunk("MOC2"),
317 =formats::countMatches("vertices"),
318 =welder::mark::no_reassign,
319 =welder::doc(
320 R"(Second vertex-color-like weights (MOC2, 10.0+), used by the
321 parallax and shader-23 materials.)")]]
322 std::vector<CImVector> vertexColors2;
323 };
324 }
325
326 namespace detail {
327 // The annotated entities; instantiate through the canonicalizing
328 // aliases below, never directly. The trait bases share this namespace,
329 // so they need no qualifier (a bare detail:: would be ambiguous against
330 // chunks::detail via the using-directive).
342 template <ClientVersion V>
343 struct [[
344 =welder::weld,
345 =welder::doc(R"(
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.)")
349 ]] WMOGroupBody
360 static constexpr ClientVersion Version = V;
361
362 [[
364 =welder::doc("The group header leading the MOGP payload.")]]
366
367 // NOT countMatches("indices", 3): a 9.0+ large-mesh group carries its
368 // indices in MOVX instead, leaving MOVI empty — the poly/face
369 // relationship is checked against whichever list is active, in
370 // validateExtra.
371 [[
372 =chunk("MOPY"),
374 =welder::mark::no_reassign,
375 =welder::doc("Per-triangle material info (MOPY).")]]
376 std::vector<SMOPoly> polys;
377
378 [[
379 =chunk("MOVI"),
382 =formats::indexes("vertices"),
383 =welder::mark::no_reassign,
384 =welder::doc("Triangle vertex indices (MOVI), three per triangle.")]]
385 std::vector<std::uint16_t> indices;
386
387 [[
388 =chunk("MOVT"),
390 =welder::mark::no_reassign,
391 =welder::doc("Vertices (MOVT).")]]
392 std::vector<C3Vector> vertices;
393
394 [[
395 =chunk("MONR"),
397 =formats::countMatches("vertices"),
398 =welder::mark::no_reassign,
399 =welder::doc("Normals (MONR).")]]
400 std::vector<C3Vector> normals;
401
402 [[
403 =chunk("MOTV"),
405 =repeats(4),
406 =formats::countMatches("vertices"),
407 =welder::mark::only(welder::lang::py),
408 =welder::doc(
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,
415 by value.)")]]
417
418 [[
419 =chunk("MOBA"),
421 =welder::mark::no_reassign,
422 =welder::doc("Render batches (MOBA).")]]
423 std::vector<SMOBatch<V>> batches;
424
425 [[
426 =chunk("MOLR"),
428 =formats::indexesInRoot("lights"),
429 =welder::mark::no_reassign,
430 =welder::doc("Light references into the root's MOLT (MOLR).")]]
431 std::vector<std::uint16_t> lightRefs;
432
433 [[
434 =chunk("MODR"),
436 =formats::indexesInRoot("doodadDefs"),
437 =welder::mark::no_reassign,
438 =welder::doc("Doodad references into the root's MODD (MODR).")]]
439 std::vector<std::uint16_t> doodadRefs;
440
441 [[
442 =chunk("MOBN"),
444 =welder::mark::no_reassign,
445 =welder::doc("Collision BSP nodes (MOBN).")]]
446 std::vector<CAaBspNode> bspNodes;
447
448 [[
449 =chunk("MOBR"),
451 =welder::mark::no_reassign,
452 =welder::doc("BSP face indices (MOBR).")]]
453 std::vector<std::uint16_t> bspFaceIndices;
454
455 [[
456 =chunk("MOCV"),
458 =repeats(2),
459 =formats::countMatches("vertices"),
460 =welder::mark::only(welder::lang::py),
461 =welder::doc(
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.)")]]
466
467 [[
468 =chunk("MLIQ"),
470 =welder::doc(
471 R"(Liquid data (MLIQ): a vertex grid and tile-flag grid with a
472 base position and material id.)")]]
475 [[
476 =chunk("MORI"),
478 =welder::mark::no_reassign,
479 =welder::doc("Triangle-strip indices (MORI).")]]
480 std::vector<std::uint16_t> transBatchIndices;
481
482 // --- the repeated-chunk slot surface (MOTV, MOCV) ----------------------
483 // The Repeated<> members above bind to Python only (a bespoke nanobind
484 // caster lists the filled slots by value; no other rod can marshal the
485 // wrapper). These accessors are the same surface for every language.
486 // The getters are ZERO-COPY live views — a slot's std::vector object
487 // has a stable address inside the Repeated's array storage, so a
488 // reference_internal view is as safe as any vector member's; out of
489 // range returns null/None instead of a Result (an expected cannot
490 // carry a reference). Only set/append copy, where handing data in is
491 // inherently a copy.
492
493 [[=welder::doc("The number of filled texture-coordinate sets (MOTV), "
494 "0 to 4.")]]
495 std::size_t texcoordSetCount() const { return texcoords.size(); }
496
497 [[nodiscard]]
498 [[=welder::return_policy(welder::rv::reference_internal),
499 =welder::doc("One filled texture-coordinate set (MOTV), as a live "
500 "zero-copy view."),
501 =welder::returns(
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;
506 }
507
508 [[nodiscard]]
509 [[=welder::doc("Replace one filled texture-coordinate set (MOTV)."),
510 =welder::returns("nothing; errors when set is out of range")]]
511 Result<void> setTexcoordSet(
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())
517 std::format(
518 "texcoord set {} out of range ({} filled)",
519 set, texcoords.size()));
520 texcoords[set] = std::move(coords);
521 return {};
522 }
523
524 [[nodiscard]]
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")]]) {
530 auto* slot = texcoords.push();
531 if (slot == nullptr)
533 "all 4 texcoord slots are filled");
534 *slot = std::move(coords);
535 return {};
536 }
537
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), "
542 "0 to 2.")]]
543 std::size_t vertexColorLayerCount() const { return vertexColors.size(); }
544
545 [[nodiscard]]
546 [[=welder::return_policy(welder::rv::reference_internal),
547 =welder::doc("One filled vertex-color layer (MOCV), as a live "
548 "zero-copy view."),
549 =welder::returns(
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;
554 }
555
556 [[nodiscard]]
557 [[=welder::doc("Replace one filled vertex-color layer (MOCV)."),
558 =welder::returns("nothing; errors when layer is out of range")]]
559 Result<void> setVertexColorLayer(
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())
565 std::format(
566 "vertex-color layer {} out of range ({} filled)",
567 layer, vertexColors.size()));
568 vertexColors[layer] = std::move(colors);
569 return {};
570 }
571
572 [[nodiscard]]
573 [[=welder::doc("Fill the next free vertex-color layer slot (MOCV)."),
574 =welder::returns("nothing; errors when both layers are filled")]]
575 Result<void> appendVertexColorLayer(
576 std::vector<CImVector> colors
577 [[=welder::doc("the colors, one per vertex")]]) {
578 auto* slot = vertexColors.push();
579 if (slot == nullptr)
581 "both vertex-color layers are filled");
582 *slot = std::move(colors);
583 return {};
584 }
585
586 [[=welder::doc("Empty every vertex-color layer slot (MOCV).")]]
587 void clearVertexColorLayers() { vertexColors.clear(); }
588
596 static constexpr std::array ChunkOrder = {
597 fourcc("MOGX"),
598 fourcc("MOPY"),
599 fourcc("MPY2"),
600 fourcc("MOVI"),
601 fourcc("MOVX"),
602 fourcc("MOVT"),
603 fourcc("MONR"),
604 fourcc("MOTV"),
605 fourcc("MOBA"),
606 fourcc("MOQG"),
607 fourcc("MOLR"),
608 fourcc("MODR"),
609 fourcc("MOBN"),
610 fourcc("MOBR"),
611 fourcc("MOCV"),
612 fourcc("MOC2"),
613 fourcc("MLIQ"),
614 fourcc("MORI"),
615 fourcc("MORB"),
616 fourcc("MOTA"),
617 fourcc("MOBS"),
618 fourcc("MDAL"),
619 fourcc("MOPL"),
620 fourcc("MOPB"),
621 fourcc("MOLS"),
622 fourcc("MOLP"),
623 fourcc("MLSS"),
624 fourcc("MLSP"),
625 fourcc("MLSK"),
626 fourcc("MOP2"),
627 fourcc("MPVR"),
628 fourcc("MAVR"),
629 fourcc("MBVR"),
630 fourcc("MFVR"),
631 fourcc("MNLR"),
632 };
633
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();
644 }
645
652 [[=welder::mark::exclude]]
653 void validateExtra(ValidationReport& report) const {
654 const std::size_t indexCount = activeIndexCount();
655
656 // one per-triangle record per three indices, against whichever index
657 // list is active (MOVI, or MOVX on a large mesh)
658 const auto perTriangle = [&](const auto& records,
659 std::string_view what) {
660 if (!records.empty() && records.size() * 3 != indexCount)
661 report.addError(std::string{what},
662 std::format(
663 "count {} x 3 != the {} active triangle indices",
664 records.size(), indexCount));
665 };
666 perTriangle(polys, "polys");
667 if constexpr (requires { this->polys2; })
668 perTriangle(this->polys2, "polys2");
669
670 // MOBA ranges: the client draws [startIndex, startIndex + count) and
671 // uploads vertices up to maxIndex
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),
676 std::format(
677 "index range [{}, {}) overruns the {} indices",
678 batch.startIndex,
679 batch.startIndex + batch.count,
680 indexCount));
681 if (batch.minIndex > batch.maxIndex)
682 report.addError(std::format("batches[{}]", i),
683 std::format("min_index {} > max_index {}",
684 batch.minIndex,
685 batch.maxIndex));
686 else if (!vertices.empty() && batch.maxIndex >= vertices.size())
687 report.addError(std::format("batches[{}]", i),
688 std::format(
689 "max_index {} out of range: {} vertices",
690 batch.maxIndex, vertices.size()));
691 }
692
693 // the header's batch partition counts what MOBA holds
694 const std::size_t declaredBatches = static_cast<std::size_t>(header.
695 transBatchCount)
696 + header.intBatchCount + header.extBatchCount;
697 if (declaredBatches != batches.size())
698 report.addError("header",
699 std::format("batch counts {}+{}+{} != {} batches",
700 header.transBatchCount,
701 header.intBatchCount,
702 header.extBatchCount, batches.size()));
703
704 // BSP: leaves reference faces through MOBR; children index MOBN
705 const std::size_t faceCount = indexCount / 3;
707 bspFaceIndices, faceCount, "bspFaceIndices",
708 "faces", report);
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.
713 size())
714 report.addError(std::format("bsp_nodes[{}]", i),
715 std::format("child {} out of range: {} nodes",
716 child,
717 bspNodes.size()));
718 if (node.faceStart + node.nFaces > bspFaceIndices.size())
719 report.addError(std::format("bsp_nodes[{}]", i),
720 std::format(
721 "face range [{}, {}) overruns the {} face indices",
722 node.faceStart, node.faceStart + node.nFaces,
723 bspFaceIndices.size()));
724 }
725
726 // header flags vs chunk presence: the base HasVertexColors flag with
727 // NO layer at all makes the client consume absent data (error); the
728 // multi-layer flags fall short in real files (corpus: a BfA kultiras
729 // group ships HasTwoMocv with a single layer), so a shortfall there
730 // only warns
731 const auto flagWants = [&](GroupFlags flag,
732 std::size_t have,
733 std::size_t want,
734 std::string_view what,
735 ValidationSeverity severity) {
736 if (hasFlag(header.flags, flag) && have < want)
737 report.add(severity, std::string{what},
738 std::format(
739 "group flag {:#x} is set but only {} of {} {} present",
740 std::to_underlying(flag), have, want, what));
741 };
742 flagWants(GroupFlags::HasVertexColors, vertexColors.size(), 1,
743 "vertexColors",
745 flagWants(GroupFlags::HasTwoMocv, vertexColors.size(), 2,
746 "vertexColors",
748 flagWants(GroupFlags::HasTwoMotv, texcoords.size(), 2, "texcoords",
750 flagWants(GroupFlags::HasThreeMotv, texcoords.size(), 3, "texcoords",
752 if (!vertexColors.empty() && !hasFlag(
753 header.flags, GroupFlags::HasVertexColors))
754 report.addWarning("vertexColors",
755 "present but the has_vertex_colors group flag is clear");
757 // MLIQ: the grids must match their declared dimensions, and the flag
758 // decides whether the client reads the chunk at all
759 if (!liquid.empty()) {
760 const auto expect = [&](const auto& grid,
761 const C2IVector& dim,
762 std::string_view what) {
763 const std::size_t cells =
764 dim.x < 0 || dim.y < 0
765 ? 0
766 : static_cast<std::size_t>(dim.x) * static_cast<std::size_t>(dim
767 .y);
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));
772 };
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)
777 report.addError("liquid",
778 std::format(
779 "vertex grid {}x{} is not one larger per axis than the "
780 "tile grid {}x{}",
781 liquid.vertsDim.x, liquid.vertsDim.y,
782 liquid.tilesDim.x, liquid.tilesDim.y));
783 if (!hasFlag(header.flags, GroupFlags::HasLiquid))
784 report.addWarning("liquid",
785 "present but the has_liquid group flag is clear");
786 }
787
788 // WoD+ terrain-cut planes: the client caps them at 32 and gates on flags2
789 if constexpr (requires { this->terrainCutPlanes; }) {
790 if (this->terrainCutPlanes.size() > 32)
791 report.addError("terrainCutPlanes",
792 std::format(
793 "{} planes exceed the client's cap of 32",
794 this->terrainCutPlanes.size()));
795 if (!this->terrainCutPlanes.empty()
796 && !hasFlag(header.flags2, GroupFlags2::CanCutTerrain))
797 report.addWarning("terrainCutPlanes",
798 "present but the can_cut_terrain flag2 is clear");
799 }
800
801 // 8.1+ light sets: (offset, count) ranges into their referenced chunks
802 if constexpr (requires { this->pointLightSets; }) {
803 const auto setRanges = [&](const auto& sets,
804 const auto& target,
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),
810 std::format(
811 "range [{}, {}) overruns the {} {}",
812 sets[i].offset,
813 sets[i].offset + sets[i].count,
814 target.size(), what));
815 };
816 setRanges(this->pointLightSets, this->pointLights,
817 "pointLightSets",
818 "point lights");
819 setRanges(this->pointLightAnimSets, this->pointLightAnims,
820 "pointLightAnimSets", "animated point lights");
821 }
822 }
823 };
824
830 template <ClientVersion V>
831 struct [[
832 =welder::weld,
833 =welder::doc(R"(
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;
840
841 [[
842 =chunk("MVER"),
844 =welder::doc("The WMO format version; 17 for every supported client.")]]
845 std::uint32_t mver = WmoVersionV17;
846
847 [[
848 =chunk("MOGP"),
850 =welder::doc("The MOGP container: group header and geometry.")]]
851 // the raw sibling is intentional: WMOGroup and WMOGroupBody share
852 // WmoGroupPivots, so at a canonical V they are the same type the
853 // canonicalizing alias would name
854 WMOGroupBody<V> body{};
855 };
856 }
857
861 template <ClientVersion V>
862 using WMOGroupBody =
863 group::detail::WMOGroupBody<canonicalVersion(V, WmoGroupPivots,
864 WmoVersions)>;
865
868 template <ClientVersion V>
869 using WMOGroup =
870 group::detail::WMOGroup<canonicalVersion(V, WmoGroupPivots, WmoVersions)>;
871}
The chunk framework, vocabulary and engine in one header.
Storage for a chunk that may appear up to N times in one entity (MOTV texcoord sets,...
void clear()
Reset to no filled slots; slot contents return to default-constructed.
T * push()
Claim the next slot (the serializer's per-encounter hook).
std::size_t size() const
void addWarning(std::string path, std::string message)
Record a warning finding (see add()).
void add(ValidationSeverity severity, std::string path, std::string message)
Record a finding, unless the report is already full().
void addError(std::string path, std::string message)
Record an error finding (see add()).
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.
Definition lang.hpp:44
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).
void validateIndexElements(const Values &values, std::size_t targetCount, std::string_view member, std::string_view target, ValidationReport &report)
Report every element of values that is not a valid index into a targetCount-element target,...
GroupFlags
Group flag bits (MOGP and MOGI flags).
Definition header.hpp:22
@ HasTwoMotv
Has two MOTV texcoord sets (SMOGroup::TVERTS2).
Definition header.hpp:50
@ HasTwoMocv
Has a second MOCV layer; only its alpha is used.
Definition header.hpp:48
@ HasLiquid
Has liquid (MLIQ), or an implicitly full volume.
Definition header.hpp:36
@ HasVertexColors
Has vertex colors (MOCV).
Definition header.hpp:26
@ HasThreeMotv
Has three MOTV texcoord sets, e.g.
Definition header.hpp:56
detail::SMOGroupHeader< canonicalVersion(V, WmoGroupHeaderPivots, WmoVersions)> SMOGroupHeader
The MOGP header — the canonicalizing face of detail::SMOGroupHeader (WmoGroupHeaderPivots: the split-...
Definition header.hpp:201
@ CanCutTerrain
Has terrain-cutting portal planes (MOPL, WoD+).
Definition header.hpp:61
The WMO group-file entities: WMOGroup, WMOGroupBody and their per-version classes.
Definition geometry.hpp:18
group::detail::WMOGroup< canonicalVersion(V, WmoGroupPivots, WmoVersions)> WMOGroup
One WMO group file — the canonicalizing face of detail::WMOGroup (same pivots as the body it contains...
Definition group.hpp:756
group::detail::WMOGroupBody< canonicalVersion(V, WmoGroupPivots, WmoVersions)> WMOGroupBody
The MOGP payload — the canonicalizing face of detail::WMOGroupBody: every client version maps to its ...
Definition group.hpp:749
constexpr std::uint32_t WmoVersionV17
The WMO format version every supported client uses (MVER payload).
constexpr std::array WmoGroupPivots
WMOGroupBody / WMOGroup: every trait-slot boundary and chunk build the group files carry,...
constexpr std::array WmoVersions
The versions WMO is instantiated (and welded) for: every targeted last-minor-of-major release,...
consteval detail::IndexesSpec indexes(std::string_view name)
Declare a referential contract: every element of this (integral) vector member is an index into the n...
ValidationSeverity
How a validation finding affects the file's fitness for the client.
@ Warning
Suspicious, but real client files ship it; the file loads.
@ Error
The client would misread or crash on a file written like this.
constexpr std::uint32_t fourcc(const char(&cc)[5], FourCCEndian endian=FourCCEndian::Reversed)
The host integer a scanned chunk id compares equal to for code cc.
Definition fourcc.hpp:33
consteval detail::CountMultipleOfSpec countMultipleOf(std::uint32_t divisor)
Declare a granularity contract: the member's element count must be a multiple of divisor (triangle in...
constexpr detail::HeaderSpec Header
Mark a member as a container payload's raw header prelude (e.g.
constexpr ClientVersion canonicalVersion(ClientVersion v, std::span< const ClientVersion > pivots, std::span< const ClientVersion > grid)
The canonical version v collapses to: the FIRST grid version in v's range.
constexpr detail::ContainerSpec Container
Mark a chunk member whose payload is itself a chunk stream (e.g.
constexpr detail::OptionalSpec Optional
Mark a chunk member the format does not require: absence on read is fine.
consteval detail::ExpectedValueSpec expectedValue(std::uint32_t value)
Declare an exact-value contract on an integral data member (format version fields: WMO MVER is always...
consteval detail::IndexesInRootSpec indexesInRoot(std::string_view name)
Declare a cross-entity referential contract: every element of this (integral) vector member is an ind...
consteval detail::ChunkSpec chunk(const char(&cc)[5], FourCCEndian endian=FourCCEndian::Reversed)
Declare the chunk a member maps to.
consteval detail::SinceSpec since(ClientVersion v)
Restrict a member to entity versions >= v (inclusive).
consteval detail::CountMatchesSpec countMatches(std::string_view name, std::uint32_t scale=1)
Declare a companion-count contract: when this member is engaged (non-empty), its element count times ...
consteval detail::RepeatsSpec repeats(std::uint32_t max)
Allow a chunk to appear up to max times (e.g.
constexpr bool hasFlag(std::underlying_type_t< E > value, E flag)
Whether flag bit flag is set in the raw binary value value.
Definition flags.hpp:26
std::conditional_t<(V.formatLineage() >=Since &&V.formatLineage()< Until), Trait, Absent< Trait > > Slot
A version-gated base: the entity inherits Trait (flattening its chunk members in) iff Since <= V < Un...
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
Definition error.hpp:100
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
Definition error.hpp:107
@ InvalidEntityState
An entity's members disagree (e.g.
Definition error.hpp:40
An unparsed chunk payload, preserved verbatim for round-trip.
The serialization face of a chunked entity, mixed in CRTP-style: an entity struct E : ChunkedFile<E> ...
A 2D integer vector.
Definition types.hpp:39
The version-agnostic base of every WMOGroup<V> (welded as "WMOGroup").
Definition group.hpp:65
The version-agnostic base of every WMOGroupBody<V> (welded as "WMOGroupBody").
Definition group.hpp:48
10.0+ (Dragonflight) group-body chunks: the query surface and the second vertex-color weights.
Definition group.hpp:165
8.1+ group-body light-set chunks.
Definition group.hpp:113
std::vector< PointLightAnim > pointLightAnims
Animated point lights (MOP2, 8.1+).
Definition group.hpp:125
std::vector< std::uint16_t > particulateRefs
Particulate volume references (MPVR, 8.3+) into the root's MPVD.
Definition group.hpp:132
9.0+ group-body chunks (large-mesh indices and the volume/light refs).
Definition group.hpp:136
std::vector< std::uint16_t > boxVolumeRefs
Box volume references (MBVR, 9.0+) into the root's MBVD.
Definition group.hpp:146
std::vector< std::uint32_t > largeIndices
Definition group.hpp:138
std::vector< std::uint16_t > ambientVolumeRefs
Ambient volume references (MAVR, 9.0+) into the root's MAVD.
Definition group.hpp:142
std::vector< std::uint16_t > fogVolumeRefs
Definition group.hpp:149
std::vector< ShadowBatch > shadowBatches
Shadow batches (MOBS, Cata+).
Definition group.hpp:84
std::vector< RenderBatchOverride > batchOverrides
Definition group.hpp:77
legion+ (7.0.1) group-body chunks.
Definition group.hpp:100
std::vector< PointLight > pointLights
Point lights (MOLP, Legion+).
Definition group.hpp:109
The MOGP container payload for one client version.
Definition group.hpp:246
Result< void > appendTexcoordSet(std::vector< C2Vector > coords)
Definition group.hpp:414
std::vector< C2Vector > * texcoordSet(std::size_t set)
Definition group.hpp:390
Result< void > appendVertexColorLayer(std::vector< CImVector > colors)
Definition group.hpp:462
Result< void > setTexcoordSet(std::size_t set, std::vector< C2Vector > coords)
Definition group.hpp:398
std::vector< CImVector > * vertexColorLayer(std::size_t layer)
Definition group.hpp:438
void validateExtra(ValidationReport &report) const
Validation hook (see detail::validateEntity): the group contracts the annotations cannot express — re...
Definition group.hpp:540
Result< void > setVertexColorLayer(std::size_t layer, std::vector< CImVector > colors)
Definition group.hpp:446
std::size_t activeIndexCount() const
The number of triangle vertex indices the group's batches and BSP reference: MOVX (the large-mesh rep...
Definition group.hpp:526
One WMO group file for one client version: the format version (MVER) and the MOGP container holding t...
Definition group.hpp:725
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).
The MOGP group header and group flag bits (namespace wowlib::formats::wmo::group::chunks).
WMO group liquid (MLIQ) (namespace wowlib::formats::wmo::group::chunks).