wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
map_chunk.hpp
Go to the documentation of this file.
1#pragma once
2
30
31#include <algorithm>
32#include <array>
33#include <cstddef>
34#include <cstdint>
35#include <cstring>
36#include <format>
37#include <meta>
38#include <optional>
39#include <ranges>
40#include <span>
41#include <tuple>
42#include <vector>
43
44#include <welder/vocabulary.hpp>
45
49#include <wowlib/core/error.hpp>
50#include <wowlib/core/lang.hpp>
65
66namespace wowlib::formats::adt {
67 using namespace wowlib::formats::adt::chunks;
68
72 enum class [[
73 =welder::weld,
74 =welder::doc(R"(
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.)")
79 ]] FileKind : std::uint8_t {
80 Monolithic [[=welder::doc("The single pre-Cataclysm .adt (every chunk).")]] = 0,
81 Root [[=welder::doc(
82 "The Cata+ root .adt (terrain heights/normals/colors, liquid, "
83 "sounds).")]] = 1,
84 Tex0 [[=welder::doc(
85 "The Cata+ _tex0.adt (texture layers, alpha/shadow maps, "
86 "materials).")]] = 2,
87 Obj0 [[=welder::doc("The Cata+ _obj0.adt (doodad/object references and "
88 "placements).")]] = 3,
89 Obj1 [[=
90 welder::doc("The Cata+ _obj1.adt (level-of-detail object placements).")]]
91 = 4,
92 Lod [[=welder::doc(
93 "The Legion+ _lod.adt (low-detail geometry and liquids).")]] = 5
94 };
95
96
99 enum class InFile : std::uint8_t {
103 };
104
105 namespace detail {
107 struct InFileSpec {
108 InFile file;
109 };
110
114 struct SerializerSpec {
115 std::meta::info codec;
116 };
117 }
118
119
123 consteval detail::InFileSpec inFile(InFile file) { return {file}; }
124
125 /** Annotate a sub-chunk member with a MapChunk-context codec (a struct with
126 static `read(self, span, ReadCtx)` / `write(self, out, WriteCtx)` /
127 `engaged(self, WriteCtx)` templates) instead of the default uniform transfer.
128 @param codec the reflection of the codec type, e.g. `^^AlphaCodec`.
129 @return the annotation payload. */
130 consteval detail::SerializerSpec serializedBy(std::meta::info codec) {
131 return {codec};
132 }
133
134
139 constexpr bool routesTo(InFile file, FileKind kind) {
140 switch (kind) {
141 case FileKind::Monolithic: return true;
142 case FileKind::Root: return file == InFile::Root;
143 case FileKind::Tex0: return file == InFile::Tex;
144 case FileKind::Obj0: return file == InFile::Obj;
145 default: return false;
147 }
148
152
153 constexpr bool fileHasHeader(FileKind kind) {
154 return kind == FileKind::Monolithic || kind == FileKind::Root;
156
160 struct [[
161 =welder::weld,
162 =welder::weld_as("MapChunk"),
164 =welder::doc(R"(
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.)")
169 ]] MapChunkBase {};
170
171 namespace detail {
173 inline constexpr std::size_t McvtCount = 145;
176
179 const SMChunk& header;
180 const std::vector<SMLayer>& layers;
182 bool fix;
184 };
187 layout is pre-built once (MCLY must emit each layer's offsetInMcal, which
188 MCAL computes) into these two fields; the layer/alpha codecs read them. */
189 struct MapChunkWriteCtx {
190 const std::vector<SMLayer>& stampedLayers;
191 // layers with offsetInMcal set
192 std::span<const std::byte> alphaBlob; // the pre-encoded MCAL blob
193 };
194
196 inline void appendBytes(FileBuffer& out, const void* p, std::size_t n) {
197 const auto* b = static_cast<const std::byte*>(p);
198 out.insert(out.end(), b, b + n);
199 }
200
202 (a near-constant client pattern preserved for the semantic round-trip). */
203 struct NormalCodec {
204 template <typename Chunk>
205 static Result<void> read(Chunk& self, std::span<const std::byte> sub, const MapChunkReadCtx&) {
206 self.normals.resize(McvtCount);
207 std::memcpy(self.normals.data(), sub.data(), std::min(sub.size(), McvtCount * 3));
208 if (sub.size() >= McvtCount * 3 + 13)
209 std::memcpy(self.mcnrPadding.data(), sub.data() + McvtCount * 3, 13);
210 return {};
211 }
213 template <typename Chunk>
214 static void write(const Chunk& self, FileBuffer& out, const MapChunkWriteCtx&) {
215 appendBytes(out, self.normals.data(), self.normals.size() * 3);
216 appendBytes(out, self.mcnrPadding.data(), 13);
217 }
218
219 template <typename Chunk>
220 static bool engaged(const Chunk& self, const MapChunkWriteCtx&) {
221 return !self.normals.empty();
222 }
223 };
224
225 /** MCLY codec: the texture layers, plus sizing alphaMaps to the layer count
226 (MCAL may be absent on Cata+ when empty, so alphaMaps must be sized here
227 to keep it aligned with layers). */
228 struct LayerCodec {
229 template <typename Chunk>
230 static Result<void> read(Chunk& self, std::span<const std::byte> sub, const MapChunkReadCtx&) {
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(), {});
234 return {};
235 }
236
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);
240 }
241
242 template <typename Chunk>
243 static bool engaged(const Chunk& self, const MapChunkWriteCtx&) {
244 return !self.layers.empty();
245 }
246 };
247
250 struct AlphaCodec {
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());
254 const AlphaMapCodec codec{ctx.af};
255 for (auto&& [layer, outMap] : std::views::zip(self.layers, self.alphaMaps)) {
256 if (!hasFlag(layer.flags, LayerFlags::UseAlphaMap)) continue;
257 if (layer.offsetInMcal > sub.size()) continue;
258 const bool compressed = hasFlag(layer.flags, LayerFlags::AlphaMapCompressed);
259 outMap = codec.decode(sub.subspan(layer.offsetInMcal), compressed, ctx.fix);
260 }
261 return {};
262 }
263
267
269 @param stamped receives the layers with offsetInMcal stamped. */
270 template <typename Chunk>
271 static void prepare(const Chunk& self,
272 AlphaFormat af,
273 std::vector<std::byte>& blob,
274 std::vector<SMLayer>& stamped) {
275 stamped = self.layers;
276 const AlphaMapCodec codec{af};
277 for (std::size_t i = 0; i < stamped.size(); ++i) {
278 const bool has = i < self.alphaMaps.size() && !self.alphaMaps[i].empty() && hasFlag(
279 stamped[i].flags, LayerFlags::UseAlphaMap);
280 stamped[i].offsetInMcal = static_cast<std::uint32_t>(blob.size());
281 if (!has) continue;
282 codec.encode(self.alphaMaps[i], hasFlag(stamped[i].flags, LayerFlags::AlphaMapCompressed), blob);
283 }
285
286 template <typename Chunk>
287 static void write(const Chunk&, FileBuffer& out, const MapChunkWriteCtx& ctx) {
288 appendBytes(out, ctx.alphaBlob.data(), ctx.alphaBlob.size());
289 }
291 template <typename Chunk>
292 static bool engaged(const Chunk&, const MapChunkWriteCtx& ctx) {
293 // MCAL is omitted when no layer carries a map (most chunks): emitting an
294 // empty one would round-trip a no-MCAL chunk to a 1-entry alphaMaps.
295 return !ctx.alphaBlob.empty();
296 }
297 };
298
300 struct ShadowCodec {
301 template <typename Chunk>
302 static Result<void> read(Chunk& self, std::span<const std::byte> sub, const MapChunkReadCtx& ctx) {
303 self.shadowMap = ShadowMapCodec{}.decode(sub, ctx.fix);
304 return {};
305 }
307 template <typename Chunk>
308 static void write(const Chunk& self, FileBuffer& out, const MapChunkWriteCtx&) {
309 std::vector<std::byte> packed;
310 ShadowMapCodec{}.encode(self.shadowMap, packed);
311 appendBytes(out, packed.data(), packed.size());
312 }
313
314 template <typename Chunk>
315 static bool engaged(const Chunk& self, const MapChunkWriteCtx&) {
316 return !self.shadowMap.empty();
317 }
318 };
324 struct SoundEmitterCodec {
325 template <typename Chunk>
326 static Result<void> read(Chunk& self, std::span<const std::byte> sub, const MapChunkReadCtx&) {
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());
331 return {};
332 }
333 return self.mcseRaw.read(sub);
334 }
336 template <typename Chunk>
337 static void write(const Chunk& self, FileBuffer& out, const MapChunkWriteCtx&) {
338 using Entry = std::remove_cvref_t<decltype(self.soundEmitters )>::value_type;
339 if (!self.mcseRaw.empty()) {
340 (void)self.mcseRaw.write(out);
341 return;
342 }
343 appendBytes(out, self.soundEmitters.data(), self.soundEmitters.size() * sizeof(Entry));
344 }
346 template <typename Chunk>
347 static bool engaged(const Chunk& self, const MapChunkWriteCtx&) {
348 return !self.soundEmitters.empty() || !self.mcseRaw.empty();
349 }
350 };
351
353 struct MapChunkColor {
354 [[=chunk("MCCV"),
357 =welder::doc(
358 "Per-vertex colors (MCCV, WotLK+): 145 BGRA entries blended onto the "
359 "terrain (0x7F = neutral)."),
360 =welder::mark::no_reassign]]
361 std::vector<CImVector> vertexColors;
362
363 [[=welder::mark::exclude]]
364
365 bool operator==(const MapChunkColor&) const = default;
366 };
367
368
369 struct MapChunkCata {
370 [[=chunk("MCLV"),
373 =welder::doc(
374 "Per-vertex baked lighting (MCLV, Cata+): 145 ARGB entries from "
375 "level-designer omni lights."),
376 =welder::mark::no_reassign]]
377 std::vector<CArgb> vertexLighting;
378
379 [[=chunk("MCMT"),
381 =welder::doc("Per-layer terrain material ids (MCMT, Cata+)."),
382 =welder::mark::no_reassign]]
383 std::vector<SMTerrainMaterial> materialIds;
385 [[=welder::mark::exclude]]
386
387 bool operator==(const MapChunkCata&) const = default;
388 };
389
392 struct MapChunkLegacyLiquid {
393 [[=chunk("MCLQ"),
395 =welder::doc(
396 "The legacy per-chunk liquid (MCLQ, up to and including WotLK).")]]
399 [[=welder::mark::exclude]]
400
401 bool operator==(const MapChunkLegacyLiquid&) const = default;
402 };
403 }
404
405 namespace detail {
408 template <ClientVersion V>
409 struct [[
410 =welder::weld,
411 =welder::doc(R"(
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.)")
419 ]] MapChunk
420 : MapChunkBase,
424 static constexpr ClientVersion Version = V;
425
429 static constexpr std::array SubChunkOrder{
430 fourcc("MCVT"),
431 fourcc("MCCV"),
432 fourcc("MCLV"),
433 fourcc("MCNR"),
434 fourcc("MCLQ"),
435 fourcc("MCSE"),
436 fourcc("MCLY"),
437 fourcc("MCSH"),
438 fourcc("MCAL"),
439 fourcc("MCMT")
440 };
441
442 [[=welder::doc(
443 "The chunk header (flags, grid position, area, holes, origin).")]]
444 SMChunk header{};
445
446 [[=chunk("MCVT"),
449 =welder::doc(
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]]
453 std::vector<float> heights;
454
455 [[=chunk("MCNR"),
459 =welder::doc("The 145 terrain normals (MCNR), X Z Y signed bytes."),
460 =welder::mark::no_reassign]]
461 std::vector<MCNREntry> normals;
462
463 [[=chunk("MCLY"),
466 =welder::doc(
467 "The texture layers (MCLY); layer 0 is opaque, later layers blend "
468 "through their alpha map."),
469 =welder::mark::no_reassign]]
470 std::vector<SMLayer> layers;
471
472 [[=chunk("MCAL"),
475 =formats::countMatches("layers"),
476 =welder::doc(
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;
482 [[=chunk("MCSH"),
486 =welder::doc(
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]]
490 std::vector<std::uint8_t> shadowMap;
491
492 [[=chunk("MCRD"),
494 =formats::indexesInRoot("doodadPlacements"),
495 =welder::doc(
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]]
499 std::vector<std::uint32_t> doodadRefs;
500
501 [[=chunk("MCRW"),
503 =formats::indexesInRoot("wmoPlacements"),
504 =welder::doc(
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]]
508 std::vector<std::uint32_t> objectRefs;
509
510 [[=chunk("MCSE"),
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;
519
523 [[=welder::mark::exclude]]
525
529 [[=welder::mark::exclude]]
530 std::array<std::uint8_t, 13> mcnrPadding{};
531
539 [[=welder::mark::exclude]]
540 void validateExtra(ValidationReport& report) const {
541 // wowlib always holds alpha maps decoded to the full 64x64 edit
542 // surface, whatever encoding they had on disk; layer 0 is opaque and
543 // carries none
544 for (std::size_t i = 0; i < alphaMaps.size(); ++i)
545 if (!alphaMaps[i].empty() && alphaMaps[i].size() != detail::AlphaTexels)
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");
551 }
552
553 // --- physical-file serialization (definitions below) -------------------
554
561 [[=welder::mark::exclude]]
562 Result<void> readFrom(std::span<const std::byte> payload, FileKind kind, AlphaFormat af);
563
571 [[=welder::mark::exclude]]
572 Result<void> writeTo(FileBuffer& out, FileKind kind, AlphaFormat af) const;
573
574 [[=welder::mark::exclude]]
575 bool operator==(const MapChunk&) const = default;
576
577 private:
579 struct Emitted {
580 std::uint32_t magic = 0;
581 std::uint32_t offset = 0; // chunk-start-relative (points at the fourcc)
582 std::uint32_t total = 0;
583 // fourcc + size + payload, for the size_* fields
584 };
585
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;
598 appendBytes(out, &magic, 4);
599 const std::size_t sizeAt = out.size();
600 out.insert(out.end(), 4, std::byte{0});
601 body();
602 const auto size = static_cast<std::uint32_t>(out.size() - sizeAt - 4);
603 std::memcpy(out.data() + sizeAt, &size, 4);
604 return ofs;
605 }
606
615 void _writeRefs(FileBuffer& out,
616 std::size_t base,
617 FileKind kind,
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)};
624 };
625 if (kind != FileKind::Monolithic) {
626 if (!doodadRefs.empty())
627 emit(fourcc("MCRD"), [&] {
628 appendBytes(out, doodadRefs.data(), doodadRefs.size() * 4);
629 });
630 if (!objectRefs.empty())
631 emit(fourcc("MCRW"), [&] {
632 appendBytes(out, objectRefs.data(), objectRefs.size() * 4);
633 });
634 }
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);
639 });
640 }
641 }
642
647 void _readCombinedRefs(std::span<const std::byte> sub, FileKind kind) {
648 const std::size_t total = sub.size() / 4;
649 const std::size_t nDd = std::min<std::size_t>(fileHasHeader(kind) ? header.nDoodadRefs : 0, total);
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);
654 }
655
663 void _stampHeader(FileBuffer& out, std::size_t base, std::span<const Emitted> emitted) const {
664 SMChunk h;
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;
670 return std::nullopt;
671 };
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;
675 return 0;
676 };
677
678 if (auto o = ofs(fourcc("MCVT")); o && !highRes) h.ofsHeight = *o;
679 if (auto o = ofs(fourcc("MCNR")); o && !highRes) h.ofsNormal = *o;
680 if (auto o = ofs(fourcc("MCCV")); o) h.ofsMccv = *o;
681 if (auto o = ofs(fourcc("MCLV")); o) h.ofsMclv = *o;
682 if (auto o = ofs(fourcc("MCLY")); o) {
683 h.ofsLayer = *o;
684 h.nLayers = static_cast<std::uint32_t>(layers.size());
685 }
686 if (auto o = ofs(fourcc("MCAL")); o) {
687 h.ofsAlpha = *o;
688 h.sizeAlpha = sz(fourcc("MCAL"));
689 }
690 if (auto o = ofs(fourcc("MCSH")); o) {
691 h.ofsShadow = *o;
692 h.sizeShadow = sz(fourcc("MCSH"));
693 }
694 if (auto o = ofs(fourcc("MCLQ")); o) {
695 h.ofsLiquid = *o;
696 h.sizeLiquid = sz(fourcc("MCLQ"));
697 }
698 if (auto o = ofs(fourcc("MCRF")); o) h.ofsRefs = *o;
699 if (ofs(fourcc("MCRF")) || ofs(fourcc("MCRD")) || ofs(fourcc("MCRW"))) {
700 h.nDoodadRefs = static_cast<std::uint32_t>(doodadRefs.size());
701 h.nMapObjRefs = static_cast<std::uint32_t>(objectRefs.size());
702 }
703 if (auto o = ofs(fourcc("MCSE")); o) {
704 h.ofsSndEmitters = *o;
705 using Entry = std::remove_cvref_t<decltype(soundEmitters )>::value_type;
706 h.nSndEmitters = static_cast<std::uint32_t>(mcseRaw.empty()
707 ? soundEmitters.size()
708 : mcseRaw.size() / sizeof(Entry));
709 }
710 std::memcpy(out.data() + base, &h, sizeof(SMChunk));
711 }
712
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;
724 if (fileHasHeader(kind)) {
725 // The header's size fields are authoritative for MCAL/MCLQ even when
726 // they say "empty" (<= 8, header only): the vanilla map tool wrote
727 // garbage declared sizes into empty sub-chunk headers (every
728 // AhnQiraj MCAL with no alpha data declares -2048 — 243 tiles in
729 // the 1.12.1 fleet audit).
730 if (magic == fourcc("MCAL") && header.sizeAlpha != 0)
731 return header.sizeAlpha <= 8
732 ? 0
733 : header.sizeAlpha - 8;
734 if (magic == fourcc("MCLQ") && header.sizeLiquid != 0)
735 return header.sizeLiquid <= 8
736 ? 0
737 : header.sizeLiquid - 8;
738 }
739 return declared;
740 }
741
747
748 void _clearDerivedOnRead(FileKind kind) {
749 for (auto& layer : layers) layer.offsetInMcal = 0;
750 if (!fileHasHeader(kind)) return;
752 header.ofsHeight = 0;
753 header.ofsNormal = 0;
754 }
755 header.nLayers = 0;
756 header.nDoodadRefs = 0;
757 header.ofsLayer = header.ofsRefs = header.ofsAlpha = header.sizeAlpha = 0;
758 header.ofsShadow = header.sizeShadow = 0;
759 header.nMapObjRefs = 0;
760 header.ofsSndEmitters = header.nSndEmitters = 0;
761 header.ofsLiquid = header.sizeLiquid = 0;
762 header.ofsMccv = header.ofsMclv = 0;
763 }
764 };
765 }
766
769 template <ClientVersion V>
771
772 namespace detail {
773 template <ClientVersion V>
774 Result<void> MapChunk<V>::readFrom(std::span<const std::byte> payload, FileKind kind, AlphaFormat af) {
775 using Self = MapChunk<V>;
776 static constexpr auto Members = formats::detail::membersOf<Self>();
777
778 std::size_t pos = 0;
779 if (fileHasHeader(kind)) {
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));
784 pos = sizeof(SMChunk);
785 }
786
787 const MapChunkReadCtx ctx{
788 .header = header,
789 .layers = layers,
790 .af = af,
792 .kind = kind
793 };
794
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;
800
801 std::uint32_t effective = _subchunkLength(magic, declared, kind);
802 if (dataAt + effective > payload.size()) effective = declared; // last resort: trust the declared size
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;
808
809 // MCRF is the one sub-chunk mapping to two members (doodad + object refs,
810 // split by the header count) — the MCNK analogue of ADT's special MCNK.
811 if (magic == fourcc("MCRF")) {
812 _readCombinedRefs(sub, kind);
813 continue;
814 }
815
816 // Every other sub-chunk routes to its `chunk()`-annotated member by
817 // fourcc: uniform members transfer through readValue, the ones that need
818 // a codec (external length / transform / companion member) go through it.
819 Result<void> outcome{};
820 bool matched = false;
821 template for (constexpr auto m : Members) {
822 if constexpr (constexpr auto spec = formats::detail::annotation<formats::detail::ChunkSpec, m>(); spec.
823 has_value()) {
824 if (!matched && magic == spec->magic) {
825 matched = true;
826 if constexpr (constexpr auto ser = formats::detail::annotation<SerializerSpec, m>(); ser.has_value()) {
827 using Codec = [:ser->codec:];
828 outcome = Codec::read(*this, sub, ctx);
829 }
830 else
831 outcome = formats::detail::readValue(this->[:m:], sub, magic, pos, spec->endian);
832 }
833 }
834 }
835 if (!outcome) return outcome;
836 }
837
838 _clearDerivedOnRead(kind);
839 return {};
840 }
841
842 template <ClientVersion V>
844 using Self = MapChunk<V>;
845 static constexpr auto Members = formats::detail::membersOf<Self>();
846
847 const std::size_t base = out.size();
848 if (fileHasHeader(kind)) appendBytes(out, &header, sizeof(SMChunk));
849
850 // Pre-build the alpha layout once: MCLY must emit each layer's derived
851 // offsetInMcal, which MCAL computes, yet MCLY precedes MCAL on disk.
852 std::vector<std::byte> alphaBlob;
853 std::vector<SMLayer> stampedLayers;
854 if (routesTo(InFile::Tex, kind)) AlphaCodec::prepare(*this, af, alphaBlob, stampedLayers);
855 const MapChunkWriteCtx wctx{.stampedLayers = stampedLayers, .alphaBlob = alphaBlob};
856
857 std::array<Emitted, SubChunkOrder.size() + 2> emitted{};
858 // +2 for MCRD/MCRW
859 std::size_t nEmitted = 0;
860
861 for (const std::uint32_t want : SubChunkOrder) {
862 template for (constexpr auto m : Members) {
863 if constexpr (constexpr auto spec = formats::detail::annotation<formats::detail::ChunkSpec, m>(); spec.
864 has_value()) {
865 if constexpr (constexpr auto route = formats::detail::annotation<InFileSpec, m>(); route.has_value()) {
866 if (spec->magic == want && routesTo(route->file, kind)) {
867 if constexpr (constexpr auto ser = formats::detail::annotation<SerializerSpec, m>(); ser.has_value()) {
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)};
873 }
874 }
875 else {
876 // splice the member HERE (m is a constant expression); inside the
877 // lambda m is captured by reference and would not be.
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, [&] {
882 std::ignore = formats::detail::writeValue(member, out);
883 });
884 emitted[nEmitted++] = {want, o, static_cast<std::uint32_t>(out.size() - before)};
885 }
886 }
887 }
888 }
889 }
890 }
891 }
892
893 // References are the era-shifting special (MCRF vs MCRD/MCRW), emitted last.
894 if (routesTo(InFile::Obj, kind)) _writeRefs(out, base, kind, emitted, nEmitted);
895
896 if (fileHasHeader(kind))
897 _stampHeader(out, base, std::span<const Emitted>{emitted.data(), nEmitted});
898 return {};
899 }
900 }
901}
ADT version grids, layout pivots and the alpha-format context.
ADT header binary structs (namespace wowlib::formats::adt::chunks): the per-cell MCNK header (SMChunk...
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.
void addWarning(std::string path, std::string message)
Record a warning finding (see add()).
void addError(std::string path, std::string message)
Record an error finding (see add()).
std::vector< std::uint8_t > decode(std::span< const std::byte > src, bool compressed, bool fixEdges) const
Decode one layer's on-disk alpha bytes to the 4096-texel edit surface.
Definition codec.hpp:73
void encode(std::span< const std::uint8_t > map, bool compressed, std::vector< std::byte > &out) const
Encode a 4096-texel edit surface to its on-disk alpha bytes, appended to out.
Definition codec.hpp:87
The MCSH shadow-map codec: a 512-byte 1-bit bitmap on disk, a 4096-texel 0/1 surface in memory.
Definition codec.hpp:203
std::vector< std::uint8_t > decode(std::span< const std::byte > src, bool fixEdges) const
Decode a 512-byte 1-bit shadow bitmap to 4096 texels (0/1), LSB first.
Definition codec.hpp:210
void encode(std::span< const std::uint8_t > map, std::vector< std::byte > &out) const
Encode 4096 texels (0/1) to a 512-byte 1-bit bitmap, LSB first.
Definition codec.hpp:223
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.
The MCAL alpha-map and MCSH shadow-map codecs (namespace wowlib::formats::adt::detail): AlphaMapCodec...
The chunk annotation vocabulary format entities declare their binary mapping with.
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.
Definition lang.hpp:44
constexpr ClientVersion Cata
Cataclysm (any 4.x client).
@ UseAlphaMap
This layer blends through an MCAL alpha map (set on every layer after the first).
Definition texture.hpp:24
@ AlphaMapCompressed
The layer's MCAL alpha map is RLE-compressed on disk.
Definition texture.hpp:25
void appendBytes(FileBuffer &out, const void *p, std::size_t n)
Append n raw bytes at p to out (a byte-copy primitive).
constexpr std::size_t AlphaTexels
The texel count of a decoded map (64 * 64 = 4096).
Definition codec.hpp:35
constexpr std::size_t McvtCount
The 9x9 + 8x8 = 145 vertices of the MCVT height / MCNR normal grid.
constexpr std::array AdtVersions
The versions ADT is instantiated (and welded) for: every targeted last-minor-of-major release,...
constexpr bool fileHasHeader(FileKind kind)
Whether a given physical file carries the 128-byte MCNK header (the root or the pre-Cata monolithic f...
constexpr std::array MapChunkPivots
MapChunk<V> content pivots — only boundaries that change what a chunk CURRENTLY carries: WotLK (MCCV ...
consteval detail::SerializerSpec serializedBy(std::meta::info codec)
Annotate a sub-chunk member with a MapChunk-context codec (a struct with static read(self,...
consteval detail::InFileSpec inFile(InFile file)
Annotate a binary chunk member with the physical file it is routed to on write (the monolithic file c...
InFile
The physical-file group a binary chunk (tile-level or MCNK sub-chunk) is routed to on write; the mono...
Definition map_chunk.hpp:85
@ Tex
The _tex0.adt (textures, layers, alpha/shadow maps, materials).
Definition map_chunk.hpp:87
@ Obj
The _obj0.adt (model/WMO names, placements, references).
Definition map_chunk.hpp:88
@ Root
The root .adt (header, terrain, water, sounds, flying bounds).
Definition map_chunk.hpp:86
detail::MapChunk< canonicalVersion(V, MapChunkPivots, AdtVersions)> MapChunk
A terrain chunk — the canonicalizing face of detail::MapChunk: every client version collapses to its ...
FileKind
Which physical ADT file a chunk's portion belongs to.
Definition map_chunk.hpp:72
@ Monolithic
The single pre-Cataclysm .adt (every chunk).
Definition map_chunk.hpp:73
@ Obj0
The Cata+ _obj0.adt (doodad/object references and placements).
Definition map_chunk.hpp:76
@ Lod
The Legion+ _lod.adt (low-detail geometry and liquids).
Definition map_chunk.hpp:79
@ Tex0
The Cata+ _tex0.adt (texture layers, alpha/shadow maps, materials).
Definition map_chunk.hpp:75
@ Obj1
The Cata+ _obj1.adt (level-of-detail object placements).
Definition map_chunk.hpp:77
@ Root
The Cata+ root .adt (terrain heights/normals/colors, liquid, sounds).
Definition map_chunk.hpp:74
constexpr bool routesTo(InFile file, FileKind kind)
Whether a member routed to file participates in physical file kind: the monolithic file carries every...
consteval std::optional< Spec > annotation()
The first annotation of type Spec on reflected member M, if any.
Result< void > writeValue(const M &src, FileBuffer &out)
Append member src's chunk payload to out (payload only — the caller emits the chunk header).
consteval auto membersOf()
The reflected member list of E, public bases flattened in (see collectMembers).
Result< void > readValue(M &dst, std::span< const std::byte > payload, std::uint32_t fourcc, std::size_t offset, FourCCEndian endian)
Read one chunk payload into member dst, dispatching on the member kind (nested entity,...
consteval detail::CountExactlySpec countExactly(std::uint32_t count)
Declare a fixed-size contract: when engaged, the member holds exactly count elements because the form...
constexpr std::uint32_t fourcc(const char(&cc)[5], FourCCEndian endian=FourCCEndian::Reversed)
The host integer a scanned chunk id compares equal to for code cc.
Definition fourcc.hpp:33
constexpr std::string fourccToString(std::uint32_t fourcc, FourCCEndian endian=FourCCEndian::Reversed)
The readable four-character spelling of a scanned chunk id.
Definition fourcc.hpp:46
constexpr ClientVersion canonicalVersion(ClientVersion v, std::span< const ClientVersion > pivots, std::span< const ClientVersion > grid)
The canonical version v collapses to: the FIRST grid version in v's range.
consteval detail::IndexesInRootSpec indexesInRoot(std::string_view name)
Declare a cross-entity referential contract: every element of this (integral) vector member is an ind...
consteval detail::ChunkSpec chunk(const char(&cc)[5], FourCCEndian endian=FourCCEndian::Reversed)
Declare the chunk a member maps to.
consteval detail::CountMatchesSpec countMatches(std::string_view name, std::uint32_t scale=1)
Declare a companion-count contract: when this member is engaged (non-empty), its element count times ...
constexpr bool hasFlag(std::underlying_type_t< E > value, E flag)
Whether flag bit flag is set in the raw binary value value.
Definition flags.hpp:26
std::conditional_t<(V.formatLineage() >=Since &&V.formatLineage()< Until), Trait, Absent< Trait > > Slot
A version-gated base: the entity inherits Trait (flattening its chunk members in) iff Since <= V < Un...
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
@ ChunkTruncated
A chunk header or payload overruns the file buffer.
Definition error.hpp:36
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
Definition buffer.hpp:16
ADT object/misc binary structs (namespace wowlib::formats::adt::chunks): the per-cell sound emitter (...
An unparsed chunk payload, preserved verbatim for round-trip.
std::size_t size() const
The payload size in bytes.
bool empty() const
Whether the payload holds any bytes.
The legacy per-chunk liquid (MCLQ, up to and including WotLK): a 9x9 vertex grid, an 8x8 tile-flag gr...
Definition liquid.hpp:141
The version-agnostic base of every MapChunk<V> (welded as "MapChunk"): the language bindings attach f...
The per-cell MCNK header — 128 bytes, memcpy'd verbatim.
Definition header.hpp:42
MapChunkFlags flags
Flags; MapChunkFlags bits.
Definition header.hpp:44
MCAL codec: decode/encode the per-layer 64x64 alpha maps through AlphaMapCodec, indexing the sibling ...
static void write(const Chunk &, FileBuffer &out, const MapChunkWriteCtx &ctx)
static void prepare(const Chunk &self, AlphaFormat af, std::vector< std::byte > &blob, std::vector< SMLayer > &stamped)
Build the MCAL blob and the layers copy carrying each layer's derived offsetInMcal (MCLY emits that c...
static Result< void > read(Chunk &self, std::span< const std::byte > sub, const MapChunkReadCtx &ctx)
static bool engaged(const Chunk &, const MapChunkWriteCtx &ctx)
Stored form of an inFile annotation: a binary chunk's physical-file group.
Definition map_chunk.hpp:93
MCLY codec: the texture layers, plus sizing alphaMaps to the layer count (MCAL may be absent on Cata+...
static bool engaged(const Chunk &self, const MapChunkWriteCtx &)
static Result< void > read(Chunk &self, std::span< const std::byte > sub, const MapChunkReadCtx &)
static void write(const Chunk &, FileBuffer &out, const MapChunkWriteCtx &ctx)
Vertex lighting (MCLV) and terrain materials (MCMT), Cata+.
std::vector< SMTerrainMaterial > materialIds
Per-layer terrain material ids (MCMT, Cata+).
std::vector< CArgb > vertexLighting
Per-vertex baked lighting (MCLV, Cata+): 145 ARGB entries from level-designer omni lights.
bool operator==(const MapChunkCata &) const =default
Vertex colors (MCCV), WotLK+.
bool operator==(const MapChunkColor &) const =default
std::vector< CImVector > vertexColors
Per-vertex colors (MCCV, WotLK+): 145 BGRA entries blended onto the terrain (0x7F = neutral).
MCLQData legacyLiquid
The legacy per-chunk liquid (MCLQ, up to and including WotLK).
bool operator==(const MapChunkLegacyLiquid &) const =default
Context a sub-chunk codec needs to read one MCNK sub-chunk: the (already parsed) header,...
const std::vector< SMLayer > & layers
Context a sub-chunk codec needs to write one MCNK sub-chunk.
const std::vector< SMLayer > & stampedLayers
A terrain chunk (MCNK) for one client version.
Result< void > writeTo(FileBuffer &out, FileKind kind, AlphaFormat af) const
Append this chunk's portion for kind to out (the MCNK payload; the caller wraps the fourcc+size).
std::vector< std::conditional_t<(V< builds::WotLK), CWSoundEmitterVanilla, CWSoundEmitter > > soundEmitters
void validateExtra(ValidationReport &report) const
Validation hook (see formats::detail::validateValue): the terrain chunk's contracts the annotations c...
bool operator==(const MapChunk &) const =default
Result< void > readFrom(std::span< const std::byte > payload, FileKind kind, AlphaFormat af)
Decode one physical file's portion of this terrain chunk into the accumulating entity,...
MCNR codec: 145 XZY normal triples followed by 13 undeclared padding bytes (a near-constant client pa...
static Result< void > read(Chunk &self, std::span< const std::byte > sub, const MapChunkReadCtx &)
static bool engaged(const Chunk &self, const MapChunkWriteCtx &)
static void write(const Chunk &self, FileBuffer &out, const MapChunkWriteCtx &)
Stored form of a serializedBy annotation: the reflection of a codec type handling a sub-chunk that a ...
MCSH codec: the 64x64 shadow map through ShadowMapCodec.
static void write(const Chunk &self, FileBuffer &out, const MapChunkWriteCtx &)
static bool engaged(const Chunk &self, const MapChunkWriteCtx &)
static Result< void > read(Chunk &self, std::span< const std::byte > sub, const MapChunkReadCtx &ctx)
MCSE codec: typed emitter records — the era picks the entry (pre-WotLK 52-byte CWSoundEmitterVanilla,...
static void write(const Chunk &self, FileBuffer &out, const MapChunkWriteCtx &)
static bool engaged(const Chunk &self, const MapChunkWriteCtx &)
static Result< void > read(Chunk &self, std::span< const std::byte > sub, const MapChunkReadCtx &)
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...