screeps/local/
room_name.rs

1use std::{
2    cmp::Ordering,
3    error::Error,
4    fmt::{self, Write},
5    ops,
6    str::FromStr,
7};
8
9use arrayvec::ArrayString;
10use js_sys::JsString;
11use wasm_bindgen::{JsCast, JsValue};
12
13use crate::prelude::*;
14
15use super::{HALF_WORLD_SIZE, VALID_ROOM_NAME_COORDINATES};
16
17/// A structure representing a room name.
18///
19/// # Ordering
20///
21/// To facilitate use as a key in a [`BTreeMap`] or other similar data
22/// structures, `RoomName` implements [`PartialOrd`] and [`Ord`].
23///
24/// `RoomName`s are ordered first by y position, then by x position. North is
25/// considered less than south, and west less than east.
26///
27/// The total ordering is `N127W127`, `N127W126`, `N127W125`, ..., `N127W0`,
28/// `N127E0`, ..., `N127E127`, `N126W127`, ..., `S127E126`, `S127E127`.
29///
30/// This follows left-to-right reading order when looking at the Screeps map
31/// from above.
32///
33/// [`BTreeMap`]: std::collections::BTreeMap
34#[derive(Copy, Clone, Eq, PartialEq, Hash)]
35pub struct RoomName {
36    /// A bit-packed integer, containing, from highest-order to lowest:
37    ///
38    /// - 1 byte: (room_x) + 128
39    /// - 1 byte: (room_y) + 128
40    ///
41    /// For `Wxx` rooms, `room_x = -xx - 1`. For `Exx` rooms, `room_x = xx`.
42    ///
43    /// For `Nyy` rooms, `room_y = -yy - 1`. For `Syy` rooms, `room_y = yy`.
44    ///
45    /// This is the same representation of the upper 16 bits of [`Position`]'s
46    /// packed representation.
47    ///
48    /// [`Position`]: crate::local::Position
49    packed: u16,
50}
51
52impl fmt::Display for RoomName {
53    /// Formats this room name into the format the game expects.
54    ///
55    /// Resulting string will be `(E|W)[0-9]+(N|S)[0-9]+`, and will result
56    /// in the same RoomName if passed into [`RoomName::new`].
57    ///
58    /// If the `sim` feature is enabled, the room corresponding to W127N127
59    /// outputs `sim` instead.
60    ///
61    /// [`RoomName::new`]: struct.RoomName.html#method.new
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        if cfg!(feature = "sim") && self.packed == 0 {
64            write!(f, "sim")?;
65            return Ok(());
66        }
67
68        let x_coord = self.x_coord();
69        let y_coord = self.y_coord();
70
71        if x_coord >= 0 {
72            write!(f, "E{}", x_coord)?;
73        } else {
74            write!(f, "W{}", -x_coord - 1)?;
75        }
76
77        if y_coord >= 0 {
78            write!(f, "S{}", y_coord)?;
79        } else {
80            write!(f, "N{}", -y_coord - 1)?;
81        }
82
83        Ok(())
84    }
85}
86
87impl fmt::Debug for RoomName {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.debug_struct("RoomName")
90            .field("packed", &self.packed)
91            .field("real", &self.to_array_string())
92            .finish()
93    }
94}
95
96impl RoomName {
97    /// Parses a room name from a string.
98    ///
99    /// This will parse the input string, returning an error if it is in an
100    /// invalid room name.
101    ///
102    /// The expected format can be represented by the regex
103    /// `[ewEW][0-9]+[nsNS][0-9]+`. If the `sim` feature is enabled, `sim` is
104    /// also valid and uses the packed position of W127N127 (0), matching the
105    /// game's internal implementation of the sim room's packed positions.
106    #[inline]
107    pub fn new<T>(x: &T) -> Result<Self, RoomNameParseError>
108    where
109        T: AsRef<str> + ?Sized,
110    {
111        x.as_ref().parse()
112    }
113
114    /// Get the [`RoomName`] represented by a packed integer
115    #[inline]
116    pub const fn from_packed(packed: u16) -> Self {
117        RoomName { packed }
118    }
119
120    /// Creates a new room name from room coords with direction implicit in
121    /// sign.
122    ///
123    /// For `Wxx` rooms, `room_x = -xx - 1`. For `Exx` rooms, `room_x = xx`.
124    ///
125    /// For `Nyy` rooms, `room_y = -yy - 1`. For `Syy` rooms, `room_y = yy`.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if the coordinates are outside of the valid room name
130    /// bounds.
131    pub(super) fn from_coords(x_coord: i32, y_coord: i32) -> Result<Self, RoomNameParseError> {
132        if !VALID_ROOM_NAME_COORDINATES.contains(&x_coord)
133            || !VALID_ROOM_NAME_COORDINATES.contains(&y_coord)
134        {
135            return Err(RoomNameParseError::PositionOutOfBounds { x_coord, y_coord });
136        }
137
138        let room_x = (x_coord + HALF_WORLD_SIZE) as u16;
139        let room_y = (y_coord + HALF_WORLD_SIZE) as u16;
140
141        Ok(Self::from_packed((room_x << 8) | room_y))
142    }
143
144    /// Gets the x coordinate.
145    ///
146    /// For `Wxx` rooms, returns `-xx - 1`. For `Exx` rooms, returns `xx`.
147    #[inline]
148    pub const fn x_coord(&self) -> i32 {
149        ((self.packed >> 8) & 0xFF) as i32 - HALF_WORLD_SIZE
150    }
151
152    /// Gets the y coordinate.
153    ///
154    /// For `Nyy` rooms, returns `-yy - 1`. For `Syy` rooms, returns `yy`.
155    #[inline]
156    pub const fn y_coord(&self) -> i32 {
157        (self.packed & 0xFF) as i32 - HALF_WORLD_SIZE
158    }
159
160    /// Get the inner packed representation of the room name.
161    ///
162    /// This data structure matches the implementation of the upper 16 bits of
163    /// the js Position type.
164    #[inline]
165    pub const fn packed_repr(&self) -> u16 {
166        self.packed
167    }
168
169    /// Adds an `(x, y)` pair to this room's name.
170    ///
171    /// # Errors
172    /// Returns an error if the coordinates are outside of the valid room name
173    /// bounds.
174    ///
175    /// For a panicking variant of this function, use the implementation of
176    /// [`ops::Add`] for `(i32, i32)`.
177    pub fn checked_add(&self, offset: (i32, i32)) -> Option<RoomName> {
178        let (x1, y1) = (self.x_coord(), self.y_coord());
179        let (x2, y2) = offset;
180        let new_x = x1.checked_add(x2)?;
181        let new_y = y1.checked_add(y2)?;
182        Self::from_coords(new_x, new_y).ok()
183    }
184
185    /// Converts this RoomName into an efficient, stack-based string.
186    ///
187    /// This is equivalent to [`ToString::to_string`], but involves no
188    /// allocation.
189    pub fn to_array_string(&self) -> ArrayString<8> {
190        let mut res = ArrayString::new();
191        write!(res, "{self}").expect("expected ArrayString write to be infallible");
192        res
193    }
194}
195
196impl From<RoomName> for JsValue {
197    fn from(name: RoomName) -> JsValue {
198        let array = name.to_array_string();
199
200        JsValue::from_str(array.as_str())
201    }
202}
203
204impl From<&RoomName> for JsValue {
205    fn from(name: &RoomName) -> JsValue {
206        let array = name.to_array_string();
207
208        JsValue::from_str(array.as_str())
209    }
210}
211
212impl From<RoomName> for JsString {
213    fn from(name: RoomName) -> JsString {
214        let val: JsValue = name.into();
215
216        val.unchecked_into()
217    }
218}
219
220impl From<&RoomName> for JsString {
221    fn from(name: &RoomName) -> JsString {
222        let val: JsValue = name.into();
223
224        val.unchecked_into()
225    }
226}
227
228/// An error representing when a string can't be parsed into a
229/// [`RoomName`].
230///
231/// [`RoomName`]: struct.RoomName.html
232#[derive(Clone, Debug)]
233pub enum RoomNameConversionError {
234    InvalidType,
235    ParseError { err: RoomNameParseError },
236}
237
238impl Error for RoomNameConversionError {}
239
240impl fmt::Display for RoomNameConversionError {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        match self {
243            RoomNameConversionError::InvalidType => {
244                write!(f, "got invalid input type to room name conversion")
245            }
246            RoomNameConversionError::ParseError { err } => err.fmt(f),
247        }
248    }
249}
250
251impl TryFrom<JsValue> for RoomName {
252    type Error = RoomNameConversionError;
253
254    fn try_from(val: JsValue) -> Result<RoomName, Self::Error> {
255        let val: String = val
256            .as_string()
257            .ok_or(RoomNameConversionError::InvalidType)?;
258
259        RoomName::from_str(&val).map_err(|err| RoomNameConversionError::ParseError { err })
260    }
261}
262
263impl TryFrom<JsString> for RoomName {
264    type Error = <RoomName as FromStr>::Err;
265
266    fn try_from(val: JsString) -> Result<RoomName, Self::Error> {
267        let val: String = val.into();
268
269        RoomName::from_str(&val)
270    }
271}
272
273impl JsCollectionIntoValue for RoomName {
274    fn into_value(self) -> JsValue {
275        self.into()
276    }
277}
278
279impl JsCollectionFromValue for RoomName {
280    fn from_value(val: JsValue) -> Self {
281        let val: JsString = val.unchecked_into();
282        let val: String = val.into();
283
284        RoomName::from_str(&val).expect("expected parseable room name")
285    }
286}
287
288impl ops::Add<(i32, i32)> for RoomName {
289    type Output = Self;
290
291    /// Offsets this room name by a given horizontal and vertical (x, y) pair.
292    ///
293    /// The first number offsets to the west when negative and to the east when
294    /// positive. The first number offsets to the north when negative and to
295    /// the south when positive.
296    ///
297    /// # Panics
298    ///
299    /// Will panic if the addition overflows the boundaries of RoomName.
300    #[inline]
301    fn add(self, (x, y): (i32, i32)) -> Self {
302        RoomName::from_coords(self.x_coord() + x, self.y_coord() + y)
303            .expect("expected addition to keep RoomName in-bounds")
304    }
305}
306
307impl ops::Sub<(i32, i32)> for RoomName {
308    type Output = Self;
309
310    /// Offsets this room name in the opposite direction from the coordinates.
311    ///
312    /// See the implementation for `Add<(i32, i32)>`.
313    ///
314    /// # Panics
315    ///
316    /// Will panic if the subtraction overflows the boundaries of RoomName.
317    #[inline]
318    fn sub(self, (x, y): (i32, i32)) -> Self {
319        RoomName::from_coords(self.x_coord() - x, self.y_coord() - y)
320            .expect("expected addition to keep RoomName in-bounds")
321    }
322}
323
324impl ops::Sub<RoomName> for RoomName {
325    type Output = (i32, i32);
326
327    /// Subtracts one room name from the other, extracting the difference.
328    ///
329    /// The first return value represents east/west offset, with 'more east'
330    /// being positive and 'more west' being negative.
331    ///
332    /// The second return value represents north/south offset, with 'more south'
333    /// being positive and 'more north' being negative.
334    ///
335    /// This coordinate system agrees with the implementations `Add<(i32, i32)>
336    /// for RoomName` and `Sub<(i32, i32)> for RoomName`.
337    #[inline]
338    fn sub(self, other: RoomName) -> (i32, i32) {
339        (
340            self.x_coord() - other.x_coord(),
341            self.y_coord() - other.y_coord(),
342        )
343    }
344}
345
346impl FromStr for RoomName {
347    type Err = RoomNameParseError;
348
349    fn from_str(s: &str) -> Result<Self, Self::Err> {
350        parse_to_coords(s)
351            .map_err(|()| RoomNameParseError::new(s))
352            .and_then(|(x, y)| RoomName::from_coords(x, y))
353    }
354}
355
356fn parse_to_coords(s: &str) -> Result<(i32, i32), ()> {
357    if cfg!(feature = "sim") && s == "sim" {
358        return Ok((-HALF_WORLD_SIZE, -HALF_WORLD_SIZE));
359    }
360
361    let mut chars = s.char_indices();
362
363    let east = match chars.next() {
364        Some((_, 'E')) | Some((_, 'e')) => true,
365        Some((_, 'W')) | Some((_, 'w')) => false,
366        _ => return Err(()),
367    };
368
369    let (x_coord, south): (i32, bool) = {
370        // we assume there's at least one number character. If there isn't,
371        // we'll catch it when we try to parse this substr.
372        let (start_index, _) = chars.next().ok_or(())?;
373        let end_index;
374        let south;
375        loop {
376            match chars.next().ok_or(())? {
377                (i, 'N') | (i, 'n') => {
378                    end_index = i;
379                    south = false;
380                    break;
381                }
382                (i, 'S') | (i, 's') => {
383                    end_index = i;
384                    south = true;
385                    break;
386                }
387                _ => continue,
388            }
389        }
390
391        let x_coord = s[start_index..end_index].parse().map_err(|_| ())?;
392
393        (x_coord, south)
394    };
395
396    let y_coord: i32 = {
397        let (start_index, _) = chars.next().ok_or(())?;
398
399        s[start_index..s.len()].parse().map_err(|_| ())?
400    };
401
402    let room_x = if east { x_coord } else { -x_coord - 1 };
403    let room_y = if south { y_coord } else { -y_coord - 1 };
404
405    Ok((room_x, room_y))
406}
407
408/// An error representing when a string can't be parsed into a
409/// [`RoomName`].
410///
411/// [`RoomName`]: struct.RoomName.html
412#[derive(Clone, Debug)]
413pub enum RoomNameParseError {
414    TooLarge { length: usize },
415    InvalidString { string: ArrayString<8> },
416    PositionOutOfBounds { x_coord: i32, y_coord: i32 },
417}
418
419impl RoomNameParseError {
420    /// Private method to construct a `RoomNameParseError`.
421    fn new(failed_room_name: &str) -> Self {
422        match ArrayString::from(failed_room_name) {
423            Ok(string) => RoomNameParseError::InvalidString { string },
424            Err(_) => RoomNameParseError::TooLarge {
425                length: failed_room_name.len(),
426            },
427        }
428    }
429}
430
431impl Error for RoomNameParseError {}
432
433impl fmt::Display for RoomNameParseError {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        match self {
436            RoomNameParseError::TooLarge { length } => write!(
437                f,
438                "got invalid room name, too large to stick in error. \
439                 expected length 8 or less, got length {length}"
440            ),
441            RoomNameParseError::InvalidString { string } => write!(
442                f,
443                "expected room name formatted `[ewEW][0-9]+[nsNS][0-9]+`, found `{string}`"
444            ),
445            RoomNameParseError::PositionOutOfBounds { x_coord, y_coord } => write!(
446                f,
447                "expected room name with coords within -128..+128, found {x_coord}, {y_coord}"
448            ),
449        }
450    }
451}
452
453impl PartialEq<str> for RoomName {
454    fn eq(&self, other: &str) -> bool {
455        let s = self.to_array_string();
456        s.eq_ignore_ascii_case(other)
457    }
458}
459impl PartialEq<RoomName> for str {
460    #[inline]
461    fn eq(&self, other: &RoomName) -> bool {
462        // Explicitly call the impl for `PartialEq<str>` so that we don't end up
463        // accidentally calling one of the other implementations and ending up in an
464        // infinite loop.
465        //
466        // This one in particular would probably be OK, but I've written it this way to
467        // be consistent with the others, and to ensure that if this code changes in
468        // this future it'll stay working.
469        <RoomName as PartialEq<str>>::eq(other, self)
470    }
471}
472
473impl PartialEq<&str> for RoomName {
474    #[inline]
475    fn eq(&self, other: &&str) -> bool {
476        <RoomName as PartialEq<str>>::eq(self, other)
477    }
478}
479
480impl PartialEq<RoomName> for &str {
481    #[inline]
482    fn eq(&self, other: &RoomName) -> bool {
483        <RoomName as PartialEq<str>>::eq(other, self)
484    }
485}
486
487impl PartialEq<String> for RoomName {
488    #[inline]
489    fn eq(&self, other: &String) -> bool {
490        <RoomName as PartialEq<str>>::eq(self, other)
491    }
492}
493
494impl PartialEq<RoomName> for String {
495    #[inline]
496    fn eq(&self, other: &RoomName) -> bool {
497        <RoomName as PartialEq<str>>::eq(other, self)
498    }
499}
500
501impl PartialEq<&String> for RoomName {
502    #[inline]
503    fn eq(&self, other: &&String) -> bool {
504        <RoomName as PartialEq<str>>::eq(self, other)
505    }
506}
507
508impl PartialEq<RoomName> for &String {
509    #[inline]
510    fn eq(&self, other: &RoomName) -> bool {
511        <RoomName as PartialEq<str>>::eq(other, self)
512    }
513}
514
515impl PartialOrd for RoomName {
516    #[inline]
517    fn partial_cmp(&self, other: &RoomName) -> Option<Ordering> {
518        Some(self.cmp(other))
519    }
520}
521
522impl Ord for RoomName {
523    fn cmp(&self, other: &Self) -> Ordering {
524        self.y_coord()
525            .cmp(&other.y_coord())
526            .then_with(|| self.x_coord().cmp(&other.x_coord()))
527    }
528}
529
530mod serde {
531    use std::fmt;
532
533    use serde::{
534        de::{Error, Unexpected, Visitor},
535        Deserialize, Deserializer, Serialize, Serializer,
536    };
537
538    use super::RoomName;
539
540    impl Serialize for RoomName {
541        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
542        where
543            S: Serializer,
544        {
545            serializer.serialize_str(&self.to_array_string())
546        }
547    }
548
549    struct RoomNameVisitor;
550
551    impl Visitor<'_> for RoomNameVisitor {
552        type Value = RoomName;
553
554        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
555            formatter.write_str(
556                "room name formatted `(E|W)[0-9]+(N|S)[0-9]+` with both numbers within -128..128",
557            )
558        }
559
560        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
561        where
562            E: Error,
563        {
564            v.parse()
565                .map_err(|_| E::invalid_value(Unexpected::Str(v), &self))
566        }
567    }
568
569    impl<'de> Deserialize<'de> for RoomName {
570        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
571        where
572            D: Deserializer<'de>,
573        {
574            deserializer.deserialize_str(RoomNameVisitor)
575        }
576    }
577}
578
579#[cfg(test)]
580mod test {
581    use crate::RoomName;
582
583    #[test]
584    fn test_string_equality() {
585        use super::RoomName;
586        let top_left_room = if cfg!(feature = "sim") {
587            "sim"
588        } else {
589            "W127N127"
590        };
591        let room_names = vec!["E21N4", "w6S42", "W17s5", "e2n5", top_left_room];
592        for room_name in room_names {
593            assert_eq!(room_name, RoomName::new(room_name).unwrap());
594            assert_eq!(RoomName::new(room_name).unwrap(), room_name);
595            assert_eq!(RoomName::new(room_name).unwrap(), &room_name.to_string());
596            assert_eq!(&room_name.to_string(), RoomName::new(room_name).unwrap());
597        }
598    }
599
600    #[test]
601    fn checked_add() {
602        let w0n0 = RoomName::new("W0N0").unwrap();
603        let e0n0 = RoomName::new("E0N0").unwrap();
604        let e10n75 = RoomName::new("E10N75").unwrap();
605        let w3n53 = RoomName::new("W3N53").unwrap();
606
607        // corners
608        let w127n127 = RoomName::new("W127N127").unwrap();
609        let w127s127 = RoomName::new("W127S127").unwrap();
610        let e127n127 = RoomName::new("E127N127").unwrap();
611        let e127s127 = RoomName::new("E127S127").unwrap();
612
613        // side
614        let w127n5 = RoomName::new("W127N5").unwrap();
615
616        // valid
617        assert_eq!(w0n0.checked_add((1, 0)), Some(e0n0));
618        assert_eq!(e0n0.checked_add((10, -75)), Some(e10n75));
619        assert_eq!(e10n75.checked_add((-14, 22)), Some(w3n53));
620        assert_eq!(w3n53.checked_add((-124, -74)), Some(w127n127));
621
622        assert_eq!(w127n127.checked_add((127, 127)), Some(w0n0));
623        assert_eq!(w127s127.checked_add((127, -128)), Some(w0n0));
624        assert_eq!(e127n127.checked_add((-128, 127)), Some(w0n0));
625        assert_eq!(e127s127.checked_add((-128, -128)), Some(w0n0));
626        assert_eq!(w127n5.checked_add((127, 5)), Some(w0n0));
627
628        // overflow
629        assert_eq!(w127n127.checked_add((-1, 0)), None);
630        assert_eq!(w127n127.checked_add((-10, 10)), None);
631        assert_eq!(w127n127.checked_add((i32::MIN, 0)), None);
632        assert_eq!(w127n127.checked_add((i32::MIN, i32::MAX)), None);
633
634        assert_eq!(w127s127.checked_add((-1, 0)), None);
635        assert_eq!(w127s127.checked_add((-10, 10)), None);
636        assert_eq!(w127s127.checked_add((i32::MIN, 0)), None);
637        assert_eq!(w127s127.checked_add((i32::MIN, i32::MAX)), None);
638
639        assert_eq!(e127n127.checked_add((1, 0)), None);
640        assert_eq!(e127n127.checked_add((-1, -10)), None);
641        assert_eq!(e127n127.checked_add((i32::MIN, 0)), None);
642        assert_eq!(e127n127.checked_add((i32::MIN, i32::MAX)), None);
643
644        assert_eq!(e127s127.checked_add((1, 0)), None);
645        assert_eq!(e127s127.checked_add((-1, 10)), None);
646        assert_eq!(e127s127.checked_add((i32::MIN, 0)), None);
647        assert_eq!(e127s127.checked_add((i32::MIN, i32::MAX)), None);
648
649        assert_eq!(w127n5.checked_add((-1, 0)), None);
650        assert_eq!(w127n5.checked_add((-1, 10)), None);
651        assert_eq!(w127n5.checked_add((i32::MIN, 0)), None);
652        assert_eq!(w127n5.checked_add((i32::MIN, i32::MAX)), None);
653    }
654}