Skip to content

Commit

Permalink
feat: ResetWorld may be provided with resources to override res state
Browse files Browse the repository at this point in the history
after reset.
  • Loading branch information
MaxCWhitehead committed Nov 23, 2024
1 parent 437ca6a commit 69f9a1b
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 43 deletions.
115 changes: 115 additions & 0 deletions framework_crates/bones_ecs/src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,121 @@ impl<T: HasSchema + FromWorld> AtomicResource<T> {
}
}

/// Utility container for storing set of [`UntypedResource`].
/// Cloning
#[derive(Default)]
pub struct UntypedResourceSet {
resources: Vec<UntypedResource>,
}

impl Clone for UntypedResourceSet {
fn clone(&self) -> Self {
Self {
resources: self
.resources
.iter()
.map(|res| {
if let Some(clone) = res.clone_data() {
UntypedResource::new(clone)
} else {
UntypedResource::empty(res.schema())
}
})
.collect(),
}
}
}

impl UntypedResourceSet {
/// Insert a startup resource. On stage / session startup (first step), will be inserted into [`World`].
///
/// If already exists, will be overwritten.
pub fn insert_resource<T: HasSchema>(&mut self, resource: T) {
// Update an existing resource of the same type.
for r in &mut self.resources {
if r.schema() == T::schema() {
let mut borrow = r.borrow_mut();

if let Some(b) = borrow.as_mut() {
*b.cast_mut() = resource;
} else {
*borrow = Some(SchemaBox::new(resource))
}
return;
}
}

// Or insert a new resource if we couldn't find one
self.resources
.push(UntypedResource::new(SchemaBox::new(resource)))
}

/// Init resource with default, and return mutable ref for modification.
/// If already exists, returns mutable ref to existing resource.
pub fn init_resource<T: HasSchema + Default>(&mut self) -> RefMut<T> {
if !self.resources.iter().any(|x| x.schema() == T::schema()) {
self.insert_resource(T::default());
}
self.resource_mut::<T>().unwrap()
}

/// Get mutable reference to startup resource if found.
#[track_caller]
pub fn resource_mut<T: HasSchema>(&self) -> Option<RefMut<T>> {
let res = self.resources.iter().find(|x| x.schema() == T::schema())?;
let borrow = res.borrow_mut();

if borrow.is_some() {
// SOUND: We know the type matches T
Some(RefMut::map(borrow, |b| unsafe {
b.as_mut().unwrap().as_mut().cast_into_mut_unchecked()
}))
} else {
None
}
}

/// Insert an [`UntypedResource`] with empty cell for [`Schema`] of `T`.
/// If resource already exists for this schema, overwrite it with empty.
pub fn insert_empty<T: HasSchema>(&mut self) {
for r in &mut self.resources {
if r.schema() == T::schema() {
let mut borrow = r.borrow_mut();
*borrow = None;
return;
}
}

// Or insert a new empty resource if we couldn't find one
self.resources.push(UntypedResource::empty(T::schema()));
}

/// Get immutable ref to Vec of resources
pub fn resources(&self) -> &Vec<UntypedResource> {
&self.resources
}

/// Insert resources in world. If resource already exists, overwrites it.
///
/// If `remove_empty` is set, if resource cell is empty, it will remove the
/// resource from cell on world.
pub fn insert_on_world(&self, world: &mut World, remove_empty: bool) {
for resource in self.resources.iter() {
let resource_cell = world.resources.untyped().get_cell(resource.schema());

// Deep copy resource and insert into world.
if let Some(resource_copy) = resource.clone_data() {
resource_cell
.insert(resource_copy)
.expect("Schema mismatch error");
} else if remove_empty {
// Remove the resource on world
resource_cell.remove();
}
}
}
}

