wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
liquid.hpp
Go to the documentation of this file.
1#pragma once
2
19
20#include <algorithm>
21#include <bit>
22#include <cstddef>
23#include <cstdint>
24#include <cstring>
25#include <format>
26#include <span>
27#include <vector>
28
29#include <welder/vocabulary.hpp>
30
32#include <wowlib/core/error.hpp>
35
36namespace wowlib::formats::adt {
37 using namespace wowlib::formats::adt::chunks;
38
43 struct [[
44 =welder::weld,
45 =welder::doc(R"(
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
51 stored.)")
53 [[=welder::doc(
54 "The liquid type (a LiquidType foreign key: 1 ocean, 2 ocean-flat, "
55 "3 slime, 5/6 magma, …).")]]
56 std::uint16_t liquidType = 0;
58 [[=welder::doc(
59 "The stored liquid_object_or_lvf field: a LiquidObject id (>= 42) or "
60 "a raw LiquidVertexFormat; vertex_format holds the resolved layout.")]]
61 std::uint16_t liquidObjectOrLvf = 0;
63 [[=welder::doc(
64 "The resolved vertex layout wowlib decoded (and will re-lay).")]]
67 [[=welder::doc(
68 "Minimum surface height (the flat height when no heightmap).")]]
69 float minHeight = 0;
70
71 [[=welder::doc("Maximum surface height.")]]
72 float maxHeight = 0;
73
74 [[=welder::doc("The liquid rectangle's x offset within the cell (0-7).")]]
75 std::uint8_t xOffset = 0;
76 [[=welder::doc("The liquid rectangle's y offset within the cell (0-7).")]]
77 std::uint8_t yOffset = 0;
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).")]]
81 std::uint8_t height = 8;
82
83 [[=welder::doc(
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]]
87 std::vector<std::uint8_t> existsBitmap;
88
89 [[=welder::doc(
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;
99
100 [[=welder::doc(
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;
105
106 bool operator==(const LiquidInstance&) const = default;
109 std::size_t vertexCount() const {
110 return static_cast<std::size_t>(width + 1) * static_cast<std::size_t>(height + 1);
111 }
112 };
113
115 struct [[
116 =welder::weld,
117 =welder::doc(R"(
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.)")
121 ]] MapChunkLiquid {
122 [[=welder::doc(
123 "The 8x8 fishable bit mask (visibility); 0 when the cell omits "
124 "attributes.")]]
125 std::uint64_t fishable = 0;
126
127 [[=welder::doc(
128 "The 8x8 deep bit mask (fatigue water); 0 when the cell omits "
129 "attributes.")]]
130 std::uint64_t deep = 0;
131
132 [[=welder::doc(
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;
136
137 [[=welder::doc("The stacked liquid layers over this chunk."),
138 =welder::mark::no_reassign]]
139 std::vector<LiquidInstance> instances;
140
141 bool operator==(const MapChunkLiquid&) const = default;
142
144 [[nodiscard]]
145 [[=welder::getter, =welder::doc("Whether the chunk carries any liquid.")]]
146 bool empty() const { return instances.empty(); }
147 };
148
151 struct [[
152 =welder::weld,
153 =welder::doc(R"(
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.)")
158 ]] MH2OData {
159 [[=welder::doc("Liquid for each of the 256 terrain chunks (y * 16 + x)."),
160 =welder::mark::no_reassign]]
161 std::vector<MapChunkLiquid> cells;
162
166 [[=welder::mark::exclude]]
167 Result<void> read(std::span<const std::byte> payload);
168
173 [[=welder::mark::exclude]]
174 Result<void> write(FileBuffer& out) const;
175
176 [[nodiscard]]
177 [[=welder::getter, =welder::doc("Whether the tile has no liquid at all.")]]
178 bool empty() const {
179 return std::ranges::none_of(cells, [](const MapChunkLiquid& c) {
180 return !c.instances.empty();
181 });
182 }
183
184 bool operator==(const MH2OData&) const = default;
185 };
186
190 struct [[
191 =welder::weld,
192 =welder::doc(R"(
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.)")
198 ]] MCLQData {
199 [[=welder::doc("The surface height range (CRange: min, max).")]]
200 CRange heightRange{};
201
202 [[=welder::doc("The 9x9 = 81 liquid vertices, row-major."),
203 =welder::mark::no_reassign]]
204 std::vector<SLVert> vertices;
205
206 [[=welder::doc(
207 "The 8x8 = 64 tile flag bytes (bits: liquid type, don't-render, "
208 "fatigue)."),
209 =welder::mark::no_reassign]]
210 std::vector<std::uint8_t> tiles;
211
212 [[=welder::doc("The active flow vectors (0-2)."),
213 =welder::mark::no_reassign]]
214 std::vector<SWFlowv> flows;
215
220 [[=welder::mark::exclude]]
221 Result<void> read(std::span<const std::byte> payload);
222
227 [[=welder::mark::exclude]]
228 Result<void> write(FileBuffer& out) const;
229
231 [[nodiscard]]
232 [[=welder::getter, =welder::doc(
233 "Whether the chunk carries no legacy liquid.")]]
234 bool empty() const { return vertices.empty(); }
235
236 bool operator==(const MCLQData&) const = default;
237 };
238
239 // --- MH2OData ---------------------------------------------------------------
240
241 namespace detail {
246 inline LiquidVertexFormat resolveVertexFormat(std::uint16_t stored, std::size_t totalBytes, std::size_t n) {
247 if (n != 0 && totalBytes % n == 0)
248 switch (totalBytes / n) {
249 case 5: return LiquidVertexFormat::HeightDepth; // float + byte
250 case 8: return LiquidVertexFormat::HeightUv; // float + 2 u16
251 case 1: return LiquidVertexFormat::DepthOnly; // byte
253 // float + 2 u16 + byte
254 default: break;
255 }
256 // no vertex block (ocean): trust the stored LVF when small, else flat depth
257 if (stored <= 3) return static_cast<LiquidVertexFormat>(stored);
259 }
260 }
261
262 inline Result<void> MH2OData::read(std::span<const std::byte> payload) {
263 constexpr std::size_t headerBytes = 256 * 12;
264 if (payload.size() < headerBytes)
266 std::format("MH2O needs {} header bytes, got {}", headerBytes, payload.size()));
267 cells.assign(256, MapChunkLiquid{});
268
269 const auto u32At = [&](std::size_t off) {
270 std::uint32_t v = 0;
271 std::memcpy(&v, payload.data() + off, 4);
272 return v;
273 };
274
275 // Collect every data-block start offset first, so a block's size is the gap
276 // to the next start (the vertex-format multiplier method needs the size).
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())
286 return makeError(ErrorCode::ChunkTruncated, std::format("MH2O instance of cell {} overruns the chunk", c));
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);
291 }
292 }
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;
298 return end;
299 };
300
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];
306 if (ofsAttr) {
307 if (ofsAttr + 16 > payload.size())
308 return makeError(ErrorCode::ChunkTruncated, std::format("MH2O attributes of cell {} overrun", c));
309 std::memcpy(&cell.fishable, payload.data() + ofsAttr, 8);
310 std::memcpy(&cell.deep, payload.data() + ofsAttr + 8, 8);
311 cell.hasAttributes = true;
312 }
313 for (std::uint32_t k = 0; k < count; ++k) {
314 const std::size_t base = ofsInst + 24 * k;
315 LiquidInstance inst;
316 std::memcpy(&inst.liquidType, payload.data() + base + 0, 2);
317 std::memcpy(&inst.liquidObjectOrLvf, payload.data() + base + 2, 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);
326 const std::size_t n = inst.vertexCount();
327
328 if (ofsExists) {
329 const std::size_t bytes = (static_cast<std::size_t>(inst.width) * inst.height + 7) / 8;
330 if (ofsExists + bytes > payload.size())
331 return makeError(ErrorCode::ChunkTruncated, std::format("MH2O exists mask of cell {} overruns", c));
332 inst.existsBitmap.assign(reinterpret_cast<const std::uint8_t*>(payload.data() + ofsExists),
333 reinterpret_cast<const std::uint8_t*>(payload.data() + ofsExists + bytes));
334 }
335
336 if (ofsVerts) {
337 const std::size_t total = blockEnd(ofsVerts) - ofsVerts;
339 std::size_t p = ofsVerts;
340 const auto hasHeight = inst.vertexFormat == LiquidVertexFormat::HeightDepth || inst.vertexFormat ==
342 const auto hasUv = inst.vertexFormat == LiquidVertexFormat::HeightUv || inst.vertexFormat ==
344 const auto hasDepth = inst.vertexFormat == LiquidVertexFormat::HeightDepth || inst.vertexFormat ==
346 if (hasHeight) {
347 inst.heightmap.resize(n);
348 std::memcpy(inst.heightmap.data(), payload.data() + p, n * 4);
349 p += n * 4;
350 }
351 if (hasUv) {
352 inst.uvmap.resize(n);
353 std::memcpy(inst.uvmap.data(), payload.data() + p, n * 4);
354 p += n * 4;
355 }
356 if (hasDepth) {
357 inst.depthmap.assign(reinterpret_cast<const std::uint8_t*>(payload.data() + p),
358 reinterpret_cast<const std::uint8_t*>(payload.data() + p + n));
359 p += n;
360 }
361 }
362 else {
364 }
365 cell.instances.push_back(std::move(inst));
366 }
367 }
368 return {};
369 }
370
371 inline Result<void> MH2OData::write(FileBuffer& out) const {
372 // Canonical relayout: [256 headers][instances per cell][per-cell data blocks].
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});
376
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);
380 };
381 const auto setU32 = [&](std::size_t at, std::uint32_t v) {
382 std::memcpy(out.data() + at, &v, 4);
383 };
384 const auto rel = [&](std::size_t abs) {
385 return static_cast<std::uint32_t>(abs - start);
386 };
387
388 // instances first (so their offsets are known before data blocks)
389 struct InstLoc {
390 std::size_t headerAt;
391 std::size_t existsAt;
392 std::size_t vertsAt;
393 };
394 std::vector<std::vector<InstLoc>> locs(256);
395 for (std::size_t c = 0; c < nCells; ++c) {
396 const MapChunkLiquid& cell = c < cells.size() ? cells[c] : MapChunkLiquid{};
397 if (cell.instances.empty()) continue;
398 setU32(start + c * 12 + 0, rel(out.size()));
399 setU32(start + c * 12 + 4, static_cast<std::uint32_t>(cell.instances.size()));
400 locs[c].resize(cell.instances.size());
401 for (std::size_t k = 0; k < cell.instances.size(); ++k) {
402 const LiquidInstance& inst = cell.instances[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});
411 putU32(0); // offset_exists placeholder
412 putU32(0); // offset_vertex placeholder
413 }
414 }
415
416 // per-cell data: attributes, then each instance's exists mask + vertex data
417 for (std::size_t c = 0; c < nCells; ++c) {
418 const MapChunkLiquid& cell = c < cells.size() ? cells[c] : MapChunkLiquid{};
419 if (cell.hasAttributes) {
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));
425 }
426 for (std::size_t k = 0; k < cell.instances.size(); ++k) {
427 const LiquidInstance& inst = cell.instances[k];
428 const InstLoc& loc = locs[c][k];
429 if (!inst.existsBitmap.empty()) {
430 setU32(loc.headerAt + 16, rel(out.size()));
431 const auto* b = reinterpret_cast<const std::byte*>(inst.existsBitmap.data());
432 out.insert(out.end(), b, b + inst.existsBitmap.size());
433 }
434 const bool any_verts = !inst.heightmap.empty() || !inst.depthmap.empty() || !inst.uvmap.empty();
435 if (any_verts) {
436 setU32(loc.headerAt + 20, rel(out.size()));
437 if (!inst.heightmap.empty()) {
438 const auto* b = reinterpret_cast<const std::byte*>(inst.heightmap.data());
439 out.insert(out.end(), b, b + inst.heightmap.size() * 4);
440 }
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);
444 }
445 if (!inst.depthmap.empty()) {
446 const auto* b = reinterpret_cast<const std::byte*>(inst.depthmap.data());
447 out.insert(out.end(), b, b + inst.depthmap.size());
448 }
449 }
450 }
451 }
452 return {};
453 }
454
455 // --- MCLQData ---------------------------------------------------------------
456
457 inline Result<void> MCLQData::read(std::span<const std::byte> payload) {
458 // CRange height (8) + 81 verts (8 each = 648) + 64 tile bytes + u32 nFlowvs
459 // + 2 flow vectors (40 each). The trailing flow pair is always present.
460 constexpr std::size_t fixed = 8 + 81 * 8 + 64 + 4;
461 // An "empty" MCLQ (the MCNK header's sizeLiquid was <= 8, so the sub-chunk
462 // carries no record) decodes to no liquid — Outland WotLK tiles ship these.
463 if (payload.size() < fixed) return {};
464 std::memcpy(&heightRange, payload.data(), 8);
465 vertices.resize(81);
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;
472 flows.clear();
473 for (std::uint32_t i = 0; i < 2 && p + 40 <= payload.size(); ++i, p += 40) {
474 if (i < nFlows) {
475 SWFlowv f;
476 std::memcpy(&f, payload.data() + p, 40);
477 flows.push_back(f);
478 }
479 }
480 return {};
481 }
482
483 inline Result<void> MCLQData::write(FileBuffer& out) const {
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);
487 };
488 put(&heightRange, 8);
489 // exactly 81 verts / 64 tiles are written; pad or truncate to the fixed grid
490 std::vector<SLVert> v = vertices;
491 v.resize(81);
492 put(v.data(), 81 * 8);
493 std::vector<std::uint8_t> t = tiles;
494 t.resize(64);
495 put(t.data(), 64);
496 const std::uint32_t nFlows = static_cast<std::uint32_t>(flows.size());
497 put(&nFlows, 4);
498 SWFlowv pair[2]{};
499 for (std::size_t i = 0; i < flows.size() && i < 2; ++i) pair[i] = flows[i];
500 put(pair, 80);
501 return {};
502 }
503}
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...
@ HeightUv
Case 1: a float height map and a UV texture-coordinate map.
Definition liquid.hpp:21
@ HeightUvDepth
Case 3: a float height map, a UV map and a byte depth map.
Definition liquid.hpp:23
@ HeightDepth
Case 0: a float height map and a byte depth map (the pre-WoD go-to).
Definition liquid.hpp:20
@ DepthOnly
Case 2: a byte depth map only; the surface is flat at 0.0 (ocean).
Definition liquid.hpp:22
LiquidVertexFormat resolveVertexFormat(std::uint16_t stored, std::size_t totalBytes, std::size_t n)
Resolve a liquid instance's vertex layout from its stored field and the span of its vertex-data block...
Definition liquid.hpp:181
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
One liquid layer of one terrain cell, fully decoded (an MH2O instance).
Definition liquid.hpp:43
std::vector< float > heightmap
Surface heights, (width+1) x (height+1) row-major; present for vertex formats height_depth,...
Definition liquid.hpp:72
std::uint8_t width
The liquid rectangle width in tiles (1-8).
Definition liquid.hpp:64
std::uint8_t yOffset
The liquid rectangle's y offset within the cell (0-7).
Definition liquid.hpp:62
std::vector< std::uint8_t > depthmap
Depth bytes, (width+1) x (height+1) row-major; present for height_depth, depth_only,...
Definition liquid.hpp:75
std::uint8_t height
The liquid rectangle height in tiles (1-8).
Definition liquid.hpp:66
float minHeight
Minimum surface height (the flat height when no heightmap).
Definition liquid.hpp:54
std::uint16_t liquidType
The liquid type (a LiquidType foreign key: 1 ocean, 2 ocean-flat, 3 slime, 5/6 magma,...
Definition liquid.hpp:45
std::vector< UVMapEntry > uvmap
UV texture coordinates, (width+1) x (height+1) row-major; present for height_uv and height_uv_depth.
Definition liquid.hpp:78
std::uint8_t xOffset
The liquid rectangle's x offset within the cell (0-7).
Definition liquid.hpp:60
std::uint16_t liquidObjectOrLvf
The stored liquid_object_or_lvf field: a LiquidObject id (>= 42) or a raw LiquidVertexFormat; vertex_...
Definition liquid.hpp:48
float maxHeight
Maximum surface height.
Definition liquid.hpp:57
std::size_t vertexCount() const
The vertex count each present array holds.
Definition liquid.hpp:83
LiquidVertexFormat vertexFormat
The resolved vertex layout wowlib decoded (and will re-lay).
Definition liquid.hpp:51
std::vector< std::uint8_t > existsBitmap
The per-tile exists mask, width x height bits row-major (LSB first); empty means every tile renders.
Definition liquid.hpp:69
bool operator==(const LiquidInstance &) const =default
The legacy per-chunk liquid (MCLQ, up to and including WotLK): a 9x9 vertex grid, an 8x8 tile-flag gr...
Definition liquid.hpp:141
bool empty() const
Whether the chunk carries no legacy liquid.
Definition liquid.hpp:169
std::vector< std::uint8_t > tiles
The 8x8 = 64 tile flag bytes (bits: liquid type, don't-render, fatigue).
Definition liquid.hpp:149
Result< void > read(std::span< const std::byte > payload)
Decode an MCLQ payload into the height range, 9x9 vertex grid, 8x8 tile flags and active flow vectors...
Definition liquid.hpp:392
bool operator==(const MCLQData &) const =default
std::vector< SWFlowv > flows
The active flow vectors (0-2).
Definition liquid.hpp:152
CRange heightRange
The surface height range (CRange: min, max).
Definition liquid.hpp:143
Result< void > write(FileBuffer &out) const
Re-lay the legacy liquid into a fixed-grid MCLQ payload (81 verts, 64 tiles, the two-slot flow pair),...
Definition liquid.hpp:418
std::vector< SLVert > vertices
The 9x9 = 81 liquid vertices, row-major.
Definition liquid.hpp:146
The whole-tile MH2O chunk: liquid for each of the 256 cells, decoded.
Definition liquid.hpp:112
Result< void > read(std::span< const std::byte > payload)
Decode the whole-tile MH2O offset structure into the 256 per-chunk entries.
Definition liquid.hpp:197
bool empty() const
Whether the tile has no liquid at all.
Definition liquid.hpp:129
std::vector< MapChunkLiquid > cells
Liquid for each of the 256 terrain chunks (y * 16 + x).
Definition liquid.hpp:114
Result< void > write(FileBuffer &out) const
Re-lay the 256 entries into a fresh canonical MH2O payload (every binary offset derived),...
Definition liquid.hpp:306
bool operator==(const MH2OData &) const =default
All liquid of one terrain cell: the attribute masks plus its layers.
Definition liquid.hpp:89
std::uint64_t fishable
The 8x8 fishable bit mask (visibility); 0 when the cell omits attributes.
Definition liquid.hpp:91
bool hasAttributes
Whether this cell wrote an explicit attributes block (so an all-zero mask round-trips as present rath...
Definition liquid.hpp:97
std::uint64_t deep
The 8x8 deep bit mask (fatigue water); 0 when the cell omits attributes.
Definition liquid.hpp:94
std::vector< LiquidInstance > instances
The stacked liquid layers over this chunk.
Definition liquid.hpp:100
bool empty() const
Whether the chunk carries any liquid.
Definition liquid.hpp:107
bool operator==(const MapChunkLiquid &) const =default
One legacy liquid flow vector (MCLQ SWFlowv, 40 bytes).
Definition liquid.hpp:93
A float range: minimum and maximum.
Definition types.hpp:154
The binary-level math and color primitives shared across WoW file formats (wowdev....