use crate::abi::{FogInfo, Header, Status};
use crate::{Host, Load, Off};
#[derive(Clone, Copy, Debug)]
pub struct FogOfWar {
host: Host,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FogState {
pub width: u32,
pub height: u32,
pub metres_per_pixel: f64,
pub size_z: f64,
pub version: u64,
pub session: u64,
}
impl FogState {
fn from_abi(info: &FogInfo) -> Self {
Self {
width: info.width,
height: info.height,
metres_per_pixel: f64::from(info.metres_per_pixel),
size_z: f64::from(info.size_z),
version: info.version,
session: info.session,
}
}
#[must_use]
pub fn pixel_of(&self, position: [f64; 3]) -> Option<[u32; 2]> {
if self.metres_per_pixel <= 0.0 {
return None;
}
let x = (position[0] / self.metres_per_pixel).floor();
let y = ((self.size_z - position[2]) / self.metres_per_pixel).floor();
let inside = |value: f64, limit: u32| value >= 0.0 && value < f64::from(limit);
if !inside(x, self.width) || !inside(y, self.height) {
return None;
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "both lie in 0..limit of a u32, checked above"
)]
Some([x as u32, y as u32])
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FogImage {
state: FogState,
min: [u32; 2],
size: [u32; 2],
coverage: Vec<u8>,
}
impl FogImage {
#[must_use]
pub const fn state(&self) -> FogState {
self.state
}
#[must_use]
pub const fn min(&self) -> [u32; 2] {
self.min
}
#[must_use]
pub const fn size(&self) -> [u32; 2] {
self.size
}
#[must_use]
pub fn coverage(&self) -> &[u8] {
&self.coverage
}
#[must_use]
pub fn at(&self, pixel: [u32; 2]) -> Option<u8> {
let x = pixel[0]
.checked_sub(self.min[0])
.filter(|&x| x < self.size[0])?;
let y = pixel[1]
.checked_sub(self.min[1])
.filter(|&y| y < self.size[1])?;
self.coverage
.get(y as usize * self.size[0] as usize + x as usize)
.copied()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum FogError {
#[error("no fog of war to read")]
Missing,
#[error("the box leaves the fog of war")]
Outside,
#[error("the loader refused the fog read")]
Refused,
}
impl FogError {
fn check(status: Status) -> Result<(), Self> {
match status {
Status::OK => Ok(()),
Status::MISSING => Err(Self::Missing),
Status::OUTSIDE_WORLD => Err(Self::Outside),
_ => Err(Self::Refused),
}
}
}
impl FogOfWar {
pub(crate) fn open(load: &Load<'_>) -> Result<Self, Off> {
let host = load.host();
let status = (host.api().fog_open)(host.handle());
crate::load::ok(status, Self { host }, || "the fog of war".to_owned())
}
pub fn state(self) -> Result<FogState, FogError> {
let mut info = FogInfo::EMPTY;
let status = (self.host.api().fog_read)(
self.host.handle(),
&raw mut info,
0,
0,
0,
0,
core::ptr::null_mut(),
0,
);
FogError::check(status)?;
Ok(FogState::from_abi(&info))
}
pub fn read(self, min: [u32; 2], size: [u32; 2]) -> Result<FogImage, FogError> {
let len = (size[0] as usize)
.checked_mul(size[1] as usize)
.ok_or(FogError::Outside)?;
let capacity = u32::try_from(len).map_err(|_| FogError::Outside)?;
let mut coverage = vec![0u8; len];
let mut info = FogInfo {
header: Header::of::<FogInfo>(),
..FogInfo::EMPTY
};
let status = (self.host.api().fog_read)(
self.host.handle(),
&raw mut info,
min[0],
min[1],
size[0],
size[1],
coverage.as_mut_ptr(),
capacity,
);
FogError::check(status)?;
Ok(FogImage {
state: FogState::from_abi(&info),
min,
size,
coverage,
})
}
pub fn coverage(self, position: [f64; 3]) -> Result<Option<u8>, FogError> {
let Some(pixel) = self.state()?.pixel_of(position) else {
return Ok(None);
};
match self.read(pixel, [1, 1]) {
Ok(image) => Ok(image.at(pixel)),
Err(FogError::Outside) => Ok(None),
Err(why) => Err(why),
}
}
}