Skip to main content

enshwick/map/
fog.rs

1//! What the player has uncovered of the map, on the client.
2
3use crate::abi::{FogInfo, Header, Status};
4use crate::{Host, Load, Off};
5
6/// The client's fog of war: how much of each part of the map the player
7/// has uncovered, the game's `FogOfWar`. Only the player's own machine
8/// has it, from the save of the world they play.
9///
10/// The loader copies the fog as the game uncovers it, so a mod reads it
11/// from any thread. Pixels are square, one byte each, from 0 (hidden) to
12/// 255 (uncovered); row 0 is the world's high z edge, and x grows with
13/// world x.
14///
15/// # Examples
16///
17/// ```
18/// use core::ops::ControlFlow;
19/// use std::time::Duration;
20///
21/// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
22///     let fog = load.fog_of_war()?;
23///     load.scheduler()?.every(Duration::from_secs(5), move || {
24///         if let Ok(Some(seen)) = fog.coverage([1_200.0, 40.0, 3_400.0]) {
25///             enshwick::debug!("coverage {seen}");
26///         }
27///         ControlFlow::Continue(())
28///     });
29///     Ok(())
30/// }
31/// ```
32#[derive(Clone, Copy, Debug)]
33pub struct FogOfWar {
34    host: Host,
35}
36
37/// The fog's size, scale and how often it changed, as one read found it.
38///
39/// # Examples
40///
41/// ```
42/// use enshwick::map::FogState;
43///
44/// fn corner(state: FogState) -> Option<[u32; 2]> {
45///     state.pixel_of([0.0, 0.0, state.size_z - 1.0])
46/// }
47/// ```
48#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct FogState {
50    /// The image's width in pixels.
51    pub width: u32,
52    /// The image's height in pixels.
53    pub height: u32,
54    /// The metres one pixel covers along each axis.
55    pub metres_per_pixel: f64,
56    /// The world's z size the fog covers, in metres.
57    pub size_z: f64,
58    /// Grows whenever pixels changed: a mod that compares it reads again
59    /// only when it moved.
60    pub version: u64,
61    /// Changes with each world the client enters; the copy of a world is
62    /// gone once the client leaves it.
63    pub session: u64,
64}
65
66impl FogState {
67    fn from_abi(info: &FogInfo) -> Self {
68        Self {
69            width: info.width,
70            height: info.height,
71            metres_per_pixel: f64::from(info.metres_per_pixel),
72            size_z: f64::from(info.size_z),
73            version: info.version,
74            session: info.session,
75        }
76    }
77
78    /// The pixel that holds `position`, in metres as
79    /// [`World::position`](crate::world::World::position) gives it; `None`
80    /// outside the fog.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use enshwick::map::FogState;
86    ///
87    /// let state = FogState {
88    ///     width: 1280,
89    ///     height: 1280,
90    ///     metres_per_pixel: 8.0,
91    ///     size_z: 10_240.0,
92    ///     version: 0,
93    ///     session: 1,
94    /// };
95    /// assert_eq!(state.pixel_of([12.0, 50.0, 10_230.0]), Some([1, 1]));
96    /// assert_eq!(state.pixel_of([-1.0, 0.0, 0.0]), None);
97    /// ```
98    #[must_use]
99    pub fn pixel_of(&self, position: [f64; 3]) -> Option<[u32; 2]> {
100        if self.metres_per_pixel <= 0.0 {
101            return None;
102        }
103        let x = (position[0] / self.metres_per_pixel).floor();
104        let y = ((self.size_z - position[2]) / self.metres_per_pixel).floor();
105        let inside = |value: f64, limit: u32| value >= 0.0 && value < f64::from(limit);
106        if !inside(x, self.width) || !inside(y, self.height) {
107            return None;
108        }
109        #[expect(
110            clippy::cast_possible_truncation,
111            clippy::cast_sign_loss,
112            reason = "both lie in 0..limit of a u32, checked above"
113        )]
114        Some([x as u32, y as u32])
115    }
116}
117
118/// A box of the fog, as [`FogOfWar::read`] copied it.
119///
120/// # Examples
121///
122/// ```
123/// use enshwick::map::FogOfWar;
124///
125/// fn uncovered(fog: FogOfWar) -> usize {
126///     let Ok(state) = fog.state() else { return 0 };
127///     fog.read([0, 0], [state.width, state.height])
128///         .map_or(0, |image| image.coverage().iter().filter(|&&c| c == 255).count())
129/// }
130/// ```
131#[derive(Clone, Debug, PartialEq)]
132pub struct FogImage {
133    state: FogState,
134    min: [u32; 2],
135    size: [u32; 2],
136    coverage: Vec<u8>,
137}
138
139impl FogImage {
140    /// The fog as the copy found it.
141    ///
142    /// # Examples
143    ///
144    /// See [`FogImage`].
145    #[must_use]
146    pub const fn state(&self) -> FogState {
147        self.state
148    }
149
150    /// The box's corner, in pixels.
151    ///
152    /// # Examples
153    ///
154    /// See [`FogImage`].
155    #[must_use]
156    pub const fn min(&self) -> [u32; 2] {
157        self.min
158    }
159
160    /// The box's width and height, in pixels.
161    ///
162    /// # Examples
163    ///
164    /// See [`FogImage`].
165    #[must_use]
166    pub const fn size(&self) -> [u32; 2] {
167        self.size
168    }
169
170    /// The box's coverage, row by row.
171    ///
172    /// # Examples
173    ///
174    /// See [`FogImage`].
175    #[must_use]
176    pub fn coverage(&self) -> &[u8] {
177        &self.coverage
178    }
179
180    /// The coverage of fog pixel `pixel`; `None` outside the box.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use enshwick::map::FogImage;
186    ///
187    /// fn first(image: &FogImage) -> Option<u8> {
188    ///     image.at(image.min())
189    /// }
190    /// ```
191    #[must_use]
192    pub fn at(&self, pixel: [u32; 2]) -> Option<u8> {
193        let x = pixel[0]
194            .checked_sub(self.min[0])
195            .filter(|&x| x < self.size[0])?;
196        let y = pixel[1]
197            .checked_sub(self.min[1])
198            .filter(|&y| y < self.size[1])?;
199        self.coverage
200            .get(y as usize * self.size[0] as usize + x as usize)
201            .copied()
202    }
203}
204
205/// Why the fog does not read.
206///
207/// # Examples
208///
209/// ```
210/// use enshwick::map::FogError;
211///
212/// assert_eq!(FogError::Missing.to_string(), "no fog of war to read");
213/// ```
214#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
215#[non_exhaustive]
216pub enum FogError {
217    /// The client is in no world, this game build lacks the fog, or the
218    /// loader stopped copying it after a fault.
219    #[error("no fog of war to read")]
220    Missing,
221    /// The box leaves the fog.
222    #[error("the box leaves the fog of war")]
223    Outside,
224    /// The loader refused the read.
225    #[error("the loader refused the fog read")]
226    Refused,
227}
228
229impl FogError {
230    fn check(status: Status) -> Result<(), Self> {
231        match status {
232            Status::OK => Ok(()),
233            Status::MISSING => Err(Self::Missing),
234            Status::OUTSIDE_WORLD => Err(Self::Outside),
235            _ => Err(Self::Refused),
236        }
237    }
238}
239
240impl FogOfWar {
241    pub(crate) fn open(load: &Load<'_>) -> Result<Self, Off> {
242        let host = load.host();
243        let status = (host.api().fog_open)(host.handle());
244        crate::load::ok(status, Self { host }, || "the fog of war".to_owned())
245    }
246
247    /// The fog's size, scale and version.
248    ///
249    /// # Errors
250    ///
251    /// [`FogError::Missing`] while the client is in no world, on a game
252    /// build without the fog, or after the loader stopped copying it.
253    ///
254    /// # Examples
255    ///
256    /// See [`FogImage`].
257    pub fn state(self) -> Result<FogState, FogError> {
258        let mut info = FogInfo::EMPTY;
259        let status = (self.host.api().fog_read)(
260            self.host.handle(),
261            &raw mut info,
262            0,
263            0,
264            0,
265            0,
266            core::ptr::null_mut(),
267            0,
268        );
269        FogError::check(status)?;
270        Ok(FogState::from_abi(&info))
271    }
272
273    /// Copies the box of `size` pixels at `min`.
274    ///
275    /// # Errors
276    ///
277    /// [`FogError::Outside`] for a box past the fog; otherwise as
278    /// [`FogOfWar::state`].
279    ///
280    /// # Examples
281    ///
282    /// See [`FogImage`].
283    pub fn read(self, min: [u32; 2], size: [u32; 2]) -> Result<FogImage, FogError> {
284        let len = (size[0] as usize)
285            .checked_mul(size[1] as usize)
286            .ok_or(FogError::Outside)?;
287        let capacity = u32::try_from(len).map_err(|_| FogError::Outside)?;
288        let mut coverage = vec![0u8; len];
289        let mut info = FogInfo {
290            header: Header::of::<FogInfo>(),
291            ..FogInfo::EMPTY
292        };
293        let status = (self.host.api().fog_read)(
294            self.host.handle(),
295            &raw mut info,
296            min[0],
297            min[1],
298            size[0],
299            size[1],
300            coverage.as_mut_ptr(),
301            capacity,
302        );
303        FogError::check(status)?;
304        Ok(FogImage {
305            state: FogState::from_abi(&info),
306            min,
307            size,
308            coverage,
309        })
310    }
311
312    /// The coverage where `position` is, in metres; `None` outside the fog.
313    ///
314    /// # Errors
315    ///
316    /// As [`FogOfWar::state`].
317    ///
318    /// # Examples
319    ///
320    /// See [`FogOfWar`].
321    pub fn coverage(self, position: [f64; 3]) -> Result<Option<u8>, FogError> {
322        let Some(pixel) = self.state()?.pixel_of(position) else {
323            return Ok(None);
324        };
325        match self.read(pixel, [1, 1]) {
326            Ok(image) => Ok(image.at(pixel)),
327            Err(FogError::Outside) => Ok(None),
328            Err(why) => Err(why),
329        }
330    }
331}