diff --git a/.travis.yml b/.travis.yml index 774dfee8..ca8c81cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,3 +30,4 @@ script: - cargo web build --target=wasm32-unknown-unknown --verbose - cargo web build --target=wasm32-unknown-unknown --all-features --verbose - cargo test --verbose +- cargo web test --verbose --nodejs diff --git a/javascript/utils.js b/javascript/utils.js index 9b8ac515..dac84b23 100644 --- a/javascript/utils.js +++ b/javascript/utils.js @@ -262,3 +262,30 @@ function pos_from_packed(repr) { }); return pos; } + +function object_id_from_packed(slice) { + // reconstruct string in JS + let res = ""; + for (var i = 0; i < slice.length; i++) { + if (i > 0) { + res += slice[i].toString(16).padStart(8, "0"); + } else { + res += slice[i].toString(16); + } + } + return res; +} + +function object_id_to_packed(id) { + let packed = [0, 0, 0]; + if (id.length > 16) { + packed[0] = parseInt(id.slice(0, -16), 16); + } + if (id.length > 8) { + packed[1] = parseInt(id.slice(-16, -8), 16); + } + if (id.length > 0) { + packed[2] = parseInt(id.slice(-8), 16); + } + return packed; +} diff --git a/src/game.rs b/src/game.rs index 2b4abcca..9ef30d29 100644 --- a/src/game.rs +++ b/src/game.rs @@ -5,6 +5,7 @@ //! //! [`Game`]: http://docs.screeps.com/api/#Game use crate::{ + local::{ObjectId, RawObjectId}, macros::*, objects::{HasId, RoomObject, SizedRoomObject}, traits::TryInto, @@ -111,12 +112,47 @@ pub fn time() -> u32 { /// If all you want to assume is that something has an ID, use /// [`get_object_erased`]. /// +/// This uses the typed id type, [`ObjectId`]. Note that if you'd rather store +/// an untyped ID, it's free to convert from [`RawObjectId`] to [`ObjectId`]. +/// +/// # Example +/// +/// ```no_run +/// use screeps::{game, prelude::*, Creep, ObjectId}; +/// +/// // get your id however +/// let id: ObjectId = "aaaa".parse().unwrap(); +/// +/// let creep = game::get_object_typed(id).unwrap(); +/// match creep { +/// Some(creep) => println!("creep with id aaaa has name {}", creep.name()), +/// None => println!("no creep with id aaaa! such a surprise!"), +/// } +/// ``` +/// +/// Or, using `RawObjectId`, +/// +/// ```no_run +/// use screeps::{game, prelude::*, Creep, RawObjectId}; +/// +/// let id: RawObjectId = "bbbb".parse().unwrap(); +/// +/// let creep = game::get_object_typed::(id.into()).unwrap(); +/// if let Some(creep) = creep { +/// println!("creep with id bbbb exists, and has name {}", creep.name()); +/// } +/// ``` +/// /// [http://docs.screeps.com/api/#Game.getObjectById]: http://docs.screeps.com/api/#Game.getObjectById -pub fn get_object_typed(id: &str) -> Result, ConversionError> +pub fn get_object_typed(id: ObjectId) -> Result, ConversionError> where T: HasId + SizedRoomObject, { - js!(return Game.getObjectById(@{id});).try_into() + let array_view = unsafe { id.unsafe_as_uploaded() }; + (js! { + return Game.getObjectById(object_id_from_packed(@{array_view})); + }) + .try_into() } /// See [http://docs.screeps.com/api/#Game.getObjectById] @@ -126,9 +162,14 @@ where /// /// If a more specific type is expected, [`get_object_typed`] can be used. /// +/// The ID passed in must be either an [`ObjectId`], or a [`RawObjectId`]. Both +/// work, and the type of [`ObjectId`] if passed will be ignored. +/// /// [http://docs.screeps.com/api/#Game.getObjectById]: http://docs.screeps.com/api/#Game.getObjectById -pub fn get_object_erased(id: &str) -> Option { - js_unwrap_ref!(Game.getObjectById(@{id})) +pub fn get_object_erased(id: impl Into) -> Option { + let id = id.into(); + let array_view = unsafe { id.unsafe_as_uploaded() }; + js_unwrap_ref!(Game.getObjectById(object_id_from_packed(@{array_view}))) } pub fn notify(message: &str, group_interval: Option) { diff --git a/src/lib.rs b/src/lib.rs index 6f4228f1..b82abade 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ pub use stdweb::private::ConversionError; pub use crate::{ constants::*, js_collections::JsVec, - local::{Position, RoomName, RoomNameParseError}, + local::{ObjectId, Position, RawObjectId, RawObjectIdParseError, RoomName, RoomNameParseError}, objects::*, traits::{FromExpectedType, IntoExpectedType}, }; diff --git a/src/local.rs b/src/local.rs index 165acddf..ba4aeac8 100644 --- a/src/local.rs +++ b/src/local.rs @@ -1,6 +1,7 @@ //! Pure-data structures relating to Screeps. use std::ops::Range; +mod object_id; mod room_name; mod room_position; @@ -17,4 +18,4 @@ const HALF_WORLD_SIZE: i32 = 128; /// Valid room name coordinates. const VALID_ROOM_NAME_COORDINATES: Range = (-HALF_WORLD_SIZE..HALF_WORLD_SIZE); -pub use self::{room_name::*, room_position::*}; +pub use self::{object_id::*, room_name::*, room_position::*}; diff --git a/src/local/object_id.rs b/src/local/object_id.rs new file mode 100644 index 00000000..b581306a --- /dev/null +++ b/src/local/object_id.rs @@ -0,0 +1,289 @@ +use std::{ + cmp::{Eq, PartialEq}, + fmt, + hash::{Hash, Hasher}, + marker::PhantomData, + str::FromStr, +}; + +use arrayvec::ArrayString; +use serde::{Deserialize, Serialize}; +use stdweb::{Reference, UnsafeTypedArray}; + +use crate::{ + objects::{HasId, SizedRoomObject}, + ConversionError, +}; + +mod errors; +mod raw; + +pub use errors::*; +pub use raw::*; + +/// Represents an Object ID and a type that the ID points to. +/// +/// Each object id in screeps is represented by a Mongo GUID, which, +/// while not guaranteed, is unlikely to change. This takes advantage of that by +/// storing a packed representation of 12 bytes. +/// +/// This object ID is typed, but not strictly. It's completely safe to create an +/// ObjectId with an incorrect type, and all operations which use the type will +/// double-check at runtime. +/// +/// With that said, using this can provide nice type inference, and should have +/// few disadvantages to the lower-level alternative, [`RawObjectId`]. +/// +/// --- +/// +/// Use `into` to convert between `ObjectId` and [`RawObjectId`], and +/// [`ObjectId::into_type`] to change the type this `ObjectId` points to freely. +// Copy, Clone, Debug, PartialEq, Eq, Hash implemented manually below +#[derive(Serialize, Deserialize)] +#[serde(transparent, bound = "")] +pub struct ObjectId { + raw: RawObjectId, + #[serde(skip)] + phantom: PhantomData, +} + +// traits implemented manually so they don't depend on `T` implementing them. +impl Copy for ObjectId {} +impl Clone for ObjectId { + fn clone(&self) -> ObjectId { + ObjectId { + raw: self.raw.clone(), + phantom: PhantomData, + } + } +} +impl fmt::Debug for ObjectId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.raw.fmt(f) + } +} +impl PartialEq for ObjectId { + fn eq(&self, o: &ObjectId) -> bool { + self.raw.eq(&o.raw) + } +} +impl Eq for ObjectId {} +impl Hash for ObjectId { + fn hash(&self, state: &mut H) { + self.raw.hash(state) + } +} + +impl FromStr for ObjectId { + type Err = RawObjectIdParseError; + + fn from_str(s: &str) -> Result { + let raw: RawObjectId = s.parse()?; + + Ok(raw.into()) + } +} + +impl fmt::Display for ObjectId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.raw.fmt(f) + } +} + +impl ObjectId { + /// Changes the type this [`ObjectId`] points to, unchecked. + /// + /// This will allow changing to any type - `ObjectId` makes no guarantees + /// about its ID matching the type of any object in the game that it + /// actually points to. + pub fn into_type(self) -> ObjectId { + RawObjectId::from(self).into() + } + + /// Creates an object ID from its packed representation. + /// + /// The input to this function is the bytes representing the up-to-24 hex + /// digits in the object id. + /// + /// See also [`RawObjectId::from_packed`]. + pub fn from_packed(packed: [u32; 3]) -> Self { + RawObjectId::from_packed(packed).into() + } + + /// Creates an object ID from a packed representation stored in JavaScript. + /// + /// The input must be a reference to a length-3 array of integers. + /// + /// Recommended to be used with the `object_id_to_packed` JavaScript utility + /// function, which takes in a string and returns the array of three + /// integers that this function expects. + /// + /// # Example + /// + /// ```no_run + /// use screeps::{prelude::*, traits::TryInto, Creep, ObjectId}; + /// use stdweb::js; + /// + /// let packed_obj_id = (js! { + /// let creep = _.sample(Game.creeps); + /// return object_id_to_packed(creep.id); + /// }) + /// .try_into() + /// .unwrap(); + /// + /// let parsed: ObjectId = ObjectId::from_packed_js_val(packed_obj_id).unwrap(); + /// println!("found creep with id {}", parsed); + /// ``` + /// + /// See also [`RawObjectId::from_packed_js_val`]. + pub fn from_packed_js_val(packed_val: Reference) -> Result { + RawObjectId::from_packed_js_val(packed_val).map(Into::into) + } + + /// Formats this object ID as a string on the stack. + /// + /// This is equivalent to [`ToString::to_string`], but involves no + /// allocation. + /// + /// To use the produced string in stdweb, use `&*` to convert it to a string + /// slice. + /// + /// This is less efficient than [`ObjectId::unsafe_as_uploaded`], but + /// easier to get right. + /// + /// # Example + /// + /// ```no_run + /// use screeps::{prelude::*, Creep, ObjectId}; + /// use stdweb::js; + /// + /// let object_id = screeps::game::creeps::values()[0].id(); + /// + /// let str_repr = object_id.to_array_string(); + /// + /// js! { + /// let id = @{&*str_repr}; + /// console.log("we have a creep with the id " + id); + /// } + /// ``` + /// + /// See also [`RawObjectId::to_array_string`]. + pub fn to_array_string(&self) -> ArrayString<[u8; 24]> { + self.raw.to_array_string() + } + + /// Creates an array accessible from JavaScript which represents part of + /// this object id's packed representation. + /// + /// Specifically, the resulting array will contain the first non-zero number + /// in this object id, and all following numbers. This allows for a more + /// efficient `object_id_from_packed` implementation. + /// + /// # Safety + /// + /// This is highly unsafe. + /// + /// This creates an `UnsafeTypedArray` and does not use it in JS, so the + /// restrictions from [`UnsafeTypedArray`] apply. When you call into + /// JavaScript using it, you must "use" it immediately before calling into + /// any Rust code whatsoever. + /// + /// There are other safety concerns as well, but all deriving from + /// [`UnsafeTypedArray`]. See [`UnsafeTypedArray`]. + /// + /// # Example + /// + /// ```no_run + /// use screeps::{prelude::*, Creep, ObjectId}; + /// use stdweb::js; + /// + /// let object_id = screeps::game::creeps::values()[0].id(); + /// + /// let array_view = unsafe { object_id.unsafe_as_uploaded() }; + /// + /// js! { + /// let id = object_id_from_packed(@{array_view}); + /// console.log("we have a creep with the id " + id); + /// } + /// ``` + /// + /// See also [`RawObjectId::unsafe_as_uploaded`]. + pub unsafe fn unsafe_as_uploaded(&self) -> UnsafeTypedArray<'_, u32> { + self.raw.unsafe_as_uploaded() + } + + /// Resolves this object ID into an object. + /// + /// This is a shortcut for [`game::get_object_typed(id)`][1] + /// + /// # Errors + /// + /// Will return an error if this ID's type does not match the object it + /// points to. + /// + /// Will return `Ok(None)` if the object no longer exists, or is in a room + /// we don't have vision for. + /// + /// [1]: crate::game::get_object_typed + pub fn try_resolve(self) -> Result, ConversionError> + where + T: HasId + SizedRoomObject, + { + crate::game::get_object_typed(self) + } + + /// Resolves this ID into an object, panicking on type mismatch. + /// + /// This is a shortcut for [`id.try_resolve().expect(...)`][1] + /// + /// # Panics + /// + /// Will panic if this ID points to an object which is not of type `T`. + /// + /// Will return `None` if this object no longer exists, or is in a room we + /// don't have vision for. + /// + /// [1]: ObjectId::try_resolve + pub fn resolve(self) -> Option + where + T: HasId + SizedRoomObject, + { + match self.try_resolve() { + Ok(v) => v, + Err(e) => panic!("error resolving id {}: {}", self, e), + } + } +} + +impl From for ObjectId { + fn from(raw: RawObjectId) -> Self { + ObjectId { + raw, + phantom: PhantomData, + } + } +} + +impl From> for RawObjectId { + fn from(id: ObjectId) -> Self { + id.raw + } +} + +impl From> for ArrayString<[u8; 24]> { + fn from(id: ObjectId) -> Self { + id.to_array_string() + } +} + +impl From> for String { + fn from(id: ObjectId) -> Self { + id.to_string() + } +} + +impl From<[u32; 3]> for ObjectId { + fn from(packed: [u32; 3]) -> Self { + Self::from_packed(packed) + } +} diff --git a/src/local/object_id/errors.rs b/src/local/object_id/errors.rs new file mode 100644 index 00000000..74465603 --- /dev/null +++ b/src/local/object_id/errors.rs @@ -0,0 +1,44 @@ +use std::{error::Error, fmt, num::ParseIntError}; + +#[derive(Debug, Clone)] +pub enum RawObjectIdParseError { + Parse(ParseIntError), + LargeValue(u128), +} + +impl fmt::Display for RawObjectIdParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RawObjectIdParseError::Parse(e) => { + write!(f, "error parsing object id hex digits: {}", e) + } + RawObjectIdParseError::LargeValue(value) => write!( + f, + "string contained hex value too big be object id. \ + value {} bigger than maximum for 24 digits", + value + ), + } + } +} + +impl Error for RawObjectIdParseError { + fn cause(&self) -> Option<&dyn Error> { + match self { + RawObjectIdParseError::Parse(e) => Some(e), + RawObjectIdParseError::LargeValue(_) => None, + } + } +} + +impl From for RawObjectIdParseError { + fn from(e: ParseIntError) -> Self { + RawObjectIdParseError::Parse(e) + } +} + +impl RawObjectIdParseError { + pub(crate) fn value_too_large(val: u128) -> Self { + RawObjectIdParseError::LargeValue(val) + } +} diff --git a/src/local/object_id/raw.rs b/src/local/object_id/raw.rs new file mode 100644 index 00000000..b9a47c68 --- /dev/null +++ b/src/local/object_id/raw.rs @@ -0,0 +1,278 @@ +use std::{ + fmt::{self, Write}, + str::FromStr, +}; + +use arrayvec::ArrayString; +use serde::{Deserialize, Serialize}; +use stdweb::{Reference, UnsafeTypedArray}; + +use super::errors::RawObjectIdParseError; +use crate::{macros::*, traits::TryInto, ConversionError}; + +const MAX_PACKED_VAL: u128 = 1 << (32 * 3); + +/// Represents an Object ID using a packed 12-byte representation +/// +/// Each object id in screeps is represented by a Mongo GUID, which, +/// while not guaranteed, is unlikely to change. This takes advantage of that by +/// storing a packed representation. +/// +/// To convert to a String in JavaScript, either use +/// [`RawObjectId::to_array_string`], or [`RawObjectId::unsafe_as_uploaded`]. +/// See method documentation for more information. +#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct RawObjectId { + packed: [u32; 3], +} + +impl fmt::Debug for RawObjectId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RawObjectId") + .field("packed", &self.packed) + .field("real", &self.to_array_string()) + .finish() + } +} + +impl fmt::Display for RawObjectId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut ints = self.non_zero_packed_ints().iter(); + if let Some(first) = ints.next() { + write!(f, "{:x}", first)?; + } + for next in ints { + write!(f, "{:08x}", next)?; + } + Ok(()) + } +} + +impl FromStr for RawObjectId { + type Err = RawObjectIdParseError; + + fn from_str(s: &str) -> Result { + let u128_val = u128::from_str_radix(s, 16)?; + + if u128_val > MAX_PACKED_VAL { + return Err(RawObjectIdParseError::value_too_large(u128_val)); + } + + // if the endianness is right, then I think this should optimize down to a + // transmute. If it isn't, then it should be pretty efficient anyways and will + // still be _correct_. + let as_array = [ + ((u128_val >> 64) & 0xFFFF_FFFF) as u32, + ((u128_val >> 32) & 0xFFFF_FFFF) as u32, + (u128_val & 0xFFFF_FFFF) as u32, + ]; + + Ok(RawObjectId::from_packed(as_array)) + } +} + +impl RawObjectId { + /// Creates an object ID from its packed representation. + /// + /// The input to this function is the bytes representing the up-to-24 hex + /// digits in the object id. + pub fn from_packed(packed: [u32; 3]) -> Self { + RawObjectId { packed } + } + + /// Creates an object ID from a packed representation stored in JavaScript. + /// + /// The input must be a reference to a length-3 array of integers. + /// + /// Recommended to be used with the `object_id_to_packed` JavaScript utility + /// function, which takes in a string and returns the array of three + /// integers that this function expects. + /// + /// # Example + /// + /// ```no_run + /// use screeps::{prelude::*, traits::TryInto, RawObjectId}; + /// use stdweb::js; + /// + /// let packed_obj_id = (js! { + /// let creep = _.sample(Game.creeps); + /// return object_id_to_packed(creep.id); + /// }) + /// .try_into() + /// .unwrap(); + /// + /// let parsed = RawObjectId::from_packed_js_val(packed_obj_id).unwrap(); + /// println!("found creep with id {}", parsed); + /// ``` + pub fn from_packed_js_val(packed_val: Reference) -> Result { + let mut packed = [0u32; 3]; + // TODO: make this more efficient, once we get mutable UnsafeTypedArrays. + // See https://github.com/koute/stdweb/issues/360. + packed[0] = js! {return @{&packed_val}[0]}.try_into()?; + packed[1] = js! {return @{&packed_val}[1]}.try_into()?; + packed[2] = js! {return @{&packed_val}[2]}.try_into()?; + + Ok(Self::from_packed(packed)) + } + + /// Internal function which trims off leading zero integers. + fn non_zero_packed_ints(&self) -> &[u32] { + for i in 0..3 { + if self.packed[i] != 0 { + return &self.packed[i..3]; + } + } + // fallback to static zero-sized slice if we have no non-zero integers... + &[] + } + + /// Formats this object ID as a string on the stack. + /// + /// This is equivalent to [`ToString::to_string`], but involves no + /// allocation. + /// + /// To use the produced string in stdweb, use `&*` to convert it to a string + /// slice. + /// + /// This is less efficient than [`RawObjectId::unsafe_as_uploaded`], but + /// easier to get right. + /// + /// # Example + /// + /// ```no_run + /// use screeps::{prelude::*, RawObjectId}; + /// use stdweb::js; + /// + /// let object_id: RawObjectId = screeps::game::creeps::values()[0].untyped_id(); + /// + /// let str_repr = object_id.to_array_string(); + /// + /// js! { + /// let id = @{&*str_repr}; + /// console.log("we have a creep with the id " + id); + /// } + /// ``` + pub fn to_array_string(&self) -> ArrayString<[u8; 24]> { + let mut res = ArrayString::new(); + write!(res, "{}", self).expect("expected formatting into a fixed-sized buffer to succeed"); + res + } + + /// Creates an array accessible from JavaScript which represents part of + /// this object id's packed representation. + /// + /// Specifically, the resulting array will contain the first non-zero number + /// in this object id, and all following numbers. This allows for a more + /// efficient `object_id_from_packed` implementation. + /// + /// # Safety + /// + /// This is highly unsafe. + /// + /// This creates an `UnsafeTypedArray` and does not use it in JS, so the + /// restrictions from [`UnsafeTypedArray`] apply. When you call into + /// JavaScript using it, you must "use" it immediately before calling into + /// any Rust code whatsoever. + /// + /// There are other safety concerns as well, but all deriving from + /// [`UnsafeTypedArray`]. See [`UnsafeTypedArray`]. + /// + /// # Example + /// + /// ```no_run + /// use screeps::{prelude::*, RawObjectId}; + /// use stdweb::js; + /// + /// let object_id: RawObjectId = screeps::game::creeps::values()[0].untyped_id(); + /// + /// let array_view = unsafe { object_id.unsafe_as_uploaded() }; + /// + /// js! { + /// let id = object_id_from_packed(@{array_view}); + /// console.log("we have a creep with the id " + id); + /// } + /// ``` + pub unsafe fn unsafe_as_uploaded(&self) -> UnsafeTypedArray<'_, u32> { + UnsafeTypedArray::new(self.non_zero_packed_ints()) + } +} + +impl From for ArrayString<[u8; 24]> { + fn from(id: RawObjectId) -> Self { + id.to_array_string() + } +} + +impl From for String { + fn from(id: RawObjectId) -> Self { + id.to_string() + } +} + +impl From<[u32; 3]> for RawObjectId { + fn from(packed: [u32; 3]) -> Self { + Self::from_packed(packed) + } +} + +#[cfg(test)] +mod test { + use super::RawObjectId; + + #[cfg(target_arch = "wasm32")] + use crate::macros::*; + + const TEST_IDS: &[&str] = &[ + "bc03381d32f6790", + "1", + "ffffffffffffffffffffffff", + "100000000000000000000000", + "10000000000000000", + "1000000000000000", + "100000000", + "10000000", + ]; + + #[test] + fn rust_display_rust_fromstr_roundtrip() { + for id in TEST_IDS { + let parsed: RawObjectId = id.parse().unwrap(); + assert_eq!(&*parsed.to_string(), *id); + } + } + + #[test] + fn rust_to_array_string_rust_fromstr_roundtrip() { + for id in TEST_IDS { + let parsed: RawObjectId = id.parse().unwrap(); + assert_eq!(&*parsed.to_array_string(), *id); + } + } + + #[test] + #[cfg(target_arch = "wasm32")] + fn js_format_rust_fromstr_roundtrip() { + for id in TEST_IDS { + let parsed: RawObjectId = id.parse().unwrap(); + let array_view = unsafe { parsed.unsafe_as_uploaded() }; + let js_produced_string: String = js_unwrap!(object_id_from_packed(@{array_view})); + let reparsed: RawObjectId = js_produced_string + .parse() + .expect("expected to successfully reparse object id"); + assert_eq!(parsed, reparsed); + } + } + + #[test] + #[cfg(target_arch = "wasm32")] + fn rust_display_js_parse_roundtrip() { + for id in TEST_IDS { + let parsed: RawObjectId = id.parse().unwrap(); + let string = parsed.to_array_string(); + let js_produced_vals = js_unwrap!(object_id_to_packed(@{&*string})); + let recreated = RawObjectId::from_packed_js_val(js_produced_vals).unwrap(); + assert_eq!(parsed, recreated); + } + } +} diff --git a/src/objects.rs b/src/objects.rs index 61af4ff3..2be42c89 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -18,7 +18,7 @@ use stdweb_derive::ReferenceType; use crate::{ constants::{ResourceType, ReturnCode, StructureType}, - local::Position, + local::{ObjectId, Position, RawObjectId}, macros::*, traits::{IntoExpectedType, TryFrom, TryInto}, ConversionError, @@ -134,8 +134,33 @@ where /// Trait covering all objects with an id. pub unsafe trait HasId: RoomObjectProperties { - fn id(&self) -> String { - js_unwrap!(@{self.as_ref()}.id) + /// Retrieves this object's id as an untyped, packed value. + /// + /// This has no major differences from [`HasId::id`] except for the return + /// value not being typed by the kind of thing it points to. As the type of + /// an `ObjectId` can be freely changed, that isn't a big deal. + fn untyped_id(&self) -> RawObjectId { + RawObjectId::from_packed_js_val(js_unwrap!(object_id_to_packed(@{self.as_ref()}.id))) + .expect("expected HasId type's JavaScript id to be a 12-byte number encoded in hex") + } + + /// Retrieves this object's id as a typed, packed value. + /// + /// This can be helpful for use with [`game::get_object_typed`][1], as it + /// will force rust to infer the proper return type. + /// + /// If an ID without these protections is needed, use [`HasId::untyped_id`], + /// or `RawObjectId::from(x.id())`. + /// + /// Note that the ID returned is also stored as a packed, 12-byte value on + /// the stack, so it's fairly efficient to move and copy around. + /// + /// [1]: crate::game::get_object_typed + fn id(&self) -> ObjectId + where + Self: Sized, + { + self.untyped_id().into() } }