wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
adt.hpp
Go to the documentation of this file.
1#pragma once
2
30
31#include <array>
32#include <cstddef>
33#include <cstdint>
34#include <cstring>
35#include <format>
36#include <optional>
37#include <span>
38#include <string>
39#include <string_view>
40#include <tuple>
41#include <utility>
42#include <vector>
43
44#include <welder/vocabulary.hpp>
45
49#include <wowlib/core/error.hpp>
51#include <wowlib/core/lang.hpp>
67
69 using namespace wowlib::formats::adt::chunks;
70
71 // InFile / inFile() / routesTo() / FileKind are defined in map_chunk.hpp and
72 // shared: the same physical-file routing drives both the tile-level chunks here
73 // and the MCNK sub-chunks there.
74
78 struct [[
79 =welder::weld,
80 =welder::weld_as("ADT"),
82 =welder::doc(R"(
83 A terrain map tile, abstract over the client version — the .adt file (and,
84 since Cataclysm, its _tex0/_obj0/_obj1/_lod split files) as one entity.
85 Construct the concrete version with ADT.for_version(expansion), then
86 read()/write(); the per-version ADT* classes are subclasses. See
87 https://wowdev.wiki/ADT/v18.)")
89
90 namespace detail {
92 struct ADTFlying {
93 [[=chunk("MFBO"),
95 =welder::doc(
96 "The flying bounds (MFBO, BC+); engaged by the header has_mfbo flag.")
97 ]]
99 };
100
102 struct ADTWotlk {
103 [[=chunk("MH2O"),
106 =welder::doc(
107 "The tile's water (MH2O, WotLK+): one liquid entry per chunk.")]]
108 MH2OData water{};
110 [[=chunk("MTXF"),
113 =welder::doc(
114 "Per-texture flags (MTXF, WotLK+): one entry per MTEX texture."),
115 =welder::mark::no_reassign]]
116 std::vector<SMTextureFlags> textureFlags;
117 };
118
123 struct ADTSplit {
124 [[=chunk("MAMP"),
126 =welder::doc(
127 "The MAMP alpha-map downscale value (Cata+): overrides the MHDR "
128 "inline value; alpha texture size is 64 / (2^value).")]]
129 std::uint32_t mamp = 0;
130
131 [[=welder::doc(
132 "Whether this tile stores its textures as MDID/MHID FileDataIDs "
133 "(8.1+ height-texturing maps) rather than MTEX names; set from the "
134 "chunk present on read and honored on write.")]]
135 bool usesTextureFdids = false;
136
137 [[=chunk("MTXP"),
140 =welder::doc(
141 "Height-blend texture parameters (MTXP, MoP+): one per texture."),
142 =welder::mark::no_reassign]]
143 std::vector<SMTextureParams> textureParams;
144
145 [[=welder::mark::exclude]] std::vector<std::byte> obj1Data;
146 // raw _obj1.adt
147 [[=welder::mark::exclude]] std::vector<std::byte> lodData;
148 // raw _lod.adt
149
150 [[=welder::mark::exclude]]
151
152 bool operator==(const ADTSplit&) const = default;
153 };
154
157 struct ADTTexFdids {
158 [[=chunk("MDID"),
161 =welder::doc(
162 "Diffuse-texture FileDataIDs (MDID, 8.1+): the _s.blp tileset "
163 "textures MapChunk layers index, in place of MTEX names."),
164 =welder::mark::no_reassign]]
165 std::vector<std::uint32_t> diffuseTextureIds;
166
167 [[=chunk("MHID"),
170 =welder::doc(
171 "Height-texture FileDataIDs (MHID, 8.1+): the _h.blp map paired with "
172 "each diffuse texture (0 for none)."),
173 =welder::mark::no_reassign]]
174 std::vector<std::uint32_t> heightTextureIds;
175
176 [[=welder::mark::exclude]]
177
178 bool operator==(const ADTTexFdids&) const = default;
179 };
180 }
181
182 namespace detail {
185 template <ClientVersion V>
186 struct [[
187 =welder::weld,
188 =welder::doc(R"(
189 A terrain map tile for one client version: the 256 terrain chunks plus the
190 tile-wide texture, model and placement tables, unified across the split ADT
191 files the tile is stored in. Adding a texture, model or placement does not
192 depend on which file it lands in — the writer routes it. An instance read
193 from a client and left unmodified rewrites to a semantically equal tile
194 (alpha maps and offset tables are re-derived, not byte-preserved). See
195 https://wowdev.wiki/ADT/v18.)")
197 : ADTBase,
202 static constexpr ClientVersion Version = V;
203
207 static constexpr std::array ChunkOrder{
208 fourcc("MVER"),
209 fourcc("MHDR"),
210 fourcc("MCIN"),
211 fourcc("MAMP"),
212 fourcc("MTEX"),
213 fourcc("MDID"),
214 fourcc("MHID"),
215 fourcc("MMDX"),
216 fourcc("MMID"),
217 fourcc("MWMO"),
218 fourcc("MWID"),
219 fourcc("MDDF"),
220 fourcc("MODF"),
221 fourcc("MH2O"),
222 fourcc("MCNK"),
223 fourcc("MFBO"),
224 fourcc("MTXF"),
225 fourcc("MTXP")
226 };
228 [[=chunk("MVER"),
229 =welder::doc(
230 "The ADT format version (MVER); 18 for every supported client.")]]
231 std::uint32_t mver = AdtVersion18;
232
233 [[=chunk("MHDR"),
235 =welder::doc(
236 "The tile header (MHDR): flags; the chunk offsets are derived.")]]
238
239 [[=chunk("MTEX"),
241 =welder::doc(
242 "The tileset texture filenames (MTEX): the paths MapChunk layers "
243 "index. Present unless the tile uses MDID/MHID FileDataIDs (8.1+ "
244 "height-texturing maps)."),
245 =welder::mark::no_reassign]]
247
248 [[=chunk("MMDX"),
250 =welder::doc(
251 "The M2 model filenames (MMDX) placements reference by MMID index."),
252 =welder::mark::no_reassign]]
254
255 [[=chunk("MMID"),
257 =welder::doc(
258 "Byte offsets into model_filenames (MMID): a doodad placement's "
259 "name_id indexes this list."),
260 =welder::mark::no_reassign]]
261 std::vector<std::uint32_t> modelNameOffsets;
262
263 [[=chunk("MWMO"),
265 =welder::doc(
266 "The WMO filenames (MWMO) placements reference by MWID index."),
267 =welder::mark::no_reassign]]
269
270 [[=chunk("MWID"),
272 =welder::doc(
273 "Byte offsets into wmo_filenames (MWID): a WMO placement's name_id "
274 "indexes this list."),
275 =welder::mark::no_reassign]]
276 std::vector<std::uint32_t> wmoNameOffsets;
277
278 [[=chunk("MDDF"),
280 =welder::doc("Doodad (M2) placements on this tile (MDDF)."),
281 =welder::mark::no_reassign]]
282 std::vector<common::SMDoodadDef> doodadPlacements;
283
284 [[=chunk("MODF"),
286 =welder::doc("WMO placements on this tile (MODF)."),
287 =welder::mark::no_reassign]]
288 std::vector<common::SMMapObjDef> wmoPlacements;
289
290 [[=welder::doc(
291 "The 256 terrain chunks (MCNK), row-major (index = y * 16 + x)."),
292 =welder::mark::no_reassign]]
293 std::vector<adt::MapChunk<V>> chunks;
294
295 [[=welder::doc(
296 "How this tile's alpha maps were laid out on disk, recorded from the "
297 "AlphaFormat passed to read(); write() takes its own explicit "
298 "argument. wowlib always presents decoded 64x64 maps.")]]
300
301 // --- fs I/O (definitions at the bottom of this header) ------------------
302
303 [[=welder::mark::only(welder::lang::lua, wowlib::lang::Cs),
304 =welder::doc("Load the tile — every split file present — from a client "
305 "filesystem, replacing this entity's contents. The alpha-map bit "
306 "depth (from the map's WDT) is supplied by the caller.")]]
307 Result<void> read(fs::FileSystem& fs [[=welder::doc("the filesystem gateway")]],
308 const FileKey& key
309 [[=welder::doc("the tile identity (root .adt path and/or "
310 "FileDataID)")]],
311 AlphaFormat alpha
312 [[=welder::doc("the on-disk alpha-map bit depth for this tile's "
313 "map (from its WDT MPHD flags)")]]);
314
315 [[=welder::mark::only(welder::lang::lua, wowlib::lang::Cs),
316 =welder::doc(
317 "Serialize the tile (and, Cata+, every split file) through the "
318 "filesystem's project overlay; the file names derive from the key, "
319 "which must resolve to a path. The alpha-map bit depth to encode is "
320 "supplied by the caller.")]]
321 Result<void> write(fs::FileSystem& fs [[=welder::doc("the filesystem gateway")]],
322 const FileKey& key [[=welder::doc("the tile identity; must resolve to a path")]],
323 AlphaFormat alpha [[=welder::doc("the on-disk alpha-map bit depth to encode")]]) const;
324
325 // Beyond each terrain chunk's own contracts, this sees the tile-wide
326 // ones: layers resolving in the tile's texture table (MTEX names or MDID
327 // FileDataIDs, whichever the tile uses), chunk references landing in
328 // MDDF/MODF, the placements' own name references, and the 256-chunk grid.
329 [[nodiscard]]
330 [[=welder::doc(R"(
331 Check the logical integrity contracts this tile must satisfy to LOAD
332 in the client — across every terrain chunk AND the tile-wide tables —
333 which write() deliberately never enforces. Call it before writing when
334 you want to know the tile will load. A tile read from a client and
335 left unmodified reports no errors; warnings mark states real client
336 files ship.)"),
337 =welder::returns(R"(every violated contract, each with its member path
338 ("chunks[i]..."))")]]
339 ValidationReport validate() const;
340
341 [[nodiscard]]
342 [[=welder::doc("Validate and raise on the first error instead of "
343 "returning a report — the assert-style face of "
344 "validate()."),
345 =welder::returns("nothing; raises when validate() finds any error")]]
346 Result<void> ensureValid() const;
347
349 reflecting over the `chunk()`-annotated members.
350 @param data the file bytes.
351 @param kind which split file it is.
352 @return a structural error or success. */
353 [[=welder::mark::exclude]]
354 Result<void> parseFile(std::span<const std::byte> data, FileKind kind);
355
362 [[=welder::mark::exclude]]
364
365 private:
368 void normalizeChunks();
369
373 void _normalizeMhdr() {
374 header.ofsMcin = header.ofsMtex = header.ofsMmdx = header.ofsMmid = 0;
375 header.ofsMwmo = header.ofsMwid = header.ofsMddf = header.ofsModf = 0;
376 header.ofsMfbo = header.ofsMh2O = header.ofsMtxf = 0;
377 }
378
386 template <std::meta::info M>
387 bool _writeEngaged() const {
388 constexpr std::string_view id = std::meta::identifier_of(M);
389 if constexpr (id == "flyingBounds") return hasFlag(header.flags, MapHeaderFlags::HasMfbo);
390 else if constexpr (id == "textures") {
391 if constexpr (requires { this->usesTextureFdids; }) return !this->usesTextureFdids;
392 // MTEX unless the tile uses FileDataIDs
393 else return true; // pre-8.1: always MTEX
394 }
395 else if constexpr (id == "diffuseTextureIds" || id == "heightTextureIds") {
396 if constexpr (requires { this->usesTextureFdids; }) return this->usesTextureFdids;
397 else return false;
398 }
400 return !this->
401 [:M:].empty();
402 else return true;
403 }
404
407
409 static void _put(FileBuffer& out, const void* p, std::size_t n) {
410 const auto* b = static_cast<const std::byte*>(p);
411 out.insert(out.end(), b, b + n);
412 }
413
414 /** Emit one top-level chunk (fourcc + size + body) into @a out and return
415 the fourcc position (for MHDR/MCIN offset stamping).
416 @tparam Body the payload-writing callable type.
417 @param out the destination buffer.
418 @param magic the chunk fourcc.
419 @param body writes the chunk payload into @a out.
420 @return the chunk's fourcc position in @a out. */
421 template <typename Body>
422 static std::size_t _emitChunk(FileBuffer& out, std::uint32_t magic, Body&& body) {
423 const std::size_t at = out.size();
424 _put(out, &magic, 4);
425 const std::size_t sizeAt = out.size();
426 out.insert(out.end(), 4, std::byte{0});
427 body();
428 const auto size = static_cast<std::uint32_t>(out.size() - sizeAt - 4);
429 std::memcpy(out.data() + sizeAt, &size, 4);
430 return at;
431 }
432 };
433 }
434
436 template <ClientVersion V>
438
439 namespace detail {
440 template <ClientVersion V>
441 void ADT<V>::normalizeChunks() {
442 for (auto& chunk : chunks)
444 }
445
446 template <ClientVersion V>
448 ValidationReport report;
449
450 // a tile is a full 16x16 grid; the client indexes chunks positionally
451 if (!chunks.empty() && chunks.size() != ChunksPerTile)
452 report.addError("chunks", std::format("count {} != the {} chunks of a tile", chunks.size(), ChunksPerTile));
453
454 // the texture table a layer's textureId addresses: MTEX names, or the
455 // MDID FileDataIDs once the tile uses them (a per-MAP choice, not a
456 // version one - see usesTextureFdids)
457 const std::size_t textureCount = [&] {
458 if constexpr (requires { this->diffuseTextureIds; })
459 if (this->usesTextureFdids) return this->diffuseTextureIds.size();
460 return textures.entries().size();
461 }();
462
463 for (std::size_t i = 0; i < chunks.size() && !report.full(); ++i) {
464 const std::size_t mark = report.size();
465 const auto& chunk = chunks[i];
467
468 for (std::size_t j = 0; j < chunk.layers.size(); ++j)
469 if (chunk.layers[j].textureId >= textureCount)
470 report.addError(std::format("layers[{}]", j),
471 std::format("texture_id {} out of range: {} textures", chunk.layers[j].textureId,
472 textureCount));
473
474 // the chunk's placement references index the TILE's tables
475 formats::detail::validateIndexElements(chunk.doodadRefs, doodadPlacements.size(), "doodadRefs",
476 "doodadPlacements", report);
477 formats::detail::validateIndexElements(chunk.objectRefs, wmoPlacements.size(), "objectRefs",
478 "wmoPlacements", report);
479 report.prefixFrom(mark, std::format("chunks[{}]", i));
480 }
481
482 // A placement's nameId indexes the tile's name-offset table, EXCEPT
483 // when its EntryIsFdid flag makes it a FileDataID the client loads
484 // directly (Legion+) — then there is nothing local to resolve against.
485 const auto checkPlacements = [&](const auto& placements,
486 auto fdidFlag,
487 const auto& offsets,
488 std::string_view what,
489 std::string_view table) {
490 for (std::size_t i = 0; i < placements.size() && !report.full(); ++i) {
491 if (hasFlag(placements[i].flags, fdidFlag)) continue;
492 if (placements[i].nameId >= offsets.size())
493 report.addError(std::format("{}[{}]", what, i),
494 std::format("name_id {} out of range: {} holds {} entries", placements[i].nameId, table,
495 offsets.size()));
496 }
497 };
498 checkPlacements(doodadPlacements, common::DoodadDefFlags::EntryIsFdid, modelNameOffsets,
499 "doodadPlacements", "modelNameOffsets");
500 checkPlacements(wmoPlacements, common::MapObjDefFlags::EntryIsFdid, wmoNameOffsets, "wmoPlacements",
501 "wmoNameOffsets");
502
503 return report;
504 }
505
506 template <ClientVersion V>
508 return validate().toResult();
509 }
510
511 template <ClientVersion V>
512 Result<void> ADT<V>::parseFile(std::span<const std::byte> data, FileKind kind) {
513 using Self = ADT<V>;
514 static constexpr auto Members = formats::detail::membersOf<Self>();
515
516 std::size_t pos = 0;
517 std::size_t chunkIndex = 0;
518 while (pos + 8 <= data.size()) {
519 std::uint32_t magic = 0, size = 0;
520 std::memcpy(&magic, data.data() + pos, 4);
521 std::memcpy(&size, data.data() + pos + 4, 4);
522 if (size > data.size() - pos - 8)
524 std::format("ADT chunk {} at {:#x} overruns the file", fourccToString(magic), pos));
525 const auto payload = data.subspan(pos + 8, size);
526 pos += 8 + size;
527
528 // MCNK is the one non-reflective chunk: 256 repeats, each a per-file
529 // portion merged into a MapChunk via readFrom.
530 if (magic == fourcc("MCNK")) {
531 if (chunks.size() < chunkIndex + 1) chunks.resize(chunkIndex + 1);
532 if (auto r = chunks[chunkIndex].readFrom(payload, kind, alphaFormat); !r) return r;
533 ++chunkIndex;
534 continue;
535 }
536
537 // Every other chunk routes to its `chunk()`-annotated member by fourcc.
538 // Version-gated members live in trait bases: for a version where a trait
539 // is inactive the member is absent from `members`, so an unmodeled chunk
540 // (e.g. MCIN, whose fields are derived and needs no member) simply does
541 // not match and is skipped.
542 Result<void> outcome{};
543 bool matched = false;
544 template for (constexpr auto m : Members) {
545 if constexpr (constexpr auto spec = formats::detail::annotation<formats::detail::ChunkSpec, m>(); spec.
546 has_value()) {
547 if (!matched && magic == spec->magic) {
548 matched = true;
549 outcome = formats::detail::readValue(this->[:m:], payload, magic, pos, spec->endian);
550 // the two members that need a post-read fix-up: MHDR's derived
551 // offsets are zeroed, and MDID flips the texture-scheme flag.
552 if constexpr (std::meta::identifier_of(m) == "header") _normalizeMhdr();
553 else if constexpr (std::meta::identifier_of(m) == "diffuseTextureIds")
554 if constexpr (requires { this->usesTextureFdids; }) this->usesTextureFdids = true;
555 }
556 }
557 }
558 if (!outcome) return outcome;
559 }
560 return {};
561 }
562
563 template <ClientVersion V>
565 using Self = ADT<V>;
566 static constexpr auto Members = formats::detail::membersOf<Self>();
567 const bool mono = kind == FileKind::Monolithic;
568
569 FileBuffer out;
570 std::size_t mhdrAt = 0, mcinAt = 0;
571 std::array<std::pair<std::size_t, std::size_t>, 256> mcnkLoc{};
572 // (fourcc pos, payload size)
573 // Emitted chunk fourcc positions, for the MHDR offset table.
574 std::array<std::pair<std::uint32_t, std::size_t>, ChunkOrder.size()> emitted{};
575 std::size_t nEmitted = 0;
576 std::optional<Error> err;
577
578 const auto record = [&](std::uint32_t magic, std::size_t at) {
579 emitted[nEmitted++] = {magic, at};
580 };
581 const auto positionOf = [&](std::uint32_t magic) -> std::size_t {
582 for (std::size_t i = 0; i < nEmitted; ++i)
583 if (emitted[i].first == magic) return emitted[i].second;
584 return 0;
585 };
586
587 for (const std::uint32_t want : ChunkOrder) {
588 if (want == fourcc("MVER")) {
589 _emitChunk(out, want, [&] {
590 const std::uint32_t v = AdtVersion18;
591 _put(out, &v, 4);
592 });
593 continue;
594 }
595 if (want == fourcc("MCIN")) {
596 if (mono)
597 mcinAt = _emitChunk(out, want, [&] {
598 out.insert(out.end(), 256 * 16, std::byte{0});
599 });
600 continue;
601 }
602 if (want == fourcc("MCNK")) {
603 for (std::size_t i = 0; i < chunks.size() && i < 256; ++i) {
604 const std::size_t at = _emitChunk(out, want, [&] {
605 if (auto r = chunks[i].writeTo(out, kind, alpha); !r) err = r.error();
606 });
607 mcnkLoc[i] = {at, out.size() - (at + 8)};
608 }
609 continue;
610 }
611
612 // Reflective emit: the member whose chunk() magic == want, if it is routed
613 // to this file and engaged.
614 template for (constexpr auto m : Members) {
615 if constexpr (constexpr auto spec = formats::detail::annotation<formats::detail::ChunkSpec, m>(); spec.
616 has_value()) {
617 if constexpr (constexpr auto route = formats::detail::annotation<InFileSpec, m>(); route.has_value()) {
618 if (spec->magic == want && routesTo(route->file, kind) && _writeEngaged<m>()) {
619 // Splice the member reference HERE (m is a constant expression);
620 // inside the lambda m is captured by reference and would not be.
621 const auto& member = this->[:m:];
622 const std::size_t at = _emitChunk(out, want, [&] {
623 if (auto r = formats::detail::writeValue(member, out); !r) err = r.error();
624 });
625 record(want, at);
626 if (want == fourcc("MHDR")) mhdrAt = at;
627 }
628 }
629 }
630 }
631 }
632 if (err) return std::unexpected{*err};
633
634 // Stamp the derived tables. MCIN: absolute offset at each MCNK fourcc, size
635 // including the 8-byte header (monolithic only). MHDR: offsets relative to
636 // the MHDR data start, pointing at each target chunk's fourcc.
637 if (mono)
638 for (std::size_t i = 0; i < 256; ++i) {
639 const std::uint32_t entry[4]{
640 static_cast<std::uint32_t>(mcnkLoc[i].first),
641 static_cast<std::uint32_t>(mcnkLoc[i].second + 8),
642 0,
643 0
644 };
645 std::memcpy(out.data() + mcinAt + 8 + i * 16, entry, 16);
646 }
647 if (fileHasHeader(kind)) {
648 const std::size_t base = mhdrAt + 8;
649 const auto rel = [&](std::uint32_t magic) {
650 const std::size_t at = positionOf(magic);
651 return at == 0 ? 0u : static_cast<std::uint32_t>(at - base);
652 };
653 MHDRData h = header;
654 h.ofsMcin = mcinAt == 0 ? 0u : static_cast<std::uint32_t>(mcinAt - base);
655 h.ofsMtex = rel(fourcc("MTEX"));
656 h.ofsMmdx = rel(fourcc("MMDX"));
657 h.ofsMmid = rel(fourcc("MMID"));
658 h.ofsMwmo = rel(fourcc("MWMO"));
659 h.ofsMwid = rel(fourcc("MWID"));
660 h.ofsMddf = rel(fourcc("MDDF"));
661 h.ofsModf = rel(fourcc("MODF"));
662 h.ofsMfbo = rel(fourcc("MFBO"));
663 h.ofsMh2O = rel(fourcc("MH2O"));
664 h.ofsMtxf = rel(fourcc("MTXF"));
665 std::memcpy(out.data() + mhdrAt + 8, &h, sizeof(MHDRData));
666 }
667 return out;
668 }
669
670 template <ClientVersion V>
672 *this = ADT{};
673 alphaFormat = alpha;
674
675 if constexpr (V < builds::Cata) {
676 // The pre-Cata tile is a single monolithic .adt carrying every chunk.
677 return fs.readFile(key).and_then([&](FileBuffer data) -> Result<void> {
678 chunks.assign(256, adt::MapChunk<V>{});
679 if (auto r = parseFile(data, FileKind::Monolithic); !r) return r;
680 normalizeChunks();
681 return {};
682 });
683 }
684 else {
685 // Cata+ split tile: the root .adt plus its _tex0/_obj0/_obj1/_lod split
686 // files, located by the "{stem}_<suffix>.adt" naming convention.
687 // root/tex0/obj0 are parsed and MERGED into the one entity (their 256
688 // MCNK streams accumulate per chunk); _obj1/_lod are preserved verbatim
689 // this stage (structured later).
690 const FileKey resolved = fs.resolve(key);
691 if (!resolved.path)
692 return makeError(ErrorCode::PathNotResolvable, "loading a split ADT needs the root file path");
693 std::string_view stem = *resolved.path;
694 if (stem.ends_with(".adt")) stem.remove_suffix(4);
695 const auto sibling = [&](std::string_view suffix) {
696 return std::format("{}{}.adt", stem, suffix);
697 };
698
699 const auto rootData = fs.readFile(key);
700 if (!rootData) return std::unexpected{rootData.error()};
701 chunks.assign(256, adt::MapChunk<V>{});
702 if (auto r = parseFile(*rootData, FileKind::Root); !r) return r;
703
704 const auto load = [&](std::string_view suffix, FileKind fk) -> Result<void> {
705 const FileKey k{sibling(suffix)};
706 if (!fs.exists(k)) return {};
707 const auto data = fs.readFile(k);
708 if (!data)
709 return makeError(data.error().code, std::format("{} split file: {}", suffix, data.error().message));
710 return parseFile(*data, fk);
711 };
712 if (auto r = load("_tex0", FileKind::Tex0); !r) return r;
713 if (auto r = load("_obj0", FileKind::Obj0); !r) return r;
714
715 // preserve the unmodeled split files verbatim
716 if constexpr (requires { this->obj1Data; }) {
717 const auto keep = [&](std::string_view suffix, std::vector<std::byte>& into) {
718 const FileKey k{sibling(suffix)};
719 if (fs.exists(k))
720 if (const auto data = fs.readFile(k)) into = *data;
721 };
722 keep("_obj1", this->obj1Data);
723 keep("_lod", this->lodData);
724 }
725
726 normalizeChunks();
727 return {};
728 }
729 }
730
731 template <ClientVersion V>
732 Result<void> ADT<V>::write(fs::FileSystem& fs, const FileKey& key, AlphaFormat alpha) const {
733 const FileKey resolved = fs.resolve(key);
734 if (!resolved.path)
735 return makeError(ErrorCode::PathNotResolvable, "saving an ADT needs a path");
736
737 // addFile returns Result<FileDataID>; a save only cares whether it failed.
738 const auto add = [&](std::string_view path, std::span<const std::byte> bytes) -> Result<void> {
739 return fs.addFile(path, bytes).transform([](auto&&) {});
740 };
741
742 if constexpr (V < builds::Cata) {
743 return writeFile(FileKind::Monolithic, alpha).and_then([&](FileBuffer data) {
744 return add(*resolved.path, data);
745 });
746 }
747 else {
748 std::string_view stem = *resolved.path;
749 if (stem.ends_with(".adt")) stem.remove_suffix(4);
750 const auto sibling = [&](std::string_view suffix) {
751 return std::format("{}{}.adt", stem, suffix);
752 };
753 const auto store = [&](FileKind fk, std::string_view suffix) -> Result<void> {
754 return writeFile(fk, alpha).and_then([&](FileBuffer data) { return add(sibling(suffix), data); });
755 };
756 // the root file keeps the bare "{stem}.adt" name
757 if (auto r = writeFile(FileKind::Root, alpha).and_then([&](FileBuffer data) {
758 return add(*resolved.path, data);
759 }); !r)
760 return r;
761 if (auto r = store(FileKind::Tex0, "_tex0"); !r) return r;
762 if (auto r = store(FileKind::Obj0, "_obj0"); !r) return r;
763 if constexpr (requires { this->obj1Data; }) {
764 if (!this->obj1Data.empty())
765 if (auto r = add(sibling("_obj1"), this->obj1Data); !r) return r;
766 if (!this->lodData.empty())
767 if (auto r = add(sibling("_lod"), this->lodData); !r) return r;
768 }
769 return {};
770 }
771 }
772 }
773}
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...
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.
const std::vector< Entry > & entries() const
The decoded entries, in blob order.
void addError(std::string path, std::string message)
Record an error finding (see add()).
FileKey resolve(const FileKey &key) const
Result< FileBuffer > readFile(const FileKey &key)
bool exists(const FileKey &key)
Whether a file is reachable in the overlay or the storage.
Result< FileDataID > addFile(std::string_view path, std::span< const std::byte > content)
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...
File identity types: the strong FileDataID and the FileKey a read request travels as.
The runtime facade over the static compositions — the primary welder binding surface of the fs layer.
Flag-testing convenience for the binary formats' bit-mask enums.
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
The per-chunk terrain entity (namespace wowlib::formats::adt): MapChunk<V>, one of the 256 MCNK terra...
The map-placement binary records SHARED across the world file formats (namespace wowlib::formats::com...
constexpr ClientVersion Cata
Cataclysm (any 4.x client).
ADT chunk binary structs (the MCNK cell header, texture layers, liquid records, flying bounds) and th...
Definition header.hpp:22
@ HasMfbo
The tile has a flying bounds chunk (MFBO).
Definition header.hpp:113
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::uint32_t AdtVersion18
The ADT format version every supported client writes (the MVER payload).
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...
@ 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
constexpr std::size_t ChunksPerTile
The terrain chunks a tile is divided into: a 16x16 grid, indexed row-major (y * 16 + x) — the client ...
detail::MapChunk< canonicalVersion(V, MapChunkPivots, AdtVersions)> MapChunk
A terrain chunk — the canonicalizing face of detail::MapChunk: every client version collapses to its ...
detail::ADT< canonicalVersion(V, AdtPivots, AdtVersions)> ADT
A terrain tile — the canonicalizing face of detail::ADT.
Definition adt.hpp:339
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
@ Tex0
The Cata+ _tex0.adt (texture layers, alpha/shadow maps, materials).
Definition map_chunk.hpp:75
@ Root
The Cata+ root .adt (terrain heights/normals/colors, liquid, sounds).
Definition map_chunk.hpp:74
@ Lowres4Bit
2048-byte 4-bit alpha maps (the default when the WDT sets neither big-alpha flag).
constexpr std::array AdtPivots
ADT<V> assembly content pivots — only boundaries that change what the tile CURRENTLY carries: TBC (MF...
constexpr bool routesTo(InFile file, FileKind kind)
Whether a member routed to file participates in physical file kind: the monolithic file carries every...
@ EntryIsFdid
name_id is a FileDataID to load directly, not an MMID index (Legion+).
@ EntryIsFdid
name_id is a FileDataID to load directly, not a name-table index (Legion+).
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 ...
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,...
void validateIndexElements(const Values &values, std::size_t targetCount, std::string_view member, std::string_view target, ValidationReport &report)
Report every element of values that is not a valid index into a targetCount-element target,...
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.
constexpr detail::OptionalSpec Optional
Mark a chunk member the format does not require: absence on read is fine.
consteval detail::ChunkSpec chunk(const char(&cc)[5], FourCCEndian endian=FourCCEndian::Reversed)
Declare the chunk a member maps to.
constexpr void setFlag(E &value, E flag, bool on=true)
Set (or clear) flag bit flag in the enum-typed field value.
Definition flags.hpp:48
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...
constexpr welder::lang Cs
C#/.NET — the welder-csharp rod's identity (user-range slot 0), respelled for wowlib's annotation sit...
Definition lang.hpp:23
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
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
Definition error.hpp:23
@ 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
StringBlock — the decoded representation of a chunk of zero-terminated strings (MOTX,...
std::optional< std::string > path
The canonical client-internal path, if known.
Definition file_key.hpp:31
The version-agnostic root of every file-level entity (welded as "FileEntity").
The version-agnostic base of every ADT<V> (welded as "ADT"): the language bindings attach for_version...
Definition adt.hpp:84
The whole-tile MH2O chunk: liquid for each of the 256 cells, decoded.
Definition liquid.hpp:112
The flying bounding box (MFBO, BC+): two 3x3 grids of int16 heights — a maximum plane you fall throug...
Definition header.hpp:149
The tile header (MHDR, 64 bytes, root/monolithic).
Definition header.hpp:120
MapHeaderFlags flags
Flags; MapHeaderFlags bits.
Definition header.hpp:122
The flying bounds (MFBO), BC+.
Definition adt.hpp:88
MFBOPlanes flyingBounds
The flying bounds (MFBO, BC+); engaged by the header has_mfbo flag.
Definition adt.hpp:91
Cataclysm+ split-file tile chunks (the tile now spans root/_tex0/_obj0/ _obj1/_lod files).
Definition adt.hpp:109
std::vector< std::byte > obj1Data
Definition adt.hpp:121
bool operator==(const ADTSplit &) const =default
std::vector< SMTextureParams > textureParams
Height-blend texture parameters (MTXP, MoP+): one per texture.
Definition adt.hpp:119
std::vector< std::byte > lodData
Definition adt.hpp:123
bool usesTextureFdids
Whether this tile stores its textures as MDID/MHID FileDataIDs (8.1+ height-texturing maps) rather th...
Definition adt.hpp:115
std::uint32_t mamp
The MAMP alpha-map downscale value (Cata+): overrides the MHDR inline value; alpha texture size is 64...
Definition adt.hpp:112
The 8.1+ FileDataID texture tables (_tex0), which replace MTEX names on height-texturing maps.
Definition adt.hpp:132
std::vector< std::uint32_t > diffuseTextureIds
Diffuse-texture FileDataIDs (MDID, 8.1+): the _s.blp tileset textures MapChunk layers index,...
Definition adt.hpp:135
bool operator==(const ADTTexFdids &) const =default
std::vector< std::uint32_t > heightTextureIds
Height-texture FileDataIDs (MHID, 8.1+): the _h.blp map paired with each diffuse texture (0 for none)...
Definition adt.hpp:139
WotLK+ tile chunks: the water (MH2O) and the texture flags (MTXF).
Definition adt.hpp:95
MH2OData water
The tile's water (MH2O, WotLK+): one liquid entry per chunk.
Definition adt.hpp:98
std::vector< SMTextureFlags > textureFlags
Per-texture flags (MTXF, WotLK+): one entry per MTEX texture.
Definition adt.hpp:102
A terrain tile for one client version.
Definition adt.hpp:155
Result< FileBuffer > writeFile(FileKind kind, AlphaFormat alpha) const
Serialize one physical file of the tile (monolithic pre-Cata, or one of root/_tex0/_obj0),...
Definition adt.hpp:466
ValidationReport validate() const
Definition adt.hpp:349
Result< void > write(fs::FileSystem &fs, const FileKey &key, AlphaFormat alpha) const
Serialize the tile (and, Cata+, every split file) through the filesystem's project overlay; the file ...
Definition adt.hpp:634
Result< void > parseFile(std::span< const std::byte > data, FileKind kind)
Parse one split file's chunk stream into this entity (merging), by reflecting over the chunk()-annota...
Definition adt.hpp:414
Result< void > ensureValid() const
Validate and raise on the first error instead of returning a report — the assert-style face of valida...
Definition adt.hpp:409
Result< void > read(fs::FileSystem &fs, const FileKey &key, AlphaFormat alpha)
Load the tile — every split file present — from a client filesystem, replacing this entity's contents...
Definition adt.hpp:573
ADT texture binary structs (namespace wowlib::formats::adt::chunks): the per-cell texture layer (SMLa...
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...