1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
// mh2o.rs - Enhanced water data parsing for WotLK+ ADT files
use crate::ParserContext;
use crate::error::Result;
use crate::io_helpers::ReadLittleEndian;
use std::io::{Read, Seek, SeekFrom};
/// MH2O chunk - water data (WotLK+)
#[derive(Debug, Clone)]
pub struct Mh2oChunk {
/// Water data for each map chunk (256 entries)
pub chunks: Vec<Mh2oEntry>,
}
/// Water data for a single chunk
#[derive(Debug, Clone)]
pub struct Mh2oEntry {
/// Header for this entry
pub header: Mh2oHeader,
/// Water instances (layers) for this chunk
pub instances: Vec<Mh2oInstance>,
/// Attributes for liquid properties (fishable, deep water zones)
pub attributes: Option<Mh2oAttributes>,
/// Render mask for liquid rendering
pub render_mask: Option<Mh2oRenderMask>,
}
/// MH2O header for a single map chunk
#[derive(Debug, Clone)]
pub struct Mh2oHeader {
/// Offset to instance data, relative to the start of the MH2O chunk
pub offset_instances: u32,
/// Number of water layers in this chunk
pub layer_count: u32,
/// Offset to attributes, relative to the start of the MH2O chunk
pub offset_attributes: u32,
}
/// MH2O chunk attributes (8x8 bitmasks for liquid properties)
#[derive(Debug, Clone)]
pub struct Mh2oAttributes {
/// Fishable/visibility flags (8x8 grid of bits, stored as 64-bit value)
pub fishable: u64,
/// Deep water/fatigue zone flags (8x8 grid of bits, stored as 64-bit value)
pub deep: u64,
}
/// MH2O water instance (layer)
#[derive(Debug, Clone)]
pub struct Mh2oInstance {
/// Liquid type ID
pub liquid_type: u16,
/// Liquid object ID
pub liquid_object: u16,
/// X offset within chunk (0-8)
pub x_offset: u8,
/// Y offset within chunk (0-8)
pub y_offset: u8,
/// Width in cells (not vertices)
pub width: u8,
/// Height in cells (not vertices)
pub height: u8,
/// Water level (height) values
pub level_data: WaterLevelData,
/// Vertex data for water surface
pub vertex_data: Option<WaterVertexData>,
/// Attributes for this water layer
pub attributes: Vec<u64>,
}
/// Water height information
#[derive(Debug, Clone)]
pub enum WaterLevelData {
/// Single water level for the entire chunk
Uniform {
/// Minimum height of water level
min_height: f32,
/// Maximum height of water level
max_height: f32,
},
/// Variable water heights
Variable {
/// Minimum height of water level
min_height: f32,
/// Maximum height of water level
max_height: f32,
/// Offset to height map, relative to MH2O chunk
offset_height_map: u32,
/// Height values for each vertex
heights: Option<Vec<f32>>,
},
}
/// Water vertex information
#[derive(Debug, Clone)]
pub struct WaterVertexData {
/// Offset to vertex data, relative to MH2O chunk
pub offset_vertex_data: u32,
/// Number of vertices in x direction
pub x_vertices: u8,
/// Number of vertices in y direction
pub y_vertices: u8,
/// Actual vertex data
pub vertices: Option<Vec<WaterVertex>>,
}
/// Individual water vertex
#[derive(Debug, Clone)]
pub struct WaterVertex {
/// Depth at this vertex
pub depth: f32,
/// Flow direction and velocity
pub flow: [u8; 2],
}
/// Render mask for water
#[derive(Debug, Clone)]
pub struct Mh2oRenderMask {
/// The mask is an 8x8 grid of bits, stored as 8 bytes
pub mask: [u8; 8],
}
impl Mh2oChunk {
/// Parse a MH2O chunk from a reader
pub(crate) fn read_full<R: Read + Seek>(
context: &mut ParserContext<R>,
chunk_start: u64,
chunk_size: u32,
) -> Result<Self> {
// Start position for calculating relative offsets
let start_pos = chunk_start;
// MH2O has up to 256 headers (one for each map chunk)
// Each header is 3 integers (12 bytes) according to WoWDev wiki
// Some files may have fewer headers if not all chunks have water
let max_possible_headers = chunk_size / 12;
let header_count = std::cmp::min(256, max_possible_headers) as usize;
// Read the headers first (only what's available)
let mut headers = Vec::with_capacity(256);
// Read available headers
for _ in 0..header_count {
let offset_instances = context.reader.read_u32_le()?;
let layer_count = context.reader.read_u32_le()?;
let offset_attributes = context.reader.read_u32_le()?;
headers.push(Mh2oHeader {
offset_instances,
layer_count,
offset_attributes,
});
}
// Fill remaining headers with empty entries
for _ in header_count..256 {
headers.push(Mh2oHeader {
offset_instances: 0,
layer_count: 0,
offset_attributes: 0,
});
}
// Process each header to read its data
let mut chunks = Vec::with_capacity(256);
for header in headers {
let mut instances = Vec::new();
let mut attributes = None;
let render_mask = None;
// Read water instances
if header.offset_instances > 0 && header.layer_count > 0 {
// Validate that the offset is within the chunk bounds
if header.offset_instances >= chunk_size {
// Skip this header if offset is invalid
chunks.push(Mh2oEntry {
header,
instances: Vec::new(),
attributes: None,
render_mask: None,
});
continue;
}
context
.reader
.seek(SeekFrom::Start(start_pos + header.offset_instances as u64))?;
for _layer_idx in 0..header.layer_count {
// Try to read instance header, break on EOF
let instance_result = (|| -> Result<Mh2oInstance> {
// Read the 24-byte SMLiquidInstance structure per wowdev.wiki spec
let liquid_type = context.reader.read_u16_le()?; // 0x00
let liquid_object = context.reader.read_u16_le()?; // 0x02
let min_height = context.reader.read_f32_le()?; // 0x04
let max_height = context.reader.read_f32_le()?; // 0x08
let x_offset = context.reader.read_u8()?; // 0x0C
let y_offset = context.reader.read_u8()?; // 0x0D
let width = context.reader.read_u8()?; // 0x0E
let height = context.reader.read_u8()?; // 0x0F
let offset_exists_bitmap = context.reader.read_u32_le()?; // 0x10
let offset_vertex_data = context.reader.read_u32_le()?; // 0x14
// Instance header ends at 0x18 (24 bytes)
// Determine liquid vertex format (LVF)
// If liquid_object < 42, it encodes LVF directly:
// 0 = height + depth (has heightmap)
// 1 = height + UV (has heightmap)
// 2 = depth only (NO heightmap)
// 3 = height + UV + depth (has heightmap)
// If liquid_object >= 42, it's a LiquidObjectID (need DB lookup)
let has_heightmap = if liquid_object < 42 {
// liquid_object is LVF directly
liquid_object != 2 // LVF 2 (depth-only) has no heightmap
} else {
// For LiquidObjectID, assume has heightmap if vertex data present
// TODO: Proper DB lookup when implementing full liquid support
offset_vertex_data > 0
};
// Vertex data will be read later using offset_vertex_data
// The vertex data section contains both height map AND vertex attributes
let vertex_data = if offset_vertex_data > 0 {
Some(WaterVertexData {
offset_vertex_data,
x_vertices: width + 1, // Grid has width+1 vertices
y_vertices: height + 1, // Grid has height+1 vertices
vertices: None, // Will be read later
})
} else {
None
};
// Determine level data type based on whether heightmap exists
let level_data = if has_heightmap && offset_vertex_data > 0 {
// Variable height water - heightmap in vertex data section
WaterLevelData::Variable {
min_height,
max_height,
offset_height_map: offset_vertex_data, // Height data is first in vertex section
heights: None, // Will be read later
}
} else {
// Uniform height water (flat surface at min_height)
WaterLevelData::Uniform {
min_height,
max_height,
}
};
Ok(Mh2oInstance {
liquid_type,
liquid_object,
x_offset,
y_offset,
width,
height,
level_data,
vertex_data,
attributes: Vec::new(), // Attributes are in separate chunk header section
})
})();
match instance_result {
Ok(instance) => {
instances.push(instance);
}
Err(_) => {
// EOF or other read error - stop reading instances
// This is normal for some ADT files with incomplete water data
break;
}
}
}
// Now go back and read all the variable data
// (height maps, vertex data, etc.)
for instance in &mut instances {
// Read height map data if needed
if let WaterLevelData::Variable {
offset_height_map,
heights,
..
} = &mut instance.level_data
{
if *offset_height_map > 0 && *offset_height_map < chunk_size {
// Try to read height map data, skip on error
let height_result = (|| -> Result<Vec<f32>> {
context
.reader
.seek(SeekFrom::Start(start_pos + *offset_height_map as u64))?;
// Calculate actual vertex count based on water dimensions
// IMPORTANT: width/height in MH2O are cell counts, vertices = cells + 1
// SPECIAL CASE: width=0 or height=0 means full chunk coverage (8 cells = 9 vertices)
let width_vertices = if instance.width == 0 {
9
} else {
instance.width + 1
};
let height_vertices = if instance.height == 0 {
9
} else {
instance.height + 1
};
let vertex_count =
(width_vertices as usize) * (height_vertices as usize);
let mut height_data = Vec::with_capacity(vertex_count);
for _ in 0..vertex_count {
let height = context.reader.read_f32_le()?;
height_data.push(height);
}
Ok(height_data)
})();
if let Ok(height_data) = height_result {
*heights = Some(height_data);
}
}
}
// Read vertex data if needed
if let Some(vertex_data) = &mut instance.vertex_data {
if vertex_data.offset_vertex_data > 0
&& vertex_data.offset_vertex_data < chunk_size
{
// Try to read vertex data, skip on error
let vertex_result = (|| -> Result<Vec<WaterVertex>> {
context.reader.seek(SeekFrom::Start(
start_pos + vertex_data.offset_vertex_data as u64,
))?;
let x_verts = vertex_data.x_vertices as usize;
let y_verts = vertex_data.y_vertices as usize;
let vert_count = x_verts * y_verts;
let mut verts = Vec::with_capacity(vert_count);
for _ in 0..vert_count {
let depth = context.reader.read_f32_le()?;
let mut flow = [0u8; 2];
context.reader.read_exact(&mut flow)?;
verts.push(WaterVertex { depth, flow });
}
Ok(verts)
})();
if let Ok(verts) = vertex_result {
vertex_data.vertices = Some(verts);
}
}
}
}
}
// Read attributes
if header.offset_attributes > 0 && header.offset_attributes < chunk_size {
// Try to read attributes, skip on error
let attr_result = (|| -> Result<Mh2oAttributes> {
context
.reader
.seek(SeekFrom::Start(start_pos + header.offset_attributes as u64))?;
let fishable = context.reader.read_u64_le()?;
let deep = context.reader.read_u64_le()?;
Ok(Mh2oAttributes { fishable, deep })
})();
if let Ok(attr) = attr_result {
attributes = Some(attr);
}
}
// Note: Render mask is deprecated in WotLK+ and replaced by attributes
// We keep this for backward compatibility with pre-WotLK data
// In WotLK+, offset_attributes points to the new 16-byte structure
chunks.push(Mh2oEntry {
header,
instances,
attributes,
render_mask,
});
}
Ok(Self { chunks })
}
}