diff --git a/Cargo.toml b/Cargo.toml index 191fbaa7..3ff34403 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,8 @@ seasonal-season-5 = [] # enable compatibility with the sim environment for positions sim = [] +snippets = [] + # Enable unsafe conversions of return codes with undefined behavior when values # aren't in the expected range unsafe-return-conversion = [] diff --git a/js/object_id.js b/js/object_id.js new file mode 100644 index 00000000..a36bce4b --- /dev/null +++ b/js/object_id.js @@ -0,0 +1,26 @@ +module.exports.object_id_into_uint8array = function(id, arr) { + // passed array is exactly 16 bytes -- 12 for id, 3 blank, 1 for string pad length + // initialize all of them so that we can assume they're initialized on the rust side + const padding = id.length; + let skip_bytes = 12 - Math.ceil(padding / 2); + + for (let byte = 0; byte < skip_bytes; byte++) { + arr[byte] = 0; + } + + const offset = padding % 2; + // if there's an odd number of characters, grab one for the next byte + if (offset === 1) { + arr[skip_bytes] = parseInt(id.substr(0, 1), 16) + skip_bytes++; + } + + for (let byte = 0; byte < 12 - skip_bytes; byte++) { + arr[byte + skip_bytes] = parseInt(id.substr(byte * 2 + offset, 2), 16) + } + + arr[12] = 0; + arr[13] = 0; + arr[14] = 0; + arr[15] = padding; +} diff --git a/js/part.js b/js/part.js new file mode 100644 index 00000000..4d802e2a --- /dev/null +++ b/js/part.js @@ -0,0 +1,9 @@ +module.exports.bodypart_to_part_num = function(part_str_to_num_map, bodypart) { + return part_str_to_num_map.get(bodypart.type) +} + +module.exports.part_nums_to_str_array = function(part_num_to_str_map, body_num_array) { + // this is a Uint8Array and its map can't produce strings as-is, + // spread it first so the map can result in an array with constant strings + return [...body_num_array].map((v) => part_num_to_str_map.get(v)); +} diff --git a/src/constants.rs b/src/constants.rs index ae4ad3a2..526def3a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -67,13 +67,14 @@ pub mod extra; pub mod find; pub mod look; mod numbers; +mod part; mod recipes; pub mod seasonal; mod small_enums; mod types; pub use self::{ - extra::*, find::FindConstant, look::LookConstant, numbers::*, recipes::FactoryRecipe, + extra::*, find::FindConstant, look::LookConstant, numbers::*, part::*, recipes::FactoryRecipe, small_enums::*, types::*, }; @@ -95,7 +96,7 @@ pub mod creep { MAX_CREEP_SIZE, RANGED_HEAL_POWER, REPAIR_COST, REPAIR_POWER, SPAWN_RENEW_RATIO, UPGRADE_CONTROLLER_POWER, }, - small_enums::Part, + part::Part, }; } diff --git a/src/constants/numbers.rs b/src/constants/numbers.rs index f0313415..ed441dad 100644 --- a/src/constants/numbers.rs +++ b/src/constants/numbers.rs @@ -11,7 +11,7 @@ use super::types::{ResourceType, StructureType}; // OBSTACLE_OBJECT_TYPES not yet implemented -// BODYPART_COST defined in `small_enums.rs` +// body parts and BODYPART_COST defined in `part.rs` // WORLD_WIDTH/HEIGHT deprecated, not implemented @@ -817,7 +817,7 @@ pub const fn stronghold_rampart_hits(core_level: u32) -> Option { pub const STRONGHOLD_DECAY_TICKS: u32 = 75_000; // POWER_INFO not yet implemented -// BODYPARTS_ALL implemented via Sequence trait in `small_enums.rs` +// BODYPARTS_ALL implemented via Sequence trait in `part.rs` // RESOURCES_ALL implemented via Sequence trait in `types.rs` // COLORS_ALL implemented via Sequence trait in `small_enums.rs` // INTERSHARD_RESOURCES defined in `types.rs` diff --git a/src/constants/part.rs b/src/constants/part.rs new file mode 100644 index 00000000..4facf1b1 --- /dev/null +++ b/src/constants/part.rs @@ -0,0 +1,181 @@ +use enum_iterator::Sequence; +use js_sys::{Array, JsString, Map}; +use num_derive::FromPrimitive; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use wasm_bindgen::prelude::*; + +use crate::objects::BodyPart; + +/// Translates body part type and `BODYPARTS_ALL` constants +#[wasm_bindgen] +#[derive( + Debug, + PartialEq, + Eq, + Clone, + Copy, + Hash, + FromPrimitive, + Serialize_repr, + Deserialize_repr, + Sequence, +)] +#[repr(u8)] +pub enum Part { + Move = 0, + Work = 1, + Carry = 2, + Attack = 3, + RangedAttack = 4, + Tough = 5, + Heal = 6, + Claim = 7, +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = MOVE)] + static MOVE_JS: JsString; + #[wasm_bindgen(js_name = WORK)] + static WORK_JS: JsString; + #[wasm_bindgen(js_name = CARRY)] + static CARRY_JS: JsString; + #[wasm_bindgen(js_name = ATTACK)] + static ATTACK_JS: JsString; + #[wasm_bindgen(js_name = RANGED_ATTACK)] + static RANGED_ATTACK_JS: JsString; + #[wasm_bindgen(js_name = TOUGH)] + static TOUGH_JS: JsString; + #[wasm_bindgen(js_name = HEAL)] + static HEAL_JS: JsString; + #[wasm_bindgen(js_name = CLAIM)] + static CLAIM_JS: JsString; +} + +thread_local! { + static PART_NUM_TO_STR_MAP: js_sys::Map = { + js_sys::Map::new() + .set(&JsValue::from(Part::Move as u8), &MOVE_JS) + .set(&JsValue::from(Part::Work as u8), &WORK_JS) + .set(&JsValue::from(Part::Carry as u8), &CARRY_JS) + .set(&JsValue::from(Part::Attack as u8), &ATTACK_JS) + .set(&JsValue::from(Part::RangedAttack as u8), &RANGED_ATTACK_JS) + .set(&JsValue::from(Part::Tough as u8), &TOUGH_JS) + .set(&JsValue::from(Part::Heal as u8), &HEAL_JS) + .set(&JsValue::from(Part::Claim as u8), &CLAIM_JS) + }; + + static PART_STR_TO_NUM_MAP: js_sys::Map = { + js_sys::Map::new() + .set(&MOVE_JS, &JsValue::from(Part::Move as u8)) + .set(&WORK_JS, &JsValue::from(Part::Work as u8)) + .set(&CARRY_JS, &JsValue::from(Part::Carry as u8)) + .set(&ATTACK_JS, &JsValue::from(Part::Attack as u8)) + .set(&RANGED_ATTACK_JS, &JsValue::from(Part::RangedAttack as u8)) + .set(&TOUGH_JS, &JsValue::from(Part::Tough as u8)) + .set(&HEAL_JS, &JsValue::from(Part::Heal as u8)) + .set(&CLAIM_JS, &JsValue::from(Part::Claim as u8)) + }; +} + +#[cfg(feature = "snippets")] +#[wasm_bindgen(module = "/js/part.js")] +extern "C" { + fn bodypart_to_part_num(map: &Map, body_part: &BodyPart) -> Part; + fn part_nums_to_str_array(map: &Map, part_array: &[u8]) -> Array; +} + +#[cfg(not(feature = "snippets"))] +fn bodypart_to_part_num(map: &Map, body_part: &BodyPart) -> Part { + use num_traits::FromPrimitive; + + let n = map.get(&body_part.part_jsvalue()).as_f64().expect("number") as u8; + Part::from_u8(n).expect("known part") +} + +#[cfg(not(feature = "snippets"))] +fn part_nums_to_str_array(map: &Map, part_array: &[u8]) -> Array { + let array = Array::new(); + for part_num in part_array { + array.push(&map.get(&JsValue::from(*part_num))); + } + array +} + +impl Part { + /// Translates the `BODYPART_COST` constant. + #[inline] + pub const fn cost(self) -> u32 { + match self { + Part::Move => 50, + Part::Work => 100, + Part::Carry => 50, + Part::Attack => 80, + Part::RangedAttack => 150, + Part::Tough => 10, + Part::Heal => 250, + Part::Claim => 600, + } + } + + pub(crate) fn slice_to_js_array(parts: &[Self]) -> Array { + PART_NUM_TO_STR_MAP.with(|map| { + // SAFETY: &[Part] contains u8 values because it's repr(u8), safe to transmute + // to &[u8] + part_nums_to_str_array(map, unsafe { std::mem::transmute::<&[Part], &[u8]>(parts) }) + }) + } + + pub(crate) fn from_bodypart(body_part: &BodyPart) -> Self { + PART_STR_TO_NUM_MAP.with(|map| bodypart_to_part_num(map, body_part)) + } +} + +#[cfg(test)] +mod test { + use wasm_bindgen::prelude::*; + use wasm_bindgen_test::*; + + use super::{part_nums_to_str_array, Part}; + + thread_local! { + static TEST_PART_NUM_TO_STR_MAP: js_sys::Map = { + js_sys::Map::new() + .set(&JsValue::from(Part::Move as u8), &JsValue::from_str("move")) + .set(&JsValue::from(Part::Work as u8), &JsValue::from_str("work")) + .set(&JsValue::from(Part::Carry as u8), &JsValue::from_str("carry")) + .set(&JsValue::from(Part::Attack as u8), &JsValue::from_str("attack")) + .set(&JsValue::from(Part::RangedAttack as u8), &JsValue::from_str("ranged_attack")) + .set(&JsValue::from(Part::Tough as u8), &JsValue::from_str("tough")) + .set(&JsValue::from(Part::Heal as u8), &JsValue::from_str("heal")) + .set(&JsValue::from(Part::Claim as u8), &JsValue::from_str("claim")) + }; + + static TEST_PART_STR_TO_NUM_MAP: js_sys::Map = { + js_sys::Map::new() + .set(&JsValue::from_str("move"), &JsValue::from(Part::Move as u8)) + .set(&JsValue::from_str("work"), &JsValue::from(Part::Work as u8)) + .set(&JsValue::from_str("carry"), &JsValue::from(Part::Carry as u8)) + .set(&JsValue::from_str("attack"), &JsValue::from(Part::Attack as u8)) + .set(&JsValue::from_str("ranged_attack"), &JsValue::from(Part::RangedAttack as u8)) + .set(&JsValue::from_str("tough"), &JsValue::from(Part::Tough as u8)) + .set(&JsValue::from_str("heal"), &JsValue::from(Part::Heal as u8)) + .set(&JsValue::from_str("claim"), &JsValue::from(Part::Claim as u8)) + }; + } + + #[wasm_bindgen_test] + pub fn parts_to_array() { + let body = [Part::Work, Part::Carry, Part::Move, Part::Move].as_slice(); + let array = TEST_PART_NUM_TO_STR_MAP.with(|map| { + // SAFETY: &[Part] contains u8 values because it's repr(u8), safe to transmute + // to &[u8] + part_nums_to_str_array(map, unsafe { std::mem::transmute(body) }) + }); + assert_eq!(array.length(), 4); + assert_eq!(array.get(0), "work"); + assert_eq!(array.get(1), "carry"); + assert_eq!(array.get(2), "move"); + assert_eq!(array.get(3), "move"); + } +} diff --git a/src/constants/small_enums.rs b/src/constants/small_enums.rs index e4d9709d..28e22216 100644 --- a/src/constants/small_enums.rs +++ b/src/constants/small_enums.rs @@ -1,18 +1,14 @@ //! Various constants translated as small enums. -use std::{borrow::Cow, fmt, slice::Iter}; +use std::{fmt, slice::Iter}; use enum_iterator::Sequence; use js_sys::JsString; use num_derive::FromPrimitive; use num_traits::FromPrimitive; -use serde::{ - de::{Error as _, Unexpected}, - Deserialize, Serialize, -}; +use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use wasm_bindgen::prelude::*; -use super::{macros::named_enum_serialize_deserialize, InvalidConstantString}; use crate::{ constants::find::{Exit, Find}, prelude::*, @@ -459,42 +455,6 @@ impl Terrain { } } -/// Translates body part type and `BODYPARTS_ALL` constants -#[wasm_bindgen] -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Sequence)] -pub enum Part { - Move = "move", - Work = "work", - Carry = "carry", - Attack = "attack", - RangedAttack = "ranged_attack", - Tough = "tough", - Heal = "heal", - Claim = "claim", -} - -named_enum_serialize_deserialize!(Part); - -impl Part { - /// Translates the `BODYPART_COST` constant. - #[inline] - pub const fn cost(self) -> u32 { - match self { - Part::Move => 50, - Part::Work => 100, - Part::Carry => 50, - Part::Attack => 80, - Part::RangedAttack => 150, - Part::Tough => 10, - Part::Heal => 250, - Part::Claim => 600, - // I guess bindgen is adding a `#[non_exhaustive]` onto the enum and forcing us to do - // this: - _ => 0, - } - } -} - /// Translates the `DENSITY_*` constants. #[wasm_bindgen] #[derive( diff --git a/src/local/object_id/raw.rs b/src/local/object_id/raw.rs index 96bd1907..9de01e01 100644 --- a/src/local/object_id/raw.rs +++ b/src/local/object_id/raw.rs @@ -69,7 +69,7 @@ impl Serialize for RawObjectId { struct RawObjectIdVisitor; -impl<'de> Visitor<'de> for RawObjectIdVisitor { +impl Visitor<'_> for RawObjectIdVisitor { type Value = RawObjectId; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { diff --git a/src/local/room_name.rs b/src/local/room_name.rs index 7e299f99..c7ffe648 100644 --- a/src/local/room_name.rs +++ b/src/local/room_name.rs @@ -548,7 +548,7 @@ mod serde { struct RoomNameVisitor; - impl<'de> Visitor<'de> for RoomNameVisitor { + impl Visitor<'_> for RoomNameVisitor { type Value = RoomName; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/src/objects/impls/creep.rs b/src/objects/impls/creep.rs index 408980c4..9889a5cb 100644 --- a/src/objects/impls/creep.rs +++ b/src/objects/impls/creep.rs @@ -679,8 +679,14 @@ extern "C" { pub fn boost(this: &BodyPart) -> Option; #[wasm_bindgen(method, getter = type)] - pub fn part(this: &BodyPart) -> Part; + pub(crate) fn part_jsvalue(this: &BodyPart) -> JsValue; #[wasm_bindgen(method, getter)] pub fn hits(this: &BodyPart) -> u32; } + +impl BodyPart { + pub fn part(&self) -> Part { + Part::from_bodypart(self) + } +} diff --git a/src/objects/impls/structure_spawn.rs b/src/objects/impls/structure_spawn.rs index 5eea00b5..d39745da 100644 --- a/src/objects/impls/structure_spawn.rs +++ b/src/objects/impls/structure_spawn.rs @@ -56,7 +56,7 @@ extern "C" { #[wasm_bindgen(method, js_name = spawnCreep)] fn spawn_creep_internal( this: &StructureSpawn, - body: &Array, + body: Array, name: &str, options: Option<&Object>, ) -> i8; @@ -79,9 +79,9 @@ impl StructureSpawn { /// /// [Screeps documentation](https://docs.screeps.com/api/#StructureSpawn.spawnCreep) pub fn spawn_creep(&self, body: &[Part], name: &str) -> Result<(), ErrorCode> { - let body = body.iter().cloned().map(JsValue::from).collect(); + let body_array = Part::slice_to_js_array(body); - ErrorCode::result_from_i8(Self::spawn_creep_internal(self, &body, name, None)) + ErrorCode::result_from_i8(Self::spawn_creep_internal(self, body_array, name, None)) } /// Create a new creep with the specified body part [`Array`], name @@ -99,7 +99,7 @@ impl StructureSpawn { name: &str, opts: &SpawnOptions, ) -> Result<(), ErrorCode> { - let body = body.iter().cloned().map(JsValue::from).collect(); + let body_array = Part::slice_to_js_array(body); let js_opts = ObjectExt::unchecked_from_js(JsValue::from(Object::new())); @@ -121,7 +121,7 @@ impl StructureSpawn { ErrorCode::result_from_i8(Self::spawn_creep_internal( self, - &body, + body_array, name, Some(&js_opts), ))