diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef0e4b8..15c6deb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ Unreleased - Add `s7_score_cycle_at_tick` seasonal constant function to reflect the reversed score cycle in season 7 +- Add indexing implementations for `RoomCoordinate` and `RoomXY` as well as `XMajor` / `YMajor` + wrapper types to control which indexing approach is used; switch to using these for + `LocalCostMatrix` and `LocalRoomTerrain` +- Add `RoomOffset` type representing a difference between coordinates and associated functions + for manipulating `RoomCoordinate` and `RoomXY` 0.22.0 (2024-08-27) =================== diff --git a/src/constants/extra.rs b/src/constants/extra.rs index 10d9e9b3..5e479784 100644 --- a/src/constants/extra.rs +++ b/src/constants/extra.rs @@ -270,10 +270,15 @@ pub const ROOM_VISUAL_PER_ROOM_SIZE_LIMIT: u32 = 500 * 1024; /// [`Room`]: crate::objects::Room pub const ROOM_SIZE: u8 = 50; +/// ['ROOM_SIZE'] cast into a `usize`, for indexing convenience. +/// +/// ['ROOM_SIZE']: self::ROOM_SIZE +pub const ROOM_USIZE: usize = ROOM_SIZE as usize; + /// The number of total tiles in each [`Room`] in the game /// /// [`Room`]: crate::objects::Room -pub const ROOM_AREA: usize = (ROOM_SIZE as usize) * (ROOM_SIZE as usize); +pub const ROOM_AREA: usize = ROOM_USIZE * ROOM_USIZE; /// Owner username of hostile non-player structures and creeps which occupy /// sector center rooms. diff --git a/src/local/cost_matrix.rs b/src/local/cost_matrix.rs index ede28ba0..4ec88b69 100644 --- a/src/local/cost_matrix.rs +++ b/src/local/cost_matrix.rs @@ -8,7 +8,7 @@ use crate::{ traits::{CostMatrixGet, CostMatrixSet}, }; -use super::{linear_index_to_xy, xy_to_linear_index, Position, RoomXY}; +use super::{linear_index_to_xy, Position, RoomXY, XMajor}; /// A matrix of pathing costs for a room, stored in Rust memory. /// @@ -121,19 +121,29 @@ impl From<&CostMatrix> for LocalCostMatrix { } } +impl AsRef> for LocalCostMatrix { + fn as_ref(&self) -> &XMajor { + XMajor::from_flat_ref(&self.bits) + } +} + +impl AsMut> for LocalCostMatrix { + fn as_mut(&mut self) -> &mut XMajor { + XMajor::from_flat_mut(&mut self.bits) + } +} + impl Index for LocalCostMatrix { type Output = u8; fn index(&self, xy: RoomXY) -> &Self::Output { - // SAFETY: RoomXY is always a valid coordinate. - unsafe { self.bits.get_unchecked(xy_to_linear_index(xy)) } + &self.bits[xy.x][xy.y] } } impl IndexMut for LocalCostMatrix { fn index_mut(&mut self, xy: RoomXY) -> &mut Self::Output { - // SAFETY: RoomXY is always a valid coordinate. - unsafe { self.bits.get_unchecked_mut(xy_to_linear_index(xy)) } + &mut self.bits[xy.x][xy.y] } } diff --git a/src/local/object_id/raw.rs b/src/local/object_id/raw.rs index 96bd1907..9de01e01 100644 --- a/src/local/object_id/raw.rs +++ b/src/local/object_id/raw.rs @@ -69,7 +69,7 @@ impl Serialize for RawObjectId { struct RawObjectIdVisitor; -impl<'de> Visitor<'de> for RawObjectIdVisitor { +impl Visitor<'_> for RawObjectIdVisitor { type Value = RawObjectId; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { diff --git a/src/local/room_coordinate.rs b/src/local/room_coordinate.rs index 936ef202..51dc66b2 100644 --- a/src/local/room_coordinate.rs +++ b/src/local/room_coordinate.rs @@ -1,8 +1,14 @@ -use std::{error::Error, fmt}; +use std::{ + error::Error, + fmt, + hint::assert_unchecked, + ops::{Index, IndexMut, Neg, Sub}, +}; use serde::{Deserialize, Serialize}; +use wasm_bindgen::UnwrapThrowExt; -use crate::constants::ROOM_SIZE; +use crate::constants::{ROOM_AREA, ROOM_SIZE, ROOM_USIZE}; #[derive(Debug, Clone, Copy)] pub struct OutOfBoundsError(pub u8); @@ -16,14 +22,19 @@ impl fmt::Display for OutOfBoundsError { impl Error for OutOfBoundsError {} /// An X or Y coordinate in a room, restricted to the valid range of -/// coordinates. +/// coordinates. This restriction can be used in safety constraints, and is +/// enforced by all safe `RoomCoordinate` constructors. #[derive( Debug, Hash, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, )] #[serde(try_from = "u8", into = "u8")] +#[repr(transparent)] pub struct RoomCoordinate(u8); impl RoomCoordinate { + pub const MAX: Self = Self(ROOM_SIZE - 1); + pub const MIN: Self = Self(0); + /// Create a `RoomCoordinate` from a `u8`, returning an error if the /// coordinate is not in the valid room size range #[inline] @@ -50,6 +61,18 @@ impl RoomCoordinate { RoomCoordinate(coord) } + /// Provides a hint to the compiler that the contained `u8` is smaller than + /// `ROOM_SIZE`. Allows for better optimized safe code that uses this + /// property. + pub fn assume_bounds_constraint(self) { + debug_assert!(self.0 < ROOM_SIZE); + // SAFETY: It is only safe to construct `RoomCoordinate` when self.0 < + // ROOM_SIZE. + unsafe { + assert_unchecked(self.0 < ROOM_SIZE); + } + } + /// Get the integer value of this coordinate pub const fn u8(self) -> u8 { self.0 @@ -77,17 +100,22 @@ impl RoomCoordinate { /// assert_eq!(forty_nine.checked_add(1), None); /// ``` pub fn checked_add(self, rhs: i8) -> Option { - match (self.0 as i8).checked_add(rhs) { - Some(result) => match result { - // less than 0 - i8::MIN..=-1 => None, - // greater than 49 - 50..=i8::MAX => None, - // SAFETY: we've checked that this coord is in the valid range - c => Some(unsafe { RoomCoordinate::unchecked_new(c as u8) }), - }, - None => None, - } + self.assume_bounds_constraint(); + // Why this works, assuming ROOM_SIZE < i8::MAX + 1 == 128 and ignoring the + // test: + // - if rhs < 0: the smallest value this can produce is -128, which casted to + // u8 is 128. The closer rhs is to 0, the larger the cast sum is. So if + // ROOM_SIZE <= i8::MAX, any underflow will fail the x < ROOM_SIZE check. + // - if rhs > 0: as long as self.0 <= i8::MAX, self.0 + rhs <= 2 * i8::MAX < + // 256, so there isn't unsigned overflow. + RoomCoordinate::new(self.0.wrapping_add_signed(rhs)).ok() + } + + /// [`checked_add`](Self::checked_add) that accepts a [`RoomOffset`]. + pub fn checked_add_offset(self, rhs: RoomOffset) -> Option { + self.assume_bounds_constraint(); + rhs.assume_bounds_constraint(); + RoomCoordinate::new(self.0.wrapping_add_signed(rhs.0)).ok() } /// Get the coordinate adjusted by a certain value, saturating at the edges @@ -108,21 +136,139 @@ impl RoomCoordinate { /// assert_eq!(forty_nine.saturating_add(i8::MIN), zero); /// ``` pub fn saturating_add(self, rhs: i8) -> RoomCoordinate { - let result = match (self.0 as i8).saturating_add(rhs) { - // less than 0, saturate to 0 - i8::MIN..=-1 => 0, - // greater than 49, saturate to 49 - 50..=i8::MAX => ROOM_SIZE - 1, - c => c as u8, - }; - // SAFETY: we've ensured that this coord is in the valid range - unsafe { RoomCoordinate::unchecked_new(result) } + self.assume_bounds_constraint(); + let (res, overflow) = self.0.overflowing_add_signed(rhs); + if overflow { + RoomCoordinate::MIN + } else { + // Optimizer will see the return is always Ok + RoomCoordinate::new(res.min(ROOM_SIZE - 1)).unwrap_throw() + } + } + + /// [`saturating_add`](Self::saturating_add) that accepts a [`RoomOffset`]. + pub fn saturating_add_offset(self, rhs: RoomOffset) -> Self { + self.assume_bounds_constraint(); + rhs.assume_bounds_constraint(); + let result = (self.0 as i8 + rhs.0).clamp(0, ROOM_SIZE_I8 - 1); + RoomCoordinate::new(result as u8).unwrap_throw() + } + + /// Get the coordinate adjusted by a certain value, wrapping around ta the + /// edges of the room if the result would be outside of the valid range. + /// Returns a [`bool`] indicating whether there was wrapping. + /// + /// Can be used to e.g. implement addition for + /// [`Position`](crate::Position)s. + /// + /// Example usage: + /// + /// ``` + /// use screeps::local::RoomCoordinate; + /// + /// assert_eq!( + /// RoomCoordinate::MIN.overflowing_add(1), + /// (RoomCoordinate::new(1).unwrap(), false) + /// ); + /// assert_eq!( + /// RoomCoordinate::MIN.overflowing_add(-1), + /// (RoomCoordinate::MAX, true) + /// ); + /// assert_eq!( + /// RoomCoordinate::MAX.overflowing_add(1), + /// (RoomCoordinate::MIN, true) + /// ); + /// ``` + pub fn overflowing_add(self, rhs: i8) -> (RoomCoordinate, bool) { + self.assume_bounds_constraint(); + let raw = self.0 as i16 + rhs as i16; + if raw >= ROOM_SIZE as i16 { + ( + RoomCoordinate::new((raw % ROOM_SIZE as i16) as u8).unwrap_throw(), + true, + ) + } else if raw < 0 { + ( + RoomCoordinate::new(((raw + 150) % ROOM_SIZE as i16) as u8).unwrap_throw(), + true, + ) + } else { + (RoomCoordinate::new(raw as u8).unwrap_throw(), false) + } + } + + /// [`overflowing_add`](Self::overflowing_add) that accepts a + /// [`RoomOffset`]. + pub fn overflowing_add_offset(self, rhs: RoomOffset) -> (RoomCoordinate, bool) { + self.assume_bounds_constraint(); + rhs.assume_bounds_constraint(); + let raw = self.0 as i8 + rhs.0; + if raw >= ROOM_SIZE_I8 { + ( + RoomCoordinate::new((raw - ROOM_SIZE_I8) as u8).unwrap_throw(), + true, + ) + } else if raw < 0 { + ( + RoomCoordinate::new((raw + ROOM_SIZE_I8) as u8).unwrap_throw(), + true, + ) + } else { + (RoomCoordinate::new(raw as u8).unwrap_throw(), false) + } + } + + /// Get the coordinate adjusted by a certain value, wrapping around ta the + /// edges of the room if the result would be outside of the valid range. + /// + /// Example usage: + /// + /// ``` + /// use screeps::local::RoomCoordinate; + /// + /// assert_eq!( + /// RoomCoordinate::MIN.wrapping_add(1), + /// RoomCoordinate::new(1).unwrap() + /// ); + /// assert_eq!(RoomCoordinate::MIN.wrapping_add(-1), RoomCoordinate::MAX); + /// assert_eq!(RoomCoordinate::MAX.wrapping_add(1), RoomCoordinate::MIN); + /// ``` + pub fn wrapping_add(self, rhs: i8) -> Self { + self.overflowing_add(rhs).0 + } + + /// [`wrapping_add`](Self::wrapping_add) that accepts a [`RoomOffset`]. + pub fn wrapping_add_offset(self, rhs: RoomOffset) -> Self { + self.overflowing_add_offset(rhs).0 + } + + /// Get the coordinate adjusted by a certain value. + /// + /// # Safety + /// + /// After adding rhs to the integer coordinate of self, the result must lie + /// within `[0, ROOM_SIZE)`. + pub unsafe fn unchecked_add(self, rhs: i8) -> Self { + self.assume_bounds_constraint(); + Self::unchecked_new((self.0 as i8).unchecked_add(rhs) as u8) + } + + /// [`unchecked_add`](Self::unchecked_add) that accepts a [`RoomOffset`]. + /// + /// # Safety + /// + /// The result of adding the integer coordinate of self and the integer + /// offset in `rhs` must lie within `[0, ROOM_SIZE)`. + pub unsafe fn unchecked_add_offset(self, rhs: RoomOffset) -> Self { + self.assume_bounds_constraint(); + rhs.assume_bounds_constraint(); + Self::unchecked_new((self.0 as i8).unchecked_add(rhs.0) as u8) } } impl fmt::Display for RoomCoordinate { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) + self.0.fmt(f) } } @@ -139,3 +285,358 @@ impl TryFrom for RoomCoordinate { RoomCoordinate::new(coord) } } + +impl AsRef for RoomCoordinate { + fn as_ref(&self) -> &u8 { + &self.0 + } +} + +impl Index for [T; ROOM_USIZE] { + type Output = T; + + fn index(&self, index: RoomCoordinate) -> &Self::Output { + index.assume_bounds_constraint(); + &self[index.0 as usize] + } +} + +impl IndexMut for [T; ROOM_USIZE] { + fn index_mut(&mut self, index: RoomCoordinate) -> &mut Self::Output { + index.assume_bounds_constraint(); + &mut self[index.0 as usize] + } +} + +impl Index for [T; ROOM_AREA] { + type Output = [T; ROOM_USIZE]; + + fn index(&self, index: RoomCoordinate) -> &Self::Output { + // SAFETY: ROOM_USIZE * ROOM_USIZE = ROOM_AREA, so [T; ROOM_AREA] and [[T; + // ROOM_USIZE]; ROOM_USIZE] have the same layout. + let this = + unsafe { &*(self as *const [T; ROOM_AREA] as *const [[T; ROOM_USIZE]; ROOM_USIZE]) }; + &this[index] + } +} + +impl IndexMut for [T; ROOM_AREA] { + fn index_mut(&mut self, index: RoomCoordinate) -> &mut Self::Output { + // SAFETY: ROOM_USIZE * ROOM_USIZE = ROOM_AREA, so [T; ROOM_AREA] and [[T; + // ROOM_USIZE]; ROOM_USIZE] have the same layout. + let this = + unsafe { &mut *(self as *mut [T; ROOM_AREA] as *mut [[T; ROOM_USIZE]; ROOM_USIZE]) }; + &mut this[index] + } +} + +impl Sub for RoomCoordinate { + type Output = RoomOffset; + + fn sub(self, rhs: Self) -> Self::Output { + self.assume_bounds_constraint(); + rhs.assume_bounds_constraint(); + RoomOffset::new(self.0 as i8 - rhs.0 as i8).unwrap_throw() + } +} + +const ROOM_SIZE_I8: i8 = { + // If this fails, we need to rework the arithmetic code + debug_assert!(2 * ROOM_SIZE <= i8::MAX as u8); + ROOM_SIZE as i8 +}; + +/// An offset between two coordinates in a room. Restricted to the open range +/// (-[`ROOM_SIZE`], [`ROOM_SIZE`]). This bound can be used in safety +/// constraints. +#[derive( + Debug, Hash, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, +)] +#[serde(try_from = "i8", into = "i8")] +#[repr(transparent)] +pub struct RoomOffset(i8); + +impl RoomOffset { + pub const MAX: Self = Self(ROOM_SIZE_I8 - 1); + pub const MIN: Self = Self(1 - ROOM_SIZE_I8); + + /// Create a `RoomOffset` from an `i8`, returning an error if it's not + /// within the valid range. + pub const fn new(offset: i8) -> Result { + if -ROOM_SIZE_I8 < offset && offset < ROOM_SIZE_I8 { + Ok(Self(offset)) + } else { + Err(OffsetOutOfBoundsError(offset)) + } + } + + /// Create a `RoomOffset` from an `i8`, without checking whether it's in the + /// range of valid values. + /// + /// # Safety + /// Calling this method with `offset.abs() >= ROOM_SIZE_I8` can result in + /// undefined behaviour when the resulting `RoomOffset` is used. + pub unsafe fn unchecked_new(offset: i8) -> Self { + debug_assert!( + -ROOM_SIZE_I8 < offset && offset < ROOM_SIZE_I8, + "Out of bounds unchecked offset: {offset}" + ); + Self(offset) + } + + /// Provides a hint to the compiler that the contained `i8` is within + /// `(-ROOM_SIZE_I8, ROOM_SIZE_I8)`. Allows for better optimized safe code + /// that uses this property. + pub fn assume_bounds_constraint(self) { + debug_assert!(-ROOM_SIZE_I8 < self.0 && self.0 < ROOM_SIZE_I8); + // SAFETY: It is only safe to construct `RoomOffset` when `-ROOM_SIZE_I8 < + // self.0 < ROOM_SIZE_I8`. + unsafe { + assert_unchecked(-ROOM_SIZE_I8 < self.0 && self.0 < ROOM_SIZE_I8); + } + } + + /// Add two offsets together, returning `None` if the result would be + /// outside the valid range. + /// + /// Example usage: + /// + /// ``` + /// use screeps::local::RoomOffset; + /// + /// let zero = RoomOffset::new(0).unwrap(); + /// let one = RoomOffset::new(1).unwrap(); + /// + /// assert_eq!(RoomOffset::MIN.checked_add(RoomOffset::MAX), Some(zero)); + /// assert_eq!(RoomOffset::MAX.checked_add(one), None); + /// assert_eq!(RoomOffset::MIN.checked_add(-one), None); + /// ``` + pub fn checked_add(self, rhs: Self) -> Option { + self.assume_bounds_constraint(); + rhs.assume_bounds_constraint(); + Self::new(self.0 + rhs.0).ok() + } + + /// Add two offsets together, saturating at the boundaries of the valid + /// range if the result would be outside. + /// + /// Example usage: + /// + /// ``` + /// use screeps::local::RoomOffset; + /// + /// let zero = RoomOffset::new(0).unwrap(); + /// let one = RoomOffset::new(1).unwrap(); + /// + /// assert_eq!(RoomOffset::MIN.saturating_add(RoomOffset::MAX), zero); + /// assert_eq!(RoomOffset::MAX.saturating_add(one), RoomOffset::MAX); + /// assert_eq!(RoomOffset::MIN.saturating_add(-one), RoomOffset::MIN); + /// ``` + pub fn saturating_add(self, rhs: Self) -> Self { + self.assume_bounds_constraint(); + rhs.assume_bounds_constraint(); + Self::new((self.0 + rhs.0).clamp(-ROOM_SIZE_I8 + 1, ROOM_SIZE_I8 - 1)).unwrap_throw() + } + + /// Add two offsets together, wrapping around at the ends of the valid + /// range. Returns a [`bool`] indicating whether there was wrapping. + /// + /// Example usage: + /// + /// ``` + /// use screeps::local::RoomOffset; + /// + /// let zero = RoomOffset::new(0).unwrap(); + /// let one = RoomOffset::new(1).unwrap(); + /// + /// assert_eq!( + /// RoomOffset::MAX.overflowing_add(one), + /// (RoomOffset::MIN, true) + /// ); + /// assert_eq!( + /// RoomOffset::MIN.overflowing_add(-one), + /// (RoomOffset::MAX, true) + /// ); + /// assert_eq!( + /// RoomOffset::MIN.overflowing_add(RoomOffset::MAX), + /// (zero, false) + /// ); + /// ``` + pub fn overflowing_add(self, rhs: Self) -> (Self, bool) { + const RANGE_WIDTH: i8 = 2 * ROOM_SIZE_I8 - 1; + self.assume_bounds_constraint(); + rhs.assume_bounds_constraint(); + let raw = self.0 + rhs.0; + if raw <= -ROOM_SIZE_I8 { + (Self::new(raw + RANGE_WIDTH).unwrap_throw(), true) + } else if raw >= ROOM_SIZE_I8 { + (Self::new(raw - RANGE_WIDTH).unwrap_throw(), true) + } else { + (Self::new(raw).unwrap_throw(), false) + } + } + + /// Add two offsets together, wrapping around at the ends of the valid + /// range. + /// + /// Example usage: + /// + /// ``` + /// use screeps::local::RoomOffset; + /// + /// let zero = RoomOffset::new(0).unwrap(); + /// let one = RoomOffset::new(1).unwrap(); + /// + /// assert_eq!(RoomOffset::MAX.wrapping_add(one), RoomOffset::MIN); + /// assert_eq!(RoomOffset::MIN.wrapping_add(-one), RoomOffset::MAX); + /// assert_eq!(RoomOffset::MIN.wrapping_add(RoomOffset::MAX), zero); + /// ``` + pub fn wrapping_add(self, rhs: Self) -> Self { + self.overflowing_add(rhs).0 + } + + /// Add two offsets together, without checking that the result is in the + /// valid range. + /// + /// # Safety + /// + /// The result of adding the two offsets as integers must lie within + /// `(-ROOM_SIZE_I8, ROOM_SIZE_I8)`. + pub unsafe fn unchecked_add(self, rhs: Self) -> Self { + self.assume_bounds_constraint(); + rhs.assume_bounds_constraint(); + Self::unchecked_new(self.0.unchecked_add(rhs.0)) + } + + /// Get the absolute value of the offset. + /// + /// Can be used for distance computations, e.g. + /// ``` + /// use screeps::local::{RoomOffset, RoomXY}; + /// + /// fn get_movement_distance(a: RoomXY, b: RoomXY) -> u8 { + /// (a.x - b.x).abs().max((a.y - b.y).abs()) + /// } + /// ``` + pub fn abs(self) -> u8 { + self.assume_bounds_constraint(); + self.0.unsigned_abs() + } +} + +impl From for i8 { + fn from(offset: RoomOffset) -> i8 { + offset.0 + } +} + +#[derive(Debug, Clone, Copy)] +pub struct OffsetOutOfBoundsError(pub i8); + +impl fmt::Display for OffsetOutOfBoundsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Out of bounds offset: {}", self.0) + } +} + +impl TryFrom for RoomOffset { + type Error = OffsetOutOfBoundsError; + + fn try_from(offset: i8) -> Result { + Self::new(offset) + } +} + +impl AsRef for RoomOffset { + fn as_ref(&self) -> &i8 { + &self.0 + } +} + +impl Neg for RoomOffset { + type Output = Self; + + fn neg(self) -> Self::Output { + self.assume_bounds_constraint(); + Self::new(-self.0).unwrap_throw() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn checked_add() { + for coord_inner in 0..ROOM_SIZE { + let coord = RoomCoordinate::new(coord_inner).unwrap(); + for rhs in i8::MIN..=i8::MAX { + let sum = coord.checked_add(rhs); + assert_eq!( + sum.is_some(), + (0..ROOM_SIZE as i16).contains(&(coord_inner as i16 + rhs as i16)) + ); + if let Some(res) = sum { + assert_eq!(res.u8(), (coord_inner as i16 + rhs as i16) as u8); + } + } + } + } + + #[test] + fn saturating_add() { + for coord_inner in 0..ROOM_SIZE { + let coord = RoomCoordinate::new(coord_inner).unwrap(); + for rhs in i8::MIN..=i8::MAX { + assert_eq!( + coord.saturating_add(rhs).u8(), + (coord_inner as i16 + rhs as i16).clamp(0, ROOM_SIZE as i16 - 1) as u8 + ) + } + } + } + + #[test] + fn index_room_size() { + let mut base: Box<[u8; ROOM_USIZE]> = (0..50) + .collect::>() + .into_boxed_slice() + .try_into() + .unwrap(); + for i in 0..ROOM_SIZE { + let coord = RoomCoordinate::new(i).unwrap(); + assert_eq!(base[coord], i); + base[coord] += 1; + } + base.iter() + .copied() + .zip(1..(ROOM_SIZE + 1)) + .for_each(|(actual, expected)| assert_eq!(actual, expected)); + } + + #[test] + fn index_room_area() { + let mut base: Box<[u16; ROOM_AREA]> = Box::new([0; ROOM_AREA]); + for i in 0..ROOM_USIZE { + for j in 0..ROOM_USIZE { + base[i * ROOM_USIZE + j] = i as u16 * ROOM_SIZE as u16; + } + } + + for i in 0..ROOM_SIZE { + let coord = RoomCoordinate::new(i).unwrap(); + assert!(base[coord] + .iter() + .copied() + .all(|val| val == i as u16 * ROOM_SIZE as u16)); + for j in 0..ROOM_USIZE { + base[coord][j] += j as u16; + } + } + + assert_eq!( + (0..ROOM_AREA as u16).collect::>().as_slice(), + base.as_slice() + ); + } +} diff --git a/src/local/room_name.rs b/src/local/room_name.rs index 7e299f99..c7ffe648 100644 --- a/src/local/room_name.rs +++ b/src/local/room_name.rs @@ -548,7 +548,7 @@ mod serde { struct RoomNameVisitor; - impl<'de> Visitor<'de> for RoomNameVisitor { + impl Visitor<'_> for RoomNameVisitor { type Value = RoomName; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/src/local/room_xy.rs b/src/local/room_xy.rs index 78d328fe..bf523d20 100644 --- a/src/local/room_xy.rs +++ b/src/local/room_xy.rs @@ -1,9 +1,13 @@ -use std::{cmp::Ordering, fmt}; +use std::{ + cmp::Ordering, + fmt, + ops::{Index, IndexMut}, +}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use super::room_coordinate::{OutOfBoundsError, RoomCoordinate}; -use crate::constants::{Direction, ROOM_AREA, ROOM_SIZE}; +use crate::constants::{Direction, ROOM_AREA, ROOM_USIZE}; mod approximate_offsets; mod extra_math; @@ -16,7 +20,7 @@ mod game_math; /// [`LocalCostMatrix`]: crate::local::LocalCostMatrix #[inline] pub const fn xy_to_linear_index(xy: RoomXY) -> usize { - xy.x.u8() as usize * ROOM_SIZE as usize + xy.y.u8() as usize + xy.x.u8() as usize * ROOM_USIZE + xy.y.u8() as usize } /// Converts a linear index from the internal representation of a [`CostMatrix`] @@ -30,8 +34,8 @@ pub fn linear_index_to_xy(idx: usize) -> RoomXY { assert!(idx < ROOM_AREA, "Out of bounds index: {idx}"); // SAFETY: bounds checking above ensures both are within range. RoomXY { - x: unsafe { RoomCoordinate::unchecked_new((idx / (ROOM_SIZE as usize)) as u8) }, - y: unsafe { RoomCoordinate::unchecked_new((idx % (ROOM_SIZE as usize)) as u8) }, + x: unsafe { RoomCoordinate::unchecked_new((idx / (ROOM_USIZE)) as u8) }, + y: unsafe { RoomCoordinate::unchecked_new((idx % (ROOM_USIZE)) as u8) }, } } @@ -42,7 +46,7 @@ pub fn linear_index_to_xy(idx: usize) -> RoomXY { /// [`LocalRoomTerrain`]: crate::local::LocalRoomTerrain #[inline] pub const fn xy_to_terrain_index(xy: RoomXY) -> usize { - xy.y.u8() as usize * ROOM_SIZE as usize + xy.x.u8() as usize + xy.y.u8() as usize * ROOM_USIZE + xy.x.u8() as usize } /// Converts a terrain index from the internal representation of a @@ -56,8 +60,8 @@ pub fn terrain_index_to_xy(idx: usize) -> RoomXY { assert!(idx < ROOM_AREA, "Out of bounds index: {idx}"); // SAFETY: bounds checking above ensures both are within range. RoomXY { - x: unsafe { RoomCoordinate::unchecked_new((idx % (ROOM_SIZE as usize)) as u8) }, - y: unsafe { RoomCoordinate::unchecked_new((idx / (ROOM_SIZE as usize)) as u8) }, + x: unsafe { RoomCoordinate::unchecked_new((idx % (ROOM_USIZE)) as u8) }, + y: unsafe { RoomCoordinate::unchecked_new((idx / (ROOM_USIZE)) as u8) }, } } @@ -86,8 +90,8 @@ impl RoomXY { /// the range of valid values. /// /// # Safety - /// Calling this method with `x >= ROOM_SIZE` or `y >= ROOM_SIZE` can result - /// in undefined behaviour when the resulting `RoomXY` is used. + /// Calling this method with `x >= ROOM_SIZE` or `y >= ROOM_SIZE` can + /// result in undefined behaviour when the resulting `RoomXY` is used. #[inline] pub unsafe fn unchecked_new(x: u8, y: u8) -> Self { RoomXY { @@ -120,14 +124,8 @@ impl RoomXY { /// assert_eq!(forty_nine.checked_add((1, 1)), None); /// ``` pub fn checked_add(self, rhs: (i8, i8)) -> Option { - let x = match self.x.checked_add(rhs.0) { - Some(x) => x, - None => return None, - }; - let y = match self.y.checked_add(rhs.1) { - Some(y) => y, - None => return None, - }; + let x = self.x.checked_add(rhs.0)?; + let y = self.y.checked_add(rhs.1)?; Some(RoomXY { x, y }) } @@ -338,9 +336,127 @@ impl<'de> Deserialize<'de> for RoomXY { RoomXY::try_from(xy).map_err(|err: OutOfBoundsError| { de::Error::invalid_value( de::Unexpected::Unsigned(err.0 as u64), - &format!("a non-negative integer less-than {ROOM_SIZE}").as_str(), + &format!("a non-negative integer less-than {ROOM_USIZE}").as_str(), ) }) } } } + +/// A wrapper struct indicating that the inner array should be indexed X major, +/// i.e. ``` +/// use screeps::{ +/// constants::ROOM_USIZE, +/// local::{XMajor, XY}, +/// }; +/// +/// let mut x_major = XMajor([[0_u8; ROOM_USIZE]; ROOM_USIZE]); +/// x_major.0[10][0] = 1; +/// let xy = RoomXY::checked_new(10, 0).unwrap(); +/// assert_eq!(x_major[xy], 1); +/// ``` +#[repr(transparent)] +pub struct XMajor(pub [[T; ROOM_USIZE]; ROOM_USIZE]); + +impl XMajor { + pub fn from_ref(arr: &[[T; ROOM_USIZE]; ROOM_USIZE]) -> &Self { + // SAFETY: XMajor is a repr(transparent) wrapper around [[T; ROOM_USIZE]; + // ROOM_USIZE], so casting references of one to the other is safe. + unsafe { &*(arr as *const [[T; ROOM_USIZE]; ROOM_USIZE] as *const Self) } + } + + pub fn from_flat_ref(arr: &[T; ROOM_AREA]) -> &Self { + // SAFETY: ROOM_AREA = ROOM_USIZE * ROOM_USIZE, so [T; ROOM_AREA] is identical + // in data layout to [[T; ROOM_USIZE]; ROOM_USIZE]. + Self::from_ref(unsafe { + &*(arr as *const [T; ROOM_AREA] as *const [[T; ROOM_USIZE]; ROOM_USIZE]) + }) + } + + pub fn from_mut(arr: &mut [[T; ROOM_USIZE]; ROOM_USIZE]) -> &mut Self { + // SAFETY: XMajor is a repr(transparent) wrapper around [[T; ROOM_USIZE]; + // ROOM_USIZE], so casting references of one to the other is safe. + unsafe { &mut *(arr as *mut [[T; ROOM_USIZE]; ROOM_USIZE] as *mut Self) } + } + + pub fn from_flat_mut(arr: &mut [T; ROOM_AREA]) -> &mut Self { + // SAFETY: ROOM_AREA = ROOM_USIZE * ROOM_USIZE, so [T; ROOM_AREA] is identical + // in data layout to [[T; ROOM_USIZE]; ROOM_USIZE]. + Self::from_mut(unsafe { + &mut *(arr as *mut [T; ROOM_AREA] as *mut [[T; ROOM_USIZE]; ROOM_USIZE]) + }) + } +} + +impl Index for XMajor { + type Output = T; + + fn index(&self, index: RoomXY) -> &Self::Output { + &self.0[index.x][index.y] + } +} + +impl IndexMut for XMajor { + fn index_mut(&mut self, index: RoomXY) -> &mut Self::Output { + &mut self.0[index.x][index.y] + } +} + +/// A wrapper struct indicating that the inner array should be indexed Y major, +/// i.e. ``` +/// use screeps::{ +/// constants::ROOM_USIZE, +/// local::{YMajor, XY}, +/// }; +/// +/// let mut y_major = YMajor([[0_u8; ROOM_USIZE]; ROOM_USIZE]); +/// y_major.0[0][10] = 1; +/// let xy = RoomXY::checked_new(10, 0).unwrap(); +/// assert_eq!(y_major[xy], 1); +/// ``` +#[repr(transparent)] +pub struct YMajor(pub [[T; ROOM_USIZE]; ROOM_USIZE]); + +impl YMajor { + pub fn from_ref(arr: &[[T; ROOM_USIZE]; ROOM_USIZE]) -> &Self { + // SAFETY: XMajor is a repr(transparent) wrapper around [[T; ROOM_USIZE]; + // ROOM_USIZE], so casting references of one to the other is safe. + unsafe { &*(arr as *const [[T; ROOM_USIZE]; ROOM_USIZE] as *const Self) } + } + + pub fn from_flat_ref(arr: &[T; ROOM_AREA]) -> &Self { + // SAFETY: ROOM_AREA = ROOM_USIZE * ROOM_USIZE, so [T; ROOM_AREA] is identical + // in data layout to [[T; ROOM_USIZE]; ROOM_USIZE]. + Self::from_ref(unsafe { + &*(arr as *const [T; ROOM_AREA] as *const [[T; ROOM_USIZE]; ROOM_USIZE]) + }) + } + + pub fn from_mut(arr: &mut [[T; ROOM_USIZE]; ROOM_USIZE]) -> &mut Self { + // SAFETY: XMajor is a repr(transparent) wrapper around [[T; ROOM_USIZE]; + // ROOM_USIZE], so casting references of one to the other is safe. + unsafe { &mut *(arr as *mut [[T; ROOM_USIZE]; ROOM_USIZE] as *mut Self) } + } + + pub fn from_flat_mut(arr: &mut [T; ROOM_AREA]) -> &mut Self { + // SAFETY: ROOM_AREA = ROOM_USIZE * ROOM_USIZE, so [T; ROOM_AREA] is identical + // in data layout to [[T; ROOM_USIZE]; ROOM_USIZE]. + Self::from_mut(unsafe { + &mut *(arr as *mut [T; ROOM_AREA] as *mut [[T; ROOM_USIZE]; ROOM_USIZE]) + }) + } +} + +impl Index for YMajor { + type Output = T; + + fn index(&self, index: RoomXY) -> &Self::Output { + &self.0[index.y][index.x] + } +} + +impl IndexMut for YMajor { + fn index_mut(&mut self, index: RoomXY) -> &mut Self::Output { + &mut self.0[index.y][index.x] + } +} diff --git a/src/local/terrain.rs b/src/local/terrain.rs index 8b8b8e14..9f5b918d 100644 --- a/src/local/terrain.rs +++ b/src/local/terrain.rs @@ -7,7 +7,7 @@ use crate::{ objects::RoomTerrain, }; -use super::{xy_to_terrain_index, RoomXY}; +use super::RoomXY; #[derive(Debug, Clone)] pub struct LocalRoomTerrain { @@ -20,8 +20,7 @@ pub struct LocalRoomTerrain { impl LocalRoomTerrain { /// Gets the terrain at the specified position in this room. pub fn get_xy(&self, xy: RoomXY) -> Terrain { - // SAFETY: RoomXY is always a valid coordinate. - let byte = unsafe { self.bits.get_unchecked(xy_to_terrain_index(xy)) }; + let byte = self.bits[xy.y][xy.x]; // not using Terrain::from_u8() because `0b11` value, wall+swamp, happens // in commonly used server environments (notably the private server default // map), and is special-cased in the engine code; we special-case it here