1use crate::player::{ObjRef, Player};
11use crate::value::{Array, Value};
12use crate::{Result, invalid};
13
14pub const MAX_HEIGHT: i16 = 32766;
16
17#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
19pub enum HeightmapError {
20 #[error("resolution {resolution}: a heightmap has at least 2 x 2 samples")]
22 Resolution {
23 resolution: u32,
25 },
26 #[error("resolution {resolution}: the heightmap's sample count overflows")]
28 SampleCount {
29 resolution: u32,
31 },
32 #[error("{found} heights for resolution {resolution} ({expected} expected)")]
34 Heights {
35 resolution: u32,
37 found: usize,
39 expected: usize,
41 },
42 #[error("height sample {index} (row {row}, column {column}) is {value}, outside 0..={MAX_HEIGHT}")]
44 HeightRange {
45 index: usize,
47 row: usize,
49 column: usize,
51 value: i16,
53 },
54 #[error("{found} holes for {cells} cells")]
56 Holes {
57 found: usize,
59 cells: usize,
61 },
62}
63
64impl From<HeightmapError> for crate::Error {
65 fn from(e: HeightmapError) -> Self {
66 invalid(format!("terrain: {e}"))
67 }
68}
69
70pub fn check_heightmap(resolution: u32, heights: &[i16], holes: Option<&[u8]>) -> Result<(), HeightmapError> {
73 if resolution < 2 {
74 return Err(HeightmapError::Resolution { resolution });
75 }
76 let n = usize::try_from(resolution).map_err(|_| HeightmapError::SampleCount { resolution })?;
77 let samples = n
78 .checked_mul(n)
79 .ok_or(HeightmapError::SampleCount { resolution })?;
80 if heights.len() != samples {
81 return Err(HeightmapError::Heights {
82 resolution,
83 found: heights.len(),
84 expected: samples,
85 });
86 }
87 if let Some((index, &value)) = heights
88 .iter()
89 .enumerate()
90 .find(|(_, h)| !(0..=MAX_HEIGHT).contains(*h))
91 {
92 return Err(HeightmapError::HeightRange {
93 index,
94 row: index.div_euclid(n),
95 column: index.rem_euclid(n),
96 value,
97 });
98 }
99 let cells = n.saturating_sub(1).saturating_mul(n.saturating_sub(1));
101 match holes {
102 Some(h) if h.len() != cells => Err(HeightmapError::Holes {
103 found: h.len(),
104 cells,
105 }),
106 _ => Ok(()),
107 }
108}
109
110#[derive(Clone, Debug, PartialEq)]
113pub struct TreeInstance {
114 pub position: [f32; 3],
116 pub width_scale: f32,
118 pub height_scale: f32,
120 pub rotation: f32,
122 pub color: u32,
124 pub prototype: usize,
126}
127
128#[derive(Clone, Debug, PartialEq)]
130pub struct TerrainData {
131 pub name: String,
133 pub resolution: u32,
135 pub scale: [f32; 3],
137 pub heights: Vec<i16>,
139 pub holes: Option<Vec<u8>>,
141 pub layers: Vec<Option<ObjRef>>,
143 pub alphamaps: Vec<Option<ObjRef>>,
145 pub alphamap_resolution: u32,
147 pub tree_prototypes: Vec<Option<ObjRef>>,
149 pub trees: Vec<TreeInstance>,
151}
152
153#[derive(Clone, Debug, PartialEq)]
155pub struct TerrainLayerData {
156 pub name: String,
158 pub diffuse: Option<ObjRef>,
160 pub normal: Option<ObjRef>,
162 pub mask: Option<ObjRef>,
164 pub tile_size: [f32; 2],
166 pub tile_offset: [f32; 2],
168 pub specular: [f32; 4],
170 pub metallic: f32,
172 pub smoothness: f32,
174 pub normal_scale: f32,
176 pub diffuse_remap: [[f32; 4]; 2],
178 pub mask_remap: [[f32; 4]; 2],
180}
181
182fn get<'a>(v: &'a Value, k: &str) -> Result<&'a Value> {
183 v.get(k).ok_or_else(|| invalid(format!("terrain: no {k}")))
184}
185
186fn float(v: &Value, k: &str) -> Result<f32> {
187 get(v, k)?
188 .as_f32()
189 .ok_or_else(|| invalid(format!("terrain: {k} is not a float")))
190}
191
192fn int(v: &Value, k: &str) -> Result<i64> {
193 get(v, k)?
194 .as_i64()
195 .ok_or_else(|| invalid(format!("terrain: {k} is not an integer")))
196}
197
198fn vec<const N: usize>(v: &Value, k: &str, names: [&str; N]) -> Result<[f32; N]> {
199 let x = get(v, k)?;
200 let mut out = [0.0; N];
201 for (o, n) in out.iter_mut().zip(names) {
202 *o = float(x, n)?;
203 }
204 Ok(out)
205}
206
207fn list<'a>(v: &'a Value, k: &str) -> Result<&'a [Value]> {
208 match get(v, k)? {
209 Value::Array(Array::Values(items)) => Ok(items),
210 Value::Array(a) if a.is_empty() => Ok(&[]),
211 _ => Err(invalid(format!("terrain: {k} is not a list"))),
212 }
213}
214
215fn pptrs(player: &Player, file: &str, items: &[Value], field: Option<&str>) -> Result<Vec<Option<ObjRef>>> {
216 items
217 .iter()
218 .map(|p| {
219 let p = match field {
220 Some(f) => get(p, f)?,
221 None => p,
222 };
223 player.resolve(file, p)
224 })
225 .collect()
226}
227
228fn read_trees(items: &[Value], prototypes: usize) -> Result<Vec<TreeInstance>> {
230 items
231 .iter()
232 .enumerate()
233 .map(|(n, i)| {
234 let index = int(i, "index")?;
235 let prototype = usize::try_from(index)
236 .ok()
237 .filter(|&p| p < prototypes)
238 .ok_or_else(|| {
239 invalid(format!(
240 "terrain: tree {n} uses prototype {index} of {prototypes}"
241 ))
242 })?;
243 let rgba = int(get(i, "color")?, "rgba")?;
244 let color = u32::try_from(rgba).map_err(|_| {
245 invalid(format!(
246 "terrain: tree {n}: colour {rgba} is not a 32-bit unsigned number"
247 ))
248 })?;
249 Ok(TreeInstance {
250 position: vec(i, "position", ["x", "y", "z"])?,
251 width_scale: float(i, "widthScale")?,
252 height_scale: float(i, "heightScale")?,
253 rotation: float(i, "rotation")?,
254 color,
255 prototype,
256 })
257 })
258 .collect()
259}
260
261impl TerrainData {
262 pub fn read(player: &Player, r: &ObjRef) -> Result<Self> {
264 let t = player.decode(r)?;
265 let hm = get(&t, "m_Heightmap")?;
266 let resolution = int(hm, "m_Resolution")?;
267 let resolution = u32::try_from(resolution)
268 .map_err(|_| invalid(format!("terrain: resolution {resolution} is not a sample count")))?;
269 let heights = match get(hm, "m_Heights")? {
270 Value::Array(Array::I16(h)) => h.clone(),
271 _ => return Err(invalid("terrain: m_Heights is not 16-bit")),
272 };
273 let holes: Option<Vec<u8>> = match hm.get("m_Holes") {
274 Some(Value::Array(Array::U8(h))) if !h.is_empty() => Some(h.clone()),
275 Some(Value::Array(Array::Bool(h))) if !h.is_empty() => {
276 Some(h.iter().map(|&b| u8::from(b)).collect())
277 }
278 _ => None,
279 };
280 check_heightmap(resolution, &heights, holes.as_deref())?;
281 let splat = get(&t, "m_SplatDatabase")?;
282 let detail = get(&t, "m_DetailDatabase")?;
283 let prototypes = list(detail, "m_TreePrototypes")?;
284 let trees = read_trees(list(detail, "m_TreeInstances")?, prototypes.len())?;
285 let alphamap_resolution = int(splat, "m_AlphamapResolution")?;
286 let alphamap_resolution = u32::try_from(alphamap_resolution).map_err(|_| {
287 invalid(format!(
288 "terrain: m_AlphamapResolution {alphamap_resolution} is not a 32-bit unsigned number"
289 ))
290 })?;
291 Ok(TerrainData {
292 name: t.get("m_Name").and_then(Value::as_str).unwrap_or("").to_owned(),
293 resolution,
294 scale: vec(hm, "m_Scale", ["x", "y", "z"])?,
295 heights,
296 holes,
297 layers: pptrs(player, &r.file, list(splat, "m_TerrainLayers")?, None)?,
298 alphamaps: pptrs(player, &r.file, list(splat, "m_AlphaTextures")?, None)?,
299 alphamap_resolution,
300 tree_prototypes: pptrs(player, &r.file, prototypes, Some("prefab"))?,
301 trees,
302 })
303 }
304
305 pub fn height(&self, x: usize, z: usize) -> Option<f64> {
308 let side = usize::try_from(self.resolution).ok()?;
309 if x >= side || z >= side {
310 return None;
311 }
312 let sample = self.heights.get(z.checked_mul(side)?.checked_add(x)?)?;
313 Some(f64::from(*sample) / f64::from(MAX_HEIGHT) * f64::from(self.scale[1]))
314 }
315
316 pub fn size(&self) -> [f64; 3] {
318 let cells = f64::from(self.resolution.saturating_sub(1));
319 [
320 cells * f64::from(self.scale[0]),
321 f64::from(self.scale[1]),
322 cells * f64::from(self.scale[2]),
323 ]
324 }
325}
326
327impl TerrainLayerData {
328 pub fn read(player: &Player, r: &ObjRef) -> Result<Self> {
330 let l = player.decode(r)?;
331 let tex = |k: &str| -> Result<Option<ObjRef>> {
332 match l.get(k) {
333 Some(p) => player.resolve(&r.file, p),
334 None => Ok(None),
335 }
336 };
337 let rgba = ["x", "y", "z", "w"];
338 Ok(TerrainLayerData {
339 name: l.get("m_Name").and_then(Value::as_str).unwrap_or("").to_owned(),
340 diffuse: tex("m_DiffuseTexture")?,
341 normal: tex("m_NormalMapTexture")?,
342 mask: tex("m_MaskMapTexture")?,
343 tile_size: vec(&l, "m_TileSize", ["x", "y"])?,
344 tile_offset: vec(&l, "m_TileOffset", ["x", "y"])?,
345 specular: vec(&l, "m_Specular", ["r", "g", "b", "a"])?,
346 metallic: float(&l, "m_Metallic")?,
347 smoothness: float(&l, "m_Smoothness")?,
348 normal_scale: float(&l, "m_NormalScale")?,
349 diffuse_remap: [
350 vec(&l, "m_DiffuseRemapMin", rgba)?,
351 vec(&l, "m_DiffuseRemapMax", rgba)?,
352 ],
353 mask_remap: [
354 vec(&l, "m_MaskMapRemapMin", rgba)?,
355 vec(&l, "m_MaskMapRemapMax", rgba)?,
356 ],
357 })
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
368 fn a_heightmap_is_checked_before_its_cells_are_counted() {
369 assert!(check_heightmap(2, &[0, 1, 2, MAX_HEIGHT], None).is_ok());
370 assert!(check_heightmap(2, &[0; 4], Some(&[1])).is_ok());
371 for resolution in [0, 1] {
372 assert_eq!(
373 check_heightmap(resolution, &[], Some(&[])).unwrap_err(),
374 HeightmapError::Resolution { resolution }
375 );
376 }
377 assert_eq!(
378 check_heightmap(2, &[0; 3], None).unwrap_err(),
379 HeightmapError::Heights {
380 resolution: 2,
381 found: 3,
382 expected: 4
383 }
384 );
385 let range = check_heightmap(2, &[0, 0, -1, 0], None).unwrap_err();
386 assert_eq!(
387 range,
388 HeightmapError::HeightRange {
389 index: 2,
390 row: 1,
391 column: 0,
392 value: -1
393 }
394 );
395 assert_eq!(
396 range.to_string(),
397 "height sample 2 (row 1, column 0) is -1, outside 0..=32766"
398 );
399 assert!(check_heightmap(2, &[0, 0, 0, i16::MAX], None).is_err());
400 let holes = check_heightmap(3, &[0; 9], Some(&[1; 3])).unwrap_err();
401 assert_eq!(holes, HeightmapError::Holes { found: 3, cells: 4 });
402 assert_eq!(holes.to_string(), "3 holes for 4 cells");
403 let e = crate::Error::from(holes);
405 assert!(matches!(e, crate::Error::Invalid(m) if m == "terrain: 3 holes for 4 cells"));
406 }
407
408 fn terrain(resolution: u32, heights: Vec<i16>, scale_y: f32) -> TerrainData {
409 TerrainData {
410 name: String::new(),
411 resolution,
412 scale: [1.0, scale_y, 1.0],
413 heights,
414 holes: None,
415 layers: vec![],
416 alphamaps: vec![],
417 alphamap_resolution: 0,
418 tree_prototypes: vec![],
419 trees: vec![],
420 }
421 }
422
423 #[test]
426 fn heights_are_scaled_by_max_height_and_bounded() {
427 let t = terrain(2, vec![0, MAX_HEIGHT, 16383, 0], 100.0);
428 assert_eq!(t.height(1, 0), Some(100.0));
429 assert_eq!(
430 t.height(0, 1),
431 Some(f64::from(16383) / f64::from(MAX_HEIGHT) * 100.0)
432 );
433 assert_eq!(t.height(2, 0), None);
434 assert_eq!(t.height(0, 2), None);
435 assert_eq!(t.height(usize::MAX, usize::MAX), None);
436 assert_eq!(terrain(0, vec![], 1.0).size(), [0.0, 1.0, 0.0]);
437 }
438
439 fn tree(index: i64, rgba: i64) -> Value {
440 let f = |k: &str, v: Value| (std::sync::Arc::<str>::from(k), v);
441 let v3 = Value::Struct(vec![
442 f("x", Value::Float(0.5)),
443 f("y", Value::Float(0.0)),
444 f("z", Value::Float(0.5)),
445 ]);
446 Value::Struct(vec![
447 f("position", v3),
448 f("widthScale", Value::Float(1.0)),
449 f("heightScale", Value::Float(1.0)),
450 f("rotation", Value::Float(0.0)),
451 f("color", Value::Struct(vec![f("rgba", Value::Int(rgba))])),
452 f("index", Value::Int(index)),
453 ])
454 }
455
456 #[test]
458 fn trees_name_a_prototype_and_a_colour_that_exist() {
459 let ok = read_trees(&[tree(1, 0xff00_ff00)], 2).unwrap();
460 assert_eq!((ok[0].prototype, ok[0].color), (1, 0xff00_ff00));
461 for (index, rgba, wants) in [
462 (2, 0, "tree 1 uses prototype 2 of 2"),
463 (-1, 0, "tree 1 uses prototype -1 of 2"),
464 (i64::MAX, 0, "prototype 9223372036854775807 of 2"),
465 (0, 1 << 32, "colour 4294967296"),
466 (0, -1, "colour -1"),
467 ] {
468 let e = read_trees(&[tree(0, 0), tree(index, rgba)], 2)
469 .unwrap_err()
470 .to_string();
471 assert!(e.contains(wants), "{e}");
472 }
473 }
474}