screeps/local/
object_id.rs

1use std::{
2    cmp::Ordering,
3    fmt,
4    hash::{Hash, Hasher},
5    marker::PhantomData,
6    str::FromStr,
7};
8
9use arrayvec::ArrayString;
10use js_sys::JsString;
11use serde::{Deserialize, Serialize};
12use wasm_bindgen::{JsCast, JsValue};
13
14use crate::{game, js_collections::JsCollectionFromValue, objects::RoomObject, traits::MaybeHasId};
15
16mod errors;
17mod raw;
18
19pub use errors::*;
20pub use raw::*;
21
22/// Represents an Object ID and a type that the ID points to, stored in Rust
23/// memory.
24///
25/// Use [`JsObjectId`] if a reference stored in JavaScript memory is preferred.
26///
27/// Each object ID in Screeps: World is represented by an ID of up to 24
28/// hexidemical characters, which cannot change. This implementation takes
29/// advantage of that by storing a packed representation in a `u128`, using 96
30/// bits for the ID and 32 bits for tracking the length of the ID string for
31/// reconstruction in JS.
32///
33/// # Conversion
34///
35/// Use `into` to convert between `ObjectId<T>` and [`RawObjectId`], and
36/// [`ObjectId::into_type`] to change the type this `ObjectId` points to freely.
37///
38/// # Ordering
39///
40/// To facilitate use as a key in a [`BTreeMap`] or other similar data
41/// structures, `ObjectId` implements [`PartialOrd`] and [`Ord`].
42///
43/// `ObjectId`'s are ordered by the corresponding order of their underlying
44/// byte values. This agrees with:
45///
46/// - lexicographical ordering of the object id strings
47/// - JavaScript's ordering of object id strings
48/// - ordering of [`RawObjectId`]s
49///
50/// **Note:** when running on the official screeps server, or on a private
51/// server backed by a MongoDB database, this ordering roughly corresponds to
52/// creation order. The first four bytes of a MongoDB-created `ObjectId` [are
53/// seconds since the epoch when the id was created][1], so up to a second
54/// accuracy, these ids will be sorted by object creation time.
55///
56/// [`BTreeMap`]: std::collections::BTreeMap
57/// [`JsObjectId`]: crate::js_collections::JsObjectId
58/// [1]: https://docs.mongodb.com/manual/reference/method/ObjectId/
59// Copy, Clone, Debug, PartialEq, Eq, Hash, PartialEq, Eq implemented manually below
60#[derive(Serialize, Deserialize)]
61#[serde(transparent, bound = "")]
62pub struct ObjectId<T> {
63    raw: RawObjectId,
64
65    // Needed to consider the `T` as "used" even though we mostly use it as a marker. Because of
66    // auto traits, `PhantomData<fn() -> T>` is used instead: this struct doesn't *hold* a `T`, it
67    // *produces* one.
68    #[serde(skip)]
69    phantom: PhantomData<fn() -> T>,
70}
71
72// traits implemented manually so they don't depend on `T` implementing them.
73impl<T> Copy for ObjectId<T> {}
74impl<T> Clone for ObjectId<T> {
75    fn clone(&self) -> ObjectId<T> {
76        *self
77    }
78}
79impl<T> fmt::Debug for ObjectId<T> {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        self.raw.fmt(f)
82    }
83}
84impl<T> PartialEq for ObjectId<T> {
85    fn eq(&self, o: &ObjectId<T>) -> bool {
86        self.raw.eq(&o.raw)
87    }
88}
89impl<T> Eq for ObjectId<T> {}
90impl<T> Hash for ObjectId<T> {
91    fn hash<H: Hasher>(&self, state: &mut H) {
92        self.raw.hash(state)
93    }
94}
95impl<T> PartialOrd<ObjectId<T>> for ObjectId<T> {
96    #[inline]
97    fn partial_cmp(&self, other: &ObjectId<T>) -> Option<Ordering> {
98        Some(self.cmp(other))
99    }
100}
101impl<T> Ord for ObjectId<T> {
102    #[inline]
103    fn cmp(&self, other: &Self) -> Ordering {
104        self.raw.cmp(&other.raw)
105    }
106}
107
108impl<T> FromStr for ObjectId<T> {
109    type Err = RawObjectIdParseError;
110
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        let raw: RawObjectId = s.parse()?;
113
114        Ok(raw.into())
115    }
116}
117
118impl<T> fmt::Display for ObjectId<T> {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        self.raw.fmt(f)
121    }
122}
123
124impl<T> ObjectId<T> {
125    /// Changes the type this [`ObjectId`] points to, unchecked.
126    ///
127    /// This will allow changing to any type - `ObjectId` makes no guarantees
128    /// about its ID matching the type of any object in the game that it
129    /// actually points to.
130    pub fn into_type<U>(self) -> ObjectId<U> {
131        RawObjectId::from(self).into()
132    }
133
134    /// Creates an object ID from its packed representation.
135    ///
136    /// The input to this function is the bytes representing the up-to-24 hex
137    /// digits in the object id.
138    ///
139    /// See also [`RawObjectId::from_packed`].
140    pub fn from_packed(packed: u128) -> Self {
141        RawObjectId::from_packed(packed).into()
142    }
143
144    /// Converts this object ID to a `u128` number.
145    ///
146    /// The returned number, when formatted as hex, will produce a string
147    /// parseable into this object id.
148    ///
149    /// The returned number will be less than or equal to `2^96 - 1`, as that's
150    /// the maximum value that `RawObjectId` can hold.
151    pub fn to_u128(self) -> u128 {
152        self.raw.into()
153    }
154
155    /// Formats this object ID as a string on the stack.
156    ///
157    /// This is equivalent to [`ToString::to_string`], but involves no
158    /// allocation.
159    ///
160    /// See also [`RawObjectId::to_array_string`].
161    pub fn to_array_string(&self) -> ArrayString<24> {
162        self.raw.to_array_string()
163    }
164
165    /// Resolves this object ID into an object, verifying that the returned
166    /// object matches the expected type.
167    ///
168    /// # Errors
169    ///
170    /// Will return an error if this ID's type does not match the object it
171    /// points to with the resolved [`RoomObject`] with an unknown type.
172    ///
173    /// Will return `Ok(None)` if the object no longer exists, or is in a room
174    /// we don't have vision for.
175    pub fn try_resolve(self) -> Result<Option<T>, RoomObject>
176    where
177        T: MaybeHasId + JsCast,
178    {
179        match game::get_object_by_id_erased(&self.raw) {
180            Some(v) => v.dyn_into().map(|v| Some(v)),
181            None => Ok(None),
182        }
183    }
184
185    /// Resolves this ID into an object, assuming the type `T` is the correct
186    /// type of object that this ID refers to. If the ID has been converted to
187    /// an invalid type, using the returned object in a way not valid for its
188    /// type will cause a panic.
189    ///
190    /// Will return `None` if this object no longer exists, or is in a room we
191    /// don't have vision for.
192    pub fn resolve(self) -> Option<T>
193    where
194        T: MaybeHasId + JsCast,
195    {
196        game::get_object_by_id_typed(&self)
197    }
198}
199
200impl<T> PartialEq<RawObjectId> for ObjectId<T> {
201    #[inline]
202    fn eq(&self, other: &RawObjectId) -> bool {
203        self.raw == *other
204    }
205}
206
207impl<T> PartialEq<ObjectId<T>> for RawObjectId {
208    #[inline]
209    fn eq(&self, other: &ObjectId<T>) -> bool {
210        *self == other.raw
211    }
212}
213
214impl<T> PartialOrd<RawObjectId> for ObjectId<T> {
215    #[inline]
216    fn partial_cmp(&self, other: &RawObjectId) -> Option<Ordering> {
217        Some(self.raw.cmp(other))
218    }
219}
220
221impl<T> PartialOrd<ObjectId<T>> for RawObjectId {
222    #[inline]
223    fn partial_cmp(&self, other: &ObjectId<T>) -> Option<Ordering> {
224        Some(self.cmp(&other.raw))
225    }
226}
227
228impl<T> From<RawObjectId> for ObjectId<T> {
229    fn from(raw: RawObjectId) -> Self {
230        ObjectId {
231            raw,
232            phantom: PhantomData,
233        }
234    }
235}
236
237impl<T> From<ObjectId<T>> for RawObjectId {
238    fn from(id: ObjectId<T>) -> Self {
239        id.raw
240    }
241}
242
243impl<T> From<ObjectId<T>> for ArrayString<24> {
244    fn from(id: ObjectId<T>) -> Self {
245        id.to_array_string()
246    }
247}
248
249impl<T> From<ObjectId<T>> for String {
250    fn from(id: ObjectId<T>) -> Self {
251        id.to_string()
252    }
253}
254
255impl<T> From<ObjectId<T>> for u128 {
256    fn from(id: ObjectId<T>) -> Self {
257        id.raw.into()
258    }
259}
260
261impl<T> From<u128> for ObjectId<T> {
262    fn from(packed: u128) -> Self {
263        Self::from_packed(packed)
264    }
265}
266
267impl<T> JsCollectionFromValue for ObjectId<T> {
268    fn from_value(val: JsValue) -> Self {
269        let val: JsString = val.unchecked_into();
270        let val: String = val.into();
271        val.parse().expect("valid id string")
272    }
273}