30#include <welder/vocabulary.hpp>
55 template <ClientVersion V>
58 =welder::doc(
"The SKL1 payload: the skeleton's identity.")
62 [[=welder::doc(
"Flags; 0x100 in every file observed so far.")]]
63 std::uint32_t
flags = 0x100;
65 [[=welder::doc(
"The skeleton's name.")]]
68 [[=welder::doc(
"Unknown trailing bytes; always zero so far.")]]
71 bool operator==(
const SkelHeader&)
const =
default;
74 template <ClientVersion V>
77 =welder::doc(
"The SKS1 payload: the sequence set that moved out of the "
82 [[=welder::mark::no_reassign,
83 =welder::doc(
"Global-sequence loop lengths.")]]
86 [[=welder::mark::no_reassign,
87 =welder::doc(
"The animation sequences.")]]
88 std::vector<root::record::M2Sequence<V>>
sequences;
90 [[=welder::mark::no_reassign,
91 =welder::doc(
"Animation-id hash table (see M2Root.sequence_lookups).")]]
94 [[=welder::doc(
"Unknown trailing bytes; always zero so far.")]]
95 std::array<std::uint8_t, 8>
padding{};
98 [[=welder::mark::exclude]]
100 return globalLoops.empty() && sequences.empty() && sequenceLookups.empty();
106 template <ClientVersion V>
109 =welder::doc(
"The SKB1 payload: the bones that moved out of the model "
110 "(external sequences' track data lives in the .anim AFSB "
115 [[=welder::mark::no_reassign,
116 =welder::doc(
"The bones.")]]
117 std::vector<root::record::M2CompBone<V>> bones;
119 [[=welder::mark::no_reassign,
121 "Key-bone lookup: key bone slot -> bone index, -1 if none.")]]
122 std::vector<std::int16_t> keyBoneLookup;
124 [[=welder::mark::exclude]]
126 return bones.empty() && keyBoneLookup.empty();
129 bool operator==(
const SkelBones&)
const =
default;
132 template <ClientVersion V>
135 =welder::doc(
"The SKA1 payload: the attachments that moved out of the "
136 "model (external sequences' track data lives in the .anim "
141 [[=welder::mark::no_reassign,
142 =welder::doc(
"The attachment points.")]]
143 std::vector<root::record::M2Attachment<V>> attachments;
145 [[=welder::mark::no_reassign,
146 =welder::doc(
"Attachment lookup: attachment id -> index.")]]
147 std::vector<std::uint16_t> attachmentLookupTable;
149 [[=welder::mark::exclude]]
151 return attachments.empty() && attachmentLookupTable.empty();
159 template <ClientVersion V>
162 template <ClientVersion V>
165 template <ClientVersion V>
168 template <ClientVersion V>
176 =welder::doc(
"The SKPD parent-skeleton link: the parent .skel FileDataID "
177 "whose AFID/BFID satellite files this skeleton shares.")
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{};
200 =welder::weld_as(
"Skeleton"),
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.)")
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.)")
237 =welder::doc(
"The skeleton identity (SKL1).")]]
243 =welder::mark::exclude,
244 =welder::doc(
"SKA1 transport blob; decoded into attachment_block by "
245 "read(fs, key), re-encoded on write.")]]
251 =welder::mark::exclude,
252 =welder::doc(
"SKB1 transport blob; decoded into bone_block by "
253 "read(fs, key), re-encoded on write.")]]
259 =welder::doc(
"The sequence tables (SKS1).")]]
265 =welder::mark::no_reassign,
266 =welder::doc(
"The parent-skeleton link (SKPD); 0 or 1 entries.")]]
267 std::vector<SkelParentData> parentLink;
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;
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;
288 [[=welder::doc(
"The bones (decoded SKB1).")]]
291 [[=welder::doc(
"The attachments (decoded SKA1).")]]
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;
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;
308 [[=welder::doc(
"The effective AFID entries: own when present, else the "
310 const std::vector<chunked::record::AnimFileEntry>&
311 effectiveAnimFdids()
const {
312 return animFdids.
empty() ? parentAnimFdids : animFdids;
316 [[=welder::doc(
"The effective BFID entries: own when present, else the "
318 const std::vector<std::uint32_t>& effectiveBoneFdids()
const {
319 return boneFdids.empty() ? parentBoneFdids : boneFdids;
323 =welder::doc(
"Load the skeleton: chunks, the parent's shared AFID/BFID "
324 "and the .anim-resolved bone/attachment data.")]]
326 const FileKey& key [[=welder::doc(
"the .skel identity (path and/or FileDataID)")]]);
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.")]]
333 const FileKey& key [[=welder::doc(
"the .skel identity; must resolve to a path")]])
const;
357 const auto bytes =
fs.readFile(key);
358 if (!bytes)
return std::unexpected{bytes.error()};
361 if (
auto r = ChunkedFile<
Skeleton<V>>::read(std::span<const std::byte>{*bytes}); !r)
return r;
366 if (!parentLink.empty() && parentLink.front().parentSkelFileId != 0)
367 if (
auto pbytes = fs.
readFile(FileKey{FileDataID{parentLink.front().parentSkelFileId}})) {
369 if (parent.ChunkedFile<
Skeleton<V>>::read(std::span<const std::byte>{*pbytes})) {
370 parentAnimFdids = parent.animFdids;
371 parentBoneFdids = parent.boneFdids;
376 std::optional<SatellitePaths> paths;
377 if (
const FileKey resolved = fs.
resolve(key); resolved.
path) paths.emplace(*resolved.path);
382 if (fdid != 0)
return fs.
readFile(FileKey{FileDataID{fdid}});
383 if (paths)
return fs.
readFile(FileKey{paths->anim(seq)});
388 const auto makeCtx = [&](std::span<const std::byte> inlineBase, std::uint32_t target) {
394 if (!skb1.bytes.empty()) {
395 const std::span<const std::byte> base{skb1.bytes};
397 return makeError(r.error().code, std::format(
"SKB1: {}", r.error().message), r.error().nativeError);
399 if (!ska1.bytes.empty()) {
400 const std::span<const std::byte> base{ska1.bytes};
402 return makeError(r.error().code, std::format(
"SKA1: {}", r.error().message), r.error().nativeError);
411 Result<void> detail::Skeleton<V>::write(fs::FileSystem& fs,
const FileKey& key)
const {
412 const FileKey resolved = fs.resolve(key);
414 return makeError(ErrorCode::PathNotResolvable,
"saving a skeleton needs a path for the key");
415 const SatellitePaths paths{*resolved.path};
417 Skeleton<V> copy = *
this;
421 AnimBuffers afsaBufs;
422 AnimBuffers afsbBufs;
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);
435 copy.animFdids.clear();
436 for (
const SequenceKey seq : AnimBuffers::mergedKeys({&afsaBufs, &afsbBufs})) {
438 afsaBufs.appendChunkTo(file, AnimCache::AfsaMagic, seq);
439 afsbBufs.appendChunkTo(file, AnimCache::AfsbMagic, seq);
440 const auto r = fs.addFile(paths.anim(seq), file);
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});
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()};
The chunk framework, vocabulary and engine in one header.
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.
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...
constexpr welder::lang Cs
C#/.NET — the welder-csharp rod's identity (user-range slot 0), respelled for wowlib's annotation sit...
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.
@ FileNotFound
The file exists nowhere in the overlay or storage.
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
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.
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 ....
The M2 animation vocabulary (namespace wowlib::formats::m2::root::record): the small fixed-size primi...