use crate::WorldError;
use crate::abi;
use crate::{Host, Load};
pub const READ_MAX: u64 = abi::Voxel::READ_MAX;
pub const PER_METRE: f64 = 2.0;
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
serde::Serialize,
serde::Deserialize,
)]
pub struct VoxelPos {
x: u32,
y: u32,
z: u32,
}
impl VoxelPos {
#[must_use]
pub const fn new(x: u32, y: u32, z: u32) -> Self {
Self { x, y, z }
}
#[must_use]
pub fn from_metres(metres: [f64; 3]) -> Option<Self> {
let [x, y, z] = metres.map(index);
Some(Self::new(x?, y?, z?))
}
#[must_use]
pub fn to_metres(self) -> [f64; 3] {
self.to_array().map(|axis| f64::from(axis) / PER_METRE)
}
#[must_use]
pub const fn x(self) -> u32 {
self.x
}
#[must_use]
pub const fn y(self) -> u32 {
self.y
}
#[must_use]
pub const fn z(self) -> u32 {
self.z
}
#[must_use]
pub const fn to_array(self) -> [u32; 3] {
[self.x, self.y, self.z]
}
}
impl From<[u32; 3]> for VoxelPos {
fn from([x, y, z]: [u32; 3]) -> Self {
Self::new(x, y, z)
}
}
impl From<VoxelPos> for [u32; 3] {
fn from(at: VoxelPos) -> Self {
at.to_array()
}
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the value is checked to lie in 0 to `Voxel::END` first"
)]
fn index(metres: f64) -> Option<u32> {
let voxels = (metres * PER_METRE).floor();
(voxels >= 0.0 && voxels < f64::from(abi::Voxel::END)).then_some(voxels as u32)
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
#[non_exhaustive]
pub enum VoxelMaterial {
#[default]
Air,
Terrain(u8),
Building(u8),
Unknown(u8),
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct Voxel {
material: VoxelMaterial,
fill: u8,
}
impl Voxel {
#[must_use]
pub const fn new(material: VoxelMaterial, fill: u8) -> Self {
Self { material, fill }
}
#[must_use]
pub const fn material(self) -> VoxelMaterial {
self.material
}
#[must_use]
pub const fn fill(self) -> u8 {
self.fill
}
#[must_use]
pub const fn is_air(self) -> bool {
matches!(self.material, VoxelMaterial::Air)
}
pub(crate) const fn from_abi(raw: abi::Voxel) -> Self {
let material = match raw.kind {
abi::Voxel::AIR => VoxelMaterial::Air,
abi::Voxel::TERRAIN => VoxelMaterial::Terrain(raw.material),
abi::Voxel::BUILDING => VoxelMaterial::Building(raw.material),
_ => VoxelMaterial::Unknown(raw.material),
};
Self::new(material, raw.fill)
}
#[cfg(any(test, feature = "test", feature = "unstable"))]
pub(crate) const fn to_abi(self) -> abi::Voxel {
let (kind, material) = match self.material {
VoxelMaterial::Air => (abi::Voxel::AIR, 0),
VoxelMaterial::Terrain(id) => (abi::Voxel::TERRAIN, id),
VoxelMaterial::Building(id) => (abi::Voxel::BUILDING, id),
VoxelMaterial::Unknown(id) => (u8::MAX, id),
};
abi::Voxel {
kind,
material,
fill: self.fill,
reserved: 0,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(try_from = "Parts")]
pub struct VoxelBox {
min: VoxelPos,
size: [u32; 3],
voxels: Vec<Voxel>,
}
#[derive(serde::Deserialize)]
struct Parts {
min: VoxelPos,
size: [u32; 3],
voxels: Vec<Voxel>,
}
#[derive(Debug, thiserror::Error)]
#[error("a voxel box of {size:?} from {min:?} holding {held} voxels")]
struct VoxelBoxError {
min: VoxelPos,
size: [u32; 3],
held: usize,
}
impl TryFrom<Parts> for VoxelBox {
type Error = VoxelBoxError;
fn try_from(parts: Parts) -> Result<Self, VoxelBoxError> {
let (min, size, held) = (parts.min, parts.size, parts.voxels.len());
Self::from_parts(min, size, parts.voxels).ok_or(VoxelBoxError { min, size, held })
}
}
impl VoxelBox {
pub(crate) fn from_parts(min: VoxelPos, size: [u32; 3], voxels: Vec<Voxel>) -> Option<Self> {
let within = (0..3).all(|axis| {
min.to_array()[axis]
.checked_add(size[axis])
.is_some_and(|end| end <= abi::Voxel::END)
});
let count = size
.iter()
.try_fold(1usize, |count, &axis| count.checked_mul(axis as usize))?;
(within && count == voxels.len()).then_some(Self { min, size, voxels })
}
#[cfg(feature = "unstable")]
pub(crate) fn turned(&self) -> Self {
let [width, height, depth] = self.size.map(|axis| axis as usize);
let mut voxels = Vec::with_capacity(self.voxels.len());
for new_z in 0..width {
for y in 0..height {
for new_x in 0..depth {
let (x, z) = (new_z, depth - 1 - new_x);
voxels.push(self.voxels[x + width * (y + height * z)]);
}
}
}
let [width, height, depth] = self.size;
Self {
min: self.min,
size: [depth, height, width],
voxels,
}
}
#[cfg(feature = "unstable")]
pub(crate) fn at_corner(self) -> Self {
Self {
min: VoxelPos::default(),
..self
}
}
#[cfg(feature = "unstable")]
pub(crate) fn fill_from(&mut self, block: &Self) {
let [width, height, _] = self.size.map(|axis| axis as usize);
let [row, rows, _] = block.size.map(|axis| axis as usize);
let [x, y, z] =
[0, 1, 2].map(|axis| (block.min.to_array()[axis] - self.min.to_array()[axis]) as usize);
for (index, source) in block.voxels.chunks_exact(row).enumerate() {
let start = x + width * (y + index % rows + height * (z + index / rows));
self.voxels[start..start + row].copy_from_slice(source);
}
}
#[must_use]
pub const fn min(&self) -> VoxelPos {
self.min
}
#[must_use]
pub const fn size(&self) -> [u32; 3] {
self.size
}
#[must_use]
pub fn get(&self, at: VoxelPos) -> Option<Voxel> {
self.voxels.get(self.index(at)?).copied()
}
pub fn set(&mut self, at: VoxelPos, voxel: Voxel) -> bool {
let Some(slot) = self.index(at).and_then(|index| self.voxels.get_mut(index)) else {
return false;
};
*slot = voxel;
true
}
#[must_use]
pub fn new(min: VoxelPos, size: [u32; 3], voxels: Vec<Voxel>) -> Option<Self> {
let count = size
.iter()
.try_fold(1usize, |count, &axis| count.checked_mul(axis as usize))?;
(count > 0 && count == voxels.len()).then_some(Self { min, size, voxels })
}
#[must_use]
pub fn moved_to(self, min: VoxelPos) -> Self {
Self { min, ..self }
}
fn index(&self, at: VoxelPos) -> Option<usize> {
let mut index = 0usize;
for axis in (0..3).rev() {
let offset = at.to_array()[axis].checked_sub(self.min.to_array()[axis])?;
if offset >= self.size[axis] {
return None;
}
index = index * self.size[axis] as usize + offset as usize;
}
Some(index)
}
#[must_use]
pub fn as_slice(&self) -> &[Voxel] {
&self.voxels
}
#[must_use]
pub fn iter(&self) -> Iter<'_> {
Iter {
block: self,
next: 0,
}
}
}
impl<'a> IntoIterator for &'a VoxelBox {
type Item = (VoxelPos, Voxel);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Iter<'a> {
self.iter()
}
}
#[derive(Clone, Debug)]
pub struct Iter<'a> {
block: &'a VoxelBox,
next: usize,
}
impl Iterator for Iter<'_> {
type Item = (VoxelPos, Voxel);
fn next(&mut self) -> Option<Self::Item> {
let voxel = *self.block.voxels.get(self.next)?;
let [width, height, _] = self.block.size.map(|axis| axis as usize);
let [x, y, z] = [
self.next % width,
self.next / width % height,
self.next / (width * height),
]
.map(|offset| u32::try_from(offset).unwrap_or(u32::MAX));
let [min_x, min_y, min_z] = self.block.min.to_array();
self.next += 1;
Some((VoxelPos::new(min_x + x, min_y + y, min_z + z), voxel))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let left = self.block.voxels.len() - self.next;
(left, Some(left))
}
}
impl ExactSizeIterator for Iter<'_> {}
#[derive(Clone, Copy, Debug)]
pub struct Voxels {
host: Host,
}
impl Voxels {
#[cfg(feature = "unstable")]
pub(crate) const fn new(host: Host) -> Self {
Self { host }
}
pub fn read(&self, min: VoxelPos, size: [u32; 3]) -> Result<VoxelBox, WorldError> {
let count = abi::Voxel::read_count(min.to_array(), size).ok_or(WorldError::Refused)?;
let mut raw =
vec![abi::Voxel::default(); usize::try_from(count).map_err(|_| WorldError::Refused)?];
let corner = min.to_array();
let status = (self.host.api().voxel_read)(
self.host.handle(),
&raw const corner,
&raw const size,
raw.as_mut_ptr(),
count,
);
WorldError::from_status(status)?;
Ok(VoxelBox {
min,
size,
voxels: raw.into_iter().map(Voxel::from_abi).collect(),
})
}
pub fn sample(&self, at: VoxelPos) -> Result<Voxel, WorldError> {
let mut raw = abi::Voxel::default();
let corner = at.to_array();
let size = [1u32; 3];
let status = (self.host.api().voxel_read)(
self.host.handle(),
&raw const corner,
&raw const size,
&raw mut raw,
1,
);
WorldError::from_status(status).map(|()| Voxel::from_abi(raw))
}
}
impl Load<'_> {
#[must_use]
pub fn voxels(&mut self) -> Voxels {
Voxels { host: self.host() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_point_lies_in_the_voxel_below_it_on_every_axis() {
assert_eq!(
VoxelPos::from_metres([0.0, 0.49, 0.5]),
Some(VoxelPos::new(0, 0, 1))
);
assert_eq!(VoxelPos::from_metres([-0.1, 0.0, 0.0]), None);
assert_eq!(VoxelPos::from_metres([f64::NAN, 0.0, 0.0]), None);
assert_eq!(VoxelPos::from_metres([0.0, 1e12, 0.0]), None);
}
#[test]
fn every_material_survives_the_abi() {
for material in [
VoxelMaterial::Air,
VoxelMaterial::Terrain(9),
VoxelMaterial::Building(0x83),
VoxelMaterial::Unknown(4),
] {
let voxel = Voxel::new(material, 0x40);
assert_eq!(Voxel::from_abi(voxel.to_abi()), voxel);
}
}
#[test]
fn serde_refuses_a_box_whose_count_or_end_is_wrong() {
let short = VoxelBox {
min: VoxelPos::new(0, 0, 0),
size: [2, 1, 1],
voxels: vec![Voxel::default()],
};
let bytes = postcard::to_allocvec(&short).unwrap();
assert!(postcard::from_bytes::<VoxelBox>(&bytes).is_err());
let past_the_end = VoxelBox {
min: VoxelPos::new(abi::Voxel::END - 1, 0, 0),
size: [2, 1, 1],
voxels: vec![Voxel::default(); 2],
};
let bytes = postcard::to_allocvec(&past_the_end).unwrap();
assert!(postcard::from_bytes::<VoxelBox>(&bytes).is_err());
let wrapping = VoxelBox {
min: VoxelPos::new(u32::MAX, 0, 0),
size: [2, 1, 1],
voxels: vec![Voxel::default(); 2],
};
let bytes = postcard::to_allocvec(&wrapping).unwrap();
assert!(postcard::from_bytes::<VoxelBox>(&bytes).is_err());
let fine = VoxelBox {
min: VoxelPos::new(abi::Voxel::END - 2, 0, 0),
size: [2, 1, 1],
voxels: vec![Voxel::default(); 2],
};
let bytes = postcard::to_allocvec(&fine).unwrap();
assert_eq!(postcard::from_bytes::<VoxelBox>(&bytes).unwrap(), fine);
}
#[test]
#[cfg(feature = "unstable")]
fn a_block_fills_its_place_row_by_row() {
let mut whole = VoxelBox {
min: VoxelPos::new(10, 10, 10),
size: [4, 3, 2],
voxels: vec![Voxel::default(); 24],
};
let rock = |i: u8| Voxel::new(VoxelMaterial::Terrain(i + 1), 255);
let block = VoxelBox {
min: VoxelPos::new(11, 11, 10),
size: [2, 2, 2],
voxels: (0..8).map(rock).collect(),
};
whole.fill_from(&block);
for (at, voxel) in &block {
assert_eq!(whole.get(at), Some(voxel), "{at:?}");
}
assert_eq!(whole.iter().filter(|(_, v)| !v.is_air()).count(), 8);
}
#[test]
fn a_box_finds_each_voxel_where_iter_puts_it() {
let size = [3, 2, 4];
let voxels = (0..24)
.map(|i| Voxel::new(VoxelMaterial::Terrain(i), i))
.collect();
let block = VoxelBox {
min: VoxelPos::new(10, 20, 30),
size,
voxels,
};
assert_eq!(block.iter().len(), 24);
for (at, voxel) in &block {
assert_eq!(block.get(at), Some(voxel), "{at:?}");
}
assert_eq!(
block.get(VoxelPos::new(11, 21, 32)).map(Voxel::fill),
Some(1 + 3 + 2 * 6)
);
assert_eq!(block.get(VoxelPos::new(13, 20, 30)), None);
assert_eq!(block.get(VoxelPos::new(9, 20, 30)), None);
}
}