wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
offset_block.hpp
Go to the documentation of this file.
1#pragma once
2
40
41#include <meta>
42
43#include <cstddef>
44#include <cstring>
45#include <format>
46#include <functional>
47#include <optional>
48#include <span>
49#include <string>
50#include <type_traits>
51#include <vector>
52
53#include <welder/vocabulary.hpp>
54
57#include <wowlib/core/error.hpp>
60
61namespace wowlib::formats::m2 {
65 struct M2OffsetBase {
66 // excluded: the parameter type is this unwelded base, not the entity
67 [[=welder::mark::exclude]]
68 bool operator==(const M2OffsetBase&) const = default;
69 };
70
73 function) everything is inline in the entity's own buffer. */
74 struct OffsetReadContext {
80 std::function<std::span<const std::byte>(std::size_t i)> sequenceBase;
81 };
82
86 data is written inline after the entity's own image. */
87 struct OffsetWriteContext {
91 std::function<FileBuffer * (std::size_t i
92 )
93 >
95 };
96
97 // --- member-kind classification --------------------------------------------
98 // The offset serializer is TYPE-driven, exactly like the chunk serializer:
99 // a member's C++ type decides how it maps onto the layout. These concepts
100 // name the three kinds so the dispatch reads as prose rather than as a raw
101 // `IsVectorV<M> || is_same_v<M, std::string>` test.
102
105 template <typename M> concept OffsetStringMember = std::is_same_v<M, std::string>;
106
111
115 template <typename M> concept InlineRecordMember = std::is_class_v<M> && !std::is_trivially_copyable_v<M> && !
117
120 template <typename M> concept InlineScalarMember = std::is_trivially_copyable_v<M> && !OffsetArrayMember<M>;
121
133 template <typename T, ClientVersion V>
134 consteval std::size_t layoutSize() {
135 if constexpr (OffsetArrayMember<T>) return 8; // an M2Array{count, offset} reference: two u32s
136 else if constexpr (std::is_trivially_copyable_v<T>) return sizeof(T);
137 else {
138 static_assert(InlineRecordMember<T>);
139 std::size_t total = 0;
140 static constexpr auto Members = formats::detail::membersOf<T>();
141 template for (constexpr auto m : Members) {
143 "gated_by members are only supported at the entity top level");
144 if constexpr (formats::detail::versionActive<V, m>()) {
145 using MT = [:std::meta::type_of(m):];
146 total += layoutSize<MT, V>();
147 }
148 }
149 return total;
150 }
151 }
152
165 template <typename Derived>
166 struct M2OffsetBlock : M2OffsetBase {
167 [[=welder::doc("Deserialize file bytes into this entity, replacing its "
168 "contents. Offsets resolve against the given buffer; "
169 "sequence-gated data is read inline.")]]
171 read(std::span<const std::byte> data [[=welder::doc("the file (or containing-chunk payload) bytes")]]) {
172 std::size_t cursor = 0;
173 auto& self = static_cast<Derived&>(*this);
174 return _readMembers(self, data, cursor, data, OffsetReadContext{});
175 }
176
179 @param data the file (or containing-chunk payload) bytes.
180 @param ctx per-sequence base resolution for `SequenceData` members.
181 @return nothing, or the first structural error. */
182 [[=welder::mark::exclude]]
183 Result<void> read(std::span<const std::byte> data, const OffsetReadContext& ctx) {
184 std::size_t cursor = 0;
185 auto& self = static_cast<Derived&>(*this);
186 return _readMembers(self, data, cursor, data, ctx);
187 }
188
189 [[nodiscard]]
190 [[=welder::doc("Serialize this entity in wowlib's canonical layout (an "
191 "offset format has no byte-perfect round-trip guarantee; "
192 "a written entity re-reads equal instead)."),
193 =welder::returns("the file bytes")]]
194 Result<FileBuffer> write() const {
195 return write(OffsetWriteContext{});
196 }
197
198 /** Serialize with external sequence data routed through @a ctx (the M2
199 .anim splitting path).
200 @param ctx per-sequence sink resolution for `SequenceData` members.
201 @return the file bytes, or the first error. */
202 [[=welder::mark::exclude]]
204 FileBuffer out;
205 if (auto r = _writeImage(static_cast<const Derived&>(*this), out, ctx); !r) return std::unexpected{r.error()};
206 return out;
207 }
208
209 /** Append this entity's serialized image to @a out (the chunk serializer's
210 SelfSerializing write hook — offsets are relative to the start of the
211 appended image).
212 @param out the destination buffer (appended, not cleared).
213 @return nothing, or the first error. */
214 [[=welder::mark::exclude]]
215 Result<void> write(FileBuffer& out) const {
216 return _writeImage(static_cast<const Derived&>(*this), out, OffsetWriteContext{});
218
220
223 [[=welder::mark::exclude]]
224 bool empty() const {
225 return false;
226 }
227
228 [[nodiscard]]
229 [[=welder::doc(R"(
230 Check the logical integrity contracts this file must satisfy to LOAD in
231 the client — companion-array counts, lookup ranges — which write()
232 deliberately never enforces. Call it before writing when you want to
233 know the result will load. A file read from a client and left
234 unmodified reports no errors; warnings mark states real client files
235 ship.)"),
236 =welder::returns("every violated contract, in member order")]]
237 ValidationReport validate() const {
238 ValidationReport report;
239 formats::detail::validateEntity(static_cast<const Derived&>(*this), report);
240 return report;
241 }
242
243 [[nodiscard]]
244 [[=welder::doc("Validate and raise on the first error instead of returning "
245 "a report — the assert-style face of validate()."),
246 =welder::returns("nothing; raises when validate() finds any error")]]
247 Result<void> ensureValid() const {
248 return validate().toResult();
249 }
250
256 [[=welder::mark::exclude]]
257 std::size_t imageSize() const {
258 const auto& self = static_cast<const Derived&>(*this);
259 static constexpr auto Members = formats::detail::membersOf<Derived>();
260 std::size_t total = 0;
261 template for (constexpr auto m : Members) {
263 if (_memberPresent<m>(self)) {
264 using MT = [:std::meta::type_of(m):];
266 }
267 }
268 return total;
269 }
270
282 [[=welder::mark::exclude]]
283 static consteval std::size_t memberOffset(std::string_view name) {
284 // static: `template for` needs a constant address for the range
285 static constexpr auto Members = formats::detail::membersOf<Derived>();
286 static constexpr auto Order = _memberOrder<Derived>();
287 std::size_t off = 0;
288 bool found = false;
289 template for (constexpr std::size_t idx : Order) {
290 constexpr auto m = Members[idx];
292 if (!found && std::meta::identifier_of(m) == name) found = true;
293 if (!found) {
295 "member_offset: a gated_by member precedes the target";
296 using MT = [:std::meta::type_of(m):];
298 }
299 }
300 }
301 if (!found) throw "member_offset: the entity has no such member";
302 return off;
303 }
304
305 [[=welder::mark::exclude]]
306 bool operator==(const M2OffsetBlock&) const = default;
307
308 protected:
311 struct M2ArrayRef {
312 std::uint32_t count = 0;
313 std::uint32_t offset = 0;
314 };
315
316 static_assert(sizeof(M2ArrayRef) == 8 && std::is_trivially_copyable_v<M2ArrayRef>);
317
318 // --- layout order ---------------------------------------------------------
319
323 static consteval std::optional<formats::detail::OffsetAfterSpec>
324 _offsetAnchorOf(std::meta::info member) {
325 auto anns = std::meta::annotations_of_with_type(member, ^^formats::detail::OffsetAfterSpec);
326 if (anns.empty()) return std::nullopt;
327 return std::meta::extract<formats::detail::OffsetAfterSpec>(anns[0]);
328 }
344 template <typename T>
345 static consteval auto _memberOrder() {
346 constexpr auto members = formats::detail::membersOf<T>();
347 const auto own = std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked());
348 const auto isOwn = [&](std::meta::info m) {
349 for (auto o : own)
350 if (o == m) return true;
351 return false;
352 };
353 for (std::size_t i = 0; i < members.size(); ++i)
354 if (!isOwn(members[i]) && !_offsetAnchorOf(members[i]).has_value())
355 throw "an offset entity's trait-base member needs =offset_after(\"own member\")";
356 std::vector<std::size_t> order;
357 for (auto o : own) {
358 for (std::size_t i = 0; i < members.size(); ++i)
359 if (members[i] == o) order.push_back(i);
360 if (!std::meta::has_identifier(o)) continue;
361 for (std::size_t i = 0; i < members.size(); ++i)
362 if (!isOwn(members[i]))
363 if (auto a = _offsetAnchorOf(members[i]); a && a->view() == std::meta::identifier_of(o)) order.
364 push_back(i);
365 }
366 if (order.size() != members.size()) throw "an offset_after anchor names no own member of the entity";
367 return std::define_static_array(order);
368 }
369
370 // --- per-member presence predicates ---------------------------------------
371
372
379 template <std::meta::info Mem, typename T>
380 static bool _memberPresent(const T& rec) {
381 if constexpr (constexpr auto gate = formats::detail::annotation<formats::detail::GatedBySpec, Mem>(); gate.
382 has_value()) {
383 static_assert(requires { rec.globalFlags; }, "gated_by members need a global_flags member to gate on");
384 return (static_cast<std::uint32_t>(rec.globalFlags) & gate->mask) != 0;
385 }
386 else return true;
387 }
388
397 template <bool IsSequenceData, typename T>
398 static bool _resolvesExternally(const T& rec) {
399 if constexpr (!IsSequenceData) return false;
400 else if constexpr (requires { rec.globalSequence; }) return rec.globalSequence == 0xFFFF;
401 else return true;
402 }
404 // --- read engine ----------------------------------------------------------
405
418 template <typename T>
419 static Result<void> _readMembers(T& dst,
420 const std::span<const std::byte> image,
421 std::size_t& cursor,
422 const std::span<const std::byte> base,
423 const OffsetReadContext& ctx) {
424 static constexpr auto Members = formats::detail::membersOf<T>();
425 static constexpr auto Order = _memberOrder<T>();
426 std::optional<Error> failed;
427 template for (constexpr std::size_t idx : Order) {
428 constexpr auto m = Members[idx];
430 constexpr std::string_view name = std::meta::identifier_of(m);
432 if (!failed && _memberPresent<m>(dst))
433 if (auto r = _readMember(dst.[:m:], image, cursor, base, ctx, _resolvesExternally<isSeq>(dst), name); !r)
434 failed = r.error();
435 }
436 }
437 if (failed) return std::unexpected{*failed};
438 return {};
439 }
440
452 template <typename M>
453 static Result<void> _readMember(M& dst,
454 const std::span<const std::byte> image,
455 std::size_t& cursor,
456 const std::span<const std::byte> base,
457 const OffsetReadContext& ctx,
458 const bool external,
459 const std::string_view what) {
460 if constexpr (OffsetArrayMember<M>)
461 return _readArrayMember(dst, image, cursor, base, ctx, external, what);
462 else if constexpr (InlineRecordMember<M>) return _readMembers(dst, image, cursor, base, ctx);
463 else {
464 static_assert(InlineScalarMember<M>, "offset members are arrays, records, or scalars");
465 return _readScalar(dst, image, cursor, what);
466 }
467 }
468
472
476 template <typename M>
477 static Result<void> _readScalar(M& dst,
478 const std::span<const std::byte> image,
479 std::size_t& cursor,
480 const std::string_view what) {
481 if (image.size() < sizeof(M) || image.size() - sizeof(M) < cursor)
483 std::format("{}-byte value at image offset {:#x} overruns the image", sizeof(M), cursor));
484 std::memcpy(&dst, image.data() + cursor, sizeof(M));
485 cursor += sizeof(M);
486 return {};
487 }
488
490
496 static Result<void> _readArrayRef(M2ArrayRef& ref,
497 const std::span<const std::byte> image,
498 std::size_t& cursor,
499 const std::string_view what) {
500 if (cursor > image.size() || image.size() - cursor < sizeof(M2ArrayRef))
501 return _offsetError(ErrorCode::OffsetOutOfBounds, what, "M2Array slot overruns the image");
502 std::memcpy(&ref, image.data() + cursor, sizeof ref);
503 cursor += sizeof ref;
504 return {};
505 }
506
515
518 template <typename M>
519 static Result<void> _readArrayMember(M& dst,
520 const std::span<const std::byte> image,
521 std::size_t& cursor,
522 const std::span<const std::byte> base,
523 const OffsetReadContext& ctx,
524 const bool external,
525 const std::string_view what) {
526 M2ArrayRef ref;
527 if (auto r = _readArrayRef(ref, image, cursor, what); !r) return r;
528 if constexpr (OffsetStringMember<M>) return _readStringBlock(dst, ref, base, what);
529 else return _readVectorBlock(dst, ref, base, ctx, external, what);
530 }
531
539
540 static Result<void> _readStringBlock(std::string& dst,
541 const M2ArrayRef ref,
542 const std::span<const std::byte> base,
543 const std::string_view what) {
544 dst.clear();
545 if (ref.count == 0) return {};
546 if (std::uint64_t{ref.offset} + ref.count > base.size())
547 return _offsetError(ErrorCode::OffsetOutOfBounds, what,
548 std::format("string [{} at {:#x}] overruns the {}-byte buffer", ref.count, ref.offset,
549 base.size()));
550 const auto* chars = reinterpret_cast<const char*>(base.data() + ref.offset);
551 const auto* nul = static_cast<const char*>(std::memchr(chars, 0, ref.count));
552 dst.assign(chars, nul ? static_cast<std::size_t>(nul - chars) : ref.count);
553 return {};
555
569 template <typename M>
570 static Result<void> _readVectorBlock(M& dst,
571 const M2ArrayRef ref,
572 const std::span<const std::byte> base,
573 const OffsetReadContext& ctx,
574 const bool external,
575 const std::string_view what) {
576 using U = typename M::value_type;
577 dst.clear();
578 if (ref.count == 0) return {};
579 // An external sequence element whose context base is empty (missing .anim
580 // file): leave the member empty rather than fail.
581 if (base.empty()) return {};
582 constexpr std::size_t elem = layoutSize<U, Derived::Version>();
583 if (std::uint64_t{ref.offset} + std::uint64_t{ref.count} * elem > base.size())
584 return _offsetError(ErrorCode::OffsetOutOfBounds, what,
585 std::format("array [{} x {} at {:#x}] overruns the {}-byte buffer", ref.count, elem,
586 ref.offset, base.size()));
587 dst.resize(ref.count);
588 if constexpr (std::is_trivially_copyable_v<U>) {
589 std::memcpy(dst.data(), base.data() + ref.offset, std::size_t{ref.count} * elem);
590 return {};
591 }
592 else return _readArrayElements(dst, ref, base, ctx, external, what);
593 }
594
607 template <typename M>
608 static Result<void> _readArrayElements(M& dst,
609 const M2ArrayRef ref,
610 const std::span<const std::byte> base,
611 const OffsetReadContext& ctx,
612 const bool external,
613 const std::string_view what) {
614 using U = typename M::value_type;
615 constexpr std::size_t elem = layoutSize<U, Derived::Version>();
616 for (std::size_t i = 0; i < dst.size(); ++i) {
617 const auto elementImage = base.subspan(ref.offset + i * elem, elem);
618 const std::span<const std::byte> elementBase = external && ctx.sequenceBase ? ctx.sequenceBase(i) : base;
619 std::size_t elementCursor = 0;
620 if (auto r = _readMember(dst[i], elementImage, elementCursor, elementBase, ctx, false, what); !r) return r;
621 }
622 return {};
623 }
624
625 // --- write engine ---------------------------------------------------------
626
627
635 static void _putBytes(FileBuffer& image,
636 const std::size_t imagePos,
637 std::size_t& cursor,
638 const void* bytes,
639 const std::size_t n) {
640 std::memcpy(image.data() + imagePos + cursor, bytes, n);
641 cursor += n;
642 }
643
651 static std::size_t
652 _allocBlock(FileBuffer& target, const std::size_t bytes) {
653 target.resize((target.size() + 15) / 16 * 16);
654 const std::size_t at = target.size();
655 target.resize(target.size() + bytes);
656 return at;
657 }
658
659
667 static Result<void> _writeImage(const Derived& self, FileBuffer& out, const OffsetWriteContext& ctx) {
668 FileBuffer buffer;
669 buffer.resize(self.imageSize());
670 std::size_t cursor = 0;
671 if (auto r = _writeMembers(self, buffer, 0, cursor, buffer, ctx); !r) return r;
672 out.insert(out.end(), buffer.begin(), buffer.end());
673 return {};
674 }
675
687 template <typename T>
688 static Result<void> _writeMembers(const T& src,
689 FileBuffer& image,
690 const std::size_t imagePos,
691 std::size_t& cursor,
692 FileBuffer& blocks,
693 const OffsetWriteContext& ctx) {
694 static constexpr auto Members = formats::detail::membersOf<T>();
695 static constexpr auto Order = _memberOrder<T>();
696 std::optional<Error> failed;
697 template for (constexpr std::size_t idx : Order) {
698 constexpr auto m = Members[idx];
700 constexpr std::string_view name = std::meta::identifier_of(m);
702 if (!failed && _memberPresent<m>(src))
703 if (auto r = _writeMember(src.[:m:], image, imagePos, cursor, blocks, ctx,
704 _resolvesExternally<isSeq>(src), name); !r) failed = r.error();
705 }
706 }
707 if (failed) return std::unexpected{*failed};
708 return {};
709 }
710
723 template <typename M>
724 static Result<void> _writeMember(const M& src,
725 FileBuffer& image,
726 const std::size_t imagePos,
727 std::size_t& cursor,
728 FileBuffer& blocks,
729 const OffsetWriteContext& ctx,
730 const bool external,
731 const std::string_view what) {
732 if constexpr (OffsetArrayMember<M>)
733 return _writeArrayMember(src, image, imagePos, cursor, blocks, ctx, external, what);
734 else if constexpr (InlineRecordMember<M>) return _writeMembers(src, image, imagePos, cursor, blocks, ctx);
735 else {
736 static_assert(InlineScalarMember<M>, "offset members are arrays, records, or scalars");
737 _putBytes(image, imagePos, cursor, &src, sizeof(M));
738 return {};
740 }
741
751
754 template <typename M>
755 static Result<void> _writeArrayMember(const M& src,
756 FileBuffer& image,
757 const std::size_t imagePos,
758 std::size_t& cursor,
759 FileBuffer& blocks,
760 const OffsetWriteContext& ctx,
761 const bool external,
762 const std::string_view what) {
763 if constexpr (OffsetStringMember<M>) return _writeStringBlock(src, image, imagePos, cursor, blocks);
764 else
765 return _writeVectorBlock(src, image, imagePos, cursor, blocks, ctx, external, what);
766 }
767
773
777 static Result<void> _writeStringBlock(const std::string& src,
778 FileBuffer& image,
779 const std::size_t imagePos,
780 std::size_t& cursor,
781 FileBuffer& blocks) {
782 const std::size_t at = _allocBlock(blocks, src.size() + 1);
783 std::memcpy(blocks.data() + at, src.data(), src.size());
784 blocks[at + src.size()] = std::byte{0};
785 const M2ArrayRef ref{static_cast<std::uint32_t>(src.size() + 1), static_cast<std::uint32_t>(at)};
786 _putBytes(image, imagePos, cursor, &ref, sizeof ref);
787 return {};
788 }
804 template <typename M>
805 static Result<void> _writeVectorBlock(const M& src,
806 FileBuffer& image,
807 const std::size_t imagePos,
808 std::size_t& cursor,
809 FileBuffer& blocks,
810 const OffsetWriteContext& ctx,
811 const bool external,
812 const std::string_view what) {
813 using U = typename M::value_type;
814 if (src.empty()) {
815 const M2ArrayRef empty{0, 0};
816 _putBytes(image, imagePos, cursor, &empty, sizeof empty);
817 return {};
818 }
819 constexpr std::size_t elem = layoutSize<U, Derived::Version>();
820 const std::size_t at = _allocBlock(blocks, src.size() * elem);
821 const M2ArrayRef ref{static_cast<std::uint32_t>(src.size()), static_cast<std::uint32_t>(at)};
822 _putBytes(image, imagePos, cursor, &ref, sizeof ref);
823 if constexpr (std::is_trivially_copyable_v<U>) {
824 std::memcpy(blocks.data() + at, src.data(), src.size() * elem);
825 return {};
826 }
827 else return _writeArrayElements(src, at, blocks, ctx, external, what);
828 }
844 template <typename M>
845 static Result<void> _writeArrayElements(const M& src,
846 const std::size_t blockAt,
847 FileBuffer& blocks,
848 const OffsetWriteContext& ctx,
849 const bool external,
850 const std::string_view what) {
851 using U = typename M::value_type;
852 constexpr std::size_t elem = layoutSize<U, Derived::Version>();
853 for (std::size_t i = 0; i < src.size(); ++i) {
854 FileBuffer* sink = external && ctx.sequenceSink ? ctx.sequenceSink(i) : nullptr;
855 std::size_t elementCursor = 0;
856 if (auto r = _writeMember(src[i], blocks, blockAt + i * elem, elementCursor, sink ? *sink : blocks, ctx,
857 false, what); !r) return r;
858 }
859 return {};
860 }
861
862 // --- diagnostics ----------------------------------------------------------
863
865
869 static std::unexpected<Error> _offsetError(const ErrorCode code,
870 const std::string_view member,
871 const std::string_view what) {
872 return makeError(code, std::format("offset member '{}': {}", member, what));
873 }
874 };
875
878 template <typename E> concept OffsetEntity = std::derived_from<E, M2OffsetBase> && requires {
879 { E::Version } -> std::convertible_to<ClientVersion>;
880 };
881}
The owning byte buffer file contents are read into.
The chunk framework, vocabulary and engine in one header.
Client version identity, the flavor axis that separates a client's CONTENT version from the engine ge...
A member serialized inline as a nested record: a non-trivial class the walker recurses into member-by...
A member serialized as inline raw bytes: anything trivially copyable that is not an array/string refe...
A member serialized as an M2Array{count, offset} reference to a separate data block — a std::vector (...
A type the offset engine can read and write: it is an M2 offset block and declares the client version...
A member serialized as an M2Array<char>: its bytes (NUL included) live in a data block,...
The error-handling vocabulary: ErrorCode, Error and the Result<T> alias every fallible wowlib operati...
The chunk annotation vocabulary format entities declare their binary mapping with.
consteval std::optional< Spec > annotation()
The first annotation of type Spec on reflected member M, if any.
void validateEntity(const E &entity, ValidationReport &report)
Validate a whole entity — the engine behind every validate() method; see ChunkedFile::validate() for ...
consteval bool versionActive()
Whether member M participates for entity version V, per its since/until annotations (absent bounds me...
constexpr bool IsVectorV
Trait: is T a std::vector (an array-chunk member)?
consteval auto membersOf()
The reflected member list of E, public bases flattened in (see collectMembers).
consteval std::size_t layoutSize()
The image footprint of T in an offset layout for client version V: the 8-byte M2Array slot for an arr...
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
ErrorCode
Machine-readable failure category carried by every Error.
Definition error.hpp:18
@ OffsetOutOfBounds
An offset array (M2Array) points outside its base buffer.
Definition error.hpp:39
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
Definition buffer.hpp:16
std::string error
The failure diagnostic; empty on success and on skips.
Definition roundtrip.hpp:27
Stored form of offsetAfter (offset entities): the trait-base member's positional layout position — ri...
The non-template marker base every offset block carries (what the OffsetEntity concept detects).
bool operator==(const M2OffsetBase &) const =default
The on-disk shape of an offset-array reference: element count and the byte offset of the data block,...
The serialization face of an offset block, mixed in CRTP-style: an entity struct E : M2OffsetBlock<E>...
static Result< void > _readMember(M &dst, const std::span< const std::byte > image, std::size_t &cursor, const std::span< const std::byte > base, const OffsetReadContext &ctx, const bool external, const std::string_view what)
Read one member dst — an array/string reference, an inline sub-record, or an inline scalar — advancin...
static void _putBytes(FileBuffer &image, const std::size_t imagePos, std::size_t &cursor, const void *bytes, const std::size_t n)
Write n raw bytes into image at imagePos + cursor, advancing the cursor.
static Result< void > _readMembers(T &dst, const std::span< const std::byte > image, std::size_t &cursor, const std::span< const std::byte > base, const OffsetReadContext &ctx)
Read every version-active member of record/entity dst from image at cursor, in layout order (own decl...
static Result< void > _readVectorBlock(M &dst, const M2ArrayRef ref, const std::span< const std::byte > base, const OffsetReadContext &ctx, const bool external, const std::string_view what)
Decode a vector data block referenced by ref out of base: ref.count element images of layoutSize<U> b...
static Result< void > _writeArrayMember(const M &src, FileBuffer &image, const std::size_t imagePos, std::size_t &cursor, FileBuffer &blocks, const OffsetWriteContext &ctx, const bool external, const std::string_view what)
Write an M2Array-referenced member: allocate its data block, fill it, and emit the {count,...
static bool _resolvesExternally(const T &rec)
Whether a SequenceData member of rec resolves its per-element data through the external I/O context o...
ValidationReport validate() const
static Result< void > _readArrayElements(M &dst, const M2ArrayRef ref, const std::span< const std::byte > base, const OffsetReadContext &ctx, const bool external, const std::string_view what)
Read the non-trivial elements of dst, each from its own element image inside base.
Result< FileBuffer > write() const
Serialize this entity in wowlib's canonical layout (an offset format has no byte-perfect round-trip g...
std::size_t imageSize() const
The entity's image footprint in bytes for Derived's version: every version-active member's layoutSize...
static std::unexpected< Error > _offsetError(const ErrorCode code, const std::string_view member, const std::string_view what)
Build an offset-scoped error naming the offending member.
static Result< void > _writeMember(const M &src, FileBuffer &image, const std::size_t imagePos, std::size_t &cursor, FileBuffer &blocks, const OffsetWriteContext &ctx, const bool external, const std::string_view what)
Write one member src — an array/string reference, an inline sub-record, or an inline scalar — advanci...
static bool _memberPresent(const T &rec)
Whether member Mem occupies bytes in this instance of rec: always, unless it is gatedBy(mask) and non...
Result< FileBuffer > write(const OffsetWriteContext &ctx) const
Serialize with external sequence data routed through ctx (the M2 .anim splitting path).
static Result< void > _readArrayMember(M &dst, const std::span< const std::byte > image, std::size_t &cursor, const std::span< const std::byte > base, const OffsetReadContext &ctx, const bool external, const std::string_view what)
Read an M2Array-referenced member: take its slot, then decode the data block as a string or a vector.
static Result< void > _writeStringBlock(const std::string &src, FileBuffer &image, const std::size_t imagePos, std::size_t &cursor, FileBuffer &blocks)
Emit src as an M2Array<char> whose count includes the NUL terminator — the client reads these as buff...
static Result< void > _writeImage(const Derived &self, FileBuffer &out, const OffsetWriteContext &ctx)
Serialize self in canonical layout — the image first (pre-sized to imageSize()), then data blocks dep...
Result< void > read(std::span< const std::byte > data)
Deserialize file bytes into this entity, replacing its contents.
static Result< void > _readStringBlock(std::string &dst, const M2ArrayRef ref, const std::span< const std::byte > base, const std::string_view what)
Decode a string data block referenced by ref out of base.
Result< void > ensureValid() const
Validate and raise on the first error instead of returning a report — the assert-style face of valida...
static consteval std::optional< formats::detail::OffsetAfterSpec > _offsetAnchorOf(std::meta::info member)
The first offsetAfter positional anchor on member, or nullopt.
static consteval auto _memberOrder()
The member indices (into membersOf<T>) in positional (layout) order: T's OWN members in declaration o...
static Result< void > _writeMembers(const T &src, FileBuffer &image, const std::size_t imagePos, std::size_t &cursor, FileBuffer &blocks, const OffsetWriteContext &ctx)
Write every version-active member of record/entity src into image at imagePos + cursor,...
Result< void > read(std::span< const std::byte > data, const OffsetReadContext &ctx)
Deserialize data with external sequence data resolved through ctx (the M2 .anim baking path).
bool operator==(const M2OffsetBlock &) const =default
static Result< void > _readScalar(M &dst, const std::span< const std::byte > image, std::size_t &cursor, const std::string_view what)
Read a trivially-copyable scalar dst from image at cursor.
static Result< void > _writeVectorBlock(const M &src, FileBuffer &image, const std::size_t imagePos, std::size_t &cursor, FileBuffer &blocks, const OffsetWriteContext &ctx, const bool external, const std::string_view what)
Emit src as an M2Array: an empty vector writes {0, 0}; otherwise a count * layoutSize<U> element bloc...
static Result< void > _writeArrayElements(const M &src, const std::size_t blockAt, FileBuffer &blocks, const OffsetWriteContext &ctx, const bool external, const std::string_view what)
Write the non-trivial elements of src into the block already reserved at blockAt in blocks.
Result< void > write(FileBuffer &out) const
Append this entity's serialized image to out (the chunk serializer's SelfSerializing write hook — off...
static consteval std::size_t memberOffset(std::string_view name)
The positional byte offset of member name within this entity's image, for Derived's version: the vers...
static std::size_t _allocBlock(FileBuffer &target, const std::size_t bytes)
Reserve bytes at the (16-byte aligned, zero-filled) end of target and return the start index.
static Result< void > _readArrayRef(M2ArrayRef &ref, const std::span< const std::byte > image, std::size_t &cursor, const std::string_view what)
Read and bounds-check the 8-byte M2Array{count, offset} slot at cursor into ref, advancing cursor pas...
bool empty() const
Never empty: an offset block always has a header image worth writing (the SelfSerializing engagement ...
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 ...