wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
skeleton.hpp
Go to the documentation of this file.
1#pragma once
2
20
21#include <array>
22#include <cstdint>
23#include <format>
24#include <optional>
25#include <set>
26#include <span>
27#include <string>
28#include <vector>
29
30#include <welder/vocabulary.hpp>
31
33#include <wowlib/core/error.hpp>
35#include <wowlib/core/lang.hpp>
47
48
49namespace wowlib::formats::m2 {
50 // --- .skel chunk payload records ------------------------------------------
51 // Stable across the whole chunked era (M2ChunkPayloadPivots is empty): each
52 // family canonicalizes to a single Legion instantiation.
53
54 namespace detail {
55 template <ClientVersion V>
56 struct [[
57 =welder::weld,
58 =welder::doc("The SKL1 payload: the skeleton's identity.")
60 static constexpr ClientVersion Version = V;
62 [[=welder::doc("Flags; 0x100 in every file observed so far.")]]
63 std::uint32_t flags = 0x100;
65 [[=welder::doc("The skeleton's name.")]]
66 std::string name;
68 [[=welder::doc("Unknown trailing bytes; always zero so far.")]]
69 std::array<std::uint8_t, 4> padding{};
70
71 bool operator==(const SkelHeader&) const = default;
72 };
73
74 template <ClientVersion V>
75 struct [[
76 =welder::weld,
77 =welder::doc("The SKS1 payload: the sequence set that moved out of the "
78 "model.")
80 static constexpr ClientVersion Version = V;
82 [[=welder::mark::no_reassign,
83 =welder::doc("Global-sequence loop lengths.")]]
84 std::vector<root::record::M2Loop> globalLoops;
85
86 [[=welder::mark::no_reassign,
87 =welder::doc("The animation sequences.")]]
88 std::vector<root::record::M2Sequence<V>> sequences;
89
90 [[=welder::mark::no_reassign,
91 =welder::doc("Animation-id hash table (see M2Root.sequence_lookups).")]]
92 std::vector<std::int16_t> sequenceLookups;
93
94 [[=welder::doc("Unknown trailing bytes; always zero so far.")]]
95 std::array<std::uint8_t, 8> padding{};
96
98 [[=welder::mark::exclude]]
99 bool empty() const {
100 return globalLoops.empty() && sequences.empty() && sequenceLookups.empty();
101 }
102
103 bool operator==(const SkelSequences&) const = default;
104 };
105
106 template <ClientVersion V>
107 struct [[
108 =welder::weld,
109 =welder::doc("The SKB1 payload: the bones that moved out of the model "
110 "(external sequences' track data lives in the .anim AFSB "
111 "chunks).")
113 static constexpr ClientVersion Version = V;
114
115 [[=welder::mark::no_reassign,
116 =welder::doc("The bones.")]]
117 std::vector<root::record::M2CompBone<V>> bones;
119 [[=welder::mark::no_reassign,
120 =welder::doc(
121 "Key-bone lookup: key bone slot -> bone index, -1 if none.")]]
122 std::vector<std::int16_t> keyBoneLookup;
123
124 [[=welder::mark::exclude]]
125 bool empty() const {
126 return bones.empty() && keyBoneLookup.empty();
127 }
128
129 bool operator==(const SkelBones&) const = default;
130 };
131
132 template <ClientVersion V>
133 struct [[
134 =welder::weld,
135 =welder::doc("The SKA1 payload: the attachments that moved out of the "
136 "model (external sequences' track data lives in the .anim "
137 "AFSA chunks).")
139 static constexpr ClientVersion Version = V;
140
141 [[=welder::mark::no_reassign,
142 =welder::doc("The attachment points.")]]
143 std::vector<root::record::M2Attachment<V>> attachments;
144
145 [[=welder::mark::no_reassign,
146 =welder::doc("Attachment lookup: attachment id -> index.")]]
147 std::vector<std::uint16_t> attachmentLookupTable;
148
149 [[=welder::mark::exclude]]
150 bool empty() const {
151 return attachments.empty() && attachmentLookupTable.empty();
152 }
154 bool operator==(const SkelAttachments&) const = default;
155 };
156 }
159 template <ClientVersion V>
161
162 template <ClientVersion V>
165 template <ClientVersion V>
168 template <ClientVersion V>
170
171 /** The SKPD payload: the parent-skeleton link used for de-duplication
172 (e.g. lightforgeddraeneimale -> draeneimale_hd; the child shares the
173 parent's AFID/BFID files while keeping its own SK*1 chunks). */
174 struct [[
175 =welder::weld,
176 =welder::doc("The SKPD parent-skeleton link: the parent .skel FileDataID "
177 "whose AFID/BFID satellite files this skeleton shares.")
178 ]] SkelParentData {
179 [[=welder::doc("Unknown leading bytes; always zero so far.")]]
180 std::array<std::uint8_t, 8> padding0{};
181 [[=welder::doc("The parent .skel FileDataID.")]]
182 std::uint32_t parentSkelFileId = 0;
183 [[=welder::doc("Unknown trailing bytes; always zero so far.")]]
184 std::array<std::uint8_t, 4> padding1{};
185
186 bool operator==(const SkelParentData&) const = default;
187 };
188
189 static_assert(sizeof(SkelParentData) == 16);
190
194 welded supertype for the per-version Skeleton* classes (isinstance,
195 Skeleton.for_version(expansion)). No role in the C++ API.
197 @see https://wowdev.wiki/M2/.skel */
198 struct [[
199 =welder::weld,
200 =welder::weld_as("Skeleton"),
202 =welder::doc(R"(
203 A shared model skeleton (.skel, Legion 7.3+), abstract over the client
204 version. Construct a concrete version with
205 Skeleton.for_version(expansion); the per-version Skeleton* classes are
206 subclasses. See https://wowdev.wiki/M2/.skel.)")
208 bool operator==(const SkeletonBase&) const = default;
209 };
210
223 namespace detail {
224 template <ClientVersion V> requires (V >= M2ChunkedContainer)
225 struct [[
226 =welder::weld,
227 =welder::doc(R"(
228 A shared model skeleton (.skel, Legion 7.3+): bones, attachments and
229 sequences for skel-based models, shareable between models via the
230 parent link. See https://wowdev.wiki/M2/.skel.)")
232 static constexpr ClientVersion Version = V;
234
235 [[
237 =welder::doc("The skeleton identity (SKL1).")]]
240 [[
243 =welder::mark::exclude,
244 =welder::doc("SKA1 transport blob; decoded into attachment_block by "
245 "read(fs, key), re-encoded on write.")]]
247
248 [[
251 =welder::mark::exclude,
252 =welder::doc("SKB1 transport blob; decoded into bone_block by "
253 "read(fs, key), re-encoded on write.")]]
255
256 [[
259 =welder::doc("The sequence tables (SKS1).")]]
261
262 [[
265 =welder::mark::no_reassign,
266 =welder::doc("The parent-skeleton link (SKPD); 0 or 1 entries.")]]
267 std::vector<SkelParentData> parentLink;
269 [[
272 =welder::mark::no_reassign,
273 =welder::doc(".anim FileDataIDs (AFID); absent on child skeletons, "
274 "which share the parent's (see parent_anim_fdids).")]]
275 std::vector<chunked::record::AnimFileEntry> animFdids;
276
277 [[
280 =welder::mark::no_reassign,
281 =welder::doc(".bone FileDataIDs (BFID); absent on child skeletons, "
282 "which share the parent's (see parent_bone_fdids).")]]
283 std::vector<std::uint32_t> boneFdids;
285 // --- decoded views (no chunk of their own; SKB1/SKA1 re-encode from
286 // --- these on write) ------------------------------------------------
287
288 [[=welder::doc("The bones (decoded SKB1).")]]
290
291 [[=welder::doc("The attachments (decoded SKA1).")]]
292 SkelAttachments<V> attachmentBlock{};
293
294 [[
295 =welder::mark::no_reassign,
296 =welder::doc("The parent's AFID entries when this skeleton is a child "
297 "(filled by read(fs, key); not part of this file).")]]
298 std::vector<chunked::record::AnimFileEntry> parentAnimFdids;
299
300 [[
301 =welder::mark::no_reassign,
302 =welder::doc("The parent's BFID entries when this skeleton is a child "
303 "(filled by read(fs, key); not part of this file).")]]
304 std::vector<std::uint32_t> parentBoneFdids;
305
308 [[=welder::doc("The effective AFID entries: own when present, else the "
309 "parent's.")]]
310 const std::vector<chunked::record::AnimFileEntry>&
311 effectiveAnimFdids() const {
312 return animFdids.empty() ? parentAnimFdids : animFdids;
313 }
314
316 [[=welder::doc("The effective BFID entries: own when present, else the "
317 "parent's.")]]
318 const std::vector<std::uint32_t>& effectiveBoneFdids() const {
319 return boneFdids.empty() ? parentBoneFdids : boneFdids;
320 }
321
322 [[=welder::mark::only(welder::lang::lua, wowlib::lang::Cs),
323 =welder::doc("Load the skeleton: chunks, the parent's shared AFID/BFID "
324 "and the .anim-resolved bone/attachment data.")]]
325 Result<void> read(fs::FileSystem& fs [[=welder::doc("the filesystem gateway")]],
326 const FileKey& key [[=welder::doc("the .skel identity (path and/or FileDataID)")]]);
327
328 [[=welder::mark::only(welder::lang::lua, wowlib::lang::Cs),
329 =welder::doc("Serialize the skeleton and its .anim files (AFSA/AFSB "
330 "sections) through the project overlay. A paired model's "
331 "AFM2 section is restored by the owning M2's write.")]]
332 Result<void> write(fs::FileSystem& fs [[=welder::doc("the filesystem gateway")]],
333 const FileKey& key [[=welder::doc("the .skel identity; must resolve to a path")]]) const;
334
335 // the inherited ChunkedFile read(span)/write() stay available for
336 // raw chunk-level access (blobs left undecoded)
337 using ChunkedFile<Skeleton<V>>::read;
338 using ChunkedFile<Skeleton<V>>::write;
340 bool operator==(const Skeleton&) const = default;
341 };
342 }
343
348 template <ClientVersion V> requires (V >= M2ChunkedContainer)
350}
351
352// --- fs-level read/write definitions (inline for the same implicit-
353// instantiation reason as m2.hpp's) -------------------------------------------
354namespace wowlib::formats::m2 {
355 template <ClientVersion V> requires (V >= M2ChunkedContainer)
357 const auto bytes = fs.readFile(key);
358 if (!bytes) return std::unexpected{bytes.error()};
359
360 *this = Skeleton{};
361 if (auto r = ChunkedFile<Skeleton<V>>::read(std::span<const std::byte>{*bytes}); !r) return r;
362
363 // A child skeleton shares the parent's satellite ids (only the *FID
364 // chunks are shared — the child keeps its own SK*1 data). One hop, per
365 // every known example; a missing parent degrades to inline-only decode.
366 if (!parentLink.empty() && parentLink.front().parentSkelFileId != 0)
367 if (auto pbytes = fs.readFile(FileKey{FileDataID{parentLink.front().parentSkelFileId}})) {
368 Skeleton<V> parent;
369 if (parent.ChunkedFile<Skeleton<V>>::read(std::span<const std::byte>{*pbytes})) {
370 parentAnimFdids = parent.animFdids;
371 parentBoneFdids = parent.boneFdids;
372 }
373 }
374
375 // name fallback for satellites without FileDataID entries
376 std::optional<SatellitePaths> paths;
377 if (const FileKey resolved = fs.resolve(key); resolved.path) paths.emplace(*resolved.path);
378
379 AnimCache cache{
380 [&, paths](SequenceKey seq) -> Result<FileBuffer> {
381 const std::uint32_t fdid = AnimCache::afidLookup(effectiveAnimFdids(), seq);
382 if (fdid != 0) return fs.readFile(FileKey{FileDataID{fdid}});
383 if (paths) return fs.readFile(FileKey{paths->anim(seq)});
384 return makeError(ErrorCode::FileNotFound, "no AFID entry and no path");
385 }
386 };
387
388 const auto makeCtx = [&](std::span<const std::byte> inlineBase, std::uint32_t target) {
390 ctx.sequenceBase = cache.sequenceBase(sequenceBlock.sequences, inlineBase, target);
391 return ctx;
392 };
393
394 if (!skb1.bytes.empty()) {
395 const std::span<const std::byte> base{skb1.bytes};
396 if (auto r = boneBlock.read(base, makeCtx(base, AnimCache::AfsbMagic)); !r)
397 return makeError(r.error().code, std::format("SKB1: {}", r.error().message), r.error().nativeError);
398 }
399 if (!ska1.bytes.empty()) {
400 const std::span<const std::byte> base{ska1.bytes};
401 if (auto r = attachmentBlock.read(base, makeCtx(base, AnimCache::AfsaMagic)); !r)
402 return makeError(r.error().code, std::format("SKA1: {}", r.error().message), r.error().nativeError);
403 }
404 // the blobs stay as read: an untouched skeleton written at chunk level
405 // remains byte-perfect; the fs write path re-encodes them from the
406 // typed blocks.
407 return {};
408 }
409
410 template <ClientVersion V> requires (V >= M2ChunkedContainer)
411 Result<void> detail::Skeleton<V>::write(fs::FileSystem& fs, const FileKey& key) const {
412 const FileKey resolved = fs.resolve(key);
413 if (!resolved.path)
414 return makeError(ErrorCode::PathNotResolvable, "saving a skeleton needs a path for the key");
415 const SatellitePaths paths{*resolved.path};
416
417 Skeleton<V> copy = *this;
418
419 // re-encode the bone/attachment blocks, splitting external sequences
420 // into per-sequence AFSB/AFSA buffers
421 AnimBuffers afsaBufs;
422 AnimBuffers afsbBufs;
423 {
424 auto encoded = boneBlock.write(afsbBufs.sink(sequenceBlock.sequences));
425 if (!encoded) return std::unexpected{encoded.error()};
426 copy.skb1.bytes = std::move(*encoded);
427 auto attachments = attachmentBlock.write(afsaBufs.sink(sequenceBlock.sequences));
428 if (!attachments) return std::unexpected{attachments.error()};
429 copy.ska1.bytes = std::move(*attachments);
430 }
431
432 // the skeleton's .anim files: AFSA + AFSB sections. NOTE: a paired
433 // model's AFM2 (event) section is not represented here — the owning
434 // M2's write is the full-fidelity save path.
435 copy.animFdids.clear();
436 for (const SequenceKey seq : AnimBuffers::mergedKeys({&afsaBufs, &afsbBufs})) {
437 FileBuffer file;
438 afsaBufs.appendChunkTo(file, AnimCache::AfsaMagic, seq);
439 afsbBufs.appendChunkTo(file, AnimCache::AfsbMagic, seq);
440 const auto r = fs.addFile(paths.anim(seq), file);
441 if (!r)
442 return makeError(r.error().code, std::format("anim {:04}-{:02}: {}", seq.id, seq.variation, r.error().message),
443 r.error().nativeError);
444 copy.animFdids.push_back({seq.id, seq.variation, r->value});
445 }
446
447 const auto bytes = copy.ChunkedFile<Skeleton<V>>::write();
448 if (!bytes) return std::unexpected{bytes.error()};
449 if (auto r = fs.addFile(*resolved.path, *bytes); !r) return std::unexpected{r.error()};
450 return {};
451 }
452}
The chunk framework, vocabulary and engine in one header.
The per-sequence external write buffers every M2-family write path shares: an external sequence's dat...
static std::set< SequenceKey > mergedKeys(std::initializer_list< const AnimBuffers * > sets)
The union of every set's keys — one .anim file exists per key across all sections (AFM2 + AFSA + AFSB...
void appendChunkTo(FileBuffer &out, std::uint32_t fourcc, SequenceKey key) const
Append this set's buffer for key to out as one fourcc+size+payload chunk (assembling ....
OffsetWriteContext sink(const std::vector< Sequence > &sequences)
The write context routing external sequences into this buffer set.
A lazy per-sequence .anim loader handing out resolution windows: for a chunked file (leading AFM2) th...
static std::uint32_t afidLookup(std::span< const chunked::record::AnimFileEntry > entries, SequenceKey key)
The AFID FileDataID for sequence key, 0 when unlisted.
static constexpr std::uint32_t AfsbMagic
static constexpr std::uint32_t AfsaMagic
The satellite-file naming conventions around one model path: every companion file of "creature\\x\\x....
std::string anim(SequenceKey key) const
"{stem}AAAA-SS.anim" — the external-sequence naming.
FileKey resolve(const FileKey &key) const
Result< FileBuffer > readFile(const FileKey &key)
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.
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,...
Legion+ companion-chunk records (namespace wowlib::formats::m2::chunked::record): the payload types o...
A shared model skeleton (.skel, 7.3+): the bones, attachments and sequences a skel-based model moved ...
Definition m2.hpp:62
detail::SkelSequences< canonicalVersion(V, M2ChunkPayloadPivots, M2ChunkedVersions)> SkelSequences
The SKS1 payload — canonicalizing face of detail::SkelSequences.
Definition skeleton.hpp:139
constexpr ClientVersion M2ChunkedContainer
legion (7.0.1.20740): the on-disk .m2 becomes a chunked file — the MD20 image moves into the MD21 chu...
detail::SkelBones< canonicalVersion(V, M2ChunkPayloadPivots, M2ChunkedVersions)> SkelBones
The SKB1 payload — canonicalizing face of detail::SkelBones.
Definition skeleton.hpp:142
detail::Skeleton< canonicalVersion(V, M2ChunkPayloadPivots, M2ChunkedVersions)> Skeleton
A shared model skeleton — the canonicalizing face of detail::Skeleton: the whole chunked era is one r...
Definition skeleton.hpp:277
constexpr std::array M2ChunkedVersions
The era subset the chunked container exists for (Legion+).
detail::SkelHeader< canonicalVersion(V, M2ChunkPayloadPivots, M2ChunkedVersions)> SkelHeader
The SKL1 payload — canonicalizing face of detail::SkelHeader.
Definition skeleton.hpp:136
constexpr std::array< ClientVersion, 0 > M2ChunkPayloadPivots
Skeleton and its chunk payloads plus the shell payload records: stable across the whole chunked era —...
detail::SkelAttachments< canonicalVersion(V, M2ChunkPayloadPivots, M2ChunkedVersions)> SkelAttachments
The SKA1 payload — canonicalizing face of detail::SkelAttachments.
Definition skeleton.hpp:145
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::OptionalSpec Optional
Mark a chunk member the format does not require: absence on read is fine.
FourCCEndian
How a chunk's FourCC characters are laid out on disk.
Definition fourcc.hpp:14
@ Forward
The characters are stored as written: 'AFID' appears in the file as the bytes "AFID".
Definition fourcc.hpp:22
consteval detail::ChunkSpec chunk(const char(&cc)[5], FourCCEndian endian=FourCCEndian::Reversed)
Declare the chunk a member maps to.
constexpr welder::lang Cs
C#/.NET — the welder-csharp rod's identity (user-range slot 0), respelled for wowlib's annotation sit...
Definition lang.hpp:23
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
@ FileNotFound
The file exists nowhere in the overlay or storage.
Definition error.hpp:22
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
Definition error.hpp:23
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
Definition buffer.hpp:16
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.
The M2 family's satellite-file vocabulary (namespace wowlib::formats::m2), shared by the fs-level rea...
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...
std::optional< std::string > path
The canonical client-internal path, if known.
Definition file_key.hpp:31
An unparsed chunk payload, preserved verbatim for round-trip.
std::vector< std::byte > bytes
The serialization face of a chunked entity, mixed in CRTP-style: an entity struct E : ChunkedFile<E> ...
The version-agnostic root of every file-level entity (welded as "FileEntity").
The serialization face of an offset block, mixed in CRTP-style: an entity struct E : M2OffsetBlock<E>...
Result< FileBuffer > write() const
Serialize this entity in wowlib's canonical layout (an offset format has no byte-perfect round-trip g...
Result< void > read(std::span< const std::byte > data)
Deserialize file bytes into this entity, replacing its contents.
Resolution of SequenceData members while reading: where each outer element's nested data lives.
std::function< std::span< const std::byte >(std::size_t i)> sequenceBase
The base span the inner arrays of outer element i resolve their offsets against — the matching ....
A sequence's satellite identity: the (animation id, variation) pair the .anim naming convention ("{st...
The SKPD payload: the parent-skeleton link used for de-duplication (e.g.
Definition skeleton.hpp:151
std::array< std::uint8_t, 4 > padding1
Unknown trailing bytes; always zero so far.
Definition skeleton.hpp:157
bool operator==(const SkelParentData &) const =default
std::array< std::uint8_t, 8 > padding0
Unknown leading bytes; always zero so far.
Definition skeleton.hpp:153
std::uint32_t parentSkelFileId
The parent .skel FileDataID.
Definition skeleton.hpp:155
The version-agnostic base of every Skeleton<V> (welded as "Skeleton").
Definition skeleton.hpp:176
bool operator==(const SkeletonBase &) const =default
The SKA1 payload: the attachments that moved out of the model (external sequences' track data lives i...
Definition skeleton.hpp:117
bool operator==(const SkelAttachments &) const =default
The SKB1 payload: the bones that moved out of the model (external sequences' track data lives in the ...
Definition skeleton.hpp:99
bool operator==(const SkelBones &) const =default
The SKL1 payload: the skeleton's identity.
Definition skeleton.hpp:57
bool operator==(const SkelHeader &) const =default
The SKS1 payload: the sequence set that moved out of the model.
Definition skeleton.hpp:74
bool empty() const
Chunk engagement: emitted only when any table holds data.
Definition skeleton.hpp:90
bool operator==(const SkelSequences &) const =default
Result< void > read(fs::FileSystem &fs, const FileKey &key)
Load the skeleton: chunks, the parent's shared AFID/BFID and the .anim-resolved bone/attachment data.
Definition skeleton.hpp:284
const std::vector< chunked::record::AnimFileEntry > & effectiveAnimFdids() const
The AFID set to resolve .anim files with: this file's, or the parent's when this skeleton is a child ...
Definition skeleton.hpp:245
const std::vector< std::uint32_t > & effectiveBoneFdids() const
The BFID set to resolve .bone files with (own, else parent's).
Definition skeleton.hpp:251
bool operator==(const Skeleton &) const =default
Result< void > write(fs::FileSystem &fs, const FileKey &key) const
Serialize the skeleton and its .anim files (AFSA/AFSB sections) through the project overlay.
Definition skeleton.hpp:339
The M2 animation vocabulary (namespace wowlib::formats::m2::root::record): the small fixed-size primi...