Models (M2)¶
An M2 is WoW's animated model format — characters, creatures, doodads,
spell effects. Unlike the chunked formats, an M2 root is an offset file: one
blob whose structures point at each other by byte offset. And unlike a WMO, a
modern model is spread across external companion files — .skin render
profiles from Wrath on, and the Legion+ split into .skel skeletons, .anim
streams, .bone files and the chunked .m2 wrapper.
wowlib hides all of that behind one compound entity: M2<version> reads
the root and pulls every companion its era uses; writing bakes the offsets
back and splits the companions out again. Because satellites are fetched
during the read, the compound entity always reads through the filesystem
gateway (standalone pieces like a Skin or Skeleton also read from plain
bytes).
Reading a model¶
Walking the data¶
The root exposes the model tables; the render profiles (skins) hang off the compound. Geometry and track keyframe vectors are live views — numeric ones zero-copy.
Animation data lives in tracks (M2Track<T>): per-sequence timestamp and
value arrays, typed by what they animate. The
M2 records page documents every family with
per-era layout badges.
Writing and converting¶
write(fs, key) re-bakes the offset engine and emits the era's own file set —
inline data for a vanilla model, root + .skin files for Wrath, the full
chunked split for Legion+. convert() re-targets a model between eras and
restructures the companions accordingly:
legion_model = model.convert(wowlib.Expansion.Legion)
legion_model.write(fs, wowlib.FileKey("Creature/Murloc/Murloc.m2"))
Semantic round-trip
An offset file has no single canonical byte layout, so M2 (like ADT)
guarantees a semantic round-trip — write → re-read → equal values —
rather than WMO/WDT's byte-identical one. validate() before writing
catches inconsistent cross-references; see
Validating before writing.
Animation tracks are two layouts, one surface¶
Track storage changed at WotLK: pre-WotLK tracks share one global
timeline, sliced per sequence by interpolation ranges; WotLK+ tracks nest
one timestamp/value array per sequence (external sequences keep theirs in
the .anim file). The raw members reflect whichever layout the entity is —
but every track also carries the canonical timeline surface, so
version-agnostic code never branches:
timeline_count()— one timeline per sequence (or a single timeline for a global-sequence-driven track);key_count(timeline),timeline_timestamps(timeline),timeline_values(timeline)— the slice pre-WotLK, the nested array WotLK+, returned as copies; out-of-range timelines error.
In C#, the timing half of that surface is also a real interface —
IAnimTimeline (TimelineCount/KeyCount/TimelineTimestamps),
implemented by every track family base — so sequencers and event schedulers
hold one interface across all nine value families and the event tracks:
static ulong TotalKeys(IEnumerable<Formats.M2.Root.Record.IAnimTimeline> tracks)
=> tracks.Aggregate(0UL, (n, t) => {
for (ulong s = 0; s < t.TimelineCount(); s++) n += t.KeyCount(s);
return n;
});
The raw members stay available on the concrete classes for zero-copy work — the accessors return copies, which is the right trade for correctness-first code; renderers hot-looping keys can still branch once on the two range classes and use spans.