wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
roundtrip_m2.cpp
Go to the documentation of this file.
1
9
10#include <cstddef>
11#include <format>
12#include <map>
13#include <span>
14#include <string>
15#include <type_traits>
16
20
21namespace {
22 using namespace wowlib;
23 using namespace wowlib::audit;
24 using namespace wowlib::formats;
25
30 std::optional<std::string> compareAnimCaptures(const std::map<std::size_t, FileBuffer>& first,
31 const std::map<std::size_t, FileBuffer>& second) {
32 for (const auto& [index, buffer] : first) {
33 const auto it = second.find(index);
34 if (it == second.end()) return std::format("anim {} missing from the rewrite", index);
35 if (auto divergence = audit::detail::firstDivergence(buffer, it->second)) return std::format(
36 "anim {}: {}", index, *divergence);
37 }
38 for (const auto& [index, buffer] : second)
39 if (!first.contains(index)) return std::format("anim {} appeared only in the rewrite", index);
40 return std::nullopt;
41 }
42
49 template <ClientVersion V>
50 RoundtripReport roundtripM2(fs::FileSystem& fs, const std::string& path) {
51 RoundtripReport report;
52
53 // Pre-classify the body bytes: classic clients ship zero-byte placeholder
54 // models, and Legion+ content can sit behind unknown TACT keys — both are
55 // skips. The assembly read below re-reads the body; that is cheap.
56 if (auto outcome = audit::detail::classifyRead(report, "read", fs.readFile(FileKey{path}))) return *outcome;
57
58 m2::M2 < V > model;
59 if (auto r = model.read(fs, FileKey{path}); !r) {
60 // Satellite files (.skin/.anim/.skel) can be individually encrypted.
61 if (r.error().code == ErrorCode::EncryptedContent) return audit::detail::skipped("encrypted");
62 return audit::detail::failWith(report, "read", r.error().message);
63 }
64
65 // skel-based models keep the external-data gating sequences in the
66 // skeleton, and their bone/attachment blocks round-trip separately
67 const auto* gateSequences = &model.root.sequences;
68 bool hasSkel = false;
69 if constexpr (requires { model.chunks; }) {
70 // The M2 file family stores its chunk ids forward, unlike every other
71 // chunk format — tally them unreversed (PFDC, not "CDFP").
73 hasSkel = !model.chunks.skeletonFdid.empty() && model.chunks.skeletonFdid.front() != 0;
74 if (hasSkel) {
75 gateSequences = &model.skel.sequenceBlock.sequences;
77 }
78 }
79
80 // write an offset entity splitting external sequences out per index, parse
81 // it back resolving those buffers, write again with a fresh capture, and
82 // byte-compare both the entity images and the captured .anim buffers
83 const auto resplitStability = [&](const auto& entity, const char* what) -> std::optional<RoundtripReport> {
84 using Entity = std::remove_cvref_t<decltype(entity)>;
85
86 const auto sinkInto = [&](std::map<std::size_t, FileBuffer>& animOut) {
87 return [&animOut, gateSequences](std::size_t i) -> FileBuffer* {
88 if (i >= gateSequences->size()) return nullptr;
89 const auto& s = (*gateSequences)[i];
90 if (!s.ownsAnimFile()) return nullptr;
91 return &animOut[i];
92 };
93 };
94
95 std::map<std::size_t, FileBuffer> animsFirst;
97 firstCtx.sequenceSink = sinkInto(animsFirst);
98 const auto first = entity.write(firstCtx);
99 if (!first)
100 return audit::detail::failWith(report, std::format("{} write", what), first.error().message);
101
102 const std::span<const std::byte> firstSpan{*first};
103 m2::OffsetReadContext readCtx;
104 readCtx.sequenceBase = [& ](std::size_t i) -> std::span<const std::byte> {
105 const auto it = animsFirst.find(i);
106 if (it == animsFirst.end()) return firstSpan;
107 return std::span<const std::byte>{it->second};
108 };
109 Entity reparsed;
110 if (auto r = reparsed.read(firstSpan, readCtx); !r)
111 return audit::detail::failWith(report, std::format("{} reparse", what), r.error().message);
112
113 std::map<std::size_t, FileBuffer> animsSecond;
114 m2::OffsetWriteContext secondCtx;
115 secondCtx.sequenceSink = sinkInto(animsSecond);
116 const auto second = reparsed.write(secondCtx);
117 if (!second)
118 return audit::detail::failWith(report, std::format("{} rewrite", what), second.error().message);
119
120 if (auto divergence = audit::detail::firstDivergence(*first, *second))
121 return audit::detail::failWith(report, std::format("{} compare", what), *divergence);
122 if (auto mismatch = compareAnimCaptures(animsFirst, animsSecond))
123 return audit::detail::failWith(report, std::format("{} anim compare", what), *mismatch);
124 return std::nullopt;
125 };
126
127 if (auto fail = resplitStability(model.root, "body")) return *fail;
128
129 if constexpr (requires { model.skel; })
130 if (hasSkel) {
131 if (auto fail = resplitStability(model.skel.boneBlock, "SKB1")) return *fail;
132 if (auto fail = resplitStability(model.skel.attachmentBlock, "SKA1")) return *fail;
133 if (auto fail = resplitStability(model.skel.sequenceBlock, "SKS1")) return *fail;
134 }
135
136 // every external skin write-stabilizes too (pre-WotLK skins are embedded
137 // in the root and already covered by the body round-trip above)
138 if constexpr (requires { model.skins; })
139 for (std::size_t i = 0; i < model.skins.size(); ++i) {
140 using Skin = std::remove_cvref_t<decltype(model.skins[i])>;
141 const auto first = model.skins[i].write();
142 if (!first)
143 return audit::detail::failWith(report, std::format("skin {} write", i), first.error().message);
144 Skin reparsed;
145 if (auto r = reparsed.read(std::span<const std::byte>{*first}); !r)
146 return audit::detail::failWith(report, std::format("skin {} reparse", i), r.error().message);
147 const auto second = reparsed.write();
148 if (!second)
149 return audit::detail::failWith(report, std::format("skin {} rewrite", i), second.error().message);
150 if (auto divergence = audit::detail::firstDivergence(*first, *second))
151 return audit::detail::failWith(report, std::format("skin {} compare", i), *divergence);
152 }
153
154 return report;
155 }
156}
157
158namespace wowlib::audit::detail {
160 RoundtripReport report = skipped("unsupported-version");
161 const bool matched = withVersion(version, [&]<ClientVersion V>() {
162 if constexpr (versionSupported(formats::m2::M2Versions, V)) report = guarded([&] {
163 return roundtripM2<V>(fs, path);
164 });
165 });
166 if (!matched) return unrecognizedVersion(version);
167 return report;
168 }
169}
static RoundtripReport m2(fs::FileSystem &fs, const std::string &path, ClientVersion version)
Stability round-trip of one model: the assembly read (satellites baked in), then write -> parse -> wr...
Internals of the round-trip audit (namespace wowlib::audit::detail): the per-format driver class — on...
The M2 entity (namespace wowlib::formats::m2): a whole model with its satellite files baked in,...
RoundtripReport failWith(RoundtripReport &report, std::string stage, std::string error)
Mark report failed in place and hand it back — keeps the unknown-chunk tally gathered before the fail...
Definition detail.hpp:57
std::optional< RoundtripReport > classifyRead(RoundtripReport &report, std::string stage, const Result< FileBuffer > &result)
Classify a client-file read for the audit: content behind an unknown TACT key and zero-byte archive e...
Definition detail.hpp:72
bool withVersion(const ClientVersion v, F &&f)
Invoke f with the targeted release constant matching v as a compile-time template argument — the runt...
Definition detail.hpp:166
constexpr bool versionSupported(std::span< const ClientVersion > grid, const ClientVersion v)
Whether a format's instantiation grid carries v — the per-format if constexpr guard of the version di...
Definition detail.hpp:193
RoundtripReport skipped(std::string_view reason)
A skipped outcome — ok, tagged with the reason.
Definition detail.hpp:45
RoundtripReport guarded(F &&f)
Run one file's driver, converting any escaping exception into a failed outcome — one pathological fil...
Definition detail.hpp:100
std::optional< std::string > firstDivergence(const FileBuffer &original, const FileBuffer &rewritten)
The first byte divergence between an original and a rewrite, described by offset and sizes — the stab...
Definition detail.hpp:118
void tallyUnknown(RoundtripReport &report, const formats::ChunkExtras &extras, formats::FourCCEndian endian=formats::FourCCEndian::Reversed)
Append every unmodeled chunk of a parsed entity to the report's tally, one fourcc spelling per occurr...
Definition detail.hpp:87
RoundtripReport unrecognizedVersion(const ClientVersion v)
The dispatch-failure outcome for a version outside the targeted releases.
Definition detail.hpp:202
detail::Skin< canonicalVersion(V, M2SkinPivots, M2SkinVersions)> Skin
An external LOD view — the canonicalizing face of detail::Skin: every WotLK+ version maps to its rang...
Definition skin.hpp:67
detail::M2< canonicalVersion(V, M2AssemblyPivots, M2Versions)> M2
A whole model — the canonicalizing face of detail::M2: every client version maps to its range's first...
Definition m2.hpp:348
constexpr std::array M2Versions
The versions M2 is instantiated (and welded) for: every targeted last-minor-of-major release,...
@ Forward
The characters are stored as written: 'AFID' appears in the file as the bytes "AFID".
Definition fourcc.hpp:22
@ EncryptedContent
Content is behind an unknown TACT encryption key.
Definition error.hpp:31
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.
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 ....
Resolution of SequenceData members while writing: which buffer each outer element's nested data block...
std::function< FileBuffer *(std::size_t i) > sequenceSink
The destination buffer for outer element i's nested data — the .anim file buffer being assembled for ...
Result< void > read(fs::FileSystem &fs, const FileKey &key)
Load the model and all its satellite files from a client filesystem, replacing this entity's contents...
Definition m2.hpp:525
Result< void > write(fs::FileSystem &fs, const FileKey &key) const
Serialize and store the model and every satellite file through the filesystem's project overlay; sate...
Definition m2.hpp:565
M2Root< V > root
The MD20 body — the model's root, uniform across every era (pre-Legion it IS the file; Legion+ it is ...
Definition m2.hpp:128