diff --git a/examples/python_example/src/python_example/__init__.py b/examples/python_example/src/__init__.py similarity index 100% rename from examples/python_example/src/python_example/__init__.py rename to examples/python_example/src/__init__.py diff --git a/examples/python_example/src/python_example/app.py b/examples/python_example/src/app.py similarity index 98% rename from examples/python_example/src/python_example/app.py rename to examples/python_example/src/app.py index 94e897c..5781377 100644 --- a/examples/python_example/src/python_example/app.py +++ b/examples/python_example/src/app.py @@ -7,7 +7,7 @@ from example.imports.app import App, Commands, Query, QueryFor_Mut, QueryFor_With, Schedule_Update, System class Example(example.Example): - def setup(self): + def setup(self, app: App): spin_cube = System("spin-cube") spin_cube.add_query([ QueryFor_Mut("bevy_transform::components::transform::Transform"), @@ -20,7 +20,6 @@ def setup(self): QueryFor_With("python::MyComponent") ]) - app = App() app.add_systems(Schedule_Update(), [my_system, spin_cube]) # Spin speed diff --git a/examples/python_example/src/python_example/example/__init__.py b/examples/python_example/src/example/__init__.py similarity index 74% rename from examples/python_example/src/python_example/example/__init__.py rename to examples/python_example/src/example/__init__.py index 4777068..cb786ca 100644 --- a/examples/python_example/src/python_example/example/__init__.py +++ b/examples/python_example/src/example/__init__.py @@ -29,9 +29,12 @@ def my_system(self, commands: app.Commands, query: app.Query) -> None: raise NotImplementedError @abstractmethod - def setup(self) -> None: + def setup(self, app: app.App) -> None: """ - This function is called once on startup for each WASM component (Not Bevy component). + This method is called once on startup for each WASM component (Not Bevy component). + + In this method you should register and configure `system`s via the `app` resource + passed as a parameter. """ raise NotImplementedError diff --git a/examples/python_example/src/python_example/example/imports/__init__.py b/examples/python_example/src/example/imports/__init__.py similarity index 100% rename from examples/python_example/src/python_example/example/imports/__init__.py rename to examples/python_example/src/example/imports/__init__.py diff --git a/examples/python_example/src/python_example/example/imports/app.py b/examples/python_example/src/example/imports/app.py similarity index 52% rename from examples/python_example/src/python_example/example/imports/app.py rename to examples/python_example/src/example/imports/app.py index e233fa0..91a96ce 100644 --- a/examples/python_example/src/python_example/example/imports/app.py +++ b/examples/python_example/src/example/imports/app.py @@ -78,7 +78,7 @@ class QueryFor_Without: class System: """ - An interface with which to define a new system for the host + An interface with which to define a new system for the host. Usage: 1. Construct a new system, giving it a unique name @@ -127,21 +127,77 @@ def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseExceptio class App: """ - A mod, similar to bevy::App + This is an interface (similar to bevy::App) through which mods may interact with the Bevy App. + + To access this, make sure to import the 'guest' world and implement `setup`. """ - def __init__(self) -> None: + def add_systems(self, schedule: Schedule, systems: List[System]) -> None: """ - Construct an new App: an interface through which mods may interact with the bevy world. - - Each mod may only do this once inside its setup function call. Attempting to do this - twice or outside setup will trap. + Adds systems to the mod + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. """ raise NotImplementedError - def add_systems(self, schedule: Schedule, systems: List[System]) -> None: + +class Entity: + """ + An identifier for an entity. + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: """ - Adds systems to the mod + Release this resource. + """ + raise NotImplementedError + + +class EntityCommands: + """ + A list of commands that will be run to modify an `entity`. + """ + + def id(self) -> Entity: + """ + Returns the identifier for this entity + """ + raise NotImplementedError + def insert(self, bundle: List[Tuple[str, str]]) -> None: + """ + Adds a `bundle` of components to the entity. + + This will overwrite any previous value(s) of the same component type. + """ + raise NotImplementedError + def remove(self, bundle: List[str]) -> None: + """ + Removes a Bundle of components from the entity if it exists. + """ + raise NotImplementedError + def despawn(self) -> None: + """ + Despawns the entity. + + This will emit a warning if the entity does not exist. + """ + raise NotImplementedError + def try_despawn(self) -> None: + """ + Despawns the entity. + + Unlike `despawn`, this will not emit a warning if the entity does not exist. """ raise NotImplementedError def __enter__(self) -> Self: @@ -157,10 +213,35 @@ def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseExceptio class Commands: """ - A commands system param + A `command` queue system param to perform structural changes to the world. + + Since each command requires exclusive access to the world, + all queued commands are automatically applied in sequence. + + Each command can be used to modify the world in arbitrary ways: + - spawning or despawning entities + - inserting components on new or existing entities + - etc. """ - def spawn(self, components: List[Tuple[str, str]]) -> None: + def spawn_empty(self) -> EntityCommands: + """ + Spawns a new empty `entity` and returns its corresponding `entity-commands`. + """ + raise NotImplementedError + def spawn(self, bundle: List[Tuple[str, str]]) -> EntityCommands: + """ + Spawns a new `entity` with the given components + and returns the entity's corresponding `entity-commands`. + """ + raise NotImplementedError + def entity(self, entity: Entity) -> EntityCommands: + """ + Returns the `entity-commands` for the given `entity`. + + This method does not guarantee that commands queued by the returned `entity-commands` + will be successful, since the entity could be despawned before they are executed. + """ raise NotImplementedError def __enter__(self) -> Self: """Returns self""" @@ -198,12 +279,50 @@ def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseExceptio raise NotImplementedError +class QueryResult: + """ + A query system param + """ + + def entity(self) -> Entity: + """ + Returns the entity id for the query + """ + raise NotImplementedError + def component(self, index: int) -> Component: + """ + Gets the component at the specified index. Order is the same as declared + during setup. Query filters do not count as components. + + So for example: + + ``` + spin_cube.add_query(&[ + QueryFor::Mut("A"), // component index 0 + QueryFor::With("B"), // none + QueryFor::Ref("C"), // component index 1 + QueryFor::Without("D"), // none + ]); + ``` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + class Query: """ A query system param """ - def iter(self) -> Optional[List[Component]]: + def iter(self) -> Optional[QueryResult]: """ Evaluates and returns the next query results """ diff --git a/examples/python_example/src/python_example/example/types.py b/examples/python_example/src/example/types.py similarity index 100% rename from examples/python_example/src/python_example/example/types.py rename to examples/python_example/src/example/types.py diff --git a/examples/simple/src/lib.rs b/examples/simple/src/lib.rs index ef5e16e..9c13eab 100644 --- a/examples/simple/src/lib.rs +++ b/examples/simple/src/lib.rs @@ -18,10 +18,11 @@ use serde::{Deserialize, Serialize}; struct GuestComponent; impl Guest for GuestComponent { - fn setup() { - // Define an example system with commands that runs on startup + fn setup(app: App) { + // Define an example system with commands that run on startup let spawn_entities = System::new("spawn-entities"); spawn_entities.add_commands(); + app.add_systems(&Schedule::ModStartup, &[&spawn_entities]); // Define another new system that queries for entities with a Transform and a Marker component let spin_cube = System::new("spin-cube"); @@ -29,11 +30,7 @@ impl Guest for GuestComponent { QueryFor::Mut("bevy_transform::components::transform::Transform".to_string()), QueryFor::With("host_example::MyMarker".to_string()), ]); - - // Register the systems to run in the Update schedule - let app = App::new(); - app.add_systems(&Schedule::ModStartup, vec![spawn_entities]); - app.add_systems(&Schedule::Update, vec![spin_cube]); + app.add_systems(&Schedule::Update, &[&spin_cube]); } fn spawn_entities(commands: Commands) { @@ -57,15 +54,18 @@ impl Guest for GuestComponent { } fn spin_cube(query: Query) { - while let Some(components) = query.iter() { - // Get and deserialize the first component - let mut transform: Transform = from_json(&components[0].get()); + while let Some(results) = query.iter() { + // Get the first component + let component = results.component(0); + + // Deserialize the first component + let mut transform: Transform = from_json(&component.get()); // Spin the cube transform.rotate(Quat::from_rotation_y(0.025)); // Set the new component value - components[0].set(&to_json(&transform)); + component.set(&to_json(&transform)); } } } diff --git a/justfile b/justfile index 28228b4..9e2a05c 100644 --- a/justfile +++ b/justfile @@ -8,11 +8,11 @@ run-host-example: # Requires `poetry` to run build-example-python: - cd examples/python_example/src/python_example && poetry run componentize-py --wit-path ../../wit/ --world example componentize app -o ../../../host_example/assets/mods/python.wasm + cd examples/python_example/src && poetry run componentize-py --wit-path ../wit/ --world example componentize app -o ../../host_example/assets/mods/python.wasm # Create the bindings for the python example example-bindings-python: - cd examples/python_example && poetry run componentize-py --wit-path wit/ --world example bindings src/python_example + cd examples/python_example && rm -rf ./src/example && poetry run componentize-py --wit-path wit/ --world example bindings src # For the fetching to take effect you must delete the deps folder manually example-fetch-deps example: diff --git a/src/asset.rs b/src/asset.rs index be93e3d..5ce219e 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,3 +1,5 @@ +use std::fmt; + use anyhow::{Context, Result, anyhow}; use bevy_asset::{Asset, AssetId, AssetLoader, Assets, LoadContext, io::Reader}; use bevy_ecs::{change_detection::Tick, prelude::*}; @@ -8,9 +10,10 @@ use crate::{ access::ModAccess, cleanup::DespawnModEntities, engine::{Engine, Linker}, - host::WasmHost, + host::{WasmApp, WasmHost}, mods::ModDespawnBehaviour, runner::{Config, ConfigRunSystem, ConfigSetup, Runner}, + system::AddSystems, }; /// An asset representing a loaded wasvy Mod @@ -41,26 +44,19 @@ impl ModAsset { } /// Initiates mods by running their "setup" function - /// - /// Returns [None] if the mod could not be initialized because the asset is missing. pub(crate) fn initiate( world: &mut World, asset_id: &AssetId, mod_id: Entity, mod_name: &str, accesses: &[ModAccess], - ) -> Option> { + ) -> Result<()> { let change_tick = world.change_tick(); let mut assets = world .get_resource_mut::>() .expect("ModAssets be registered"); - - // Will return None if the asset is not yet loaded - // run_setup will re-run initiate when it is finally loaded - let Some(asset) = assets.get_mut(*asset_id) else { - return None; - }; + let asset = assets.get_mut(*asset_id).ok_or(AssetNotFound)?; // Gets the version of this asset or assign a new one if it doesn't exist yet let asset_version = match asset.version { @@ -96,23 +92,35 @@ impl ModAsset { let mut runner = Runner::new(&engine); + let mut systems = AddSystems::default(); let config = Config::Setup(ConfigSetup { world, - asset_id, - asset_version, - mod_id, - mod_name, - accesses, + add_systems: &mut systems, }); - Some(call( + // The setup method takes an App parameter. + let app = runner.new_resource(WasmApp).expect("Table has space left"); + call( &mut runner, &instance_pre, config, SETUP, - &[], + &[Val::Resource(app)], &mut [], - )) + )?; + + // Now register all the mod's systems + systems.add_systems( + world, + accesses, + runner.table(), + mod_id, + mod_name, + asset_id, + &asset_version, + )?; + + Ok(()) } pub(crate) fn run_system<'a, 'b, 'c, 'd, 'e, 'f, 'g>( @@ -127,6 +135,21 @@ impl ModAsset { } } +#[derive(Debug)] +pub(crate) struct AssetNotFound; + +impl From for anyhow::Error { + fn from(_: AssetNotFound) -> Self { + anyhow::anyhow!("Asset not found. Maybe it hasn't loaded yet.") + } +} + +impl fmt::Display for AssetNotFound { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Asset not found. Maybe it hasn't loaded yet.") + } +} + fn call( runner: &mut Runner, instance_pre: &InstancePre, diff --git a/src/component.rs b/src/component.rs index 3b1be51..17cd3a3 100644 --- a/src/component.rs +++ b/src/component.rs @@ -89,6 +89,24 @@ pub(crate) fn insert_component( Ok(()) } +pub(crate) fn remove_component( + commands: &mut Commands, + wasm_registry: &WasmComponentRegistry, + entity: Entity, + type_path: String, +) -> Result<()> { + // Remove guest types (inserted as json strings) + if let Some(component_id) = wasm_registry.0.get(&type_path) { + commands.entity(entity).remove_by_id(*component_id); + } + // handle types that are known by bevy (inserted as concrete types) + else { + commands.entity(entity).remove_reflect(type_path); + } + + Ok(()) +} + /// A collection containing a [ComponentId], and a [TypeId] /// /// The type id is [None] for guest components, and [Some] for concrete host types @@ -185,15 +203,15 @@ fn get_wasm_component_id(type_path: &str, world: &mut World) -> ComponentId { /// Retrieves the value of a component on an entity given a json string pub(crate) fn get_component( entity: &FilteredEntityRef, - component_ref: ComponentRef, + component: &ComponentRef, type_registry: &AppTypeRegistry, ) -> Result { let val = entity - .get_by_id(component_ref.component_id) + .get_by_id(component.component_id) .expect("to be able to find this component id on the entity"); // Types that are known by bevy (inserted as concrete types) - if let Some(type_id) = component_ref.type_id { + if let Some(type_id) = component.type_id { let type_registry = type_registry.read(); let type_registration = type_registry .get(type_id) diff --git a/src/entity.rs b/src/entity.rs new file mode 100644 index 0000000..cd5644d --- /dev/null +++ b/src/entity.rs @@ -0,0 +1,149 @@ +use std::any::type_name; + +use anyhow::{Result, bail}; +use bevy_ecs::prelude::*; +use bevy_log::prelude::*; +use wasmtime::component::Resource; +use wasmtime_wasi::ResourceTable; + +use crate::{ + access::ModAccess, + bindings::wasvy::ecs::app::{Bundle, BundleTypes}, + cleanup::DespawnModEntity, + component::{insert_component, remove_component}, + host::WasmHost, + runner::State, +}; + +/// A helper to ingest one host resource and create another with the same entity +pub(crate) fn map_entity(host: &mut WasmHost, input: Resource) -> Result> +where + for<'a> &'a I: Into, + F: From + Send, +{ + let State::RunSystem { table, .. } = host.access() else { + bail!( + "{} resource is only accessible when running systems", + type_name::() + ) + }; + + let input = table.get(&input)?; + let entity = input.into(); + entity_resource(entity, table) +} + +pub(crate) fn spawn_empty(host: &mut WasmHost) -> Result> +where + F: From + Send, +{ + let State::RunSystem { + commands, + table, + insert_despawn_component, + access, + .. + } = host.access() + else { + bail!("Commands resource is only accessible when running systems",) + }; + + let mut entity_commands = commands.spawn_empty(); + + // Make sure the entity is not spawned outside the sandbox + // The mod can still override the ChildOf with its own value + // Note: We can't currently prevent a mod from creating a component that has a relation to a component outside the sandbox + // TODO: Restrict what entities a mod can reference via permissions + if let ModAccess::Sandbox(entity) = access { + entity_commands.insert(ChildOf(*entity)); + }; + + // Make sure this entity is despawned when the mod is despawned. See [ModDespawnBehaviour] + if let Some(mod_id) = insert_despawn_component.0 { + entity_commands.insert(DespawnModEntity(mod_id)); + } + + let entity = entity_commands.id(); + trace!("Spawn empty ({entity})"); + + entity_resource(entity, table) +} + +pub(crate) fn insert(host: &mut WasmHost, input: &Resource, bundle: Bundle) -> Result<()> +where + for<'a> &'a T: Into, +{ + if bundle.is_empty() { + return Ok(()); + } + + let State::RunSystem { + commands, + table, + type_registry, + .. + } = host.access() + else { + bail!( + "{} resource is only accessible when running systems", + type_name::() + ) + }; + + let input = table.get(input)?; + let entity = input.into(); + trace!("Insert components to ({entity})"); + for (type_path, serialized_component) in bundle { + trace!("- {type_path}: {serialized_component}"); + insert_component( + commands, + type_registry, + entity, + type_path, + serialized_component, + )?; + } + + Ok(()) +} + +pub(crate) fn remove(host: &mut WasmHost, input: Resource, bundle: BundleTypes) -> Result<()> +where + for<'a> &'a T: Into, +{ + if bundle.is_empty() { + return Ok(()); + } + + let State::RunSystem { + commands, + table, + wasm_registry, + .. + } = host.access() + else { + bail!( + "{} resource is only accessible when running systems", + type_name::() + ) + }; + + let input = table.get(&input)?; + let entity = input.into(); + trace!("Remove components from ({entity})"); + for type_path in bundle { + trace!("- {type_path}"); + remove_component(commands, wasm_registry, entity, type_path)?; + } + + Ok(()) +} + +fn entity_resource(entity: Entity, table: &mut ResourceTable) -> Result> +where + T: From + Send, +{ + let output = T::from(entity); + let output = table.push(output)?; + Ok(output) +} diff --git a/src/host/app.rs b/src/host/app.rs index bd6a229..c558df5 100644 --- a/src/host/app.rs +++ b/src/host/app.rs @@ -1,95 +1,36 @@ use anyhow::{Result, bail}; -use bevy_ecs::prelude::*; -use bevy_log::prelude::*; use wasmtime::component::Resource; use crate::{ bindings::wasvy::ecs::app::{HostApp, Schedule}, - host::{System, WasmHost}, - mods::ModSystemSet, + host::{WasmHost, WasmSystem}, runner::State, }; -pub struct App; +pub struct WasmApp; impl HostApp for WasmHost { - fn new(&mut self) -> Result> { - let State::Setup { - table, app_init, .. - } = self.access() - else { - bail!("App can only be instantiated in a setup function") - }; - - if *app_init { - bail!("App can only be instantiated once") - } - - let app_res = table.push(App)?; - *app_init = true; - - Ok(app_res) - } - fn add_systems( &mut self, - _self: Resource, + _: Resource, schedule: Schedule, - systems: Vec>, + systems: Vec>, ) -> Result<()> { if systems.is_empty() { return Ok(()); } - let State::Setup { - table, - world, - mod_id, - mod_name, - asset_id, - asset_version, - accesses, - .. - } = self.access() - else { + let State::Setup { add_systems, .. } = self.access() else { bail!("App can only be modified in a setup function") }; - // Each access needs to have dedicated systems that run inside it - for access in accesses { - // Validate that the schedule requested by the mod is enabled - let Some(schedule) = access - .schedules(world) - .evaluate(&schedule) - .map(|schedule| schedule.schedule_label()) - else { - warn!( - "Mod tried adding systems to schedule {:?}, but that system is not enabled", - schedule - ); - continue; - }; - - for system in systems.iter() { - let schedule_config = table - .get_mut(system)? - .schedule(world, mod_id, mod_name, asset_id, asset_version, &access)? - .in_set(ModSystemSet::All) - .in_set(ModSystemSet::Mod(mod_id)) - .in_set(ModSystemSet::Access(*access)); - - world - .get_resource_mut::() - .expect("running in an App") - .add_systems(schedule.clone(), schedule_config); - } - } + add_systems.push(schedule, systems); Ok(()) } // Note: this is never guaranteed to be called by the wasi binary - fn drop(&mut self, app: Resource) -> Result<()> { + fn drop(&mut self, app: Resource) -> Result<()> { let _ = self.table().delete(app)?; Ok(()) diff --git a/src/host/commands.rs b/src/host/commands.rs index 29b94f5..560bf08 100644 --- a/src/host/commands.rs +++ b/src/host/commands.rs @@ -1,66 +1,39 @@ -use anyhow::{Result, bail}; -use bevy_ecs::prelude::*; -use bevy_log::prelude::*; +use anyhow::Result; use wasmtime::component::Resource; use crate::{ - access::ModAccess, bindings::wasvy::ecs::app::HostCommands, cleanup::DespawnModEntity, - component::insert_component, host::WasmHost, runner::State, + bindings::wasvy::ecs::app::{Bundle, HostCommands}, + entity::{insert, map_entity, spawn_empty}, + host::{WasmEntity, WasmEntityCommands, WasmHost}, }; -pub struct Commands; +pub struct WasmCommands; impl HostCommands for WasmHost { + fn spawn_empty(&mut self, _: Resource) -> Result> { + spawn_empty(self) + } + fn spawn( &mut self, - _self: Resource, - components: Vec<(String, String)>, - ) -> Result<()> { - let State::RunSystem { - mut commands, - type_registry, - access, - insert_despawn_component, - .. - } = self.access() - else { - bail!("commands resource is only accessible when running systems") - }; - - let mut entity_commands = commands.spawn_empty(); - - // Make sure the entity is not spawned outside the sandbox - // The mod can still override the ChildOf with its own value - // Note: We can't currently prevent a mod from creating a component that has a relation to a component outside the sandbox - // TODO: Restrict what entities a mod can reference via permissions - if let ModAccess::Sandbox(entity) = access { - entity_commands.insert(ChildOf(*entity)); - }; - - // Make sure this entity is despawned when the mod is despawned. See [ModDespawnBehaviour] - if let Some(mod_id) = insert_despawn_component.0 { - entity_commands.insert(DespawnModEntity(mod_id)); - } - - let entity = entity_commands.id(); - trace!("Spawn empty {entity}, with components:"); - - for (type_path, serialized_component) in components { - trace!("- {type_path}: {serialized_component}"); - insert_component( - &mut commands, - type_registry, - entity, - type_path, - serialized_component, - )?; - } + _: Resource, + bundle: Bundle, + ) -> Result> { + let entity_commands = spawn_empty(self)?; + insert(self, &entity_commands, bundle)?; + Ok(entity_commands) + } - Ok(()) + fn entity( + &mut self, + _: Resource, + entity: Resource, + ) -> Result> { + map_entity(self, entity) } // Note: this is never guaranteed to be called by the wasi binary - fn drop(&mut self, commands: Resource) -> Result<()> { + fn drop(&mut self, commands: Resource) -> Result<()> { let _ = self.table().delete(commands)?; Ok(()) diff --git a/src/host/component.rs b/src/host/component.rs index f857e53..1940662 100644 --- a/src/host/component.rs +++ b/src/host/component.rs @@ -1,46 +1,32 @@ use anyhow::{Result, bail}; -use bevy_ecs::{prelude::*, world::FilteredEntityRef}; +use bevy_ecs::prelude::*; use wasmtime::component::Resource; use crate::{ - bindings::wasvy::ecs::app::{HostComponent, SerializedComponent}, - component::{ComponentRef, get_component, set_component}, - host::{QueryForComponent, WasmHost}, + bindings::wasvy::ecs::app::{ComponentIndex, HostComponent, SerializedComponent}, + host::WasmHost, + query::QueryId, runner::State, }; -pub struct Component { - query_index: usize, +pub struct WasmComponent { + index: ComponentIndex, + id: QueryId, entity: Entity, - component_ref: ComponentRef, - mutable: bool, } -impl Component { - pub(crate) fn new( - query_index: usize, - entity: &FilteredEntityRef, - component: &QueryForComponent, - ) -> Result { - let (component_ref, mutable) = match component { - QueryForComponent::Ref(component_ref) => (component_ref, false), - QueryForComponent::Mut(component_ref) => (component_ref, true), - }; - - Ok(Self { - query_index, - entity: entity.id(), - component_ref: component_ref.clone(), - mutable, - }) +impl WasmComponent { + pub(crate) fn new(index: ComponentIndex, id: QueryId, entity: Entity) -> Self { + Self { index, id, entity } } } impl HostComponent for WasmHost { - fn get(&mut self, component: Resource) -> Result { + fn get(&mut self, component: Resource) -> Result { let State::RunSystem { table, queries, + query_resolver, type_registry, .. } = self.access() @@ -48,25 +34,25 @@ impl HostComponent for WasmHost { bail!("Component can only be accessed in systems") }; - let Component { - query_index, - entity, - component_ref, - .. - } = table.get(&component)?; - - let query = queries.get_mut(*query_index); - let entity = query.get(*entity).expect("Component entity to be valid"); - - let value = get_component(&entity, component_ref.clone(), type_registry)?; - - Ok(value) + let component = table.get(&component)?; + query_resolver.get( + component.id, + component.entity, + component.index, + queries, + type_registry, + ) } - fn set(&mut self, component: Resource, value: SerializedComponent) -> Result<()> { + fn set( + &mut self, + component: Resource, + value: SerializedComponent, + ) -> Result<()> { let State::RunSystem { table, queries, + query_resolver, type_registry, .. } = self.access() @@ -74,28 +60,19 @@ impl HostComponent for WasmHost { bail!("Component can only be accessed in systems") }; - let Component { - query_index, - entity, - component_ref, - mutable, - } = table.get(&component)?; - if !mutable { - bail!("Component is not mutable!") - } - - let mut query = queries.get_mut(*query_index); - let mut entity = query - .get_mut(*entity) - .expect("Component entity to be valid"); - - set_component(&mut entity, component_ref, value, type_registry)?; - - Ok(()) + let component = table.get(&component)?; + query_resolver.set( + component.id, + component.entity, + component.index, + value, + queries, + type_registry, + ) } // Note: this is never guaranteed to be called by the wasi binary - fn drop(&mut self, component: Resource) -> Result<()> { + fn drop(&mut self, component: Resource) -> Result<()> { let _ = self.table().delete(component)?; Ok(()) diff --git a/src/host/entity.rs b/src/host/entity.rs new file mode 100644 index 0000000..51d1d9f --- /dev/null +++ b/src/host/entity.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use bevy_ecs::prelude::*; +use wasmtime::component::Resource; + +use crate::{bindings::wasvy::ecs::app::HostEntity, host::WasmHost}; + +pub struct WasmEntity(pub(crate) Entity); + +impl Into for &WasmEntity { + fn into(self) -> Entity { + self.0 + } +} + +impl From for WasmEntity { + fn from(value: Entity) -> Self { + Self(value) + } +} + +impl HostEntity for WasmHost { + // Note: this is never guaranteed to be called by the wasi binary + fn drop(&mut self, commands: Resource) -> Result<()> { + let _ = self.table().delete(commands)?; + + Ok(()) + } +} diff --git a/src/host/entity_commands.rs b/src/host/entity_commands.rs new file mode 100644 index 0000000..e88ff83 --- /dev/null +++ b/src/host/entity_commands.rs @@ -0,0 +1,85 @@ +use anyhow::{Result, bail}; +use bevy_ecs::prelude::*; +use wasmtime::component::Resource; + +use crate::{ + bindings::wasvy::ecs::app::{Bundle, BundleTypes, HostEntityCommands}, + entity::{insert, map_entity, remove}, + host::{WasmEntity, WasmHost}, + runner::State, +}; + +pub struct WasmEntityCommands(pub(crate) Entity); + +impl Into for &WasmEntityCommands { + fn into(self) -> Entity { + self.0 + } +} + +impl From for WasmEntityCommands { + fn from(value: Entity) -> Self { + Self(value) + } +} + +impl HostEntityCommands for WasmHost { + fn id( + &mut self, + entity_commands: Resource, + ) -> Result> { + map_entity(self, entity_commands) + } + + fn insert( + &mut self, + entity_commands: Resource, + bundle: Bundle, + ) -> Result<()> { + insert(self, &entity_commands, bundle) + } + + fn remove( + &mut self, + entity_commands: Resource, + bundle: BundleTypes, + ) -> Result<()> { + remove(self, entity_commands, bundle) + } + + fn despawn(&mut self, entity_commands: Resource) -> Result<()> { + let mut entity_commands = access(self, entity_commands)?; + entity_commands.despawn(); + + Ok(()) + } + + fn try_despawn(&mut self, entity_commands: Resource) -> Result<()> { + let mut entity_commands = access(self, entity_commands)?; + entity_commands.try_despawn(); + + Ok(()) + } + + // Note: this is never guaranteed to be called by the wasi binary + fn drop(&mut self, entity_commands: Resource) -> Result<()> { + let _ = self.table().delete(entity_commands)?; + + Ok(()) + } +} + +fn access( + host: &mut WasmHost, + entity_commands: Resource, +) -> Result> { + let State::RunSystem { + table, commands, .. + } = host.access() + else { + bail!("EntityCommands resource is only accessible when running systems") + }; + + let entity_commands = table.get(&entity_commands)?; + Ok(commands.entity(entity_commands.0)) +} diff --git a/src/host/mod.rs b/src/host/mod.rs index 53360db..ae7a0d5 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -1,3 +1,5 @@ +use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + use crate::{ bindings::wasvy::ecs::app::*, runner::{Data, State}, @@ -6,17 +8,21 @@ use crate::{ mod app; mod commands; mod component; +mod entity; +mod entity_commands; mod query; +mod query_result; mod system; pub use app::*; pub use commands::*; pub use component::*; +pub use entity::*; +pub use entity_commands::*; pub use query::*; +pub use query_result::*; pub use system::*; -use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; - pub struct WasmHost { data: Data, table: ResourceTable, @@ -46,7 +52,6 @@ impl WasmHost { pub(crate) fn clear(&mut self) { self.set_data(Data::uninitialized()); - self.table = ResourceTable::new(); } /// Access to the data contained in the [`WasmHost`] diff --git a/src/host/query.rs b/src/host/query.rs index c909aa3..9c6458e 100644 --- a/src/host/query.rs +++ b/src/host/query.rs @@ -1,149 +1,49 @@ use anyhow::{Result, bail}; -use bevy_ecs::{ - component::ComponentId, prelude::*, query::FilteredAccess, system::QueryParamBuilder, - world::FilteredEntityMut, -}; use wasmtime::component::Resource; use crate::{ - bindings::wasvy::ecs::app::{HostQuery, QueryFor}, - component::ComponentRef, - host::{Component, WasmHost}, + bindings::wasvy::ecs::app::HostQuery, + host::{WasmHost, WasmQueryResult}, + query::{QueryCursor, QueryId}, runner::State, }; -pub struct Query { - index: usize, - position: usize, - components: Vec, +pub struct WasmQuery { + id: QueryId, + cursor: QueryCursor, } -impl Query { - /// Generate a new query. - /// - /// Pass the index of the query that should be used from the param set, and components - pub(crate) fn new(index: usize, components: Vec) -> Self { +impl WasmQuery { + pub(crate) fn new(id: QueryId) -> Self { Self { - index, - position: 0, - components, + id, + cursor: QueryCursor::default(), } } } impl HostQuery for WasmHost { - fn iter(&mut self, query: Resource) -> Result>>> { + fn iter(&mut self, query: Resource) -> Result>> { let State::RunSystem { table, queries, .. } = self.access() else { bail!("Query can only be accessed in systems") }; let query = table.get_mut(&query)?; - - let position = query.position; - query.position += 1; - - let bevy_query = queries.get_mut(query.index); - let Some(entity) = bevy_query.iter().nth(position) else { + let cursor = query.cursor.increment(); + let Some(entity) = cursor.entity(queries, query.id) else { + // We've reached the end of the results return Ok(None); }; - // query must be dropped in order for us to be able to push new resources onto the table - let query_index = query.index; - let components = query.components.clone(); - - let mut resources = Vec::with_capacity(components.len()); - for component in components.iter() { - let resource = Component::new(query_index, &entity, component)?; - let resource = table.push(resource)?; - resources.push(resource); - } - - Ok(Some(resources)) + let result = WasmQueryResult::new(query.id, entity); + let result = table.push(result)?; + Ok(Some(result)) } // Note: this is never guaranteed to be called by the wasi binary - fn drop(&mut self, query: Resource) -> Result<()> { + fn drop(&mut self, query: Resource) -> Result<()> { let _ = self.table().delete(query)?; Ok(()) } } - -/// Needed to at runtime to construct the components wit resources returned from iter() on a query resource -/// -/// Note: Ignores query filters (with and without) since these are not relevant -#[derive(Clone)] -pub(crate) enum QueryForComponent { - Ref(ComponentRef), - Mut(ComponentRef), -} - -impl QueryForComponent { - pub(crate) fn new(original: &QueryFor, world: &mut World) -> Result> { - Ok(match original { - QueryFor::Ref(type_path) => Some(Self::Ref(ComponentRef::new(type_path, world)?)), - QueryFor::Mut(type_path) => Some(Self::Mut(ComponentRef::new(type_path, world)?)), - QueryFor::With(_) => None, - QueryFor::Without(_) => None, - }) - } -} - -pub(crate) fn create_query_builder( - original_items: &[QueryFor], - world: &mut World, - access: FilteredAccess, -) -> Result< - QueryParamBuilder>)>>, -> { - let mut items = Vec::with_capacity(original_items.len()); - for original in original_items { - items.push(QueryForId::new(original, world)?); - } - - Ok(QueryParamBuilder::new_box(move |builder| { - builder.extend_access(access); - for item in items { - match item { - QueryForId::Ref(component_id) => { - builder.ref_id(component_id); - } - QueryForId::Mut(component_id) => { - builder.mut_id(component_id); - } - QueryForId::With(component_id) => { - builder.with_id(component_id); - } - QueryForId::Without(component_id) => { - builder.without_id(component_id); - } - } - } - })) -} - -enum QueryForId { - Ref(ComponentId), - Mut(ComponentId), - With(ComponentId), - Without(ComponentId), -} - -impl QueryForId { - fn new(original: &QueryFor, world: &mut World) -> Result { - Ok(match original { - QueryFor::Ref(type_path) => { - Self::Ref(ComponentRef::new(type_path, world)?.component_id()) - } - QueryFor::Mut(type_path) => { - Self::Mut(ComponentRef::new(type_path, world)?.component_id()) - } - QueryFor::With(type_path) => { - Self::With(ComponentRef::new(type_path, world)?.component_id()) - } - QueryFor::Without(type_path) => { - Self::Without(ComponentRef::new(type_path, world)?.component_id()) - } - }) - } -} diff --git a/src/host/query_result.rs b/src/host/query_result.rs new file mode 100644 index 0000000..bb3b133 --- /dev/null +++ b/src/host/query_result.rs @@ -0,0 +1,57 @@ +use anyhow::{Result, bail}; +use bevy_ecs::prelude::*; +use wasmtime::component::Resource; + +use crate::{ + bindings::wasvy::ecs::app::{ComponentIndex, HostQueryResult}, + entity::map_entity, + host::{WasmComponent, WasmEntity, WasmHost}, + query::QueryId, + runner::State, +}; + +#[derive(Clone, Copy)] +pub struct WasmQueryResult { + id: QueryId, + entity: Entity, +} + +impl WasmQueryResult { + pub(crate) fn new(id: QueryId, entity: Entity) -> Self { + Self { id, entity } + } +} + +impl Into for &WasmQueryResult { + fn into(self) -> Entity { + self.entity + } +} + +impl HostQueryResult for WasmHost { + fn entity(&mut self, query_result: Resource) -> Result> { + map_entity(self, query_result) + } + + fn component( + &mut self, + query_result: Resource, + index: ComponentIndex, + ) -> Result> { + let State::RunSystem { table, .. } = self.access() else { + bail!("QueryResult can only be accessed in systems") + }; + + let query_result = table.get(&query_result)?; + let component = WasmComponent::new(index, query_result.id, query_result.entity); + let component = table.push(component)?; + Ok(component) + } + + // Note: this is never guaranteed to be called by the wasi binary + fn drop(&mut self, query_result: Resource) -> Result<()> { + let _ = self.table().delete(query_result)?; + + Ok(()) + } +} diff --git a/src/host/system.rs b/src/host/system.rs index 3165a70..01d9a63 100644 --- a/src/host/system.rs +++ b/src/host/system.rs @@ -1,329 +1,83 @@ -use anyhow::{Result, anyhow, bail}; -use bevy_asset::{AssetId, Assets}; -use bevy_ecs::{ - change_detection::Tick, - error::Result as BevyResult, - prelude::*, - resource::Resource as BevyResource, - schedule::ScheduleConfigs, - system::{ - BoxedSystem, Commands as BevyCommands, LocalBuilder, ParamBuilder, ParamSetBuilder, - Query as BevyQuery, - }, - world::FilteredEntityMut, -}; -use bevy_log::prelude::*; -use wasmtime::component::{Resource, Val}; +use anyhow::{Result, bail}; +use bevy_ecs::prelude::*; +use wasmtime::component::Resource; use crate::{ - access::ModAccess, - asset::ModAsset, bindings::wasvy::ecs::app::{HostSystem, QueryFor}, - cleanup::InsertDespawnComponent, - engine::Engine, - host::{Commands, Query, QueryForComponent, WasmHost, create_query_builder}, - runner::{ConfigRunSystem, Runner, State}, + host::WasmHost, + runner::State, + system::{DynamicSystemId, Param}, }; -pub struct System { - name: String, - params: Vec, - scheduled: bool, - identifier: SystemIdentifier, - after: Vec, +pub struct WasmSystem { + pub(crate) id: DynamicSystemId, + pub(crate) name: String, + pub(crate) params: Vec, + pub(crate) after: Vec, } -impl System { - fn new(name: String, identifier: SystemIdentifier) -> Self { +impl WasmSystem { + fn new(name: String, world: &mut World) -> Self { Self { + id: DynamicSystemId::new(world), name, params: Vec::new(), - scheduled: false, - identifier, after: Vec::new(), } } - pub(crate) fn schedule( - &mut self, - mut world: &mut World, - mod_id: Entity, - mod_name: &str, - asset_id: &AssetId, - asset_version: &Tick, - access: &ModAccess, - ) -> Result> { - self.scheduled = true; - - let mut built_params = Vec::new(); - for param in self.params.iter() { - built_params.push(param.build(world)?); - } - - // Used internally by the system - let input = Input { - mod_name: mod_name.to_string(), - system_name: self.name.clone(), - asset_id: asset_id.clone(), - asset_version: asset_version.clone(), - built_params, - access: *access, - insert_despawn_component: InsertDespawnComponent::new(mod_id, world), - }; - - // Generate the queries necessary to run this system - let filtered_access = access.filtered_access(world); - let mut queries = Vec::with_capacity(self.params.len()); - for items in self.params.iter().filter_map(Param::filter_query) { - queries.push(create_query_builder(items, world, filtered_access.clone())?); - } - - // Dynamic - let system = ( - LocalBuilder(input), - ParamBuilder, - ParamBuilder, - ParamBuilder, - ParamBuilder, - // TODO: FilteredResourcesMutParamBuilder::new(|builder| {}), - ParamSetBuilder(queries), - ) - .build_state(&mut world) - .build_system(system_runner) - .with_name(format!("wasvy[{mod_name}]::{}", self.name)); - - let boxed_system = Box::new(IntoSystem::into_system(system)); - - let mut schedule_config = boxed_system - // See docs for [SystemIdentifier] - .in_set(self.identifier); - - // Implement system ordering - for after in self.after.iter() { - schedule_config = schedule_config.after(*after); - } - - Ok(schedule_config) - } - - fn editable(&self) -> Result<()> { - if self.scheduled { - Err(anyhow!( - "System \"{}\" was already scheduled and thus can no longer be changed", - self.name - )) - } else { - Ok(()) - } - } - - fn add_param(host: &mut WasmHost, system: Resource, param: Param) -> Result<()> { + fn add_param(host: &mut WasmHost, system: Resource, param: Param) -> Result<()> { let State::Setup { table, .. } = host.access() else { bail!("Systems can only be modified in a setup function") }; let system = table.get_mut(&system)?; - system.editable()?; - system.params.push(param); Ok(()) } } -struct Input { - mod_name: String, - system_name: String, - asset_id: AssetId, - asset_version: Tick, - built_params: Vec, - access: ModAccess, - insert_despawn_component: InsertDespawnComponent, -} - -impl FromWorld for Input { - fn from_world(_world: &mut World) -> Self { - unreachable!("Input is created with LocalBuilder") - } -} - -fn system_runner( - input: Local, - assets: Res>, - engine: Res, - type_registry: Res, - mut commands: BevyCommands, - // TODO: mut resources: FilteredResourcesMut, - mut queries: ParamSet>>, -) -> BevyResult { - // Skip no longer loaded mods - let Some(asset) = assets.get(input.asset_id) else { - return Ok(()); - }; - - // Skip mismatching system versions - if asset.version() != Some(input.asset_version) { - return Ok(()); - } - - let mut runner = Runner::new(&engine); - - let params = initialize_params(&input.built_params, &mut runner)?; - - trace!( - "Running system \"{}\" from \"{}\"", - input.system_name, input.mod_name - ); - asset.run_system( - &mut runner, - &input.system_name, - ConfigRunSystem { - commands: &mut commands, - type_registry: &type_registry, - queries: &mut queries, - access: input.access.clone(), - insert_despawn_component: input.insert_despawn_component.clone(), - }, - ¶ms, - )?; - - Ok(()) -} - -/// A system param (what a mod system requests as parameters) -enum Param { - Commands, - Query(Vec), -} - -impl Param { - fn build(&self, world: &mut World) -> Result { - Ok(match self { - Param::Commands => BuiltParam::Commands, - Param::Query(original_items) => { - let mut items = Vec::new(); - for original in original_items { - if let Some(item) = QueryForComponent::new(original, world)? { - items.push(item); - } - } - BuiltParam::Query(items) - } - }) - } - - fn filter_query(&self) -> Option<&Vec> { - match self { - Param::Query(items) => Some(items), - _ => None, - } - } -} - -/// A system param containing all the info needed by the system at runtime -enum BuiltParam { - Commands, - Query(Vec), -} - -fn initialize_params(source: &[BuiltParam], runner: &mut Runner) -> Result> { - let mut params = Vec::with_capacity(source.len()); - let mut query_index = 0; - for param in source.iter() { - let resource = match param { - BuiltParam::Commands => runner.new_resource(Commands), - BuiltParam::Query(components) => { - let index = query_index; - query_index += 1; - runner.new_resource(Query::new(index, components.clone())) - } - }?; - params.push(Val::Resource(resource)); - } - Ok(params) -} - -/// Bevy doesn't return an identifier for systems added directly to the scheduler. There is -/// [NodeId](bevy_ecs::schedule::NodeId) but that has no clear way of being used for system ordering. -/// -/// So instead we take inspiration from bevy's [AnonymousSet](bevy_ecs::schedule::AnonymousSet) -/// and we identify each system with an extra [SystemSet] all to itself. -// Note: Using an AnonymousSet could work but unfortunately the method used to create one is private. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -struct SystemIdentifier(usize); - -impl SystemIdentifier { - /// Initialize a unique identifier in the world - fn new(world: &mut World) -> Self { - world.init_resource::(); - let mut count = world - .get_resource_mut::() - .expect("SystemIdentifierCount to be initialized"); - let identifier = SystemIdentifier(count.0); - count.0 += 1; - identifier - } -} - -impl SystemSet for SystemIdentifier { - // As of bevy 0.17.2 this function's only purpose is for debugging - fn is_anonymous(&self) -> bool { - // This is technically incorrect, but it makes bevy use the system name as node name instead of SystemIdentifier(usize) - true - } - - fn dyn_clone(&self) -> Box { - Box::new(*self) - } -} - -/// An tracker to ensure unique [SystemIdentifier]s in the world -#[derive(Default, BevyResource)] -struct SystemIdentifierCount(usize); - impl HostSystem for WasmHost { - fn new(&mut self, name: String) -> Result> { + fn new(&mut self, name: String) -> Result> { let State::Setup { table, world, .. } = self.access() else { bail!("Systems can only be instantiated in a setup function") }; - // A unique identifier for this system in the world - let identifier = SystemIdentifier::new(world); - - Ok(table.push(System::new(name, identifier))?) + Ok(table.push(WasmSystem::new(name, world))?) } - fn add_commands(&mut self, system: Resource) -> Result<()> { - System::add_param(self, system, Param::Commands) + fn add_commands(&mut self, system: Resource) -> Result<()> { + WasmSystem::add_param(self, system, Param::Commands) } - fn add_query(&mut self, system: Resource, query: Vec) -> Result<()> { - System::add_param(self, system, Param::Query(query)) + fn add_query(&mut self, system: Resource, query: Vec) -> Result<()> { + WasmSystem::add_param(self, system, Param::Query(query)) } - fn after(&mut self, system: Resource, other: Resource) -> Result<()> { + fn after(&mut self, system: Resource, other: Resource) -> Result<()> { let State::Setup { table, .. } = self.access() else { bail!("Systems can only be modified in a setup function") }; - let other = table.get(&other)?.identifier; - + let other = table.get(&other)?.id; let system = table.get_mut(&system)?; - system.editable()?; system.after.push(other); Ok(()) } - fn before(&mut self, system: Resource, other: Resource) -> Result<()> { + fn before(&mut self, system: Resource, other: Resource) -> Result<()> { // In bevy, `a.before(b)` is logically equivalent to `b.after(a)` HostSystem::after(self, other, system) } // Note: this is never guaranteed to be called by the wasi binary - fn drop(&mut self, system: Resource) -> Result<()> { - let _ = self.table().delete(system)?; + fn drop(&mut self, _: Resource) -> Result<()> { + // Don't drop! After running setup, wasvy will find and register all + // [WasmSystems] via [AddSystems]. If they are dropped, they will not be + // in the wasm resource table when [AddSystems::add_systems] is called. Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index aecbfc7..d555f73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,15 +9,18 @@ pub mod asset; pub(crate) mod cleanup; pub mod component; pub mod engine; +pub(crate) mod entity; pub mod host; pub mod mods; pub mod plugin; pub mod prelude; +pub(crate) mod query; pub(crate) mod runner; pub mod sandbox; pub mod schedule; pub mod send_sync_ptr; pub(crate) mod setup; +pub(crate) mod system; mod bindings { wasmtime::component::bindgen!({ @@ -27,11 +30,14 @@ mod bindings { // to return traps from generated functions. imports: { default: trappable }, with: { - "wasvy:ecs/app.app": crate::host::App, - "wasvy:ecs/app.system": crate::host::System, - "wasvy:ecs/app.commands": crate::host::Commands, - "wasvy:ecs/app.query": crate::host::Query, - "wasvy:ecs/app.component": crate::host::Component, + "wasvy:ecs/app.app": crate::host::WasmApp, + "wasvy:ecs/app.system": crate::host::WasmSystem, + "wasvy:ecs/app.commands": crate::host::WasmCommands, + "wasvy:ecs/app.entity": crate::host::WasmEntity, + "wasvy:ecs/app.entity-commands": crate::host::WasmEntityCommands, + "wasvy:ecs/app.query": crate::host::WasmQuery, + "wasvy:ecs/app.query-result": crate::host::WasmQueryResult, + "wasvy:ecs/app.component": crate::host::WasmComponent, }, }); } diff --git a/src/query.rs b/src/query.rs new file mode 100644 index 0000000..92ab113 --- /dev/null +++ b/src/query.rs @@ -0,0 +1,210 @@ +use anyhow::{Result, anyhow, bail}; +use bevy_ecs::{ + component::ComponentId, prelude::*, query::FilteredAccess, system::QueryParamBuilder, + world::FilteredEntityMut, +}; + +use crate::{ + bindings::wasvy::ecs::app::{ComponentIndex, QueryFor}, + component::{ComponentRef, get_component, set_component}, + system::Param, +}; + +pub(crate) type Queries<'w, 's> = + ParamSet<'w, 's, Vec>>>; + +/// A helper struct that stores a static lookup for queries. +/// +/// - The first dimension is the ParamSet index (QueryId). +/// - The second is the component index +pub(crate) struct QueryResolver(Vec>); + +impl QueryResolver { + pub(crate) fn new(params: &[Param], world: &mut World) -> Result { + let mut result = Vec::new(); + for component in params.iter().filter_map(|param| Param::filter_query(param)) { + let mut components = Vec::new(); + for original in component { + if let Some(component) = QueryForComponent::new(original, world)? { + components.push(component); + } + } + result.push(components); + } + + Ok(Self(result)) + } + + pub(crate) fn get( + &self, + id: QueryId, + entity: Entity, + index: ComponentIndex, + queries: &mut Queries<'_, '_>, + type_registry: &AppTypeRegistry, + ) -> Result { + let query_for = self.query_for(id, index)?; + + let query = queries.get_mut(id.0); + let entity = query.get(entity)?; + + get_component(&entity, &query_for.component, type_registry) + } + + pub(crate) fn set( + &self, + id: QueryId, + entity: Entity, + index: ComponentIndex, + serialized_value: String, + queries: &mut Queries<'_, '_>, + type_registry: &AppTypeRegistry, + ) -> Result<()> { + let query_for = self.query_for(id, index)?; + if !query_for.mutable { + bail!("Component is not mutable!") + } + + let mut query = queries.get_mut(id.0); + let mut entity = query.get_mut(entity)?; + + set_component( + &mut entity, + &query_for.component, + serialized_value, + type_registry, + ) + } + + fn query_for(&self, id: QueryId, index: ComponentIndex) -> Result<&QueryForComponent> { + let id = id.0; + self.0 + .get(id) + .expect("Valid query index") + .get(index as usize) + .ok_or_else(|| anyhow!("Query index {id} does not have component index {index}")) + } +} + +#[derive(Default)] +pub(crate) struct QueryIdGenerator(usize); + +impl QueryIdGenerator { + pub(crate) fn generate(&mut self) -> QueryId { + let index = self.0; + self.0 += 1; + QueryId(index) + } +} + +/// An index in the ParamSet of all queries for the system +#[derive(Clone, Copy)] +pub(crate) struct QueryId(usize); + +/// A cursor so we can resume iterating the query from the last position. +#[derive(Default, Clone, Copy)] +pub(crate) struct QueryCursor(usize); + +impl QueryCursor { + /// Increments the cursor, returning the old one + pub(crate) fn increment(&mut self) -> Self { + let original = *self; + self.0 += 1; + original + } + + /// Retrieves the entity at the cursor + pub(crate) fn entity(&self, queries: &mut Queries<'_, '_>, id: QueryId) -> Option { + let query = queries.get_mut(id.0); + + // This is not the most efficient. Ideally we wouldn't need to walk + // to the nth iter each time, but this allows to avoid unsafe. + // TODO: Store an actual proper cursor. + query.iter().nth(self.0).map(|a| a.id()) + } +} + +/// Needed at runtime to construct the components wit resources returned from iter() on a query resource +/// +/// Note: Ignores query filters (with and without) since these are not relevant +#[derive(Clone)] +struct QueryForComponent { + component: ComponentRef, + mutable: bool, +} + +impl QueryForComponent { + fn new(original: &QueryFor, world: &mut World) -> Result> { + Ok(match original { + QueryFor::Ref(type_path) => Some(Self { + component: ComponentRef::new(type_path, world)?, + mutable: false, + }), + QueryFor::Mut(type_path) => Some(Self { + component: ComponentRef::new(type_path, world)?, + mutable: true, + }), + QueryFor::With(_) => None, + QueryFor::Without(_) => None, + }) + } +} + +pub(crate) fn create_query_builder( + original_items: &[QueryFor], + world: &mut World, + access: FilteredAccess, +) -> Result< + QueryParamBuilder>)>>, +> { + let mut items = Vec::with_capacity(original_items.len()); + for original in original_items { + items.push(QueryForId::new(original, world)?); + } + + Ok(QueryParamBuilder::new_box(move |builder| { + builder.extend_access(access); + for item in items { + match item { + QueryForId::Ref(component_id) => { + builder.ref_id(component_id); + } + QueryForId::Mut(component_id) => { + builder.mut_id(component_id); + } + QueryForId::With(component_id) => { + builder.with_id(component_id); + } + QueryForId::Without(component_id) => { + builder.without_id(component_id); + } + } + } + })) +} + +enum QueryForId { + Ref(ComponentId), + Mut(ComponentId), + With(ComponentId), + Without(ComponentId), +} + +impl QueryForId { + fn new(original: &QueryFor, world: &mut World) -> Result { + Ok(match original { + QueryFor::Ref(type_path) => { + Self::Ref(ComponentRef::new(type_path, world)?.component_id()) + } + QueryFor::Mut(type_path) => { + Self::Mut(ComponentRef::new(type_path, world)?.component_id()) + } + QueryFor::With(type_path) => { + Self::With(ComponentRef::new(type_path, world)?.component_id()) + } + QueryFor::Without(type_path) => { + Self::Without(ComponentRef::new(type_path, world)?.component_id()) + } + }) + } +} diff --git a/src/runner.rs b/src/runner.rs index 346e2a4..882e054 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,20 +1,19 @@ use std::ptr::NonNull; use anyhow::Result; -use bevy_asset::AssetId; -use bevy_ecs::{ - change_detection::Tick, - entity::Entity, - reflect::AppTypeRegistry, - system::{Commands, ParamSet, Query}, - world::{FilteredEntityMut, World}, -}; +use bevy_ecs::{prelude::*, reflect::AppTypeRegistry, world::FilteredEntityMut}; use wasmtime::component::ResourceAny; use wasmtime_wasi::ResourceTable; use crate::{ - access::ModAccess, asset::ModAsset, cleanup::InsertDespawnComponent, engine::Engine, - host::WasmHost, send_sync_ptr::SendSyncPtr, + access::ModAccess, + cleanup::InsertDespawnComponent, + component::WasmComponentRegistry, + engine::Engine, + host::WasmHost, + query::{Queries, QueryResolver}, + send_sync_ptr::SendSyncPtr, + system::AddSystems, }; pub(crate) type Store = wasmtime::Store; @@ -55,30 +54,25 @@ impl Runner { self.store.data_mut().set_data(Data(match config { Config::Setup(ConfigSetup { world, - asset_id, - asset_version, - mod_id, - mod_name, - accesses, + add_systems: systems, }) => Inner::Setup { world: SendSyncPtr::new(world.into()), - app_init: false, - asset_id: *asset_id, - asset_version, - mod_id, - mod_name: mod_name.to_string(), - accesses: SendSyncPtr::new(accesses.into()), + add_systems: SendSyncPtr::new(systems.into()), }, Config::RunSystem(ConfigRunSystem { commands, type_registry, + wasm_registry, queries, + query_resolver, access, insert_despawn_component, }) => Inner::RunSystem { commands: SendSyncPtr::new(NonNull::from_mut(commands).cast()), type_registry: SendSyncPtr::new(NonNull::from_ref(type_registry)), + wasm_registry: SendSyncPtr::new(NonNull::from_ref(wasm_registry)), queries: SendSyncPtr::new(NonNull::from_ref(queries).cast()), + query_resolver: SendSyncPtr::new(NonNull::from_ref(query_resolver)), access, insert_despawn_component, }, @@ -101,25 +95,19 @@ enum Inner { Uninitialized, Setup { world: SendSyncPtr, - app_init: bool, - mod_id: Entity, - mod_name: String, - asset_id: AssetId, - asset_version: Tick, - accesses: SendSyncPtr<[ModAccess]>, + add_systems: SendSyncPtr, }, RunSystem { commands: SendSyncPtr>, type_registry: SendSyncPtr, + wasm_registry: SendSyncPtr, queries: SendSyncPtr>, + query_resolver: SendSyncPtr, access: ModAccess, insert_despawn_component: InsertDespawnComponent, }, } -type Queries<'w, 's> = - ParamSet<'w, 's, Vec>>>; - impl Data { pub(crate) fn uninitialized() -> Self { Self(Inner::Uninitialized) @@ -132,28 +120,20 @@ impl Data { match &mut self.0 { Inner::Setup { world, - app_init, - asset_id, - asset_version, - mod_id, - mod_name, - accesses, + add_systems: systems, } => Some(State::Setup { // Safety: Runner::use_store ensures that this always contains a valid reference // See the rules here: https://doc.rust-lang.org/stable/core/ptr/index.html#pointer-to-reference-conversion world: unsafe { world.as_mut() }, - app_init, - asset_id, - asset_version, - mod_id: *mod_id, - mod_name, - accesses: unsafe { accesses.as_ref() }, table, + add_systems: unsafe { systems.as_mut() }, }), Inner::RunSystem { commands, type_registry, + wasm_registry, queries, + query_resolver, access, insert_despawn_component, } => @@ -163,7 +143,9 @@ impl Data { Some(State::RunSystem { commands: commands.cast().as_mut(), type_registry: type_registry.as_ref(), + wasm_registry: wasm_registry.as_ref(), queries: queries.cast().as_mut(), + query_resolver: query_resolver.as_ref(), insert_despawn_component, access, table, @@ -178,18 +160,15 @@ pub(crate) enum State<'a> { Setup { world: &'a mut World, table: &'a mut ResourceTable, - app_init: &'a mut bool, - mod_id: Entity, - mod_name: &'a str, - asset_id: &'a AssetId, - asset_version: &'a Tick, - accesses: &'a [ModAccess], + add_systems: &'a mut AddSystems, }, RunSystem { table: &'a mut ResourceTable, commands: &'a mut Commands<'a, 'a>, type_registry: &'a AppTypeRegistry, + wasm_registry: &'a WasmComponentRegistry, queries: &'a mut Queries<'a, 'a>, + query_resolver: &'a QueryResolver, access: &'a ModAccess, insert_despawn_component: &'a InsertDespawnComponent, }, @@ -202,18 +181,16 @@ pub(crate) enum Config<'a, 'b, 'c, 'd, 'e, 'f, 'g> { pub(crate) struct ConfigSetup<'a> { pub(crate) world: &'a mut World, - pub(crate) asset_id: &'a AssetId, - pub(crate) asset_version: Tick, - pub(crate) mod_id: Entity, - pub(crate) mod_name: &'a str, - pub(crate) accesses: &'a [ModAccess], + pub(crate) add_systems: &'a mut AddSystems, } pub(crate) struct ConfigRunSystem<'a, 'b, 'c, 'd, 'e, 'f, 'g> { pub(crate) commands: &'a mut Commands<'b, 'c>, pub(crate) type_registry: &'a AppTypeRegistry, + pub(crate) wasm_registry: &'a WasmComponentRegistry, pub(crate) queries: &'a mut ParamSet<'d, 'e, Vec>>>, + pub(crate) query_resolver: &'a QueryResolver, pub(crate) access: ModAccess, pub(crate) insert_despawn_component: InsertDespawnComponent, } diff --git a/src/sandbox.rs b/src/sandbox.rs index 4e80a2d..19d8760 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -163,7 +163,7 @@ pub struct Sandbox { impl Sandbox { /// Creates a new [Sandbox] component /// - /// Mods in this Sandbox will run only during the provided [Schedules] + /// Mods in this Sandbox will run only during the provided [ModSchedules] pub fn new(world: &mut World, schedules: ModSchedules) -> Self { // Get and also increment the count let sandbox_count = world.get_resource_or_insert_with(|| SandboxCount(1)); diff --git a/src/schedule.rs b/src/schedule.rs index 3a08bc9..81ad73f 100644 --- a/src/schedule.rs +++ b/src/schedule.rs @@ -7,14 +7,23 @@ use crate::bindings::wasvy::ecs::app::Schedule as WitSchedule; /// /// See the docs for [bevy schedules](bevy_app::Main). /// -/// None of the first run schedules (like Startup) are included since mods can't be guaranteed to load fast enough to run in them. -/// So instead, many repeating schedules are run instead +/// Call [ModloaderPlugin::enable_schedule](crate::plugin::ModloaderPlugin::enable_schedule) +/// to enable new or custom schedules for mods. +/// +/// None of the startup schedules (like [PreStartup](bevy_app::PreStartup), +/// [Startup](bevy_app::Startup), etc) are included since mods can't usually run +/// within them, since mods take time to load and begin loading these schedules +/// have finished running. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ModSchedule { /// A custom schedule that runs the first time a mod is loaded. /// - /// In reality, this isn't really a Bevy schedule. - /// It is a custom schedule that runs before PreUpdate. + /// It is a custom schedule that runs during the setup schedule + /// (which defaults to [First](bevy_app::First)), see + /// [ModloaderPlugin::set_setup_schedule](crate::plugin::ModloaderPlugin::set_setup_schedule)). + /// + /// Upon being loaded, mods are guaranteed to only run this schedule once, + /// even if other mods are loaded afterwards. ModStartup, /// See the Bevy schedule [PreUpdate] @@ -35,7 +44,7 @@ pub enum ModSchedule { /// See the Bevy schedule [FixedPostUpdate] FixedPostUpdate, - /// A custom schedule. See [Schedule::new_custom] for more details. + /// A custom schedule. See [ModSchedule::new_custom] for more details. Custom { name: String, schedule: Interned, @@ -99,9 +108,16 @@ impl ModStartup { } } -/// A collection of the [Schedules] where Wasvy will run mods +/// A collection of the [ModSchedules] where Wasvy will run mod systems. +/// +/// Adjust this via [ModloaderPlugin::new](crate::plugin::ModloaderPlugin::new). This will only affect +/// mods with access to the world. +/// +/// Or more simply, call [ModloaderPlugin::enable_schedule](crate::plugin::ModloaderPlugin::enable_schedule) with +/// [ModloaderPlugin::default](crate::plugin::ModloaderPlugin::default). /// -/// This object exists in the world as a Resource, representing +/// When using a [Sandbox](crate::sandbox::Sandbox), this is provided as an argument to adjust schedules for +/// mod systems that run in that sandbox. #[derive(Resource, Debug, Clone)] pub struct ModSchedules(pub Vec); diff --git a/src/setup.rs b/src/setup.rs index c3f1f72..cfcf7c4 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -6,7 +6,12 @@ use bevy_ecs::{ use bevy_log::prelude::*; use bevy_platform::collections::HashSet; -use crate::{access::ModAccess, asset::ModAsset, mods::Mod, schedule::ModStartup}; +use crate::{ + access::ModAccess, + asset::{AssetNotFound, ModAsset}, + mods::Mod, + schedule::ModStartup, +}; /// Group all the system params we neeed to allow shared access from one &mut world #[derive(SystemParam)] @@ -94,21 +99,17 @@ pub(crate) fn run_setup( // Initiate mods with exclusive world access (runs the mod setup) let mut run_startup_schedule = false; for (asset_id, mod_id, name, accesses) in setup { - let Some(result) = ModAsset::initiate(&mut world, &asset_id, mod_id, &name, &accesses[..]) + let Err(err) = ModAsset::initiate(&mut world, &asset_id, mod_id, &name, &accesses[..]) else { + info!("Successfully initialized mod \"{name}\""); + run_startup_schedule = true; continue; }; - for access in accesses { - ran_with.insert(RanWith { mod_id, access }); - } - - if let Err(err) = result { - error!("Error initializing mod \"{name}\":\n{err:?}"); - } else { - info!("Successfully initialized mod \"{name}\""); - - run_startup_schedule = true; + // If the asset is not found it's okay, we will run the setup once it is. + // So no need to log an error + if !err.is::() { + error!("Error initializing mod \"{name}\":\n{err:?}") } } diff --git a/src/system.rs b/src/system.rs new file mode 100644 index 0000000..a896b6a --- /dev/null +++ b/src/system.rs @@ -0,0 +1,328 @@ +use anyhow::Result; +use bevy_asset::{AssetId, Assets}; +use bevy_ecs::{ + change_detection::Tick, + error::Result as BevyResult, + prelude::*, + resource::Resource as BevyResource, + schedule::{ScheduleConfigs, ScheduleLabel}, + system::{BoxedSystem, Commands, LocalBuilder, ParamBuilder, ParamSetBuilder, Query}, + world::FilteredEntityMut, +}; +use bevy_log::prelude::*; +use wasmtime::component::{Resource, Val}; +use wasmtime_wasi::ResourceTable; + +use crate::{ + access::ModAccess, + asset::ModAsset, + bindings::wasvy::ecs::app::{QueryFor, Schedule}, + cleanup::InsertDespawnComponent, + component::WasmComponentRegistry, + engine::Engine, + host::{WasmCommands, WasmQuery, WasmSystem}, + mods::ModSystemSet, + query::{QueryId, QueryIdGenerator, QueryResolver, create_query_builder}, + runner::{ConfigRunSystem, Runner}, +}; + +/// A helper struct that stores dynamic systems that a mod would like to register. +/// +/// Wasvy only registers systems after mod's setup method has successfully run. +#[derive(Default)] +pub(crate) struct AddSystems(Vec<(Schedule, Vec>)>); + +impl AddSystems { + pub(crate) fn push(&mut self, schedule: Schedule, systems: Vec>) { + self.0.push((schedule, systems)); + } + + pub(crate) fn add_systems( + self, + world: &mut World, + accesses: &[ModAccess], + table: &ResourceTable, + mod_id: Entity, + mod_name: &str, + asset_id: &AssetId, + asset_version: &Tick, + ) -> Result<()> { + // Each access needs dedicated systems that run inside it + for access in accesses { + let mod_schedules = access.schedules(world); + for (schedule, systems) in self.0.iter() { + // Validate that the schedule requested by the mod is enabled + let Some(schedule) = mod_schedules + .evaluate(&schedule) + .map(|schedule| schedule.schedule_label()) + else { + warn!( + "Mod tried adding systems to schedule {schedule:?}, but that schedule is not enabled. See ModSchedules docs." + ); + continue; + }; + + for system in systems + .iter() + .map(|system| table.get(system).expect("Resource not be dropped")) + { + Self::add_system( + schedule.clone(), + system, + world, + mod_id, + mod_name, + asset_id, + asset_version, + access, + )?; + } + } + } + + Ok(()) + } + + fn add_system( + schedule: impl ScheduleLabel, + system: &WasmSystem, + world: &mut World, + mod_id: Entity, + mod_name: &str, + asset_id: &AssetId, + asset_version: &Tick, + access: &ModAccess, + ) -> Result<()> { + let schedule_config = Self::schedule( + system, + world, + mod_id, + mod_name, + asset_id, + asset_version, + &access, + )? + .in_set(ModSystemSet::All) + .in_set(ModSystemSet::Mod(mod_id)) + .in_set(ModSystemSet::Access(*access)); + + world + .get_resource_mut::() + .expect("running in an App") + .add_systems(schedule, schedule_config); + + Ok(()) + } + + pub(crate) fn schedule( + sys: &WasmSystem, + mut world: &mut World, + mod_id: Entity, + mod_name: &str, + asset_id: &AssetId, + asset_version: &Tick, + access: &ModAccess, + ) -> Result> { + // The input struct contains various data used at runtime + let built_params = BuiltParam::new_vec(&sys.params); + let query_resolver = QueryResolver::new(&sys.params, world)?; + let insert_despawn_component = InsertDespawnComponent::new(mod_id, world); + let input = Input { + mod_name: mod_name.to_string(), + system_name: sys.name.clone(), + asset_id: asset_id.clone(), + asset_version: asset_version.clone(), + built_params, + query_resolver, + access: *access, + insert_despawn_component, + }; + + // Generate the queries necessary to run this system + let filtered_access = access.filtered_access(world); + let mut queries = Vec::with_capacity(sys.params.len()); + for items in sys.params.iter().filter_map(Param::filter_query) { + queries.push(create_query_builder(items, world, filtered_access.clone())?); + } + + // Dynamic + let system = ( + LocalBuilder(input), + LocalBuilder(Vec::with_capacity(queries.len())), + ParamBuilder, + ParamBuilder, + ParamBuilder, + ParamBuilder, + ParamBuilder, + // TODO: FilteredResourcesMutParamBuilder::new(|builder| {}), + ParamSetBuilder(queries), + ) + .build_state(&mut world) + .build_system(system_runner) + .with_name(format!("wasvy[{mod_name}]::{}", sys.name)); + + let boxed_system = Box::new(IntoSystem::into_system(system)); + + let mut schedule_config = boxed_system + // See docs for [SystemIdentifier] + .in_set(sys.id); + + // Implement system ordering + for after in sys.after.iter() { + schedule_config = schedule_config.after(*after); + } + + Ok(schedule_config) + } +} + +struct Input { + mod_name: String, + system_name: String, + asset_id: AssetId, + asset_version: Tick, + built_params: Vec, + query_resolver: QueryResolver, + access: ModAccess, + insert_despawn_component: InsertDespawnComponent, +} + +impl FromWorld for Input { + fn from_world(_: &mut World) -> Self { + unreachable!("Input is created with LocalBuilder") + } +} + +fn system_runner( + input: Local, + mut params: Local>, + assets: Res>, + engine: Res, + type_registry: Res, + wasm_registry: Res, + mut commands: Commands, + // TODO: mut resources: FilteredResourcesMut, + mut queries: ParamSet>>, +) -> BevyResult { + // Skip no longer loaded mods + let Some(asset) = assets.get(input.asset_id) else { + return Ok(()); + }; + + // Skip mismatching system versions + if asset.version() != Some(input.asset_version) { + return Ok(()); + } + + let mut runner = Runner::new(&engine); + initialize_params(&mut params, &input.built_params, &mut runner)?; + + trace!( + "Running system \"{}\" from \"{}\"", + input.system_name, input.mod_name + ); + asset.run_system( + &mut runner, + &input.system_name, + ConfigRunSystem { + commands: &mut commands, + type_registry: &type_registry, + wasm_registry: &wasm_registry, + queries: &mut queries, + query_resolver: &input.query_resolver, + access: input.access.clone(), + insert_despawn_component: input.insert_despawn_component.clone(), + }, + ¶ms[..], + )?; + + Ok(()) +} + +/// A system param (what a mod system requests as parameters) +pub(crate) enum Param { + Commands, + Query(Vec), +} + +impl Param { + pub(crate) fn filter_query(&self) -> Option<&Vec> { + match self { + Param::Query(items) => Some(items), + _ => None, + } + } +} + +/// Each time a system runs, these are used to generate the wasi resources passed to the mod (system params) +enum BuiltParam { + Commands, + Query(QueryId), +} + +impl BuiltParam { + fn new_vec(params: &[Param]) -> Vec { + let mut ids = QueryIdGenerator::default(); + params + .iter() + .map(|param| match param { + Param::Commands => BuiltParam::Commands, + Param::Query(_) => BuiltParam::Query(ids.generate()), + }) + .collect() + } +} + +fn initialize_params( + params: &mut Vec, + source: &[BuiltParam], + runner: &mut Runner, +) -> Result<()> { + params.clear(); + for param in source.iter() { + let resource = match param { + BuiltParam::Commands => runner.new_resource(WasmCommands), + BuiltParam::Query(id) => runner.new_resource(WasmQuery::new(*id)), + }?; + params.push(Val::Resource(resource)); + } + Ok(()) +} + +/// Bevy doesn't return an identifier for systems added directly to the scheduler. There is +/// [NodeId](bevy_ecs::schedule::NodeId) but that has no clear way of being used for system ordering. +/// +/// So instead we take inspiration from bevy's [AnonymousSet](bevy_ecs::schedule::AnonymousSet) +/// and we identify each system with an extra [SystemSet] all to itself. +// Note: Using an AnonymousSet could work but unfortunately the method used to create one is private. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub struct DynamicSystemId(usize); + +impl DynamicSystemId { + /// Initialize a unique identifier in the world + pub(crate) fn new(world: &mut World) -> Self { + world.init_resource::(); + let mut count = world + .get_resource_mut::() + .expect("SystemIdentifierCount to be initialized"); + let identifier = DynamicSystemId(count.0); + count.0 += 1; + identifier + } +} + +impl SystemSet for DynamicSystemId { + // As of bevy 0.18 this function's only purpose is for debugging + fn is_anonymous(&self) -> bool { + // This is technically incorrect, but it makes bevy use the system name as node name instead of DynamicSystemId(usize) + true + } + + fn dyn_clone(&self) -> Box { + Box::new(*self) + } +} + +/// An tracker to ensure unique [DynamicSystemId]s in the world +#[derive(Default, BevyResource)] +struct DynamicSystemSetCount(usize); diff --git a/wit/ecs/ecs.wit b/wit/ecs/ecs.wit index 49d26a2..4c5f0d8 100644 --- a/wit/ecs/ecs.wit +++ b/wit/ecs/ecs.wit @@ -1,39 +1,39 @@ package wasvy:ecs; -/// This is the world that the bevy host implements to give ECS functionality to the WASM component. -/// Like `register-system`. +/// This is the world that the Bevy host (the game/app) implements +/// to give ECS functionality to the WASM guest (the mod). world host { import app; } -/// This is the world that the WASM guest implements. +/// This is the world that the WASM guest (the mod) implements. /// -/// These are basically the mandatory functions that a WASM component -/// must have for it to be called from the Bevy host. +/// This is the mandatory interface that a WASM component +/// must implement for it to be called from the Bevy host (the game/app). world guest { import app; + use app.{app}; - /// This function is called once on startup for each WASM component (Not Bevy component). - export setup: func(); + /// This method is called once on startup for each WASM component (Not Bevy component). + /// + /// In this method you should register and configure `system`s via the `app` resource + /// passed as a parameter. + export setup: func(app: app); } interface app { - /// A mod, similar to bevy::App + /// This is an interface (similar to bevy::App) through which mods may interact with the Bevy App. + /// + /// To access this, make sure to import the 'guest' world and implement `setup`. resource app { - /// Construct an new App: an interface through which mods may interact with the bevy world. - /// - /// Each mod may only do this once inside its setup function call. Attempting to do this - /// twice or outside setup will trap. - constructor(); - /// Adds systems to the mod add-systems: func( schedule: schedule, - systems: list, + systems: list>, ); } - /// An interface with which to define a new system for the host + /// An interface with which to define a new system for the host. /// /// Usage: /// 1. Construct a new system, giving it a unique name @@ -58,16 +58,82 @@ interface app { before: func(other: borrow); } - /// A commands system param + /// A `command` queue system param to perform structural changes to the world. + /// + /// Since each command requires exclusive access to the world, + /// all queued commands are automatically applied in sequence. + /// + /// Each command can be used to modify the world in arbitrary ways: + /// - spawning or despawning entities + /// - inserting components on new or existing entities + /// - etc. resource commands { - spawn: func(components: list>); - // etc + /// Spawns a new empty `entity` and returns its corresponding `entity-commands`. + spawn-empty: func() -> entity-commands; + + /// Spawns a new `entity` with the given components + /// and returns the entity's corresponding `entity-commands`. + spawn: func(bundle: bundle) -> entity-commands; + + /// Returns the `entity-commands` for the given `entity`. + /// + /// This method does not guarantee that commands queued by the returned `entity-commands` + /// will be successful, since the entity could be despawned before they are executed. + entity: func(entity: borrow) -> entity-commands; + } + + /// A list of commands that will be run to modify an `entity`. + resource entity-commands { + /// Returns the identifier for this entity + id: func() -> entity; + + /// Adds a `bundle` of components to the entity. + /// + /// This will overwrite any previous value(s) of the same component type. + insert: func(bundle: bundle); + + /// Removes a Bundle of components from the entity if it exists. + remove: func(bundle: bundle-types); + + /// Despawns the entity. + /// + /// This will emit a warning if the entity does not exist. + despawn: func(); + + /// Despawns the entity. + /// + /// Unlike `despawn`, this will not emit a warning if the entity does not exist. + try-despawn: func(); } + /// An identifier for an entity. + resource entity {} + /// A query system param resource query { /// Evaluates and returns the next query results - iter: func() -> option>; + iter: func() -> option; + } + + /// A query system param + resource query-result { + /// Returns the entity id for the query + entity: func() -> entity; + + /// Gets the component at the specified index. Order is the same as declared + /// during setup. Query filters do not count as components. + /// + /// So for example: + /// + /// ``` + /// spin_cube.add_query(&[ + /// QueryFor::Mut("A"), // component index 0 + /// QueryFor::With("B"), // none + /// QueryFor::Ref("C"), // component index 1 + /// QueryFor::Without("D"), // none + /// ]); + /// ``` + component: func(index: component-index) -> component; } resource component { @@ -89,6 +155,15 @@ interface app { /// Note: for components returned by query::optional this is an option type serialized-component = string; + /// Just a simple list of tuples composed of the type-path and the serialized component string + type bundle = list>; + + /// A bundle without the serialized components + type bundle-types = list; + + /// Each query supports up to 255 components + type component-index = u8; + variant schedule { /// A custom schedule that runs the first time a mod is loaded. ///