diff --git a/Cargo.toml b/Cargo.toml index ffa0fb0..4c8d8a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,10 @@ crate_type = ["rlib"] path = "src/lib.rs" [features] -c_interface = [] +ffi = [] serde = ["dep:serde"] -default = ["serde"] +wasm = ["dep:wasmer", "dep:unique-type-id", "dep:unique-type-id-derive"] +default = ["serde", "wasm"] [profile.dev] opt-level = 0 @@ -50,7 +51,11 @@ rand_pcg = "0.3.1" hashbrown = "0.12.1" indexmap = "1.8.2" parking_lot = "0.12.1" +conquer-once = "0.3.2" serde = { version = "1.0.137", optional = true, features = ["derive"] } +wasmer = { version = "2.3.0", optional = true, default-features = false, features = ["default-cranelift", "universal", "experimental-reference-types-extern-ref"] } +unique-type-id = { version = "1.0.0", optional = true } +unique-type-id-derive = { version = "1.0.0", optional = true } [dev-dependencies] csv = "1.1.6" diff --git a/src/dynamic_data/libraries/dynamic_library.rs b/src/dynamic_data/libraries/dynamic_library.rs index 3fd9d48..df42306 100644 --- a/src/dynamic_data/libraries/dynamic_library.rs +++ b/src/dynamic_data/libraries/dynamic_library.rs @@ -1,4 +1,3 @@ -use std::ffi::c_void; use std::ops::Deref; use std::sync::Arc; @@ -15,6 +14,7 @@ use crate::{PkmnResult, StringKey}; /// The dynamic library stores a static data library, as well as holding different libraries and /// calculators that might be customized between different generations and implementations. #[derive(Debug)] +#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))] pub struct DynamicLibrary { /// The static data is the immutable storage data for this library. static_data: StaticData, @@ -75,7 +75,7 @@ impl DynamicLibrary { /// can be created with this combination, returns None. pub fn load_script( &self, - owner: *const c_void, + owner: *const u8, _category: ScriptCategory, _key: &StringKey, ) -> PkmnResult>> { diff --git a/src/dynamic_data/libraries/script_resolver.rs b/src/dynamic_data/libraries/script_resolver.rs index 6e8f31e..be168da 100644 --- a/src/dynamic_data/libraries/script_resolver.rs +++ b/src/dynamic_data/libraries/script_resolver.rs @@ -1,4 +1,3 @@ -use std::ffi::c_void; use std::fmt::Debug; use std::sync::Arc; @@ -14,7 +13,7 @@ pub trait ScriptResolver: Debug { /// can be created with this combination, returns None. fn load_script( &self, - owner: *const c_void, + owner: *const u8, category: ScriptCategory, script_key: &StringKey, ) -> PkmnResult>>; @@ -29,7 +28,8 @@ pub trait ScriptResolver: Debug { /// the same name, but a different script. It should be completely valid for a move to have the same /// name as an ability, or more commonly: for a script attached to a Pokemon to have the same name as /// a move that placed it there. -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[repr(u8)] pub enum ScriptCategory { /// A script that belongs to a move. This generally is only the script that is attached to a /// [`MoveChoice`](crate::dynamic_data::MoveChoice) and [`ExecutingMove`](crate::dynamic_data::ExecutingMove) @@ -58,7 +58,7 @@ pub struct EmptyScriptResolver {} impl ScriptResolver for EmptyScriptResolver { fn load_script( &self, - _owner: *const c_void, + _owner: *const u8, _category: ScriptCategory, _script_key: &StringKey, ) -> PkmnResult>> { diff --git a/src/lib.rs b/src/lib.rs index 235ea72..1eb342e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ #![feature(once_cell)] #![feature(const_option)] #![feature(is_some_with)] +#![feature(core_ffi_c)] +#![feature(new_uninit)] //! PkmnLib //! PkmnLib is a full featured implementation of Pokemon. while currently focused on implementing @@ -27,6 +29,8 @@ pub mod defines; /// The dynamic data module holds data that can change during execution, and things that relate to /// this. This includes things as Pokemon themselves, battles, etc. pub mod dynamic_data; +/// Script implementations handles the different ways that dynamic scripts get loaded during battle. +pub mod script_implementations; /// The static data module holds data that can be set once, and then never change. This includes /// things such as data about Pokemon species, data about items, etc. pub mod static_data; diff --git a/src/script_implementations/mod.rs b/src/script_implementations/mod.rs new file mode 100644 index 0000000..dfcd6ab --- /dev/null +++ b/src/script_implementations/mod.rs @@ -0,0 +1,3 @@ +/// The WASM module handles loading dynamic scripts through WebAssembly. +#[cfg(feature = "wasm")] +pub mod wasm; diff --git a/src/script_implementations/wasm/export_registry/mod.rs b/src/script_implementations/wasm/export_registry/mod.rs new file mode 100644 index 0000000..1f704da --- /dev/null +++ b/src/script_implementations/wasm/export_registry/mod.rs @@ -0,0 +1,103 @@ +use core::ffi::c_char; +use std::borrow::Cow; +use std::ffi::CString; +use std::mem::{align_of, size_of}; + +use wasmer::wasmparser::Data; +use wasmer::{Exports, Function, Store}; + +use crate::dynamic_data::DynamicLibrary; +use crate::script_implementations::wasm::extern_ref::ExternRef; +use crate::script_implementations::wasm::script_resolver::WebAssemblyEnv; +use crate::static_data::{DataLibrary, MoveData, MoveLibrary, StaticData}; +use crate::StringKey; + +macro_rules! register_func { + ($exports: ident, $store: ident, $func: ident) => { + $exports.insert(stringify!($func), Function::new_native($store, $func)); + }; +} + +macro_rules! register_func_with_env { + ($exports: ident, $store: ident, $func: ident, $env: expr) => { + $exports.insert( + stringify!($func), + Function::new_native_with_env($store, $env.clone(), $func), + ); + }; +} + +pub(crate) fn register_webassembly_funcs(exports: &mut Exports, store: &Store, env: WebAssemblyEnv) { + register_func_with_env!(exports, store, _print, env); + register_func_with_env!(exports, store, _error, env); + register_func_with_env!(exports, store, move_library_get_move_by_hash, env); + register_func_with_env!(exports, store, move_data_get_name, env); + register_func_with_env!(exports, store, move_data_get_base_power, env); + register_func_with_env!(exports, store, const_string_get_hash, env); + register_func_with_env!(exports, store, const_string_get_str, env); + register_func_with_env!(exports, store, battle_library_get_data_library, env); + register_func_with_env!(exports, store, data_library_get_move_library, env); +} + +fn _print(env: &WebAssemblyEnv, p: u32, len: u32) { + unsafe { + let mem: *mut u8 = env.resolver().memory().data_ptr().offset(p as isize); + let s = String::from_raw_parts(mem, len as usize, len as usize); + println!("{}", s); + } +} + +fn _error(env: &WebAssemblyEnv, message: u32, message_len: u32, file: u32, file_len: u32, line: u32, position: u32) { + unsafe { + let mem: *mut u8 = env.resolver().memory().data_ptr().offset(message as isize); + let message = String::from_raw_parts(mem, message_len as usize, message_len as usize); + let mem: *mut u8 = env.resolver().memory().data_ptr().offset(file as isize); + let file = String::from_raw_parts(mem, file_len as usize, file_len as usize); + println!("Error: {} in file {}, line: {}:{}", message, file, line, position); + } +} + +fn move_library_get_move_by_hash(env: &WebAssemblyEnv, lib: ExternRef, hash: u32) -> ExternRef { + let lib = lib.value(env); + let m = lib.get_by_hash(hash); + if let Some(v) = m { + ExternRef::new(env, v) + } else { + ExternRef::null() + } +} + +fn move_data_get_name(env: &WebAssemblyEnv, move_data: ExternRef) -> ExternRef { + let move_data = move_data.value(env); + ExternRef::new(env, move_data.name()) +} + +fn move_data_get_base_power(env: &WebAssemblyEnv, move_data: ExternRef) -> u8 { + move_data.value(env).base_power() +} + +fn const_string_get_hash(env: &WebAssemblyEnv, string_key: ExternRef) -> u32 { + string_key.value(env).hash() +} + +fn const_string_get_str(env: &WebAssemblyEnv, string_key: ExternRef) -> u32 { + let string_key = string_key.value(env).str(); + let mut s: CString = CString::new(string_key.as_bytes()).unwrap(); + let wasm_string_ptr = env + .resolver() + .allocate_mem(string_key.len() as u32, align_of::() as u32); + let mut wasm_string = unsafe { CString::from_raw(wasm_string_ptr.0 as *mut c_char) }; + s.clone_into(&mut wasm_string); + wasm_string_ptr.1 +} + +fn battle_library_get_data_library( + env: &WebAssemblyEnv, + dynamic_lib: ExternRef, +) -> ExternRef { + ExternRef::new(env, dynamic_lib.value(env).static_data()) +} + +fn data_library_get_move_library(env: &WebAssemblyEnv, data_library: ExternRef) -> ExternRef { + ExternRef::new(env, data_library.value(env).moves()) +} diff --git a/src/script_implementations/wasm/extern_ref.rs b/src/script_implementations/wasm/extern_ref.rs new file mode 100644 index 0000000..092db80 --- /dev/null +++ b/src/script_implementations/wasm/extern_ref.rs @@ -0,0 +1,54 @@ +use std::marker::PhantomData; + +use unique_type_id::UniqueTypeId; +use wasmer::FromToNativeWasmType; + +use crate::script_implementations::wasm::script_resolver::WebAssemblyEnv; + +pub(crate) struct ExternRef> { + index: u32, + _phantom: PhantomData, +} + +impl> ExternRef { + pub fn new(env: &WebAssemblyEnv, value: &T) -> Self { + Self { + index: env.resolver().get_extern_ref_index(value), + _phantom: Default::default(), + } + } + + pub fn from_index(index: u32) -> Self { + Self { + index, + _phantom: Default::default(), + } + } + + pub fn null() -> Self { + Self { + index: 0, + _phantom: Default::default(), + } + } + + pub fn value<'a, 'b>(&'a self, env: &'b WebAssemblyEnv) -> &'b T { + let ptr = env.resolver().get_extern_ref_value(self.index) as *const T; + unsafe { ptr.as_ref().unwrap() } + } +} + +unsafe impl> FromToNativeWasmType for ExternRef { + type Native = i32; + + fn from_native(native: Self::Native) -> Self { + Self { + index: native as u32, + _phantom: Default::default(), + } + } + + fn to_native(self) -> Self::Native { + self.index as i32 + } +} diff --git a/src/script_implementations/wasm/mod.rs b/src/script_implementations/wasm/mod.rs new file mode 100644 index 0000000..4a50b1a --- /dev/null +++ b/src/script_implementations/wasm/mod.rs @@ -0,0 +1,70 @@ +/// The export registry module deals with registering all functions we require in WebAssembly. +mod export_registry; +pub(crate) mod extern_ref; +/// The script module deals with the actual running of WASM functions. +pub mod script; +/// The script resolver deals with the loading of scripts. +pub mod script_resolver; + +/// The WebAssemblyScriptCapabilities define which functions are implemented on a script. This allows +/// us to not call a function if we do not need to. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +#[allow(missing_docs)] +pub enum WebAssemblyScriptCapabilities { + None = 0, + Initialize = 1, + OnStack, + OnRemove, + OnBeforeTurn, + ChangeAttack, + ModifyNumberOfHits, + PreventAttack, + FailAttack, + StopBeforeAttack, + OnBeforeAttack, + FailIncomingAttack, + IsInvulnerable, + OnAttackMiss, + ChangeAttackType, + ChangeEffectiveness, + BlockCritical, + OnIncomingHit, + OnFaintingOpponent, + PreventStatBoostChange, + ModifyStatBoostChange, + PreventSecondaryEffects, + OnSecondaryEffect, + OnAfterHits, + PreventSelfSwitch, + ModifyEffectChance, + ModifyIncomingEffectChance, + OverrideBasePower, + ChangeDamageStatsUser, + BypassDefensiveStat, + BypassOffensiveStat, + ModifyStatModifier, + ModifyDamageModifier, + OverrideDamage, + OverrideIncomingDamage, + ChangeSpeed, + ChangePriority, + OnFail, + OnOpponentFail, + PreventRunAway, + PreventOpponentRunAway, + PreventOpponentSwitch, + OnEndTurn, + OnDamage, + OnFaint, + OnAfterHeldItemConsume, + PreventIncomingCritical, + ModifyCriticalStage, + OverrideCriticalModifier, + OverrideSTABModifier, + ModifyExperienceGain, + DoesShareExperience, + BlockWeather, + OnSwitchIn, + ModifyOffensiveStatValue, + ModifyDefensiveStatValue, +} diff --git a/src/script_implementations/wasm/script.rs b/src/script_implementations/wasm/script.rs new file mode 100644 index 0000000..ea5824f --- /dev/null +++ b/src/script_implementations/wasm/script.rs @@ -0,0 +1,73 @@ +use std::any::Any; +use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize}; + +use hashbrown::HashSet; + +use crate::dynamic_data::Script; +use crate::script_implementations::wasm::script_resolver::WebAssemblyScriptResolver; +use crate::script_implementations::wasm::WebAssemblyScriptCapabilities; +use crate::StringKey; + +/// A WebAssemblyScript is there to implement the Script trait within WebAssemblyScript. +pub struct WebAssemblyScript { + /// The unique identifier of the script. + name: StringKey, + /// Returns an atomic bool for internal marking of deletion. This is currently only specifically + /// used for deletion of a script while we are holding a reference to it (i.e. executing a script + /// hook on it). + marked_for_deletion: AtomicBool, + /// A script can be suppressed by other scripts. If a script is suppressed by at least one script + /// we will not execute its methods. This holds the number of suppressions on the script. + suppressed_count: AtomicUsize, + /// The owner of this script (where the script is attached to) + owner_ptr: AtomicPtr, + /// Pointer inside WebAssembly memory where the data is for this script. + self_ptr: u32, + /// Capabilities define which functions we actually implement. + capabilities: AtomicPtr>, + /// A reference back to our resolver. + resolver: AtomicPtr, +} + +impl WebAssemblyScript { + /// Instantiates a new WebAssemblyScript. + pub fn new( + owner_ptr: *mut u8, + self_ptr: u32, + capabilities: *mut HashSet, + resolver: *mut WebAssemblyScriptResolver, + name: StringKey, + ) -> Self { + Self { + name, + marked_for_deletion: Default::default(), + suppressed_count: Default::default(), + owner_ptr: AtomicPtr::new(owner_ptr), + self_ptr, + capabilities: AtomicPtr::new(capabilities), + resolver: AtomicPtr::new(resolver), + } + } +} + +impl Script for WebAssemblyScript { + fn name(&self) -> &StringKey { + &self.name + } + + fn get_marked_for_deletion(&self) -> &AtomicBool { + &self.marked_for_deletion + } + + fn get_suppressed_count(&self) -> &AtomicUsize { + &self.suppressed_count + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} diff --git a/src/script_implementations/wasm/script_capabilities.rs b/src/script_implementations/wasm/script_capabilities.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/script_implementations/wasm/script_capabilities.rs @@ -0,0 +1 @@ + diff --git a/src/script_implementations/wasm/script_resolver.rs b/src/script_implementations/wasm/script_resolver.rs new file mode 100644 index 0000000..af75f0c --- /dev/null +++ b/src/script_implementations/wasm/script_resolver.rs @@ -0,0 +1,253 @@ +use std::any::{Any, TypeId}; +use std::fmt::{Debug, Formatter}; +use std::pin::Pin; +use std::sync::Arc; + +use hashbrown::{HashMap, HashSet}; +use parking_lot::RwLock; +use unique_type_id::UniqueTypeId; +use wasmer::{ + Cranelift, Exports, Extern, Features, Function, ImportObject, Instance, Memory, Module, NativeFunc, Store, + Universal, UniversalEngine, Value, WasmerEnv, +}; + +use crate::dynamic_data::{ItemScript, Script, ScriptResolver}; +use crate::script_implementations::wasm::export_registry::register_webassembly_funcs; +use crate::script_implementations::wasm::extern_ref::ExternRef; +use crate::script_implementations::wasm::script::WebAssemblyScript; +use crate::script_implementations::wasm::WebAssemblyScriptCapabilities; +use crate::static_data::Item; +use crate::{PkmnResult, ScriptCategory, StringKey}; + +/// A WebAssembly script resolver implements the dynamic scripts functionality with WebAssembly. +pub struct WebAssemblyScriptResolver { + engine: UniversalEngine, + store: Store, + module: Option, + instance: Option, + memory: Option, + imports: HashMap, + exports: Exports, + exported_functions: HashMap, + load_script_fn: Option), u32>>, + allocate_mem_fn: Option>, + script_capabilities: RwLock>>, + + extern_ref_pointers: RwLock>, + extern_ref_pointers_lookup: RwLock>, + extern_ref_type_lookup: RwLock>, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +struct ScriptCapabilitiesKey { + category: ScriptCategory, + script_key: StringKey, +} + +impl WebAssemblyScriptResolver { + /// Instantiates a new WebAssemblyScriptResolver. + pub fn new() -> Self { + let config = Cranelift::default(); + let mut features = Features::new(); + features.multi_value = true; + features.reference_types = true; + let universal = Universal::new(config).features(features); + let engine = universal.engine(); + let store = Store::new(&engine); + Self { + engine, + store, + module: Default::default(), + instance: Default::default(), + memory: Default::default(), + imports: Default::default(), + exports: Default::default(), + exported_functions: Default::default(), + load_script_fn: None, + allocate_mem_fn: None, + script_capabilities: Default::default(), + extern_ref_pointers: Default::default(), + extern_ref_pointers_lookup: Default::default(), + extern_ref_type_lookup: Default::default(), + } + } + + /// Load a compiled WASM module. + pub fn load_wasm_from_bytes(&mut self, bytes: &[u8]) { + // FIXME: Error handling + let module = Module::new(&self.store, bytes).unwrap(); + self.module = Some(module); + + self.finalize(); + } + + /// Initialise all the data we need. + fn finalize(&mut self) { + let mut imports = ImportObject::new(); + let mut exports = Exports::new(); + + let env = WebAssemblyEnv { + resolver: self as *const WebAssemblyScriptResolver, + }; + register_webassembly_funcs(&mut exports, &self.store, env); + imports.register("env", exports); + + self.instance = Some(Instance::new(&self.module.as_ref().unwrap(), &imports).unwrap()); + let exports = &self.instance.as_ref().unwrap().exports; + for export in exports.iter() { + match export.1 { + Extern::Function(f) => { + self.exported_functions.insert(export.0.as_str().into(), f.clone()); + } + Extern::Memory(m) => { + self.memory = Some(m.clone()); + } + _ => {} + } + } + if let Some(m) = &self.memory { + m.grow(32).unwrap(); + } + if let Some(f) = self.exported_functions.get(&"load_script".into()) { + self.load_script_fn = Some(f.native().unwrap()) + } + if let Some(f) = self.exported_functions.get(&"allocate_mem".into()) { + self.allocate_mem_fn = Some(f.native().unwrap()) + } + } + + /// Gets the internal WASM memory. + pub fn memory(&self) -> &Memory { + self.memory.as_ref().unwrap() + } + + /// Get a numeric value from any given value. This is not a true Extern Ref from WASM, as this + /// is not supported by our current WASM platform (Rust). Instead, this is simply a way to not + /// have to send arbitrary pointer values back and forth with WASM. Only values WASM can actually + /// access can be touched through this, and we ensure the value is the correct type. In the future, + /// when extern refs get actually properly implemented at compile time we might want to get rid + /// of this code. + pub fn get_extern_ref_index>(&self, value: &T) -> u32 { + let ptr = value as *const T as *const u8; + if let Some(v) = self.extern_ref_pointers_lookup.read().get(&ptr) { + return *v as u32; + } + let index = { + let mut extern_ref_guard = self.extern_ref_pointers.write(); + extern_ref_guard.push(ptr); + extern_ref_guard.len() as u32 + }; + self.extern_ref_pointers_lookup.write().insert(ptr, index); + self.extern_ref_type_lookup.write().insert(ptr, T::id().0); + index + } + + /// Gets a value from the extern ref lookup. This turns an earlier registered index back into + /// its proper value, validates its type, and returns the value. + pub fn get_extern_ref_value>(&self, index: u32) -> &T { + let read_guard = self.extern_ref_pointers.read(); + let ptr = read_guard.get(index as usize).unwrap(); + let expected_type_id = self.extern_ref_type_lookup.read()[&ptr]; + if expected_type_id != T::id().0 { + panic!("Extern ref was accessed with wrong type"); + } + + unsafe { (*ptr as *const T).as_ref().unwrap() } + } + + /// Allocates memory inside the WASM container with a given size and alignment. This memory is + /// owned by WASM, and is how we can pass memory references that the host allocated to WASM. + /// The return is a tuple containing both the actual pointer to the memory (usable by the host), + /// and the WASM offset to the memory (usable by the client). + pub fn allocate_mem(&self, size: u32, align: u32) -> (*const u8, u32) { + let wasm_ptr = self.allocate_mem_fn.as_ref().unwrap().call(size, align).unwrap(); + unsafe { + ( + self.memory.as_ref().unwrap().data_ptr().offset(wasm_ptr as isize), + wasm_ptr, + ) + } + } +} + +impl ScriptResolver for WebAssemblyScriptResolver { + fn load_script( + &self, + owner: *const u8, + category: ScriptCategory, + script_key: &StringKey, + ) -> PkmnResult>> { + let script = self + .load_script_fn + .as_ref() + .unwrap() + .call( + category as u8, + ExternRef::from_index(self.get_extern_ref_index(script_key)), + ) + .unwrap(); + if script == 0 { + return Ok(None); + } + + let key = ScriptCapabilitiesKey { + category, + script_key: script_key.clone(), + }; + + if !self.script_capabilities.read().contains_key(&key) { + let mut capabilities = HashSet::new(); + unsafe { + if let Some(get_cap) = self.exported_functions.get(&"get_script_capabilities".into()) { + let res = get_cap.call(&[Value::I32(script as i32)]).unwrap(); + let ptr = (self.memory.as_ref().unwrap().data_ptr() as *const WebAssemblyScriptCapabilities) + .offset(res[0].i32().unwrap() as isize); + let length = res[1].i32().unwrap() as usize; + for i in 0..length { + capabilities.insert(*ptr.offset(i as isize)); + } + } + } + self.script_capabilities.write().insert(key.clone(), capabilities); + } + + let read_guard = self.script_capabilities.read(); + let capabilities = read_guard.get(&key).unwrap(); + + Ok(Some(Arc::new(WebAssemblyScript::new( + owner as *mut u8, + script, + capabilities as *const HashSet + as *mut HashSet, + self as *const WebAssemblyScriptResolver as *mut WebAssemblyScriptResolver, + script_key.clone(), + )))) + } + + fn load_item_script(&self, _key: &Item) -> PkmnResult>> { + todo!() + } +} + +impl Debug for WebAssemblyScriptResolver { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("WebAssemblyScriptResolver") + } +} + +#[derive(Clone)] +pub(crate) struct WebAssemblyEnv { + pub resolver: *const WebAssemblyScriptResolver, +} + +impl WebAssemblyEnv { + pub fn resolver(&self) -> &WebAssemblyScriptResolver { + unsafe { self.resolver.as_ref().unwrap() } + } +} + +unsafe impl Sync for WebAssemblyEnv {} + +unsafe impl Send for WebAssemblyEnv {} + +impl WasmerEnv for WebAssemblyEnv {} diff --git a/src/static_data/libraries/data_library.rs b/src/static_data/libraries/data_library.rs index ed149ab..38f6b4a 100644 --- a/src/static_data/libraries/data_library.rs +++ b/src/static_data/libraries/data_library.rs @@ -26,6 +26,11 @@ pub trait DataLibrary<'a, T: 'a> { self.map().get::(key) } + /// Gets a value from the library. + fn get_by_hash(&'a self, key: u32) -> Option<&'a T> { + self.map().get::(&key) + } + /// Gets a mutable value from the library. fn get_mut(&mut self, key: &StringKey) -> Option<&mut T> { self.get_modify().get_mut(key) diff --git a/src/static_data/libraries/move_library.rs b/src/static_data/libraries/move_library.rs index 480759f..9b1661e 100644 --- a/src/static_data/libraries/move_library.rs +++ b/src/static_data/libraries/move_library.rs @@ -6,6 +6,7 @@ use crate::StringKey; /// A library to store all data for moves. #[derive(Debug)] +#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))] pub struct MoveLibrary { /// The underlying map. map: IndexMap, diff --git a/src/static_data/libraries/static_data.rs b/src/static_data/libraries/static_data.rs index 02ea402..e2a2af1 100644 --- a/src/static_data/libraries/static_data.rs +++ b/src/static_data/libraries/static_data.rs @@ -9,6 +9,7 @@ use crate::static_data::TypeLibrary; /// The storage for all different libraries. #[derive(Debug)] +#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))] pub struct StaticData { /// Several misc settings for the library. settings: LibrarySettings, diff --git a/src/static_data/moves/move_data.rs b/src/static_data/moves/move_data.rs index bb2f2fa..3612f11 100644 --- a/src/static_data/moves/move_data.rs +++ b/src/static_data/moves/move_data.rs @@ -59,6 +59,7 @@ pub enum MoveTarget { /// A move is the skill Pokémon primarily use in battle. This is the data related to that. #[derive(PartialEq, Debug)] +#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))] pub struct MoveData { /// The name of the move. name: StringKey, diff --git a/src/static_data/species_data/species.rs b/src/static_data/species_data/species.rs index fdc6dfd..e120e66 100644 --- a/src/static_data/species_data/species.rs +++ b/src/static_data/species_data/species.rs @@ -1,5 +1,3 @@ -use std::lazy::SyncLazy; - use hashbrown::{HashMap, HashSet}; use crate::static_data::Form; @@ -28,7 +26,11 @@ pub struct Species { } /// A cached String Key to get the default form. -static DEFAULT_KEY: SyncLazy = SyncLazy::new(|| StringKey::new("default")); +static DEFAULT_KEY: conquer_once::OnceCell = conquer_once::OnceCell::uninit(); + +fn get_default_key() -> StringKey { + DEFAULT_KEY.get_or_init(|| StringKey::new("default")).clone() +} impl Species { /// Creates a new species. @@ -42,7 +44,7 @@ impl Species { flags: HashSet, ) -> Species { let mut forms = HashMap::with_capacity(1); - forms.insert_unique_unchecked(DEFAULT_KEY.clone(), default_form); + forms.insert_unique_unchecked(get_default_key(), default_form); Species { id, name: name.clone(), @@ -96,7 +98,7 @@ impl Species { /// Gets the form the Pokemon will have by default, if no other form is specified. pub fn get_default_form(&self) -> &Form { - self.forms.get(&DEFAULT_KEY).unwrap() + self.forms.get(&get_default_key()).unwrap() } /// Gets a random gender. diff --git a/src/utils/string_key.rs b/src/utils/string_key.rs index 0f67756..356fc31 100644 --- a/src/utils/string_key.rs +++ b/src/utils/string_key.rs @@ -1,15 +1,17 @@ use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; -use std::lazy::SyncLazy; use std::sync::{Arc, Mutex, Weak}; +use conquer_once::OnceCell; use hashbrown::HashMap; +use indexmap::Equivalent; /// StringKey is an immutable string that is used for indexing of hashmaps or equality a lot. /// By reference counting the string instead of copying, and caching the hash, we can get some /// free speed out of it. Note that StringKeys also compare case insensitive, so that for example /// `charmander` == `Charmander`. #[derive(Clone, Debug)] +#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))] pub struct StringKey { /// The underlying reference counted string. str: Arc, @@ -19,9 +21,9 @@ pub struct StringKey { /// A cache of all allocated strings. This allows us to re-use strings that are often used without /// allocation. -static STRING_CACHE: SyncLazy>>> = SyncLazy::new(|| Mutex::new(HashMap::new())); +static STRING_CACHE: OnceCell>>> = OnceCell::uninit(); /// An empty StringKey -static EMPTY: SyncLazy = SyncLazy::new(|| StringKey::new("")); +static EMPTY: OnceCell = OnceCell::uninit(); impl StringKey { /// Calculates the hash of a string key in a const manner. @@ -49,7 +51,7 @@ impl StringKey { /// that value. pub fn new(s: &str) -> Self { let hash = StringKey::get_hash(s); - let mut cache = STRING_CACHE.lock().unwrap(); + let mut cache = STRING_CACHE.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap(); let cached_value = cache.get(&hash); if let Some(cached_value) = cached_value { if let Some(cached_value) = cached_value.upgrade() { @@ -66,7 +68,7 @@ impl StringKey { /// Gets the empty StringKey. pub fn empty() -> Self { - EMPTY.clone() + EMPTY.get_or_init(|| StringKey::new("")).clone() } /// Gets the underlying string for the StringKey. @@ -100,6 +102,12 @@ impl From<&str> for StringKey { } } +impl Equivalent for u32 { + fn equivalent(&self, key: &StringKey) -> bool { + *self == key.hash + } +} + impl Display for StringKey { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(&*self.str) diff --git a/tests/common/library_loader.rs b/tests/common/library_loader.rs index 9f06ffb..94ffe73 100644 --- a/tests/common/library_loader.rs +++ b/tests/common/library_loader.rs @@ -1,7 +1,7 @@ use std::convert::TryFrom; use std::fmt::Debug; use std::fs::File; -use std::io::Read; +use std::io::{BufReader, Read}; use hashbrown::HashSet; use num_traits::PrimInt; @@ -13,6 +13,7 @@ use pkmn_lib::dynamic_data::Gen7BattleStatCalculator; use pkmn_lib::dynamic_data::Gen7DamageLibrary; use pkmn_lib::dynamic_data::Gen7MiscLibrary; use pkmn_lib::dynamic_data::{DynamicLibrary, EmptyScriptResolver}; +use pkmn_lib::script_implementations::wasm::script_resolver::WebAssemblyScriptResolver; use pkmn_lib::static_data::{ Ability, AbilityLibrary, BattleItemCategory, DataLibrary, EffectParameter, Form, GrowthRateLibrary, Item, ItemLibrary, LearnableMoves, LibrarySettings, LookupGrowthRate, MoveData, MoveLibrary, Nature, NatureLibrary, @@ -32,12 +33,15 @@ pub fn load_library() -> DynamicLibrary { load_abilities(&path, data.abilities_mut()); load_moves(&path, &mut data); load_species(&path, &mut data); + let mut resolver = WebAssemblyScriptResolver::new(); + load_wasm(&path, &mut resolver); + let dynamic = DynamicLibrary::new( data, Box::new(Gen7BattleStatCalculator {}), Box::new(Gen7DamageLibrary::new(false)), Box::new(Gen7MiscLibrary::new()), - Box::new(EmptyScriptResolver {}), + Box::new(resolver), ); dynamic } @@ -271,6 +275,14 @@ pub fn load_species(path: &String, library: &mut StaticData) { } } +fn load_wasm(path: &String, library: &mut WebAssemblyScriptResolver) { + let mut file = File::open(path.to_string() + "gen7_scripts_rs.wasm").unwrap(); + let mut reader = BufReader::new(file); + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer); + library.load_wasm_from_bytes(&buffer); +} + fn parse_form(name: StringKey, value: &Value, library: &mut StaticData) -> Form { let mut abilities = Vec::new(); for a in value["abilities"].as_array().unwrap() { diff --git a/tests/data/gen7_scripts_rs.wasm b/tests/data/gen7_scripts_rs.wasm new file mode 100755 index 0000000..1b3ac5d Binary files /dev/null and b/tests/data/gen7_scripts_rs.wasm differ diff --git a/tests/main.rs b/tests/main.rs index 9afb94e..647e0bb 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -4,27 +4,35 @@ use std::fs::File; use std::io::Read; -use std::lazy::SyncLazy; use std::path::Path; -use pkmn_lib::dynamic_data::DynamicLibrary; +use conquer_once::OnceCell; + +use pkmn_lib::dynamic_data::{DynamicLibrary, ScriptCategory}; use crate::common::{library_loader, TestCase}; pub mod common; -static LIBRARY: SyncLazy = SyncLazy::new(|| { - let start_time = chrono::Utc::now(); - let lib = library_loader::load_library(); - let end_time = chrono::Utc::now(); - println!("Built library in {} ms", (end_time - start_time).num_milliseconds()); - lib -}); +static LIBRARY: OnceCell = OnceCell::uninit(); + +fn get_library<'a>() -> &'a DynamicLibrary { + LIBRARY.get_or_init(|| { + let start_time = chrono::Utc::now(); + let lib = library_loader::load_library(); + let end_time = chrono::Utc::now(); + println!("Built library in {} ms", (end_time - start_time).num_milliseconds()); + lib + }) +} #[test] #[cfg_attr(miri, ignore)] fn validate_library_load() { - SyncLazy::force(&LIBRARY); + let start_time = chrono::Utc::now(); + let lib = library_loader::load_library(); + let end_time = chrono::Utc::now(); + println!("Built library in {} ms", (end_time - start_time).num_milliseconds()); } #[datatest::files("tests/test_cases", { @@ -37,5 +45,16 @@ fn integration_tests(input: &Path) { file.read_to_string(&mut str).unwrap(); let test_case = serde_yaml::from_str::(&*str).unwrap(); println!("\tRunning integration test {}", test_case.name); - test_case.run_test(&LIBRARY); + test_case.run_test(get_library()); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn validate_script() { + let lib = library_loader::load_library(); + let script = lib + .load_script(0 as *const u8, ScriptCategory::Move, &"test".into()) + .unwrap() + .unwrap(); + script.on_initialize(&[]); } diff --git a/types.toml b/types.toml new file mode 100644 index 0000000..52ff811 --- /dev/null +++ b/types.toml @@ -0,0 +1,5 @@ +MoveLibrary = 0 +MoveData = 1 +StringKey = 2 +DynamicLibrary = 3 +StaticData = 4