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}