wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
root.hpp
Go to the documentation of this file.
1#pragma once
2
15
16#include <array>
17#include <cstddef>
18#include <cstdint>
19#include <format>
20#include <string>
21#include <string_view>
22#include <vector>
23
24#include <welder/vocabulary.hpp>
25
27#include <wowlib/core/lang.hpp>
40
42 using namespace wowlib::formats::m2::root::record;
43
45 inline constexpr std::uint32_t Md20Magic = 0x3032444D; // "MD20"
46
48 enum class [[
49 =welder::weld,
50 =welder::flags,
51 =welder::doc("M2 global flags: tilt behavior, the texture-combiner-combo "
52 "gate, physics participation and exporter-era markers.")
53 ]] GlobalFlags : std::uint32_t {
54 TiltX [[=welder::doc("Tilt the model over X (flying mounts).")]] = 0x1,
55 TiltY [[=welder::doc("Tilt the model over Y.")]] = 0x2,
56 UseTextureCombinerCombos [[=welder::doc("The texture_combiner_combos "
57 "block trails the header (TBC+).")]] = 0x8,
58 LoadPhysData [[=welder::doc("Request the .phys file (MoP+).")]] = 0x20,
59 Unk0x80 [[=welder::doc("Unset stops demon-hunter tattoos glowing (WoD+).")]] = 0x80,
60 CameraRelated [[=welder::doc("Camera related (WoD+).")]] = 0x100,
61 NewParticleRecord [[=welder::doc("Cata: particle records are the 492-byte "
62 "layout even below v272.")]] = 0x200,
64 [[=welder::doc("Texture transforms animate on the bone's sequence "
65 "(Legion+).")]] = 0x800,
66 ChunkedAnimFiles [[=welder::doc("The .anim files are chunked (Legion+).")]] = 0x2000
67 };
68
71 This empty base exists ENTIRELY for the language bindings: it gives the
72 per-version M2Root* classes a common welded supertype so binding users
73 can write version-agnostic code (isinstance,
74 M2Root.for_version(expansion)). It has no role in the C++ API, where you
75 use the concrete M2Root<V>.
76
77 @see https://wowdev.wiki/M2 */
78 struct [[
79 =welder::weld,
80 =welder::weld_as("M2Root"),
82 =welder::doc(R"(
83 An MD20 model body, abstract over the client version. Construct a
84 concrete version with M2Root.for_version(expansion); the per-version
85 M2Root* classes are subclasses. See https://wowdev.wiki/M2.)")
87 bool operator==(const M2RootBase&) const = default;
88 };
90 namespace detail {
91 // --- version-range trait bases (unwelded) ---------------------------------
92 // One struct per availability range; each member's offsetAfter anchor (not
93 // flatten order) fixes its interleaved position in the header.
94
97 template <ClientVersion V>
98 struct DataPreWotlk {
99 [[
101 =offsetAfter("sequenceLookups"),
102 =welder::mark::no_reassign,
103 =welder::doc("Playable-animation fallbacks, one per AnimationData.dbc "
104 "id (pre-WotLK).")]]
105 std::vector<M2SequenceFallback> playableAnimationLookup;
106
107 [[
109 =offsetAfter("vertices"),
110 =welder::mark::no_reassign,
111 =welder::doc("The skin profiles (LOD views), embedded in the model "
112 "pre-WotLK; WotLK+ moves them to .skin files.")]]
113 std::vector<skin::M2SkinProfile<V>> skinProfiles;
114
115 [[
117 =offsetAfter("textureWeights"),
118 =welder::mark::no_reassign,
119 =welder::doc("Texture flipbooks (pre-WotLK; never seen engaged).")]]
120 std::vector<M2TextureFlipbook<V>> textureFlipbooks;
121
122 [[=welder::mark::exclude]]
123 bool operator==(const DataPreWotlk&) const = default;
124 };
125
128 struct DataWotlk {
129 [[
131 =offsetAfter("vertices"),
132 =welder::mark::exclude,
133 =welder::doc("How many .skin files (LOD views) belong to the model "
134 "(WotLK+). A derived layout field: the M2 assembly's "
135 "skins vector is the source of truth — its write stamps "
136 "this from skins.size(), and the bindings hide it.")]]
137 std::uint32_t numSkinProfiles = 0;
138
139 [[=welder::mark::exclude]]
140 bool operator==(const DataWotlk&) const = default;
141 };
144 struct DataTbc {
145 [[
147 =gatedBy(0x8),
148 =offsetAfter("particleEmitters"),
149 =welder::mark::no_reassign,
150 =welder::doc("Second-texture material override combos; present in the "
151 "layout only under global flag 0x8 (TBC+).")]]
152 std::vector<std::uint16_t> textureCombinerCombos;
153
154 [[=welder::mark::exclude]]
155 bool operator==(const DataTbc&) const = default;
156 };
158
167 template <ClientVersion V>
168 struct [[
169 =welder::weld,
170 =welder::doc(R"(
171 An MD20 model body for one client version: header scalars plus every
172 offset-addressed block, decoded. A version's class carries ONLY the
173 members that version defines. See https://wowdev.wiki/M2.)")
175 // root::detail:: (never a bare detail::) — the using-directive
176 // above imports record::detail, which a bare spelling would
177 // be ambiguous against
181 M2RootBase {
182 static constexpr ClientVersion Version = V;
183
184 [[=welder::mark::exclude,
185 =welder::doc("The leading magic, 'MD20' — constant on every model; "
186 "hidden from the bindings.")]]
187 std::uint32_t magic = Md20Magic;
188
189 [[=welder::doc("The MD20 format version (256 vanilla .. 274 Legion+).")]]
190 std::uint32_t formatVersion = m2FormatVersion(V);
191
192 [[=welder::doc("The model's internal name; empty in 9.2+ files.")]]
193 std::string name;
194
195 [[=welder::doc("Global flags, see GlobalFlags.")]]
196 GlobalFlags globalFlags{};
197
198 [[=welder::mark::no_reassign,
199 =welder::doc("Global-sequence loop lengths (timestamps).")]]
200 std::vector<M2Loop> globalLoops;
201
202 [[=welder::mark::no_reassign,
203 =welder::doc("The animation sequences.")]]
204 std::vector<M2Sequence<V>> sequences;
205
206 [[=welder::mark::no_reassign,
207 =formats::indexesOptional("sequences"),
208 =welder::doc("Animation-id hash table: AnimationData.dbc id -> sequence "
209 "index, quadratic probing, -1 empty.")]]
210 std::vector<std::int16_t> sequenceLookups;
211
212 [[=welder::mark::no_reassign,
213 =welder::doc("The bones (MAX_BONES nominally 256).")]]
214 std::vector<M2CompBone<V>> bones;
215
216 // NOT indexesOptional("bones"): a skel-based model (Legion+) keeps its
217 // bones in the .skel file, leaving this body's `bones` empty while the
218 // lookups still address the skeleton's. The M2 assembly's validate()
219 // checks both lookups against whichever list actually supplies the bones.
220 [[=welder::mark::no_reassign,
221 =welder::doc(
222 "Key-bone lookup: key bone slot -> bone index, -1 if none.")]
223 ]
224 std::vector<std::int16_t> keyBoneLookup;
225
226 [[=welder::mark::no_reassign,
227 =welder::doc("The global vertex list (Z-up model space).")]]
228 std::vector<M2Vertex> vertices;
229
230 [[=welder::mark::no_reassign,
231 =welder::doc(
232 "Color and alpha animations, referenced from skin batches.")]
233 ]
234 std::vector<M2Color<V>> colors;
235
236 [[=welder::mark::no_reassign,
237 =welder::doc("The texture definitions.")]]
238 std::vector<M2Texture> textures;
239
240 [[=welder::mark::no_reassign,
241 =welder::doc("Global transparency weights.")]]
242 std::vector<M2TextureWeight<V>> textureWeights;
243
244 [[=welder::mark::no_reassign,
245 =welder::doc("UV animations.")]]
246 std::vector<M2TextureTransform<V>> textureTransforms;
247
248 [[=welder::mark::no_reassign,
249 =formats::indexesOptional("textures"),
250 =welder::doc("Replacable-texture reverse lookup: replacable id -> "
251 "texture index or -1.")]]
252 std::vector<std::int16_t> replacableTextureLookup;
253
254 [[=welder::mark::no_reassign,
255 =welder::doc("Materials: render flags + blending modes.")]]
256 std::vector<M2Material> materials;
257
258 // see keyBoneLookup: the effective bone list may live in the .skel
259 [[=welder::mark::no_reassign,
260 =welder::doc("Bone lookup: skin sections select bone subsets through it.")
261 ]]
262 std::vector<std::uint16_t> boneLookupTable;
263
264 [[=welder::mark::no_reassign,
265 =formats::indexesOptional("textures"),
266 =welder::doc("Texture lookup: batches select textures through it.")]]
267 std::vector<std::uint16_t> textureLookupTable;
268
269 [[=welder::mark::no_reassign,
270 =welder::doc("Texture-mapping lookup: -1 environment, 0 first UV set, "
271 "1 second (unused since Cata).")]]
272 std::vector<std::int16_t> textureMappingLookupTable;
273
274 [[=welder::mark::no_reassign,
275 =formats::indexesOptional("textureWeights"),
276 =welder::doc("Transparency lookup: batches select texture weights "
277 "through it.")]]
278 std::vector<std::uint16_t> transparencyLookupTable;
279
280 [[=welder::mark::no_reassign,
281 =formats::indexesOptional("textureTransforms"),
282 =welder::doc("Texture-transform lookup: batches select UV animations "
283 "through it, -1 static.")]]
284 std::vector<std::int16_t> textureTransformsLookupTable;
285
286 [[=welder::doc("The render bounds.")]]
287 CAaBox boundingBox{};
288
289 [[=welder::doc("The render bounding-sphere radius.")]]
290 float boundingSphereRadius = 0;
291
292 [[=welder::doc("The collision bounds.")]]
293 CAaBox collisionBox{};
294
295 [[=welder::doc("The collision bounding-sphere radius.")]]
296 float collisionSphereRadius = 0;
297
298 [[=welder::mark::no_reassign,
300 =formats::indexes("collisionVertices"),
301 =welder::doc("Collision-hull triangle indices (3 per face).")]]
302 std::vector<std::uint16_t> collisionTriangles;
303
304 [[=welder::mark::no_reassign,
305 =welder::doc("Collision-hull vertices.")]]
306 std::vector<C3Vector> collisionVertices;
307
308 [[=welder::mark::no_reassign,
309 =formats::countMatches("collisionTriangles", 3),
310 =welder::doc("Collision-hull per-face normals.")]]
311 std::vector<C3Vector> collisionNormals;
312
313 [[=welder::mark::no_reassign,
314 =welder::doc("Attachment points (weapons, effects, name plates).")]]
315 std::vector<M2Attachment<V>> attachments;
316
317 [[=welder::mark::no_reassign,
318 =formats::indexesOptional("attachments"),
319 =welder::doc("Attachment lookup: attachment id -> index.")]]
320 std::vector<std::uint16_t> attachmentLookupTable;
321
322 [[=welder::mark::no_reassign,
323 =welder::doc("Timed events (sounds, footsteps, death thud).")]]
324 std::vector<M2Event<V>> events;
325
326 [[=welder::mark::no_reassign,
327 =welder::doc("Model lights.")]]
328 std::vector<M2Light<V>> lights;
329
330 [[=welder::mark::no_reassign,
331 =welder::doc("Cameras (portrait, character info, flyby).")]]
332 std::vector<M2Camera<V>> cameras;
333
334 [[=welder::mark::no_reassign,
335 =formats::indexesOptional("cameras"),
336 =welder::doc("Camera lookup: camera type -> index.")]]
337 std::vector<std::uint16_t> cameraLookupTable;
338
339 [[=welder::mark::no_reassign,
340 =welder::doc("Ribbon (trail) emitters.")]]
341 std::vector<M2Ribbon<V>> ribbonEmitters;
342
343 [[=welder::mark::no_reassign,
344 =welder::doc("Particle emitters.")]]
345 std::vector<M2Particle<V>> particleEmitters;
346
354 [[=welder::mark::exclude]]
355 void validateExtra(ValidationReport& report) const {
356 // the bone hierarchy: parents exist, precede their children (the client
357 // resolves transforms in one forward pass) and never form a cycle
358 for (std::size_t i = 0; i < bones.size(); ++i) {
359 const std::int16_t parent = bones[i].parentBone;
360 if (parent < 0)
361 continue;
362 if (static_cast<std::size_t>(parent) >= bones.size())
363 report.addError(std::format("bones[{}]", i),
364 std::format("parent_bone {} out of range: {} bones",
365 parent,
366 bones.size()));
367 else if (static_cast<std::size_t>(parent) >= i)
368 report.addError(std::format("bones[{}]", i),
369 std::format(
370 "parent_bone {} does not precede the child",
371 parent));
372 }
373
374 // alias sequences own no track data: the client follows aliasNext until
375 // it reaches a non-alias, so a dangling or self-referential link hangs it
376 for (std::size_t i = 0; i < sequences.size(); ++i) {
377 if (!sequences[i].isAlias())
378 continue;
379 std::size_t at = i;
380 std::size_t steps = 0;
381 while (steps++ <= sequences.size()) {
382 const std::size_t next = sequences[at].aliasNext;
383 if (next >= sequences.size()) {
384 report.addError(std::format("sequences[{}]", at),
385 std::format(
386 "alias_next {} out of range: {} sequences", next,
387 sequences.size()));
388 break;
389 }
390 if (next == at) {
391 report.addError(std::format("sequences[{}]", at),
392 "alias_next points at itself");
393 break;
394 }
395 at = next;
396 if (!sequences[at].isAlias())
397 break;
398 }
399 if (steps > sequences.size())
400 report.addError(std::format("sequences[{}]", i),
401 "alias chain does not reach a non-alias sequence (cycle)");
402 }
403 }
404
405 bool operator==(const M2Root&) const = default;
406 };
407}
408
409namespace wowlib::formats::m2 {
413 template <ClientVersion V>
415}
void addError(std::string path, std::string message)
Record an error finding (see add()).
Client version identity, the flavor axis that separates a client's CONTENT version from the engine ge...
M2 effect-emitter records (namespace wowlib::formats::m2::root::record): ribbon emitters and the part...
The chunk annotation vocabulary format entities declare their binary mapping with.
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
The M2 version vocabulary: the layout pivots record/entity partial specializations key on,...
M2 geometry, texture and material records (namespace wowlib::formats::m2::root::record): the global v...
M2 skin-profile records (namespace wowlib::formats::m2::skin): the LOD view onto the model — local ve...
The MD20 model body (M2Root) with its per-version classes.
Definition bone.hpp:17
GlobalFlags
M2Root::globalFlags bits.
Definition root.hpp:49
@ ChunkedAnimFiles
The .anim files are chunked (Legion+).
Definition root.hpp:58
@ CameraRelated
Camera related (WoD+).
Definition root.hpp:55
@ UseTextureCombinerCombos
The texture_combiner_combos block trails the header (TBC+).
Definition root.hpp:52
@ TiltX
Tilt the model over X (flying mounts).
Definition root.hpp:50
@ NewParticleRecord
Cata: particle records are the 492-byte layout even below v272.
Definition root.hpp:56
@ LoadPhysData
Request the .phys file (MoP+).
Definition root.hpp:53
@ TiltY
Tilt the model over Y.
Definition root.hpp:51
@ Unk0x80
Unset stops demon-hunter tattoos glowing (WoD+).
Definition root.hpp:54
@ TextureTransformsUseBoneSequences
Texture transforms animate on the bone's sequence (Legion+).
Definition root.hpp:57
constexpr std::uint32_t Md20Magic
The MD20 leading magic, as memcpy'd from disk.
Definition root.hpp:45
consteval std::uint32_t m2FormatVersion(ClientVersion v)
The MD20 formatVersion wowlib writes for v — the value the client era itself exports (wowdev....
root::M2Root< canonicalVersion(V, M2DataPivots, M2Versions)> M2Root
The MD20 body — the canonicalizing face of root::M2Root: every client version maps to its range's fir...
Definition root.hpp:364
constexpr ClientVersion M2PerSequenceTimelines
WotLK (v264): every M2Track nests one timestamp/value array per sequence (the vanilla single timeline...
constexpr std::array M2DataPivots
M2Root (the MD20 body): the union of every record pivot, its own trait slots (TBC combos,...
constexpr ClientVersion M2CompressedBones
TBC (v260+): bone rotations become compressed M2CompQuat (vanilla stored raw C4Quaternion),...
constexpr std::array M2Versions
The versions M2 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...
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 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.
consteval detail::OffsetAfterSpec offsetAfter(std::string_view name)
Anchor a version-trait member at its positional layout position: the offset serializer walks the enti...
consteval detail::UntilSpec until(ClientVersion v)
Restrict a member to entity versions < v (exclusive).
consteval detail::IndexesOptionalSpec indexesOptional(std::string_view name)
Declare a referential contract that tolerates the "none" sentinel: like indexes, except an element th...
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::GatedBySpec gatedBy(std::uint32_t mask)
Make an offset-entity member's binary presence conditional on the entity's globalFlags: it occupies b...
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...
The offset-format storage vocabulary and serializer engine for the M2 family, in one header.
M2 skeleton records (namespace wowlib::formats::m2::root::record): M2CompBone across its eras.
M2 scene records (namespace wowlib::formats::m2::root::record): attachments, events,...
M2 animation-sequence records (namespace wowlib::formats::m2::root::record): M2Sequence across its th...
An axis-aligned bounding box: minimum and maximum corners.
Definition types.hpp:134
The serialization face of an offset block, mixed in CRTP-style: an entity struct E : M2OffsetBlock<E>...
The version-agnostic base of every M2Root<V> (welded as "M2Root").
Definition root.hpp:74
bool operator==(const M2RootBase &) const =default
The MD20 model body for one client version: header scalars plus every offset-addressed block,...
Definition root.hpp:138
Pre-WotLK members: the embedded skin profiles and the vanilla/TBC-only lookup blocks.
Definition root.hpp:86
bool operator==(const DataPreWotlk &) const =default
std::vector< M2TextureFlipbook< V > > textureFlipbooks
Texture flipbooks (pre-WotLK; never seen engaged).
Definition root.hpp:97
std::vector< M2SequenceFallback > playableAnimationLookup
Playable-animation fallbacks, one per AnimationData.dbc id (pre-WotLK).
Definition root.hpp:89
std::vector< skin::M2SkinProfile< V > > skinProfiles
The skin profiles (LOD views), embedded in the model pre-WotLK; WotLK+ moves them to ....
Definition root.hpp:93
TBC+ members: the flag-gated combiner-combo tail.
Definition root.hpp:113
std::vector< std::uint16_t > textureCombinerCombos
Second-texture material override combos; present in the layout only under global flag 0x8 (TBC+).
Definition root.hpp:116
bool operator==(const DataTbc &) const =default
WotLK+ members: the external-skin count replacing the embedded profiles.
Definition root.hpp:104
std::uint32_t numSkinProfiles
How many .skin files (LOD views) belong to the model (WotLK+).
Definition root.hpp:107
bool operator==(const DataWotlk &) const =default
The M2 animation vocabulary (namespace wowlib::formats::m2::root::record): the small fixed-size primi...
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...