29#include <welder/vocabulary.hpp>
37 using namespace wowlib::formats::adt::chunks;
46 One liquid layer over a terrain cell (an MH2O instance), decoded. The
47 heightmap/depthmap/uvmap arrays are present according to vertex_format and
48 each hold (width + 1) x (height + 1) entries; exists_bitmap (when not
49 empty) is a width x height bit grid selecting which tiles render. Heights
50 outside [min_height, max_height] and the binary offsets are derived, not
54 "The liquid type (a LiquidType foreign key: 1 ocean, 2 ocean-flat, "
55 "3 slime, 5/6 magma, …).")]]
59 "The stored liquid_object_or_lvf field: a LiquidObject id (>= 42) or "
60 "a raw LiquidVertexFormat; vertex_format holds the resolved layout.")]]
64 "The resolved vertex layout wowlib decoded (and will re-lay).")]]
68 "Minimum surface height (the flat height when no heightmap).")]]
71 [[=welder::doc(
"Maximum surface height.")]]
74 [[=welder::doc(
"The liquid rectangle's x offset within the cell (0-7).")]]
76 [[=welder::doc(
"The liquid rectangle's y offset within the cell (0-7).")]]
78 [[=welder::doc(
"The liquid rectangle width in tiles (1-8).")]]
79 std::uint8_t
width = 8;
80 [[=welder::doc(
"The liquid rectangle height in tiles (1-8).")]]
84 "The per-tile exists mask, width x height bits row-major (LSB first); "
85 "empty means every tile renders."),
86 =welder::mark::no_reassign]]
90 "Surface heights, (width+1) x (height+1) row-major; present for "
91 "vertex formats height_depth, height_uv, height_uv_depth."),
92 =welder::mark::no_reassign]]
93 std::vector<float> heightmap;
95 [[=welder::doc(
"Depth bytes, (width+1) x (height+1) row-major; present for "
96 "height_depth, depth_only, height_uv_depth."),
97 =welder::mark::no_reassign]]
98 std::vector<std::uint8_t> depthmap;
101 "UV texture coordinates, (width+1) x (height+1) row-major; present "
102 "for height_uv and height_uv_depth."),
103 =welder::mark::no_reassign]]
104 std::vector<UVMapEntry> uvmap;
109 std::size_t vertexCount()
const {
110 return static_cast<std::size_t
>(width + 1) *
static_cast<std::size_t
>(height + 1);
118 The liquid of one terrain cell (an MH2O chunk entry): the 8x8 fishable and
119 deep attribute bit masks and the stacked liquid layers (instances). A cell
120 with no liquid has no instances.)")
123 "The 8x8 fishable bit mask (visibility); 0 when the cell omits "
125 std::uint64_t fishable = 0;
128 "The 8x8 deep bit mask (fatigue water); 0 when the cell omits "
130 std::uint64_t deep = 0;
133 "Whether this cell wrote an explicit attributes block (so an all-zero "
134 "mask round-trips as present rather than omitted).")]]
135 bool hasAttributes =
false;
137 [[=welder::doc(
"The stacked liquid layers over this chunk."),
138 =welder::mark::no_reassign]]
139 std::vector<LiquidInstance> instances;
145 [[=welder::getter, =welder::doc(
"Whether the chunk carries any liquid.")]]
146 bool empty()
const {
return instances.empty(); }
154 The tile's liquid (MH2O, WotLK+): one MapChunkLiquid per terrain cell, in
155 the 16x16 row-major cell order. Decoded from the chunk's offset structure
156 and re-laid on write (the binary offsets are derived). Index it by
157 cell = y * 16 + x.)")
159 [[=welder::doc("Liquid for each of the 256 terrain chunks (y * 16 + x)."),
160 =welder::mark::no_reassign]]
161 std::vector<MapChunkLiquid> cells;
166 [[=welder::mark::exclude]]
173 [[=welder::mark::exclude]]
177 [[=welder::getter, =welder::doc(
"Whether the tile has no liquid at all.")]]
184 bool operator==(
const MH2OData&)
const =
default;
193 A terrain chunk's legacy liquid (MCLQ, up to WotLK, deprecated by MH2O): a
194 9x9 grid of liquid vertices (SLVert; read as magma/slime via as_magma()),
195 an 8x8 grid of tile flag bytes and the active flow vectors. The height
196 range and the flow-vector count are stored; the trailing pair of flow
197 vectors is always written.)")
199 [[=welder::doc("The surface height range (CRange: min, max).")]]
202 [[=welder::doc(
"The 9x9 = 81 liquid vertices, row-major."),
203 =welder::mark::no_reassign]]
204 std::vector<SLVert> vertices;
207 "The 8x8 = 64 tile flag bytes (bits: liquid type, don't-render, "
209 =welder::mark::no_reassign]]
210 std::vector<std::uint8_t> tiles;
212 [[=welder::doc(
"The active flow vectors (0-2)."),
213 =welder::mark::no_reassign]]
214 std::vector<SWFlowv> flows;
220 [[=welder::mark::exclude]]
227 [[=welder::mark::exclude]]
232 [[=welder::getter, =welder::doc(
233 "Whether the chunk carries no legacy liquid.")]]
234 bool empty()
const {
return vertices.empty(); }
247 if (n != 0 && totalBytes % n == 0)
248 switch (totalBytes / n) {
263 constexpr std::size_t headerBytes = 256 * 12;
264 if (payload.size() < headerBytes)
266 std::format(
"MH2O needs {} header bytes, got {}", headerBytes, payload.size()));
269 const auto u32At = [&](std::size_t off) {
271 std::memcpy(&v, payload.data() + off, 4);
277 std::vector<std::size_t> starts;
278 for (std::size_t c = 0; c < 256; ++c) {
279 const std::uint32_t ofsInst = u32At(c * 12 + 0);
280 const std::uint32_t count = u32At(c * 12 + 4);
281 const std::uint32_t ofsAttr = u32At(c * 12 + 8);
282 if (ofsAttr) starts.push_back(ofsAttr);
283 for (std::uint32_t k = 0; k < count; ++k) {
284 const std::size_t base = ofsInst + 24 * k;
285 if (base + 24 > payload.size())
287 const std::uint32_t ofsExists = u32At(base + 16);
288 const std::uint32_t ofsVerts = u32At(base + 20);
289 if (ofsExists) starts.push_back(ofsExists);
290 if (ofsVerts) starts.push_back(ofsVerts);
293 std::ranges::sort(starts);
294 const auto blockEnd = [&](std::size_t start) -> std::size_t {
295 std::size_t end = payload.size();
296 for (std::size_t s : starts)
297 if (s > start && s < end) end = s;
301 for (std::size_t c = 0; c < 256; ++c) {
302 const std::uint32_t ofsInst = u32At(c * 12 + 0);
303 const std::uint32_t count = u32At(c * 12 + 4);
304 const std::uint32_t ofsAttr = u32At(c * 12 + 8);
305 MapChunkLiquid& cell =
cells[c];
307 if (ofsAttr + 16 > payload.size())
309 std::memcpy(&cell.
fishable, payload.data() + ofsAttr, 8);
310 std::memcpy(&cell.
deep, payload.data() + ofsAttr + 8, 8);
313 for (std::uint32_t k = 0; k < count; ++k) {
314 const std::size_t base = ofsInst + 24 * k;
316 std::memcpy(&inst.
liquidType, payload.data() + base + 0, 2);
318 std::memcpy(&inst.
minHeight, payload.data() + base + 4, 4);
319 std::memcpy(&inst.
maxHeight, payload.data() + base + 8, 4);
320 inst.
xOffset = std::to_integer<std::uint8_t>(payload[base + 12]);
321 inst.
yOffset = std::to_integer<std::uint8_t>(payload[base + 13]);
322 inst.
width = std::to_integer<std::uint8_t>(payload[base + 14]);
323 inst.
height = std::to_integer<std::uint8_t>(payload[base + 15]);
324 const std::uint32_t ofsExists = u32At(base + 16);
325 const std::uint32_t ofsVerts = u32At(base + 20);
329 const std::size_t bytes = (
static_cast<std::size_t
>(inst.
width) * inst.
height + 7) / 8;
330 if (ofsExists + bytes > payload.size())
332 inst.
existsBitmap.assign(
reinterpret_cast<const std::uint8_t*
>(payload.data() + ofsExists),
333 reinterpret_cast<const std::uint8_t*
>(payload.data() + ofsExists + bytes));
337 const std::size_t total = blockEnd(ofsVerts) - ofsVerts;
339 std::size_t p = ofsVerts;
348 std::memcpy(inst.
heightmap.data(), payload.data() + p, n * 4);
352 inst.
uvmap.resize(n);
353 std::memcpy(inst.
uvmap.data(), payload.data() + p, n * 4);
357 inst.
depthmap.assign(
reinterpret_cast<const std::uint8_t*
>(payload.data() + p),
358 reinterpret_cast<const std::uint8_t*
>(payload.data() + p + n));
365 cell.
instances.push_back(std::move(inst));
373 const std::size_t start = out.size();
374 const std::size_t nCells =
cells.size() == 256 ? 256 : 256;
375 out.resize(out.size() + 256 * 12, std::byte{0});
377 const auto putU32 = [&](std::uint32_t v) {
378 const auto* b =
reinterpret_cast<const std::byte*
>(&v);
379 out.insert(out.end(), b, b + 4);
381 const auto setU32 = [&](std::size_t at, std::uint32_t v) {
382 std::memcpy(out.data() + at, &v, 4);
384 const auto rel = [&](std::size_t abs) {
385 return static_cast<std::uint32_t
>(abs - start);
390 std::size_t headerAt;
391 std::size_t existsAt;
394 std::vector<std::vector<InstLoc>> locs(256);
395 for (std::size_t c = 0; c < nCells; ++c) {
398 setU32(start + c * 12 + 0, rel(out.size()));
399 setU32(start + c * 12 + 4,
static_cast<std::uint32_t
>(cell.
instances.size()));
401 for (std::size_t k = 0; k < cell.
instances.size(); ++k) {
403 locs[c][k].headerAt = out.size();
404 putU32(inst.
liquidType | (std::uint32_t{inst.liquidObjectOrLvf} << 16));
405 putU32(std::bit_cast<std::uint32_t>(inst.
minHeight));
406 putU32(std::bit_cast<std::uint32_t>(inst.
maxHeight));
407 out.push_back(std::byte{inst.
xOffset});
408 out.push_back(std::byte{inst.
yOffset});
409 out.push_back(std::byte{inst.
width});
410 out.push_back(std::byte{inst.
height});
417 for (std::size_t c = 0; c < nCells; ++c) {
420 setU32(start + c * 12 + 8, rel(out.size()));
421 putU32(
static_cast<std::uint32_t
>(cell.
fishable & 0xFFFFFFFF));
422 putU32(
static_cast<std::uint32_t
>(cell.
fishable >> 32));
423 putU32(
static_cast<std::uint32_t
>(cell.
deep & 0xFFFFFFFF));
424 putU32(
static_cast<std::uint32_t
>(cell.
deep >> 32));
426 for (std::size_t k = 0; k < cell.
instances.size(); ++k) {
428 const InstLoc& loc = locs[c][k];
430 setU32(loc.headerAt + 16, rel(out.size()));
431 const auto* b =
reinterpret_cast<const std::byte*
>(inst.
existsBitmap.data());
436 setU32(loc.headerAt + 20, rel(out.size()));
438 const auto* b =
reinterpret_cast<const std::byte*
>(inst.
heightmap.data());
439 out.insert(out.end(), b, b + inst.
heightmap.size() * 4);
441 if (!inst.
uvmap.empty()) {
442 const auto* b =
reinterpret_cast<const std::byte*
>(inst.
uvmap.data());
443 out.insert(out.end(), b, b + inst.
uvmap.size() * 4);
446 const auto* b =
reinterpret_cast<const std::byte*
>(inst.
depthmap.data());
447 out.insert(out.end(), b, b + inst.
depthmap.size());
460 constexpr std::size_t fixed = 8 + 81 * 8 + 64 + 4;
463 if (payload.size() < fixed)
return {};
466 std::memcpy(
vertices.data(), payload.data() + 8, 81 * 8);
467 tiles.assign(
reinterpret_cast<const std::uint8_t*
>(payload.data() + 8 + 648),
468 reinterpret_cast<const std::uint8_t*
>(payload.data() + 8 + 648 + 64));
469 std::uint32_t nFlows = 0;
470 std::memcpy(&nFlows, payload.data() + 8 + 648 + 64, 4);
471 std::size_t p = fixed;
473 for (std::uint32_t i = 0; i < 2 && p + 40 <= payload.size(); ++i, p += 40) {
476 std::memcpy(&f, payload.data() + p, 40);
484 const auto put = [&](
const void* p, std::size_t n) {
485 const auto* b =
static_cast<const std::byte*
>(p);
486 out.insert(out.end(), b, b + n);
492 put(v.data(), 81 * 8);
493 std::vector<std::uint8_t> t =
tiles;
496 const std::uint32_t nFlows =
static_cast<std::uint32_t
>(
flows.size());
499 for (std::size_t i = 0; i <
flows.size() && i < 2; ++i) pair[i] =
flows[i];
ADT liquid binary pieces (namespace wowlib::formats::adt::chunks): the small trivially-copyable recor...
The owning byte buffer file contents are read into.
The error-handling vocabulary: ErrorCode, Error and the Result<T> alias every fallible wowlib operati...
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
@ ChunkTruncated
A chunk header or payload overruns the file buffer.
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
The binary-level math and color primitives shared across WoW file formats (wowdev....