Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Packed, typed object ids #229

Merged
merged 8 commits into from
Aug 20, 2019
Merged
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 27 additions & 0 deletions javascript/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
49 changes: 45 additions & 4 deletions src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//!
//! [`Game`]: http://docs.screeps.com/api/#Game
use crate::{
local::{ObjectId, RawObjectId},
macros::*,
objects::{HasId, RoomObject, SizedRoomObject},
traits::TryInto,
Expand Down Expand Up @@ -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<Creep> = "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::<Creep>(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<T>(id: &str) -> Result<Option<T>, ConversionError>
pub fn get_object_typed<T>(id: ObjectId<T>) -> Result<Option<T>, 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]
Expand All @@ -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<RoomObject> {
js_unwrap_ref!(Game.getObjectById(@{id}))
pub fn get_object_erased(id: impl Into<RawObjectId>) -> Option<RoomObject> {
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<u32>) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down
3 changes: 2 additions & 1 deletion src/local.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Pure-data structures relating to Screeps.
use std::ops::Range;

mod object_id;
mod room_name;
mod room_position;

Expand All @@ -17,4 +18,4 @@ const HALF_WORLD_SIZE: i32 = 128;
/// Valid room name coordinates.
const VALID_ROOM_NAME_COORDINATES: Range<i32> = (-HALF_WORLD_SIZE..HALF_WORLD_SIZE);

pub use self::{room_name::*, room_position::*};
pub use self::{object_id::*, room_name::*, room_position::*};
192 changes: 192 additions & 0 deletions src/local/object_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
use std::{fmt, marker::PhantomData, str::FromStr};

use arrayvec::ArrayString;
use serde::{Deserialize, Serialize};
use stdweb::{Reference, UnsafeTypedArray};

use crate::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<T>` and [`RawObjectId`], and
/// [`ObjectId::into_type`] to change the type this `ObjectId` points to freely.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent, bound = "")]
pub struct ObjectId<T> {
raw: RawObjectId,
#[serde(skip)]
phantom: PhantomData<T>,
}

impl<T> FromStr for ObjectId<T> {
type Err = RawObjectIdParseError;

fn from_str(s: &str) -> Result<Self, RawObjectIdParseError> {
let raw: RawObjectId = s.parse()?;

Ok(raw.into())
}
}

impl<T> fmt::Display for ObjectId<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.raw.fmt(f)
}
}

impl<T> ObjectId<T> {
/// 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<U>(self) -> ObjectId<U> {
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<Creep> = 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<Self, ConversionError> {
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()
}
}

impl<T> From<RawObjectId> for ObjectId<T> {
fn from(raw: RawObjectId) -> Self {
ObjectId {
raw,
phantom: PhantomData,
}
}
}

impl<T> From<ObjectId<T>> for RawObjectId {
fn from(id: ObjectId<T>) -> Self {
id.raw
}
}
44 changes: 44 additions & 0 deletions src/local/object_id/errors.rs
Original file line number Diff line number Diff line change
@@ -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<ParseIntError> for RawObjectIdParseError {
fn from(e: ParseIntError) -> Self {
RawObjectIdParseError::Parse(e)
}
}

impl RawObjectIdParseError {
pub(crate) fn value_too_large(val: u128) -> Self {
RawObjectIdParseError::LargeValue(val)
}
}
Loading