screeps/local/
position.rs

1//! Room position type and related operations and traits.
2//!
3//! This is a reimplementation/translation of the `RoomPosition` code originally
4//! written in JavaScript. All RoomPosition to RoomPosition operations in this
5//! file stay within Rust.
6use core::fmt::Debug;
7use std::{cmp::Ordering, fmt};
8
9use crate::{constants::ROOM_SIZE, objects::RoomPosition, HasPosition};
10
11use super::{RoomCoordinate, RoomName, RoomXY, HALF_WORLD_SIZE};
12
13mod approximate_offsets;
14mod extra_math;
15mod game_math;
16mod game_methods;
17mod pair_utils;
18mod world_utils;
19
20/// Represents a position in a particular room in Screeps, stored in Rust
21/// memory.
22///
23/// Use [`RoomPosition`] if a reference to an object stored in JavaScript memory
24/// is preferred.
25///
26/// This should be a very efficient type to use in most if not all situations.
27/// It's represented by a single `u32`, all math operations are implemented in
28/// pure-Rust code, and uploading to / downloading from JavaScript only requires
29/// transferring a single `i32`.
30///
31/// # Using Position
32///
33/// You can retrieve a `Position` by getting the position of a game object using
34/// [`HasPosition::pos`], or by creating one from coordinates with
35/// [`Position::new`].
36///
37/// You can use any of the math methods available on this page to manipulate
38/// [`Position`], and you can pass it to any game methods expecting a position
39/// or something with a position.
40///
41/// # Serialization
42///
43/// `Position` implements both `serde::Serialize` and
44/// `serde::Deserialize`.
45///
46/// When serializing, it will use the format `{roomName: String, x: u32, y:
47/// u32}` in "human readable" formats like JSON, and will serialize as a single
48/// `i32` in "non-human readable" formats like [`bincode`].
49///
50/// If you need a reference to a `RoomPosition` in JavaScript,
51/// convert the native [`Position`] to a [`RoomPosition`]:
52///
53/// ```no_run
54/// use screeps::{Position, RoomCoordinate, RoomPosition};
55/// use std::convert::TryFrom;
56///
57/// let pos = Position::new(
58///     RoomCoordinate::try_from(20).unwrap(),
59///     RoomCoordinate::try_from(21).unwrap(),
60///     "E5N6".parse().unwrap(),
61/// );
62/// let js_pos = RoomPosition::from(pos);
63/// let result = js_pos.room_name();
64/// ```
65///
66/// # Deserialization
67///
68/// `Position` implements `TryFrom<Value>`, allowing conversion from values
69/// retrieved from JavaScript. The implementation is fairly lenient, and will
70/// try to accept the value as any of the following things, in order:
71///
72/// - an integer representing the packedPos
73///   - this can be produced by retrieving the `__packedPos` field of a
74///     `RoomPosition`
75/// - an object with a `__packedPos` property
76///   - this allows converting from a JavaScript `RoomPosition` to a `Position`
77///     without referencing `__packedPos` manually, but is less efficient since
78///     it requires an extra callback into JavaScript to grab that field from
79///     within the conversion code
80/// - an object with `x`, `y` and `roomName` properties
81///   - this is mainly intended to decode `Position`s which were previously sent
82///     to JavaScript using `@{}` in `js!{}`, or serialized using
83///     [`serde::Serialize`]
84///   - this will also understand `RoomPosition`s in private servers versions
85///     `3.2.1` and below, prior to when `__packedPos` was added
86///
87/// # World vs. in-room coordinates
88///
89/// When converting `Position` to integer x/y coordinates, there are two main
90/// methods. The first is to use `x`/`y` as "in room" coordinates, which are
91/// bounded within `0..=49`. These coordinates only identify the location within
92/// a given room name. These are used by [`Position::x`], [`Position::y`],
93/// [`Position::new`] as well as [`Position::coords`],
94/// [`Position::coords_signed`] and the various implementations of `Into<([ui*],
95/// [ui*])>` for `Position`.
96///
97/// The second is to use `x`/`y` as "world" coordinates, which are coordinates
98/// spread across the world. To ensures they agree with in-room coordinates,
99/// south is positive `y`, north is negative `y`, east is positive `x` and west
100/// is negative `x`. One way to think of them is as extending the room
101/// coordinates of the room `E0S0` throughout the entire map.
102///
103/// World coordinates are used by [`Position::world_x`], [`Position::world_y`],
104/// [`Position::world_coords`], [`Position::from_world_coords`], and by all
105/// implementations which allow adding or subtracting positions (see [Addition
106/// and subtraction](#addition-and-subtraction)).
107///
108/// # Method Behavior
109///
110/// While this corresponds with the JavaScript [`RoomPosition`] type, it is not
111/// identical. In particular, all "calculation" methods which take in another
112/// position are re-implemented in pure Rust code, and some behave slightly
113/// different.
114///
115/// For instance, [`Position::get_range_to`] operates on positions as world
116/// coordinates, and will return accurate distances for positions in different
117/// rooms. This is in contrast to `RoomPosition.getRangeTo` in JavaScript, which
118/// will return `Infinity` for positions from different rooms.
119/// [`Position::in_range_to`] has a similar difference.
120///
121/// Besides extending behavior to work between rooms, we've tried to keep
122/// methods as in-sync with the JavaScript versions as possible. Everything
123/// will "just work", and there should be some speed advantage because of not
124/// having to call into JavaScript to perform calculations.
125///
126/// # Addition and subtraction
127///
128/// [`Position`] implements `Add<(i32, i32)>`, `Sub<(i32, i32)>` and
129/// `Sub<Position>`. All of these implementations work on positions as world
130/// positions, and will treat positions from different rooms just as if they're
131/// further apart.
132///
133/// The `Add` implementation can be used to add an offset to a position:
134///
135/// ```
136/// # use std::convert::TryFrom;
137/// # use screeps::{Position, RoomCoordinate};
138/// let pos1 = Position::new(
139///     RoomCoordinate::try_from(0).unwrap(),
140///     RoomCoordinate::try_from(0).unwrap(),
141///     "E1N1".parse().unwrap(),
142/// );
143/// let pos2 = Position::new(
144///     RoomCoordinate::try_from(40).unwrap(),
145///     RoomCoordinate::try_from(20).unwrap(),
146///     "E1N1".parse().unwrap(),
147/// );
148/// assert_eq!(pos1 + (40, 20), pos2);
149/// ```
150///
151/// And the `Sub` implementation can be used to get the offset between two
152/// positions:
153///
154/// ```
155/// # use std::convert::TryFrom;
156/// # use screeps::{Position, RoomCoordinate};
157/// let pos1 = Position::new(
158///     RoomCoordinate::try_from(4).unwrap(),
159///     RoomCoordinate::try_from(20).unwrap(),
160///     "E20S21".parse().unwrap(),
161/// );
162/// let pos2 = Position::new(
163///     RoomCoordinate::try_from(4).unwrap(),
164///     RoomCoordinate::try_from(30).unwrap(),
165///     "E20S22".parse().unwrap(),
166/// );
167/// assert_eq!(pos2 - pos1, (0, 60));
168///
169/// let pos3 = Position::new(
170///     RoomCoordinate::try_from(0).unwrap(),
171///     RoomCoordinate::try_from(0).unwrap(),
172///     "E20S21".parse().unwrap(),
173/// );
174/// assert_eq!(pos3 - pos1, (-4, -20));
175/// ```
176///
177/// # Ordering
178///
179/// To facilitate use as a key in a [`BTreeMap`] or other similar data
180/// structures, `Position` implements [`PartialOrd`] and [`Ord`].
181///
182/// `Position`s are ordered first by ascending world `y` position, then by
183/// ascending world `x` position. World `x` and `y` here simply extend the x,y
184/// coords within the room `E0S0` throughout the map.
185///
186/// Looking at positions as tuples `(world_x, world_y)`, the sorting obeys rules
187/// such as:
188///
189/// - `(a, 0) < (b, 1)` for any `a`, `b`
190/// - `(0, c) < (1, c)` for any `c`
191///
192/// This follows left-to-right reading order when looking at the Screeps map
193/// from above.
194///
195/// [`bincode`]: https://github.com/servo/bincode
196/// [`RoomObject::pos`]: crate::RoomObject::pos
197/// [`BTreeMap`]: std::collections::BTreeMap
198/// [`serde::Serialize`]: ::serde::Serialize
199#[derive(Copy, Clone, Eq, PartialEq, Hash)]
200#[repr(transparent)]
201pub struct Position {
202    /// A bit-packed integer, containing, from highest-order to lowest:
203    ///
204    /// - 1 byte: (room_x) + 128
205    /// - 1 byte: (room_y) + 128
206    /// - 1 byte: x
207    /// - 1 byte: y
208    ///
209    /// For `Wxx` rooms, `room_x = -xx - 1`. For `Exx` rooms, `room_x = xx`.
210    ///
211    /// For `Nyy` rooms, `room_y = -yy - 1`. For `Syy` rooms, `room_y = yy`.
212    ///
213    /// This is the same representation used in the Screeps server, allowing for
214    /// easy translation. Besides the method names and display representation,
215    /// this is the one part of RoomPosition copied directly from the
216    /// engine code.
217    packed: u32,
218}
219
220impl fmt::Debug for Position {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        f.debug_struct("Position")
223            .field("packed", &self.packed)
224            .field("x", &self.x())
225            .field("y", &self.y())
226            .field("room_name", &self.room_name())
227            .finish()
228    }
229}
230
231impl fmt::Display for Position {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        write!(
234            f,
235            "[room {} pos {},{}]",
236            self.room_name(),
237            self.x(),
238            self.y()
239        )
240    }
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub struct WorldPositionOutOfBoundsError(pub i32, pub i32);
245
246impl Position {
247    /// Create a new Position
248    ///
249    /// # Panics
250    ///
251    /// Will panic if either `x` or `y` is larger than 49, or if `room_name` is
252    /// outside of the range `E127N127 - W127S127`.
253    #[inline]
254    pub fn new(x: RoomCoordinate, y: RoomCoordinate, room_name: RoomName) -> Self {
255        Self::from_coords_adjusted_and_room_packed(x.into(), y.into(), room_name.packed_repr())
256    }
257
258    /// Creates a `Position` from x,y coordinates and room coordinates
259    /// already adjusted to be positive using `HALF_WORLD_SIZE`.
260    ///
261    /// Non-public as this doesn't check the bounds for any of these values.
262    #[inline]
263    const fn from_coords_and_world_coords_adjusted(x: u8, y: u8, room_x: u32, room_y: u32) -> Self {
264        Position {
265            packed: (room_x << 24) | (room_y << 16) | ((x as u32) << 8) | (y as u32),
266        }
267    }
268
269    /// Creates a `Position` from x,y coordinates and an already-packed room
270    /// representation.
271    ///
272    /// Non-public as this doesn't check the bounds for any of these values.
273    #[inline]
274    const fn from_coords_adjusted_and_room_packed(x: u8, y: u8, room_repr_packed: u16) -> Self {
275        Position {
276            packed: ((room_repr_packed as u32) << 16) | ((x as u32) << 8) | (y as u32),
277        }
278    }
279
280    #[inline]
281    pub const fn packed_repr(self) -> u32 {
282        self.packed
283    }
284
285    #[inline]
286    pub fn from_packed(packed: u32) -> Self {
287        let x = (packed >> 8) & 0xFF;
288        let y = packed & 0xFF;
289        assert!(x < ROOM_SIZE as u32, "out of bounds x: {x}");
290        assert!(y < ROOM_SIZE as u32, "out of bounds y: {y}");
291        Position { packed }
292    }
293
294    /// Gets the horizontal coordinate of this position's room name.
295    #[inline]
296    fn room_x(self) -> i32 {
297        self.room_name().x_coord()
298    }
299
300    /// Gets the vertical coordinate of this position's room name.
301    #[inline]
302    fn room_y(self) -> i32 {
303        self.room_name().y_coord()
304    }
305
306    /// Gets this position's in-room x coordinate.
307    #[inline]
308    pub fn x(self) -> RoomCoordinate {
309        // SAFETY: packed always contains a valid x coordinate
310        unsafe { RoomCoordinate::unchecked_new(((self.packed >> 8) & 0xFF) as u8) }
311    }
312
313    /// Gets this position's in-room y coordinate.
314    #[inline]
315    pub fn y(self) -> RoomCoordinate {
316        // SAFETY: packed always contains a valid y coordinate
317        unsafe { RoomCoordinate::unchecked_new((self.packed & 0xFF) as u8) }
318    }
319
320    /// Gets this position's in-room [`RoomXY`] coordinate pair
321    #[inline]
322    pub fn xy(self) -> RoomXY {
323        // SAFETY: packed always contains a valid pair
324        unsafe {
325            RoomXY::unchecked_new(
326                ((self.packed >> 8) & 0xFF) as u8,
327                (self.packed & 0xFF) as u8,
328            )
329        }
330    }
331
332    #[inline]
333    pub fn room_name(self) -> RoomName {
334        RoomName::from_packed(((self.packed >> 16) & 0xFFFF) as u16)
335    }
336
337    #[inline]
338    pub fn set_x(&mut self, x: RoomCoordinate) {
339        self.packed = (self.packed & !(0xFF << 8)) | ((u8::from(x) as u32) << 8);
340    }
341
342    #[inline]
343    pub fn set_y(&mut self, y: RoomCoordinate) {
344        self.packed = (self.packed & !0xFF) | (u8::from(y) as u32);
345    }
346
347    #[inline]
348    pub fn set_room_name(&mut self, room_name: RoomName) {
349        let room_repr_packed = room_name.packed_repr() as u32;
350        self.packed = (self.packed & 0xFFFF) | (room_repr_packed << 16);
351    }
352
353    #[inline]
354    pub fn with_x(mut self, x: RoomCoordinate) -> Self {
355        self.set_x(x);
356        self
357    }
358
359    #[inline]
360    pub fn with_y(mut self, y: RoomCoordinate) -> Self {
361        self.set_y(y);
362        self
363    }
364
365    #[inline]
366    pub fn with_room_name(mut self, room_name: RoomName) -> Self {
367        self.set_room_name(room_name);
368        self
369    }
370}
371
372impl PartialOrd for Position {
373    #[inline]
374    fn partial_cmp(&self, other: &Position) -> Option<Ordering> {
375        Some(self.cmp(other))
376    }
377}
378
379impl Ord for Position {
380    fn cmp(&self, other: &Self) -> Ordering {
381        self.world_y()
382            .cmp(&other.world_y())
383            .then_with(|| self.world_x().cmp(&other.world_x()))
384    }
385}
386
387impl HasPosition for Position {
388    fn pos(&self) -> Position {
389        *self
390    }
391}
392
393impl From<RoomPosition> for Position {
394    fn from(js_pos: RoomPosition) -> Self {
395        Position::from_packed(js_pos.packed())
396    }
397}
398
399impl From<&RoomPosition> for Position {
400    fn from(js_pos: &RoomPosition) -> Self {
401        Position::from_packed(js_pos.packed())
402    }
403}
404
405mod serde {
406    use serde::{Deserialize, Deserializer, Serialize, Serializer};
407
408    use super::{Position, RoomCoordinate, RoomName};
409
410    #[derive(Serialize, Deserialize)]
411    #[serde(rename_all = "camelCase")]
412    struct ReadableFormat {
413        room_name: RoomName,
414        x: RoomCoordinate,
415        y: RoomCoordinate,
416    }
417
418    impl From<ReadableFormat> for Position {
419        fn from(ReadableFormat { room_name, x, y }: ReadableFormat) -> Self {
420            Position::new(x, y, room_name)
421        }
422    }
423
424    impl From<Position> for ReadableFormat {
425        fn from(pos: Position) -> Self {
426            ReadableFormat {
427                room_name: pos.room_name(),
428                x: pos.x(),
429                y: pos.y(),
430            }
431        }
432    }
433
434    impl Serialize for Position {
435        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
436        where
437            S: Serializer,
438        {
439            if serializer.is_human_readable() {
440                ReadableFormat::from(*self).serialize(serializer)
441            } else {
442                self.packed_repr().serialize(serializer)
443            }
444        }
445    }
446
447    impl<'de> Deserialize<'de> for Position {
448        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
449        where
450            D: Deserializer<'de>,
451        {
452            if deserializer.is_human_readable() {
453                ReadableFormat::deserialize(deserializer).map(Into::into)
454            } else {
455                u32::deserialize(deserializer).map(Position::from_packed)
456            }
457        }
458    }
459}
460
461/// Module for use with `serde`'s [`with` attribute] to allow serialization of
462/// positions as their packed representation, even when using a human-readable
463/// serializer.
464///
465/// [`with` attribute]: https://serde.rs/field-attrs.html#with
466pub mod serde_position_packed {
467    use serde::{Deserialize, Deserializer, Serialize, Serializer};
468
469    use super::Position;
470
471    pub fn serialize<S>(pos: &Position, serializer: S) -> Result<S::Ok, S::Error>
472    where
473        S: Serializer,
474    {
475        pos.packed_repr().serialize(serializer)
476    }
477
478    pub fn deserialize<'de, D>(deserializer: D) -> Result<Position, D::Error>
479    where
480        D: Deserializer<'de>,
481    {
482        u32::deserialize(deserializer).map(Position::from_packed)
483    }
484}
485
486#[cfg(test)]
487mod test {
488    use super::{Position, RoomCoordinate};
489
490    fn gen_test_positions() -> Vec<(u32, (RoomCoordinate, RoomCoordinate, &'static str))> {
491        unsafe {
492            vec![
493                (
494                    2172526892u32,
495                    (
496                        RoomCoordinate::unchecked_new(33),
497                        RoomCoordinate::unchecked_new(44),
498                        "E1N1",
499                    ),
500                ),
501                (
502                    2491351576u32,
503                    (
504                        RoomCoordinate::unchecked_new(2),
505                        RoomCoordinate::unchecked_new(24),
506                        "E20N0",
507                    ),
508                ),
509                (
510                    2139029504u32,
511                    (
512                        RoomCoordinate::unchecked_new(0),
513                        RoomCoordinate::unchecked_new(0),
514                        "W0N0",
515                    ),
516                ),
517                (
518                    2155806720u32,
519                    (
520                        RoomCoordinate::unchecked_new(0),
521                        RoomCoordinate::unchecked_new(0),
522                        "E0N0",
523                    ),
524                ),
525                (
526                    2139095040u32,
527                    (
528                        RoomCoordinate::unchecked_new(0),
529                        RoomCoordinate::unchecked_new(0),
530                        "W0S0",
531                    ),
532                ),
533                (
534                    2155872256u32,
535                    (
536                        RoomCoordinate::unchecked_new(0),
537                        RoomCoordinate::unchecked_new(0),
538                        "E0S0",
539                    ),
540                ),
541                (
542                    2021333800u32,
543                    (
544                        RoomCoordinate::unchecked_new(27),
545                        RoomCoordinate::unchecked_new(40),
546                        "W7N4",
547                    ),
548                ),
549                // this one is in the top left room - which is either sim, or W127N127 if the
550                // sim position overrides are inactive
551                (
552                    1285u32,
553                    (
554                        RoomCoordinate::unchecked_new(5),
555                        RoomCoordinate::unchecked_new(5),
556                        if cfg!(feature = "sim") {
557                            "sim"
558                        } else {
559                            "W127N127"
560                        },
561                    ),
562                ),
563            ]
564        }
565    }
566
567    #[test]
568    fn from_u32_accurate() {
569        for (packed, (x, y, name)) in gen_test_positions().iter().copied() {
570            let pos = Position::from_packed(packed);
571            assert_eq!(pos.x(), x);
572            assert_eq!(pos.y(), y);
573            assert_eq!(&*pos.room_name().to_array_string(), name);
574        }
575    }
576
577    #[test]
578    fn from_args_accurate() {
579        for (packed, (x, y, name)) in gen_test_positions().iter().copied() {
580            let pos = Position::new(x, y, name.parse().unwrap());
581            assert_eq!(pos.packed_repr(), packed);
582        }
583    }
584}