46 using namespace wowlib::formats::wdl::chunks;
59 =welder::weld_as(
"WDL"),
62 A map's low-resolution heightmap file, abstract over the client
63 version — the background mountain silhouettes. Construct the concrete
64 version with WDL.for_version(expansion), then read()/write(); the
65 per-version WDL* classes are subclasses. See
66 https://wowdev.wiki/WDL.)")
82 R
"(Per-tile occlusion mesh vertices (MAOC, pre-Legion; optional
83 and absent from every surveyed file), one record per
84 occurrence, kept opaque. Follows its tile's heightmap in the
96 =welder::mark::no_reassign,
98 R
"(Per-tile hole masks (MAHO, TBC+): the i-th mask belongs to
99 the i-th heightmap. Blizzard writes one per tile even when
100 all zero; hole masks are all-or-nothing — leave the list
101 empty or give every heightmap its mask. (wowdev.wiki dates
102 MAHO to WotLK, but vanilla WDLs carry none and every 2.4.3
103 WDL pairs one MAHO per MARE — so it debuts in TBC.))")]]
104 std::vector<TileHoles> holes;
113 =welder::mark::no_reassign,
115 R
"(Low-resolution M2 placements (MLDD, Legion+), drawn instead
116 of the far-away real models; name_id is always a
117 FileDataID here.)")]]
124 =welder::mark::no_reassign,
126 R
"(Visibility extents for the M2 placements (MLDX, Legion+);
127 same count and order as lod_doodads.)")]]
134 =welder::mark::no_reassign,
135 =welder::doc(R
"(Low-resolution WMO placements (MLMD, Legion+); sorted by
136 their extent radius, largest first, in shipped files.)")
144 =welder::mark::no_reassign,
146 R
"(Visibility extents for the WMO placements (MLMX, Legion+);
147 same count and order as lod_map_objects.)")]]
155 =welder::mark::no_reassign,
157 R
"(Sparse per-tile ocean masks (MAOE, Legion+), emitted between
158 a tile's heightmap and its hole mask — only SOME tiles have
159 one, so use ocean_mask_tiles() for the mask -> heightmap
170 =welder::mark::no_reassign,
171 =welder::doc(R
"(Fade-distance ranges for the M2 placements (MLDF, BfA);
172 same count and order as lod_doodads. Undocumented on
173 wowdev; the layout and the BfA (not Shadowlands) debut
174 are survey findings across every 8.3.7 WDL carrying the
182 =welder::mark::no_reassign,
183 =welder::doc(R
"(One byte per WMO placement (MLMB, BfA; same count and
184 order as lod_map_objects — the ADT twin pairs with
185 MODF in _obj0 and MLMD in _obj1). Semantics unknown;
186 the 8.3.7 fleet survey (1300+ instances) shows an
187 enum-like value set (0x19/0x20/0x26/0x33/0x40/0x46/
188 0x80) that clusters per map, varies per instance of
189 the same asset, and does not correlate with the
190 placement radius; 0x80 co-occurs with other values on
191 the same asset like an override state.)")]]
192 std::vector<std::uint8_t> mlmb;
201 =welder::mark::no_reassign,
203 R
"(MLDL (9.x+): per-lod_doodads values, engaged by placement
204 flag 0x8 (as the ADT chunk of the same name).)")]]
205 std::vector<std::uint32_t> mldl;
211 =welder::doc(
"MLDB (9.x+, undocumented); preserved opaque.")]]
218 =welder::mark::no_reassign,
219 =welder::doc(
"Sky scenes (MSSN, Shadowlands+).")]]
226 =welder::mark::no_reassign,
227 =welder::doc(
"Sky-scene conditions (MSSC, Shadowlands+), ranged by the "
228 "scenes' condition_index/count.")]]
235 =welder::mark::no_reassign,
237 "Sky-scene objects (MSSO, Shadowlands+), ranged by the scenes' "
238 "object_index/count.")]]
245 =welder::mark::no_reassign,
247 R
"(Sky-scene object params (MSSF; wowdev dates it Dragonflight+
248 but 9.2.7 files carry it), referenced by the objects'
259 =welder::mark::no_reassign,
260 =welder::doc(
"Scene-living definitions (MSLD, The War Within+).")]]
267 =welder::mark::no_reassign,
269 "Scene-living indices (MSLI, The War Within+): one MSLD index "
270 "per sky-scene object.")]]
288 template <ClientVersion V>
292 A map's low-resolution heightmap file for one client version: the
293 64x64 tile offset table, one 17x17+16x16 int16 heightmap (and hole
294 mask) per present tile, the low-resolution object placements of the
295 era, and the Shadowlands+ sky scenes. Tile chunks pair by ordinal —
296 the i-th heightmap belongs to the i-th nonzero tileOffsets slot
297 (row-major); the offsets themselves are recomputed on every write,
298 so only the nonzero pattern is authored data. An instance read from
299 a client file rewrites byte-for-byte until modified. See
300 https://wowdev.wiki/WDL.)")
314 =welder::doc(
"The WDL format version; 18 for every supported client.")]]
321 R
"(WMO silhouette filenames (MWMO): zero-terminated strings,
322 referenced by offset. Every pre-Legion file carries the
323 chunk (often empty); Legion+ terrain maps replace the
324 object set with the MLDD/MLMD placements, but WMO-only
325 maps keep shipping it.)")]]
331 =welder::mark::no_reassign,
332 =welder::doc(
"MWMO filename start offsets (MWID), one per name.")]]
338 =welder::mark::no_reassign,
340 "WMO silhouette placements (MODF), one per MWMO name; the same "
341 "64-byte record the WDT and ADT use.")]]
346 =welder::mark::no_reassign,
348 R
"(The tile offset table (MAOF): 64 x 64 absolute file offsets
349 in row-major order (y outer, x inner), 0 for absent tiles.
350 The offset VALUES are derived — every write restamps them
351 from the finished layout — so only the nonzero pattern is
352 authored: the i-th nonzero slot owns the i-th heightmap.)")
360 =welder::mark::no_reassign,
362 R
"(The per-tile heightmaps (MARE), one per nonzero tileOffsets
363 slot, in row-major slot order.)")]]
401 R
"(The heightmap ordinal each ocean mask belongs to (parallel
402 to oceanMasks), derived from the read file's chunk
403 interleave. Empty when there are no ocean masks.)"),
404 =welder::returns("one heightmap ordinal per ocean mask")]]
405 std::vector<std::uint32_t> oceanMaskTiles()
const;
414 [[=welder::mark::exclude]]
425 [[=welder::mark::exclude]]
427 resequencedJournal()
const;
434 [[=welder::mark::exclude]]
435 Result<void> patchFile(std::span<std::byte> image)
const;
438 =welder::doc(
"Load the WDL from a client filesystem, replacing this "
439 "entity's contents.")]]
441 const FileKey& key [[=welder::doc(
"the file identity (path and/or FileDataID)")]]);
445 "Serialize and store the WDL through the filesystem's project "
448 const FileKey& key [[=welder::doc(
"the file identity; must resolve to a path")]])
const;
461 template <ClientVersion V>
470 template <ClientVersion V>
472 std::vector<std::uint32_t> out;
473 if constexpr (
requires { this->oceanMasks; }) {
476 const std::size_t n = this->oceanMasks.size();
477 if (n == 0)
return out;
479 std::size_t maresSeen = 0;
481 if (entry.member == mareIdx) ++maresSeen;
482 else if (entry.member == maoeIdx && out.size() < n)
483 out.push_back(maresSeen == 0 ? 0 :
static_cast<std::uint32_t
>(maresSeen - 1));
485 if (out.size() != n) {
488 const std::size_t last = heightmaps.empty() ? 0 : heightmaps.size() - 1;
489 for (std::size_t i = 0; i < n; ++i) out.push_back(
static_cast<std::uint32_t
>(std::min(i, last)));
495 template <ClientVersion V>
497 const std::size_t nTiles = heightmaps.size();
498 std::size_t engagedSlots = 0;
499 for (
const std::uint32_t offset :
tileOffsets) engagedSlots += (offset != 0);
505 "the MAOF table holds {} offsets, not 64*64 — resize " "tileOffsets to {} (0 = tile absent)",
507 if (engagedSlots != nTiles)
510 "{} nonzero tileOffsets slots but {} heightmaps — the "
511 "i-th nonzero slot owns the i-th heightmap, so the " "counts must match", engagedSlots,
514 if constexpr (
requires { this->holes; })
515 if (!this->holes.empty() && this->holes.size() != nTiles)
516 report.
addError(
"holes", std::format(
517 "{} hole masks but {} heightmaps — hole masks are "
518 "all-or-nothing (empty, or one per heightmap)", this->holes.size(), nTiles));
519 if constexpr (
requires { this->oceanMasks; })
520 if (this->oceanMasks.size() > nTiles)
521 report.
addError(
"oceanMasks", std::format(
522 "{} ocean masks but only {} heightmaps", this->oceanMasks.size(), nTiles));
523 if constexpr (
requires { this->occlusionMeshes; })
524 if (this->occlusionMeshes.size() > nTiles)
526 std::format(
"{} occlusion meshes but only {} heightmaps", this->occlusionMeshes.size(),
530 template <ClientVersion V>
538 const auto journalCount = [&](std::int32_t index) -> std::size_t {
539 if (index < 0)
return 0;
541 for (
const JournalEntry& entry : this->journal) n += (entry.member == index);
545 const std::size_t nTiles = heightmaps.size();
546 std::size_t nHoles = 0;
547 std::size_t nOcean = 0;
548 std::size_t nOcclusion = 0;
549 if constexpr (
requires { this->holes; }) nHoles = this->holes.size();
550 if constexpr (
requires { this->oceanMasks; }) nOcean = this->oceanMasks.size();
551 if constexpr (
requires { this->occlusionMeshes; }) nOcclusion = this->occlusionMeshes.size();
554 if (!this->journal.empty() && journalCount(mareIdx) == nTiles && journalCount(mahoIdx) == nHoles &&
555 journalCount(maoeIdx) == nOcean && journalCount(maocIdx) == nOcclusion)
return std::optional<std::vector<
561 ValidationReport report;
562 validateExtra(report);
563 if (
auto r = report.toResult(); !r)
return std::unexpected{r.error()};
569 const auto attach = [&](std::int32_t index, std::size_t count) {
570 std::vector<std::vector<std::uint32_t>> per(nTiles);
571 if (nTiles == 0 || count == 0)
return per;
572 if (journalCount(index) == count) {
573 std::size_t maresSeen = 0;
575 for (
const JournalEntry& entry : this->journal) {
576 if (entry.member == mareIdx) ++maresSeen;
577 else if (entry.member == index) {
578 if (maresSeen == 0 || entry.occurrence >= count) {
582 per[std::min(maresSeen - 1, nTiles - 1)].push_back(entry.occurrence);
585 if (paired)
return per;
586 per.assign(nTiles, {});
588 for (std::size_t i = 0; i < count; ++i) per[std::min(i, nTiles - 1)].push_back(
static_cast<std::uint32_t
>(i));
591 const auto occlusionPerTile = attach(maocIdx, nOcclusion);
592 const auto oceanPerTile = attach(maoeIdx, nOcean);
596 std::erase_if(out, [&](
const JournalEntry& entry) {
597 return entry.
member == mareIdx || (mahoIdx >= 0 && entry.
member == mahoIdx) || (maoeIdx >= 0 && entry.
member
598 == maoeIdx) || (maocIdx >= 0 && entry.
member == maocIdx);
602 std::size_t insertAt = out.size();
603 for (std::size_t i = 0; i < out.size(); ++i)
604 if (out[i].member == maofIdx) {
608 std::vector<JournalEntry> block;
609 for (std::size_t tile = 0; tile < nTiles; ++tile) {
610 block.push_back({
fourcc(
"MARE"), mareIdx,
static_cast<std::uint32_t
>(tile)});
611 for (
const std::uint32_t occurrence : occlusionPerTile[tile]) block.push_back({
616 for (
const std::uint32_t occurrence : oceanPerTile[tile]) block.push_back({
622 block.push_back({
fourcc(
"MAHO"), mahoIdx,
static_cast<std::uint32_t
>(tile)});
624 out.insert(out.begin() +
static_cast<std::ptrdiff_t
>(insertAt), block.begin(), block.end());
627 for (
const JournalEntry& entry : this->journal)
628 if (entry.
member < 0) out.push_back(entry);
629 return std::optional{std::move(out)};
632 template <ClientVersion V>
634 constexpr std::uint32_t maofCc =
fourcc(
"MAOF");
635 constexpr std::uint32_t mareCc =
fourcc(
"MARE");
637 std::size_t maofPayload = image.size();
638 std::uint32_t maofSize = 0;
639 std::vector<std::uint32_t> mareOffsets;
641 while (image.size() - pos >= 2 *
sizeof(std::uint32_t)) {
643 std::uint32_t size = 0;
644 std::memcpy(&
fourcc, image.data() + pos,
sizeof fourcc);
645 std::memcpy(&size, image.data() + pos +
sizeof fourcc,
sizeof size);
646 if (size > image.size() - pos - 2 *
sizeof(std::uint32_t))
break;
647 if (
fourcc == maofCc && maofPayload == image.size()) {
648 maofPayload = pos + 2 *
sizeof(std::uint32_t);
651 else if (
fourcc == mareCc) mareOffsets.push_back(
static_cast<std::uint32_t
>(pos));
652 pos += 2 *
sizeof(std::uint32_t) + size;
655 if (mareOffsets.empty() && maofSize == 0)
return {};
656 if (maofPayload == image.size())
661 "the written MAOF table holds {} bytes, not 64*64 " "offsets — resize tileOffsets to {}",
664 std::size_t next = 0;
665 for (std::size_t slot = 0; slot <
WdlTileSlots; ++slot) {
666 std::uint32_t value = 0;
667 std::byte* at = image.data() + maofPayload + slot *
sizeof value;
668 std::memcpy(&value, at,
sizeof value);
669 if (value == 0)
continue;
670 if (next >= mareOffsets.size())
672 std::format(
"more nonzero tileOffsets slots than the {} written " "heightmaps",
673 mareOffsets.size()));
674 std::memcpy(at, &mareOffsets[next++],
sizeof value);
676 if (next != mareOffsets.size())
678 std::format(
"{} written heightmaps but only {} nonzero tileOffsets " "slots",
679 mareOffsets.size(), next));
683 template <ClientVersion V>
686 if (!data)
return std::unexpected{data.error()};
691 std::format(
"WDL MVER is {}, expected {}", mver,
WdlVersion18));
695 template <ClientVersion V>
697 const FileKey resolved = fs.
resolve(key);
701 if (!data)
return std::unexpected{data.error()};
702 if (
auto r = fs.
addFile(*resolved.
path, *data); !r)
return std::unexpected{r.error()};
The chunk framework, vocabulary and engine in one header.
FileKey resolve(const FileKey &key) const
Result< FileBuffer > readFile(const FileKey &key)
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.
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 map-placement binary records SHARED across the world file formats (namespace wowlib::formats::com...
constexpr ClientVersion SL
shadowlands (any 9.x client).
constexpr ClientVersion Legion
legion (any 7.x client).
constexpr ClientVersion TWW
The War Within (any 11.x client).
constexpr ClientVersion BfA
Battle for Azeroth (any 8.x client).
constexpr ClientVersion TBC
The Burning Crusade (any 2.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.
@ InvalidEntityState
An entity's members disagree (e.g.
@ PathNotResolvable
No FileDataID is known for the given path (listfile miss).
@ FormatVersionMismatch
The file's version chunk disagrees with the requested version.
WDL object-placement chunk binary structs (namespace wowlib::formats::wdl::chunks).
WDL sky-scene chunk binary structs (namespace wowlib::formats::wdl::chunks), Shadowlands+: distant sc...
StringBlock — the decoded representation of a chunk of zero-terminated strings (MOTX,...
std::optional< std::string > path
The canonical client-internal path, if known.
One chunk encounter in file order — the write path replays the journal to reproduce the original byte...
std::int32_t member
Declaration index of the member the chunk was read into, or -1 for an unknown chunk.
WDL per-tile chunk binary structs (namespace wowlib::formats::wdl::chunks): the MARE low-resolution h...
The conditional-base mechanism that gives a versioned chunked entity exactly the fields its client ve...
WDL version grid and canonicalization pivots.