Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -20,7 +20,6 @@ def setup(self):
QueryFor_With("python::MyComponent")
])

app = App()
app.add_systems(Schedule_Update(), [my_system, spin_cube])

# Spin speed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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"""
Expand Down Expand Up @@ -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
"""
Expand Down
22 changes: 11 additions & 11 deletions examples/simple/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,19 @@ 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");
spin_cube.add_query(&[
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) {
Expand All @@ -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));
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
59 changes: 41 additions & 18 deletions src/asset.rs
Original file line number Diff line number Diff line change
@@ -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::*};
Expand All @@ -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
Expand Down Expand Up @@ -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<ModAsset>,
mod_id: Entity,
mod_name: &str,
accesses: &[ModAccess],
) -> Option<Result<()>> {
) -> Result<()> {
let change_tick = world.change_tick();

let mut assets = world
.get_resource_mut::<Assets<Self>>()
.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 {
Expand Down Expand Up @@ -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>(
Expand All @@ -127,6 +135,21 @@ impl ModAsset {
}
}

#[derive(Debug)]
pub(crate) struct AssetNotFound;

impl From<AssetNotFound> 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<WasmHost>,
Expand Down
Loading