44#include <welder/vocabulary.hpp>
80 =welder::weld_as(
"ADT"),
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.)")
96 "The flying bounds (MFBO, BC+); engaged by the header has_mfbo flag.")
107 "The tile's water (MH2O, WotLK+): one liquid entry per chunk.")]]
114 "Per-texture flags (MTXF, WotLK+): one entry per MTEX texture."),
115 =welder::mark::no_reassign]]
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;
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.")]]
141 "Height-blend texture parameters (MTXP, MoP+): one per texture."),
142 =welder::mark::no_reassign]]
145 [[=welder::mark::exclude]] std::vector<std::byte>
obj1Data;
147 [[=welder::mark::exclude]] std::vector<std::byte>
lodData;
150 [[=welder::mark::exclude]]
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]]
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]]
176 [[=welder::mark::exclude]]
185 template <ClientVersion V>
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.)")
230 "The ADT format version (MVER); 18 for every supported client.")]]
236 "The tile header (MHDR): flags; the chunk offsets are derived.")]]
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]]
251 "The M2 model filenames (MMDX) placements reference by MMID index."),
252 =welder::mark::no_reassign]]
258 "Byte offsets into model_filenames (MMID): a doodad placement's "
259 "name_id indexes this list."),
260 =welder::mark::no_reassign]]
266 "The WMO filenames (MWMO) placements reference by MWID index."),
267 =welder::mark::no_reassign]]
273 "Byte offsets into wmo_filenames (MWID): a WMO placement's name_id "
274 "indexes this list."),
275 =welder::mark::no_reassign]]
280 =welder::doc(
"Doodad (M2) placements on this tile (MDDF)."),
281 =welder::mark::no_reassign]]
286 =welder::doc(
"WMO placements on this tile (MODF)."),
287 =welder::mark::no_reassign]]
291 "The 256 terrain chunks (MCNK), row-major (index = y * 16 + x)."),
292 =welder::mark::no_reassign]]
293 std::vector<adt::MapChunk<V>>
chunks;
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.")]]
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.")]]
309 [[=welder::doc(
"the tile identity (root .adt path and/or "
312 [[=welder::doc(
"the on-disk alpha-map bit depth for this tile's "
313 "map (from its WDT MPHD flags)")]]);
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.")]]
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;
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
337 =welder::returns(R"(every violated contract, each with its member path
338 ("chunks[i]..."))")]]
342 [[=welder::doc(
"Validate and raise on the first error instead of "
343 "returning a report — the assert-style face of "
345 =welder::returns(
"nothing; raises when validate() finds any error")]]
353 [[=welder::mark::exclude]]
362 [[=welder::mark::exclude]]
368 void normalizeChunks();
373 void _normalizeMhdr() {
386 template <std::meta::info M>
387 bool _writeEngaged()
const {
388 constexpr std::string_view
id = std::meta::identifier_of(M);
390 else if constexpr (
id ==
"textures") {
391 if constexpr (
requires { this->usesTextureFdids; })
return !this->usesTextureFdids;
395 else if constexpr (
id ==
"diffuseTextureIds" ||
id ==
"heightTextureIds") {
396 if constexpr (
requires { this->usesTextureFdids; })
return this->usesTextureFdids;
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);
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});
428 const auto size =
static_cast<std::uint32_t
>(out.size() - sizeAt - 4);
429 std::memcpy(out.data() + sizeAt, &size, 4);
436 template <ClientVersion V>
440 template <ClientVersion V>
441 void ADT<V>::normalizeChunks() {
446 template <ClientVersion V>
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();
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];
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,
476 "doodadPlacements", report);
478 "wmoPlacements", report);
479 report.prefixFrom(mark, std::format(
"chunks[{}]", i));
485 const auto checkPlacements = [&](
const auto& placements,
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,
499 "doodadPlacements",
"modelNameOffsets");
506 template <ClientVersion V>
508 return validate().toResult();
511 template <ClientVersion V>
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);
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;
543 bool matched =
false;
544 template for (
constexpr auto m : Members) {
547 if (!matched && magic == spec->magic) {
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;
558 if (!outcome)
return outcome;
563 template <ClientVersion V>
570 std::size_t mhdrAt = 0, mcinAt = 0;
571 std::array<std::pair<std::size_t, std::size_t>, 256> mcnkLoc{};
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;
578 const auto record = [&](std::uint32_t magic, std::size_t at) {
579 emitted[nEmitted++] = {magic, at};
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;
587 for (
const std::uint32_t want : ChunkOrder) {
588 if (want ==
fourcc(
"MVER")) {
589 _emitChunk(out, want, [&] {
595 if (want ==
fourcc(
"MCIN")) {
597 mcinAt = _emitChunk(out, want, [&] {
598 out.insert(out.end(), 256 * 16, std::byte{0});
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();
607 mcnkLoc[i] = {at, out.size() - (at + 8)};
614 template for (
constexpr auto m : Members) {
618 if (spec->magic == want &&
routesTo(route->file, kind) && _writeEngaged<m>()) {
621 const auto& member = this->[:m:];
622 const std::size_t at = _emitChunk(out, want, [&] {
626 if (want ==
fourcc(
"MHDR")) mhdrAt = at;
632 if (err)
return std::unexpected{*err};
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),
645 std::memcpy(out.data() + mcinAt + 8 + i * 16, entry, 16);
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);
654 h.
ofsMcin = mcinAt == 0 ? 0u :
static_cast<std::uint32_t
>(mcinAt - base);
665 std::memcpy(out.data() + mhdrAt + 8, &h,
sizeof(
MHDRData));
670 template <ClientVersion V>
690 const FileKey resolved = fs.
resolve(key);
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);
699 const auto rootData = fs.
readFile(key);
700 if (!rootData)
return std::unexpected{rootData.error()};
705 const FileKey k{sibling(suffix)};
706 if (!fs.
exists(k))
return {};
709 return makeError(data.error().code, std::format(
"{} split file: {}", suffix, data.error().message));
710 return parseFile(*data, fk);
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)};
720 if (
const auto data = fs.
readFile(k)) into = *data;
722 keep(
"_obj1", this->obj1Data);
723 keep(
"_lod", this->lodData);
731 template <ClientVersion V>
733 const FileKey resolved = fs.
resolve(key);
738 const auto add = [&](std::string_view path, std::span<const std::byte> bytes) ->
Result<void> {
739 return fs.
addFile(path, bytes).transform([](
auto&&) {});
744 return add(*resolved.
path, data);
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);
754 return writeFile(fk, alpha).and_then([&](
FileBuffer data) {
return add(sibling(suffix), data); });
758 return add(*resolved.
path, data);
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;
ADT version grids, layout pivots and the alpha-format context.
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.
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.
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.
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).
constexpr welder::lang Cs
C#/.NET — the welder-csharp rod's identity (user-range slot 0), respelled for wowlib's annotation sit...
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.
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
@ 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.
StringBlock — the decoded representation of a chunk of zero-terminated strings (MOTX,...
std::optional< std::string > path
The canonical client-internal path, if known.
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...