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}