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}