44#include <welder/vocabulary.hpp>
67 using namespace wowlib::formats::adt::chunks;
75 The physical ADT file a chunk belongs to. Pre-Cataclysm tiles are a single
76 `monolithic` .adt; Cataclysm split the tile into a `root` .adt and the
77 `tex0` / `obj0` / `obj1` / `lod` split files. wowlib presents one unified
78 ADT regardless; this only surfaces in the low-level split-file API.)")
80 Monolithic [[=welder::doc("The single pre-Cataclysm .adt (every chunk).")]] = 0,
82 "The Cata+ root .adt (terrain heights/normals/colors, liquid, "
85 "The Cata+ _tex0.adt (texture layers, alpha/shadow maps, "
87 Obj0 [[=welder::doc(
"The Cata+ _obj0.adt (doodad/object references and "
88 "placements).")]] = 3,
90 welder::doc(
"The Cata+ _obj1.adt (level-of-detail object placements).")]]
93 "The Legion+ _lod.adt (low-detail geometry and liquids).")]] = 5
99 enum class InFile : std::uint8_t {
114 struct SerializerSpec {
115 std::meta::info
codec;
145 default:
return false;
162 =welder::weld_as(
"MapChunk"),
165 A terrain chunk (MCNK), abstract over the client version. Usually obtained
166 from ADT.chunks rather than constructed; the per-version MapChunk* classes
167 are subclasses. Construct a concrete version with
168 MapChunk.for_version(expansion). See https://wowdev.wiki/ADT/v18#MCNK_chunk.)")
173 inline constexpr std::size_t
McvtCount = 145;
180 const std::vector<SMLayer>&
layers;
197 const auto* b =
static_cast<const std::byte*
>(p);
198 out.insert(out.end(), b, b + n);
204 template <
typename Chunk>
207 std::memcpy(self.normals.data(), sub.data(), std::min(sub.size(),
McvtCount * 3));
209 std::memcpy(self.mcnrPadding.data(), sub.data() +
McvtCount * 3, 13);
213 template <
typename Chunk>
215 appendBytes(out, self.normals.data(), self.normals.size() * 3);
219 template <
typename Chunk>
221 return !self.normals.empty();
229 template <
typename Chunk>
231 self.layers.resize(sub.size() / 16);
232 std::memcpy(self.layers.data(), sub.data(), self.layers.size() * 16);
233 self.alphaMaps.assign(self.layers.size(), {});
237 template <
typename Chunk>
238 static void write(
const Chunk&,
FileBuffer& out,
const MapChunkWriteCtx& ctx) {
239 appendBytes(out, ctx.stampedLayers.data(), ctx.stampedLayers.size() * 16);
242 template <
typename Chunk>
243 static bool engaged(
const Chunk& self,
const MapChunkWriteCtx&) {
244 return !self.layers.empty();
251 template <
typename Chunk>
252 static Result<void> read(Chunk& self, std::span<const std::byte> sub,
const MapChunkReadCtx& ctx) {
253 if (self.alphaMaps.size() != self.layers.size()) self.alphaMaps.resize(self.layers.size());
255 for (
auto&& [layer, outMap] : std::views::zip(self.layers, self.alphaMaps)) {
257 if (layer.offsetInMcal > sub.size())
continue;
259 outMap = codec.
decode(sub.subspan(layer.offsetInMcal), compressed, ctx.fix);
270 template <
typename Chunk>
271 static void prepare(
const Chunk& self,
273 std::vector<std::byte>& blob,
274 std::vector<SMLayer>& stamped) {
275 stamped = self.layers;
277 for (std::size_t i = 0; i < stamped.size(); ++i) {
278 const bool has = i < self.alphaMaps.size() && !self.alphaMaps[i].empty() &&
hasFlag(
280 stamped[i].offsetInMcal =
static_cast<std::uint32_t
>(blob.size());
286 template <
typename Chunk>
291 template <
typename Chunk>
301 template <
typename Chunk>
307 template <
typename Chunk>
309 std::vector<std::byte> packed;
314 template <
typename Chunk>
316 return !self.shadowMap.empty();
325 template <
typename Chunk>
327 using Entry = std::remove_cvref_t<
decltype(self.soundEmitters )>::value_type;
328 if (sub.size() %
sizeof(Entry) == 0) {
329 self.soundEmitters.resize(sub.size() /
sizeof(Entry));
330 std::memcpy(self.soundEmitters.data(), sub.data(), sub.size());
333 return self.mcseRaw.read(sub);
336 template <
typename Chunk>
338 using Entry = std::remove_cvref_t<
decltype(self.soundEmitters )>::value_type;
339 if (!self.mcseRaw.empty()) {
340 (void)self.mcseRaw.write(out);
343 appendBytes(out, self.soundEmitters.data(), self.soundEmitters.size() *
sizeof(Entry));
346 template <
typename Chunk>
348 return !self.soundEmitters.empty() || !self.mcseRaw.empty();
358 "Per-vertex colors (MCCV, WotLK+): 145 BGRA entries blended onto the "
359 "terrain (0x7F = neutral)."),
360 =welder::mark::no_reassign]]
363 [[=welder::mark::exclude]]
374 "Per-vertex baked lighting (MCLV, Cata+): 145 ARGB entries from "
375 "level-designer omni lights."),
376 =welder::mark::no_reassign]]
381 =welder::doc(
"Per-layer terrain material ids (MCMT, Cata+)."),
382 =welder::mark::no_reassign]]
385 [[=welder::mark::exclude]]
396 "The legacy per-chunk liquid (MCLQ, up to and including WotLK).")]]
399 [[=welder::mark::exclude]]
408 template <ClientVersion V>
412 One terrain chunk (MCNK) of a map tile, fully decoded: the chunk header, the
413 9x9+8x8 height and normal grids, the texture layers with their decoded
414 64x64 alpha maps, the shadow map, doodad/object references, sound emitters
415 and (version dependent) vertex colors, baked lighting and legacy liquid.
416 A tile has 256 of these. wowlib decodes alpha/shadow maps to a plain 64x64
417 edit surface and re-encodes on write; the round-trip is semantic, not
418 byte-identical. See https://wowdev.wiki/ADT/v18#MCNK_chunk.)")
443 "The chunk header (flags, grid position, area, holes, origin).")]]
450 "The 9x9 + 8x8 = 145 terrain heights (MCVT), relative to the chunk "
451 "origin, in the interleaved outer/inner row order."),
452 =welder::mark::no_reassign]]
459 =welder::doc(
"The 145 terrain normals (MCNR), X Z Y signed bytes."),
460 =welder::mark::no_reassign]]
461 std::vector<MCNREntry>
normals;
467 "The texture layers (MCLY); layer 0 is opaque, later layers blend "
468 "through their alpha map."),
469 =welder::mark::no_reassign]]
477 "One decoded 64x64 (4096-byte) alpha map per layer, aligned with "
478 "layers (layer 0's is empty); 0 = base texture, 255 = this layer."),
479 =welder::mark::no_reassign]]
480 std::vector<std::vector<std::uint8_t>>
alphaMaps;
487 "The decoded 64x64 (4096-byte) shadow map (MCSH), 0/1 per texel; "
488 "empty when the chunk casts no baked shadow."),
489 =welder::mark::no_reassign]]
496 "Doodad references (MCRF doodad part pre-Cata, MCRD Cata+): indices "
497 "into the tile's MDDF placements drawn in this chunk."),
498 =welder::mark::no_reassign]]
505 "Object references (MCRF object part pre-Cata, MCRW Cata+): indices "
506 "into the tile's MODF placements drawn in this chunk."),
507 =welder::mark::no_reassign]]
513 =welder::doc(
"Sound emitters placed in this chunk (MCSE): pre-WotLK "
514 "versions carry the full 52-byte inline emitter "
515 "(CWSoundEmitterVanilla), WotLK+ the 28-byte "
516 "SoundEntriesAdvanced reference (CWSoundEmitter)."),
517 =welder::mark::no_reassign]]
518 std::vector<std::conditional_t<(V < builds::WotLK), CWSoundEmitterVanilla, CWSoundEmitter>>
soundEmitters;
523 [[=welder::mark::exclude]]
529 [[=welder::mark::exclude]]
539 [[=welder::mark::exclude]]
544 for (std::size_t i = 0; i < alphaMaps.size(); ++i)
546 report.
addError(std::format(
"alphaMaps[{}]", i),
547 std::format(
"decoded alpha map holds {} texels, not {}", alphaMaps[i].size(),
549 if (!alphaMaps.empty() && !alphaMaps[0].empty())
550 report.
addWarning(
"alphaMaps[0]",
"layer 0 is the opaque base layer; its alpha map is ignored");
561 [[=welder::mark::exclude]]
571 [[=welder::mark::exclude]]
574 [[=welder::mark::exclude]]
575 bool operator==(
const MapChunk&)
const =
default;
580 std::uint32_t magic = 0;
581 std::uint32_t offset = 0;
582 std::uint32_t total = 0;
595 template <
typename Body>
596 static std::uint32_t _emitSubchunk(
FileBuffer& out, std::size_t base, std::uint32_t magic, Body&& body) {
597 const auto ofs =
static_cast<std::uint32_t
>(out.size() - base) + 8;
599 const std::size_t sizeAt = out.size();
600 out.insert(out.end(), 4, std::byte{0});
602 const auto size =
static_cast<std::uint32_t
>(out.size() - sizeAt - 4);
603 std::memcpy(out.data() + sizeAt, &size, 4);
618 std::span<Emitted> emitted,
619 std::size_t& n)
const {
620 const auto emit = [&](std::uint32_t magic,
auto&& body) {
621 const std::size_t before = out.size();
622 const std::uint32_t ofs = _emitSubchunk(out, base, magic, body);
623 emitted[n++] = {magic, ofs,
static_cast<std::uint32_t
>(out.size() - before)};
626 if (!doodadRefs.empty())
627 emit(
fourcc(
"MCRD"), [&] {
628 appendBytes(out, doodadRefs.data(), doodadRefs.size() * 4);
630 if (!objectRefs.empty())
631 emit(
fourcc(
"MCRW"), [&] {
632 appendBytes(out, objectRefs.data(), objectRefs.size() * 4);
635 else if (!doodadRefs.empty() || !objectRefs.empty()) {
636 emit(
fourcc(
"MCRF"), [&] {
637 appendBytes(out, doodadRefs.data(), doodadRefs.size() * 4);
638 appendBytes(out, objectRefs.data(), objectRefs.size() * 4);
647 void _readCombinedRefs(std::span<const std::byte> sub,
FileKind kind) {
648 const std::size_t total = sub.size() / 4;
650 doodadRefs.resize(nDd);
651 objectRefs.resize(total - nDd);
652 std::memcpy(doodadRefs.data(), sub.data(), nDd * 4);
653 std::memcpy(objectRefs.data(), sub.data() + nDd * 4, (total - nDd) * 4);
663 void _stampHeader(
FileBuffer& out, std::size_t base, std::span<const Emitted> emitted)
const {
665 std::memcpy(&h, out.data() + base,
sizeof(SMChunk));
666 const bool highRes =
hasFlag(h.
flags, MapChunkFlags::HighResHoles);
667 const auto ofs = [&](std::uint32_t magic) -> std::optional<std::uint32_t> {
668 for (
const Emitted& e : emitted)
669 if (e.magic == magic)
return e.offset;
672 const auto sz = [&](std::uint32_t magic) -> std::uint32_t {
673 for (
const Emitted& e : emitted)
674 if (e.magic == magic)
return e.total;
682 if (
auto o = ofs(
fourcc(
"MCLY")); o) {
686 if (
auto o = ofs(
fourcc(
"MCAL")); o) {
690 if (
auto o = ofs(
fourcc(
"MCSH")); o) {
694 if (
auto o = ofs(
fourcc(
"MCLQ")); o) {
700 h.
nDoodadRefs =
static_cast<std::uint32_t
>(doodadRefs.size());
701 h.
nMapObjRefs =
static_cast<std::uint32_t
>(objectRefs.size());
703 if (
auto o = ofs(
fourcc(
"MCSE")); o) {
705 using Entry = std::remove_cvref_t<
decltype(soundEmitters )>::value_type;
707 ? soundEmitters.size()
708 : mcseRaw.
size() /
sizeof(Entry));
710 std::memcpy(out.data() + base, &h,
sizeof(SMChunk));
722 std::uint32_t _subchunkLength(std::uint32_t magic, std::uint32_t declared,
FileKind kind)
const {
723 if (magic ==
fourcc(
"MCNR") && declared <= 435)
return 448;
748 void _clearDerivedOnRead(
FileKind kind) {
749 for (
auto& layer :
layers) layer.offsetInMcal = 0;
769 template <ClientVersion V>
773 template <ClientVersion V>
780 if (payload.size() <
sizeof(
SMChunk))
782 std::format(
"MCNK header needs {} bytes, got {}",
sizeof(
SMChunk), payload.size()));
783 std::memcpy(&header, payload.data(),
sizeof(
SMChunk));
787 const MapChunkReadCtx ctx{
795 while (pos + 8 <= payload.size()) {
796 std::uint32_t magic = 0, declared = 0;
797 std::memcpy(&magic, payload.data() + pos, 4);
798 std::memcpy(&declared, payload.data() + pos + 4, 4);
799 const std::size_t dataAt = pos + 8;
801 std::uint32_t effective = _subchunkLength(magic, declared, kind);
802 if (dataAt + effective > payload.size()) effective = declared;
803 if (dataAt + effective > payload.size())
805 std::format(
"MCNK sub-chunk {} overruns the chunk",
fourccToString(magic)));
806 const auto sub = payload.subspan(dataAt, effective);
807 pos = dataAt + effective;
811 if (magic ==
fourcc(
"MCRF")) {
812 _readCombinedRefs(sub, kind);
820 bool matched =
false;
821 template for (
constexpr auto m : Members) {
824 if (!matched && magic == spec->magic) {
827 using Codec = [:ser->codec:];
828 outcome = Codec::read(*
this, sub, ctx);
835 if (!outcome)
return outcome;
838 _clearDerivedOnRead(kind);
842 template <ClientVersion V>
847 const std::size_t base = out.size();
852 std::vector<std::byte> alphaBlob;
853 std::vector<SMLayer> stampedLayers;
855 const MapChunkWriteCtx wctx{.stampedLayers = stampedLayers, .alphaBlob = alphaBlob};
857 std::array<Emitted, SubChunkOrder.size() + 2> emitted{};
859 std::size_t nEmitted = 0;
861 for (
const std::uint32_t want : SubChunkOrder) {
862 template for (
constexpr auto m : Members) {
866 if (spec->magic == want &&
routesTo(route->file, kind)) {
868 using Codec = [:ser->codec:];
869 if (Codec::engaged(*
this, wctx)) {
870 const std::size_t before = out.size();
871 const std::uint32_t o = _emitSubchunk(out, base, want, [&] { Codec::write(*
this, out, wctx); });
872 emitted[nEmitted++] = {want, o,
static_cast<std::uint32_t
>(out.size() - before)};
878 const auto& member = this->[:m:];
879 if (!member.empty()) {
880 const std::size_t before = out.size();
881 const std::uint32_t o = _emitSubchunk(out, base, want, [&] {
884 emitted[nEmitted++] = {want, o,
static_cast<std::uint32_t
>(out.size() - before)};
897 _stampHeader(out, base, std::span<const Emitted>{emitted.data(), nEmitted});
ADT version grids, layout pivots and the alpha-format context.
ADT liquid binary pieces (namespace wowlib::formats::adt::chunks): the small trivially-copyable recor...
Structured ADT liquid entities (namespace wowlib::formats::adt): the modern MH2O (WotLK+) and the leg...
The owning byte buffer file contents are read into.
The chunk framework, vocabulary and engine in one header.
Named ClientVersion constants for the exact client builds format features appeared (or vanished) at —...
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...
Flag-testing convenience for the binary formats' bit-mask enums.
FourCC chunk identifiers: compile-time conversion of the four-letter codes to the host integers chunk...
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.
constexpr ClientVersion Cata
Cataclysm (any 4.x client).
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.
@ ChunkTruncated
A chunk header or payload overruns the file buffer.
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
ADT object/misc binary structs (namespace wowlib::formats::adt::chunks): the per-cell sound emitter (...
ADT texture binary structs (namespace wowlib::formats::adt::chunks): the per-cell texture layer (SMLa...
The binary-level math and color primitives shared across WoW file formats (wowdev....
Version-range canonicalization: the machinery behind collapsing a versioned family's instantiations t...
The conditional-base mechanism that gives a versioned chunked entity exactly the fields its client ve...