use crate::abi::{Header, Status, TerrainEdit, TerrainMaterialInfo};
use crate::world::WorldError;
use crate::{Host, Load};
pub const MAX_RADIUS: f32 = 16.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Material {
pub id: u8,
pub item: u32,
pub hardness: u8,
pub health: i32,
pub flattens: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Shape {
#[default]
Round,
Square,
}
impl Shape {
const fn raw(self) -> u32 {
match self {
Self::Round => TerrainEdit::ROUND,
Self::Square => TerrainEdit::SQUARE,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Terrain {
host: Host,
dry: bool,
}
#[expect(
clippy::cast_possible_truncation,
reason = "world positions in metres fit an f32 as the game's edits take them"
)]
fn metres(position: [f64; 3]) -> [f32; 3] {
position.map(|axis| axis as f32)
}
impl Terrain {
#[must_use]
pub fn materials(&self) -> Vec<Material> {
let blank = TerrainMaterialInfo {
header: Header::of::<TerrainMaterialInfo>(),
id: 0,
flags: 0,
hardness: 0,
reserved: 0,
item: 0,
health: 0,
};
let mut infos = vec![blank; 128];
loop {
let mut count = 0u32;
let capacity = u32::try_from(infos.len()).unwrap_or(u32::MAX);
let status = (self.host.api().terrain_materials)(
self.host.handle(),
infos.as_mut_ptr(),
capacity,
&raw mut count,
);
if status != Status::OK {
return Vec::new();
}
let count = count as usize;
if count <= infos.len() {
infos.truncate(count);
break;
}
infos = vec![blank; count];
}
infos
.iter()
.map(|info| Material {
id: info.id,
item: info.item,
hardness: info.hardness,
health: info.health,
flattens: info.flags & TerrainMaterialInfo::FLATTENS != 0,
})
.collect()
}
pub fn add(
&self,
position: [f64; 3],
radius: f32,
height: f32,
material: u8,
shape: Shape,
) -> Result<f32, WorldError> {
self.edit(TerrainEdit {
kind: TerrainEdit::ADD,
shape: shape.raw(),
material: u32::from(material),
height,
..self.request(position, radius)
})
}
pub fn dig(
&self,
position: [f64; 3],
radius: f32,
shape: Shape,
protect: &[u8],
) -> Result<f32, WorldError> {
let mut bits = [0u8; 16];
for &id in protect {
if let Some(byte) = bits.get_mut(usize::from(id / 8)) {
*byte |= 1 << (id % 8);
}
}
self.edit(TerrainEdit {
kind: TerrainEdit::DIG,
shape: shape.raw(),
protect: bits,
..self.request(position, radius)
})
}
pub fn flatten(
&self,
position: [f64; 3],
radius: f32,
material: u8,
shape: Shape,
) -> Result<bool, WorldError> {
let changed = self.edit(TerrainEdit {
kind: TerrainEdit::FLATTEN,
shape: shape.raw(),
material: u32::from(material),
..self.request(position, radius)
})?;
Ok(changed > 0.0)
}
#[must_use]
pub const fn dry(self) -> Self {
Self {
host: self.host,
dry: true,
}
}
fn request(&self, position: [f64; 3], radius: f32) -> TerrainEdit {
TerrainEdit {
header: Header::of::<TerrainEdit>(),
kind: 0,
shape: 0,
flags: if self.dry { TerrainEdit::DRY_RUN } else { 0 },
material: 0,
position: metres(position),
radius,
height: 0.0,
protect: [0; 16],
}
}
fn edit(&self, edit: TerrainEdit) -> Result<f32, WorldError> {
let mut amount = 0.0f32;
let status =
(self.host.api().terrain_edit)(self.host.handle(), &raw const edit, &raw mut amount);
WorldError::from_status(status).map(|()| amount)
}
}
impl Load<'_> {
#[must_use]
pub fn terrain(&mut self) -> Terrain {
Terrain {
host: self.host(),
dry: false,
}
}
}