#[cfg(test)]
mod test {
use std::sync::Arc;
Expand Down
48 changes: 6 additions & 42 deletions framework_crates/bones_ecs/src/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub struct SystemStagesBuilder {
startup_systems: Vec<StaticSystem<(), ()>>,

/// Resources installed during session plugin installs. Copied to world as first step on startup of stages' execution.
startup_resources: Vec<UntypedResource>,
startup_resources: UntypedResourceSet,

/// Systems that are continously run until they succeed(return Some). These run before all stages. Uses Option to allow for easy usage of `?`.
single_success_systems: Vec<StaticSystem<(), Option<()>>>,
Expand Down Expand Up @@ -140,55 +140,19 @@ impl SystemStagesBuilder {
///
/// If already exists, will be overwritten.
pub fn insert_startup_resource<T: HasSchema>(&mut self, resource: T) {
// Update an existing resource of the same type.
for r in &mut self.startup_resources {
if r.schema() == T::schema() {
let mut borrow = r.borrow_mut();

if let Some(b) = borrow.as_mut() {
*b.cast_mut() = resource;
} else {
*borrow = Some(SchemaBox::new(resource))
}
return;
}
}

// Or insert a new resource if we couldn't find one
self.startup_resources
.push(UntypedResource::new(SchemaBox::new(resource)));
self.startup_resources.insert_resource(resource);
}

/// Init startup resource with default, and return mutable ref for modification.
/// If already exists, returns mutable ref to existing resource.
pub fn init_startup_resource<T: HasSchema + Default>(&mut self) -> RefMut<T> {
if !self
.startup_resources
.iter()
.any(|x| x.schema() == T::schema())
{
self.insert_startup_resource(T::default());
}
self.startup_resource_mut::<T>().unwrap()
self.startup_resources.init_resource::<T>()
}

/// Get mutable reference to startup resource if found.
#[track_caller]
pub fn startup_resource_mut<T: HasSchema>(&self) -> Option<RefMut<T>> {
let res = self
.startup_resources
.iter()
.find(|x| x.schema() == T::schema())?;
let borrow = res.borrow_mut();

if borrow.is_some() {
// SOUND: We know the type matches T
Some(RefMut::map(borrow, |b| unsafe {
b.as_mut().unwrap().as_mut().cast_into_mut_unchecked()
}))
} else {
None
}
self.startup_resources.resource_mut()
}
}

Expand All @@ -202,7 +166,7 @@ pub struct SystemStages {
startup_systems: Vec<StaticSystem<(), ()>>,

/// Resources installed during session plugin installs. Copied to world as first step on startup of stages' execution.
startup_resources: Vec<UntypedResource>,
startup_resources: UntypedResourceSet,

/// Systems that are continously run until they succeed(return Some). These run before all stages. Uses Option to allow for easy usage of `?`.
single_success_systems: Vec<StaticSystem<(), Option<()>>>,
Expand Down Expand Up @@ -322,7 +286,7 @@ impl SystemStages {

/// Insert the startup resources that [`SystemStages`] and session were built with into [`World`].
fn insert_startup_resources(&self, world: &mut World) {
for resource in self.startup_resources.iter() {
for resource in self.startup_resources.resources().iter() {
// Deep copy startup resource and insert into world.
let resource_copy = resource.clone_data().unwrap();
let resource_cell = world.resources.untyped().get_cell(resource.schema());
Expand Down
79 changes: 79 additions & 0 deletions framework_crates/bones_framework/tests/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,82 @@ pub fn single_success_system_reset() {
assert_eq!(counter.0, 2);
}
}

#[test]
pub fn reset_world_resource_override() {
let mut game = Game::new();

// insert counter resource
game.sessions.create_with("game", |builder| {
builder.init_resource::<Counter>();
});

game.step(Instant::now());
{
let session = game.sessions.get("game").unwrap();
let counter = session.world.get_resource::<Counter>().unwrap();
// asert in default state of 0
assert_eq!(counter.0, 0);
}

// Add command that will trigger reset on next step,
// and add reset resource of Counter with value 1
{
let game_session = game.sessions.get_mut("game").unwrap();
game_session.world.init_resource::<CommandQueue>().add(
|mut reset: ResMutInit<ResetWorld>| {
reset.reset = true;
reset.insert_reset_resource(Counter(1));
},
);
}

// Step to reset
game.step(Instant::now());
{
let session = game.sessions.get("game").unwrap();
let counter = session.world.get_resource::<Counter>().unwrap();
// Verify the reset resource of value 1 was applied, instead of resetting to default state
assert_eq!(counter.0, 1);
}
}

#[test]
pub fn reset_world_emtpy_resource() {
let mut game = Game::new();

// insert counter resource
game.sessions.create_with("game", |builder| {
builder.init_resource::<Counter>();
});

game.step(Instant::now());
{
let session = game.sessions.get("game").unwrap();
let counter = session.world.get_resource::<Counter>().unwrap();
// asert in default state of 0
assert_eq!(counter.0, 0);
}

// Add command that will trigger reset on next step,
// and add reset resource of Counter with value 1
{
let game_session = game.sessions.get_mut("game").unwrap();
game_session.world.init_resource::<CommandQueue>().add(
|mut reset: ResMutInit<ResetWorld>| {
reset.reset = true;
reset.insert_empty_reset_resource::<Counter>();
},
);
}

// Step to reset
game.step(Instant::now());
{
let session = game.sessions.get("game").unwrap();
let counter = session.world.get_resource::<Counter>();

// Verify resource was removed instead of reseting to initial state of session build.
assert!(counter.is_none())
}
}
38 changes: 37 additions & 1 deletion framework_crates/bones_lib/src/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,35 @@ use crate::prelude::*;
/// This is supported in bone's default session runners - but if implementing custom runner, must call `world.handle_world_reset` inside step.
///
/// `reset_world: ResMutInit<ResetWorld>` and setting `reset_world.reset = true;` may be used to trigger a reset from system execution.
#[derive(Copy, Clone, HasSchema, Default)]
#[derive(HasSchema, Clone, Default)]
pub struct ResetWorld {
/// Set to true to trigger reset of [`World`].
pub reset: bool,

/// List of resources that will be inserted into [`World`] after the reset.
/// These override any `startup resources` captured during session build.
/// If want to preserve a resource instead of having it reset, insert it here.
pub reset_resources: UntypedResourceSet,
}

impl ResetWorld {
/// Insert a resource that will be applied after reset. If resource was created
/// on session iniialization, this will overwrite it using reset resource instead.
pub fn insert_reset_resource<T: HasSchema>(&mut self, resource: T) {
self.reset_resources.insert_resource(resource);
}

/// Get a mutable reference to a reset resource if found.
pub fn reset_resource_mut<T: HasSchema>(&self) -> Option<RefMut<T>> {
self.reset_resources.resource_mut::<T>()
}

/// Insert resource in "empty" state - If resource was created
/// on session iniialization, instead of being reset to that state,
/// after reset this resource will not be on [`World`].
pub fn insert_empty_reset_resource<T: HasSchema>(&mut self) {
self.reset_resources.insert_empty::<T>();
}
}

/// Extension of [`World`]
Expand Down Expand Up @@ -47,6 +72,11 @@ impl WorldExt for World {
}

fn reset_internals(&mut self, stages: &mut SystemStages) {
// Copy resources to be inserted after the reset.
let post_reset_resources = self
.get_resource::<ResetWorld>()
.map(|x| x.reset_resources.clone());

// Clear all component stores
self.components = ComponentStores::default();

Expand All @@ -72,5 +102,11 @@ impl WorldExt for World {

// Immediately run startup tasks, ensuring startup resources are present and run startup systems.
stages.handle_startup(self);

// Apply any reset resources to world, overwriting startup resources.
if let Some(resources) = post_reset_resources {
let remove_empty = true;
resources.insert_on_world(self, remove_empty);
}
}
}

0 comments on commit 69f9a1b

Please sign in to comment.