perovskite_core/
coordinates.rs

1// Copyright 2023 drey7925
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17use std::cmp::Ordering;
18use std::str::FromStr;
19use std::{
20    fmt::Debug,
21    hash::{Hash, Hasher},
22    ops::RangeInclusive,
23};
24
25use crate::protocol::coordinates::{WireBlockCoordinate, WireChunkCoordinate};
26use anyhow::{bail, ensure, Context, Result};
27use cgmath::{Angle, Deg};
28use rustc_hash::FxHasher;
29
30/// A 3D coordinate in the world.
31///
32/// Note that the impls of PartialOrd and Ord are meant for tiebreaking (e.g. for sorted data structures) and don't
33/// have a lot of semantic meaning on their own.
34#[derive(PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord)]
35pub struct BlockCoordinate {
36    pub x: i32,
37    pub y: i32,
38    pub z: i32,
39}
40
41impl Debug for BlockCoordinate {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.write_fmt(format_args!("[{}, {}, {}]", self.x, self.y, self.z))
44    }
45}
46impl BlockCoordinate {
47    pub const fn new(x: i32, y: i32, z: i32) -> Self {
48        Self { x, y, z }
49    }
50    #[inline]
51    pub const fn offset(&self) -> ChunkOffset {
52        // rem_euclid(16) result should always fit into u8.
53        ChunkOffset {
54            x: self.x.rem_euclid(16) as u8,
55            y: self.y.rem_euclid(16) as u8,
56            z: self.z.rem_euclid(16) as u8,
57        }
58    }
59    #[inline]
60    pub const fn chunk(&self) -> ChunkCoordinate {
61        ChunkCoordinate {
62            x: self.x.div_euclid(16),
63            y: self.y.div_euclid(16),
64            z: self.z.div_euclid(16),
65        }
66    }
67
68    pub fn try_delta(&self, x: i32, y: i32, z: i32) -> Option<BlockCoordinate> {
69        let x = self.x.checked_add(x)?;
70        let y = self.y.checked_add(y)?;
71        let z = self.z.checked_add(z)?;
72
73        Some(BlockCoordinate { x, y, z })
74    }
75}
76impl ToString for BlockCoordinate {
77    fn to_string(&self) -> String {
78        // TODO: Can this be optimized further?
79        let mut result = String::new();
80        result += self.x.to_string().as_str();
81        result += ",";
82        result += self.y.to_string().as_str();
83        result += ",";
84        result += self.z.to_string().as_str();
85        result
86    }
87}
88impl FromStr for BlockCoordinate {
89    type Err = anyhow::Error;
90
91    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
92        // Likewise, this probably merits optimization if it becomes hot
93        let pieces: Vec<_> = s.split(',').collect();
94        if pieces.len() != 3 {
95            bail!("Wrong number of components");
96        };
97        Ok(BlockCoordinate::new(
98            pieces[0].parse()?,
99            pieces[1].parse()?,
100            pieces[2].parse()?,
101        ))
102    }
103}
104
105impl From<BlockCoordinate> for cgmath::Vector3<f64> {
106    fn from(val: BlockCoordinate) -> Self {
107        cgmath::Vector3::new(val.x as f64, val.y as f64, val.z as f64)
108    }
109}
110
111impl From<BlockCoordinate> for WireBlockCoordinate {
112    fn from(value: BlockCoordinate) -> Self {
113        WireBlockCoordinate {
114            x: value.x,
115            y: value.y,
116            z: value.z,
117        }
118    }
119}
120impl From<&WireBlockCoordinate> for BlockCoordinate {
121    fn from(value: &WireBlockCoordinate) -> Self {
122        BlockCoordinate {
123            x: value.x,
124            y: value.y,
125            z: value.z,
126        }
127    }
128}
129impl From<WireBlockCoordinate> for BlockCoordinate {
130    fn from(value: WireBlockCoordinate) -> Self {
131        (&value).into()
132    }
133}
134
135#[inline]
136fn try_convert(value: f64) -> Result<i32> {
137    ensure!(value.is_finite(), "val was not finite");
138    ensure!(
139        value <= (i32::MAX as f64) && value >= (i32::MIN as f64),
140        "Value is out of bounds as i32"
141    );
142    Ok(value.round() as i32)
143}
144
145impl TryFrom<cgmath::Vector3<f64>> for BlockCoordinate {
146    type Error = anyhow::Error;
147
148    fn try_from(value: cgmath::Vector3<f64>) -> std::result::Result<Self, Self::Error> {
149        Ok(BlockCoordinate {
150            x: try_convert(value.x)?,
151            y: try_convert(value.y)?,
152            z: try_convert(value.z)?,
153        })
154    }
155}
156
157/// Represents an offset of a block within a chunk.
158///
159/// The most cache-friendly iteration order has x in the outer loop, z in the middle loop, and y in the innermost loop
160#[derive(PartialEq, Eq, Hash, Clone, Copy)]
161pub struct ChunkOffset {
162    pub x: u8,
163    pub y: u8,
164    pub z: u8,
165}
166impl ChunkOffset {
167    pub const fn new(x: u8, y: u8, z: u8) -> Self {
168        Self { x, y, z }
169    }
170
171    #[cfg(debug_assertions)]
172    #[inline(always)]
173    fn debug_check(&self) {
174        debug_assert!(self.x < 16);
175        debug_assert!(self.y < 16);
176        debug_assert!(self.z < 16);
177    }
178
179    #[cfg(not(debug_assertions))]
180    #[inline(always)]
181    fn debug_check(&self) {}
182
183    #[inline]
184    pub fn as_index(&self) -> usize {
185        self.debug_check();
186        // The unusual order here is to provide a cache-friendly iteration order
187        // for innermost loops that traverse vertically (since that is a common pattern for
188        // lighting calculations).
189        256 * (self.x as usize) + 16 * (self.z as usize) + (self.y as usize)
190    }
191    #[inline]
192    pub fn from_index(index: usize) -> ChunkOffset {
193        assert!(index < 4096);
194        ChunkOffset {
195            y: (index % 16) as u8,
196            z: ((index / 16) % 16) as u8,
197            x: ((index / 256) % 16) as u8,
198        }
199    }
200    pub fn try_delta(&self, x: i8, y: i8, z: i8) -> Option<ChunkOffset> {
201        let x = self.x as i8 + x;
202        let y = self.y as i8 + y;
203        let z = self.z as i8 + z;
204        if !(0..16).contains(&x) || !(0..16).contains(&y) || !(0..16).contains(&z) {
205            None
206        } else {
207            Some(ChunkOffset {
208                x: x as u8,
209                y: y as u8,
210                z: z as u8,
211            })
212        }
213    }
214}
215impl Debug for ChunkOffset {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        f.write_fmt(format_args!("Δ({}, {}, {})", self.x, self.y, self.z))
218    }
219}
220impl PartialOrd<Self> for ChunkOffset {
221    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
222        Some(self.cmp(other))
223    }
224}
225
226impl Ord for ChunkOffset {
227    fn cmp(&self, other: &Self) -> Ordering {
228        self.x
229            .cmp(&other.x)
230            .then(self.z.cmp(&other.z))
231            .then(self.y.cmp(&other.y))
232    }
233}
234
235/// Represents a location of a map chunk.
236///
237/// Each coordinate spans 16 blocks, covering the range [chunk_coord.x * 16, chunk_coord.x * 16 + 15].
238/// e.g. chunk 0,1,2 covers x:[0, 15], y:[16, 31], z:[32, 47]
239#[derive(PartialEq, Eq, Hash, Clone, Copy)]
240#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
241pub struct ChunkCoordinate {
242    pub x: i32,
243    pub y: i32,
244    pub z: i32,
245}
246impl ChunkCoordinate {
247    pub fn new(x: i32, y: i32, z: i32) -> Self {
248        let result = Self { x, y, z };
249        debug_assert!(result.is_in_bounds());
250        result
251    }
252
253    pub fn try_new(x: i32, y: i32, z: i32) -> Option<Self> {
254        let result = Self { x, y, z };
255        if result.is_in_bounds() {
256            Some(result)
257        } else {
258            None
259        }
260    }
261
262    pub fn bounds_check(x: i32, y: i32, z: i32) -> bool {
263        let result = Self { x, y, z };
264        result.is_in_bounds()
265    }
266
267    /// Returns a new block coordinate with the given offset within this chunk.
268    #[inline]
269    pub fn with_offset(&self, offset: ChunkOffset) -> BlockCoordinate {
270        offset.debug_check();
271        BlockCoordinate {
272            x: self.x * 16 + (offset.x as i32),
273            y: self.y * 16 + (offset.y as i32),
274            z: self.z * 16 + (offset.z as i32),
275        }
276    }
277    /// Returns the Manhattan distance between the two coordinates
278    pub fn manhattan_distance(&self, other: ChunkCoordinate) -> u32 {
279        self.x
280            .abs_diff(other.x)
281            .saturating_add(self.y.abs_diff(other.y))
282            .saturating_add(self.z.abs_diff(other.z))
283    }
284    /// Returns the L-infinity (max distance along all three dimensions) norm between the two coordinates
285    pub fn l_infinity_norm_distance(&self, other: ChunkCoordinate) -> u32 {
286        self.x
287            .abs_diff(other.x)
288            .max(self.y.abs_diff(other.y))
289            .max(self.z.abs_diff(other.z))
290    }
291    /// Returns true if the coordinate is in-bounds. Because *block* coordinates need to
292    /// fit into an i32, not every possible chunk coordinate is actually in-bounds.
293    pub fn is_in_bounds(&self) -> bool {
294        const BOUNDS_RANGE: RangeInclusive<i32> = (i32::MIN / 16)..=(i32::MAX / 16);
295        BOUNDS_RANGE.contains(&self.x)
296            && BOUNDS_RANGE.contains(&self.y)
297            && BOUNDS_RANGE.contains(&self.z)
298    }
299    /// Adds the given offset to the coordinate, and returns it, if it is in-bounds.
300    pub fn try_delta(&self, x: i32, y: i32, z: i32) -> Option<ChunkCoordinate> {
301        let x = self.x.checked_add(x)?;
302        let y = self.y.checked_add(y)?;
303        let z = self.z.checked_add(z)?;
304        let candidate = ChunkCoordinate { x, y, z };
305        if candidate.is_in_bounds() {
306            Some(candidate)
307        } else {
308            None
309        }
310    }
311    /// Convenience helper to hash a ChunkCoordinate to a u64.
312    /// The result is not guaranteed to be the same between versions or runs,
313    /// and hence should not be persisted.
314    pub fn hash_u64(&self) -> u64 {
315        let mut hasher = FxHasher::default();
316        self.hash(&mut hasher);
317        hasher.finish()
318    }
319    /// A hash function for ChunkCoordinate that keeps close coordinates together,
320    /// and does not consider the y coordinate. All chunks in a vertical stack are
321    /// guaranteed to have the same hash *within a process*. No guarantees are made
322    /// for serialized hashes.
323    pub fn coarse_hash_no_y(&self) -> u64 {
324        let mut hasher = FxHasher::default();
325        (self.x >> 4).hash(&mut hasher);
326        (self.z >> 4).hash(&mut hasher);
327        hasher.finish()
328    }
329}
330impl Debug for ChunkCoordinate {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        f.write_fmt(format_args!("chunk[{}, {}, {}]", self.x, self.y, self.z))
333    }
334}
335
336impl From<ChunkCoordinate> for WireChunkCoordinate {
337    fn from(value: ChunkCoordinate) -> Self {
338        WireChunkCoordinate {
339            x: value.x,
340            y: value.y,
341            z: value.z,
342        }
343    }
344}
345impl From<&WireChunkCoordinate> for ChunkCoordinate {
346    fn from(value: &WireChunkCoordinate) -> Self {
347        ChunkCoordinate {
348            x: value.x,
349            y: value.y,
350            z: value.z,
351        }
352    }
353}
354impl From<WireChunkCoordinate> for ChunkCoordinate {
355    fn from(value: WireChunkCoordinate) -> Self {
356        (&value).into()
357    }
358}
359
360impl TryFrom<cgmath::Vector3<f64>> for crate::protocol::coordinates::Vec3D {
361    type Error = anyhow::Error;
362
363    fn try_from(value: cgmath::Vector3<f64>) -> std::result::Result<Self, Self::Error> {
364        ensure!(
365            value.x.is_finite() && value.y.is_finite() && value.z.is_finite(),
366            "vec3D contained NaN or inf"
367        );
368        Ok(crate::protocol::coordinates::Vec3D {
369            x: value.x,
370            y: value.y,
371            z: value.z,
372        })
373    }
374}
375impl TryFrom<&crate::protocol::coordinates::Vec3D> for cgmath::Vector3<f64> {
376    type Error = anyhow::Error;
377
378    fn try_from(
379        value: &crate::protocol::coordinates::Vec3D,
380    ) -> std::result::Result<Self, Self::Error> {
381        ensure!(
382            value.x.is_finite() && value.y.is_finite() && value.z.is_finite(),
383            "vec3D contained NaN or inf"
384        );
385        Ok(cgmath::Vector3 {
386            x: value.x,
387            y: value.y,
388            z: value.z,
389        })
390    }
391}
392impl TryFrom<crate::protocol::coordinates::Vec3D> for cgmath::Vector3<f64> {
393    type Error = anyhow::Error;
394
395    fn try_from(value: crate::protocol::coordinates::Vec3D) -> Result<Self> {
396        (&value).try_into()
397    }
398}
399
400impl TryFrom<cgmath::Vector3<f32>> for crate::protocol::coordinates::Vec3F {
401    type Error = anyhow::Error;
402
403    fn try_from(value: cgmath::Vector3<f32>) -> std::result::Result<Self, Self::Error> {
404        ensure!(
405            value.x.is_finite() && value.y.is_finite() && value.z.is_finite(),
406            "vec3D contained NaN or inf"
407        );
408        Ok(crate::protocol::coordinates::Vec3F {
409            x: value.x,
410            y: value.y,
411            z: value.z,
412        })
413    }
414}
415impl TryFrom<&crate::protocol::coordinates::Vec3F> for cgmath::Vector3<f32> {
416    type Error = anyhow::Error;
417
418    fn try_from(
419        value: &crate::protocol::coordinates::Vec3F,
420    ) -> std::result::Result<Self, Self::Error> {
421        ensure!(
422            value.x.is_finite() && value.y.is_finite() && value.z.is_finite(),
423            "vec3D contained NaN or inf"
424        );
425        Ok(cgmath::Vector3 {
426            x: value.x,
427            y: value.y,
428            z: value.z,
429        })
430    }
431}
432impl TryFrom<crate::protocol::coordinates::Vec3F> for cgmath::Vector3<f32> {
433    type Error = anyhow::Error;
434
435    fn try_from(value: crate::protocol::coordinates::Vec3F) -> Result<Self> {
436        (&value).try_into()
437    }
438}
439
440#[derive(Copy, Clone, Debug)]
441pub struct PlayerPositionUpdate {
442    // The position, blocks
443    pub position: cgmath::Vector3<f64>,
444    // The velocity, blocks per second
445    pub velocity: cgmath::Vector3<f64>,
446    // The facing direction, normalized, in degrees. (azimuth, elevation)
447    pub face_direction: (f64, f64),
448}
449impl PlayerPositionUpdate {
450    pub fn to_proto(&self) -> Result<crate::protocol::game_rpc::PlayerPosition> {
451        Ok(crate::protocol::game_rpc::PlayerPosition {
452            position: Some(self.position.try_into()?),
453            velocity: Some(self.velocity.try_into()?),
454            face_direction: Some(crate::protocol::coordinates::Angles {
455                deg_azimuth: self.face_direction.0,
456                deg_elevation: self.face_direction.1,
457            }),
458        })
459    }
460    /// The direction the player is facing, Y-up
461    pub fn face_unit_vector(&self) -> cgmath::Vector3<f64> {
462        let (sin_az, cos_az) = Deg(self.face_direction.0).sin_cos();
463        let (sin_el, cos_el) = Deg(self.face_direction.1).sin_cos();
464        cgmath::vec3(cos_el * sin_az, sin_el, cos_el * cos_az)
465    }
466}
467impl TryFrom<&crate::protocol::game_rpc::PlayerPosition> for PlayerPositionUpdate {
468    type Error = anyhow::Error;
469
470    fn try_from(value: &crate::protocol::game_rpc::PlayerPosition) -> Result<Self> {
471        let angles = value.face_direction.as_ref().context("missing angles")?;
472        Ok(PlayerPositionUpdate {
473            position: value
474                .position
475                .as_ref()
476                .context("Missing position")?
477                .try_into()?,
478            velocity: value
479                .velocity
480                .as_ref()
481                .context("Missing velocity")?
482                .try_into()?,
483            face_direction: (angles.deg_azimuth, angles.deg_elevation),
484        })
485    }
486}