use std::alloc::Layout;
use bevy_ecs::{
component::{ComponentCloneBehavior, ComponentDescriptor, ComponentId, StorageType},
entity::EntityHashSet,
lifecycle::HookContext,
prelude::*,
query::FilteredAccess,
relationship::Relationship,
world::{DeferredWorld, WorldId},
};
use crate::{cleanup::DisableSystemSet, mods::ModSystemSet, schedule::ModSchedules};
#[derive(Component)]
#[require(Name::new("Sandbox"))]
#[component(clone_behavior = Ignore, immutable)]
#[component(on_insert = Self::on_insert, on_replace = Self::on_replace, on_remove = Self::on_remove, on_despawn = Self::on_despawn)]
pub struct Sandbox {
component_id: ComponentId,
access: FilteredAccess,
schedules: ModSchedules,
world_id: WorldId,
}
impl Sandbox {
pub fn new(world: &mut World, schedules: ModSchedules) -> Self {
let sandbox_count = world.get_resource_or_insert_with(|| SandboxCount(1));
let count = sandbox_count.0;
sandbox_count.into_inner().0 += 1;
if count == 1 {
world.add_observer(Sandboxed::propagate);
}
let name = format!("Sandbox{count}");
let clone_behavior = ComponentCloneBehavior::Ignore;
let descriptor = unsafe {
ComponentDescriptor::new_with_layout(
name,
StorageType::default(),
Layout::new::<SandboxedMarker>(),
None,
false,
clone_behavior,
None,
)
};
let component_id = world.register_component_with_descriptor(descriptor);
let access = Self::generate_access(component_id, world);
let world_id = world.id();
Self {
component_id,
access,
world_id,
schedules,
}
}
pub fn schedules(&self) -> &ModSchedules {
&self.schedules
}
pub fn access(&self) -> &FilteredAccess {
&self.access
}
pub fn access_non_sandboxed(world: &World) -> FilteredAccess {
let mut access = FilteredAccess::default();
access.and_without(
world
.components()
.component_id::<Sandboxed>()
.expect("Sandboxed be registered"),
);
access
}
fn generate_access(component_id: ComponentId, world: &mut World) -> FilteredAccess {
let mut access = FilteredAccess::default();
access.and_with(component_id);
access.and_with(
world
.components()
.component_id::<Sandboxed>()
.expect("Sandboxed be registered"),
);
for other_sandbox in world
.query::<&Sandbox>()
.iter(world)
.filter(|sandbox| sandbox.component_id != component_id)
{
access.and_without(other_sandbox.component_id);
}
access
}
fn on_insert(mut world: DeferredWorld, ctx: HookContext) {
let Self { world_id, .. } = world
.entity(ctx.entity)
.get()
.expect("Sandbox was inserted");
if world.id() != *world_id {
panic!("Sandbox was created from one world, but spawned in another");
}
Sandboxed::add_children(ctx.entity, ctx.entity, &mut world);
}
fn on_replace(mut world: DeferredWorld, ctx: HookContext) {
let component_id = world
.entity(ctx.entity)
.get::<Self>()
.expect("Sandbox was replaced")
.component_id;
if let Some(SandboxedEntities(entities)) = world.entity(ctx.entity).get() {
for entity in entities.clone() {
world.commands().entity(entity).remove_by_id(component_id);
}
}
}
fn on_remove(mut world: DeferredWorld, ctx: HookContext) {
world
.commands()
.entity(ctx.entity)
.remove::<SandboxedEntities>();
}
fn on_despawn(mut world: DeferredWorld, ctx: HookContext) {
let schedules = world
.entity(ctx.entity)
.get::<Self>()
.expect("Sandbox was replaced")
.schedules()
.clone();
world.commands().queue(DisableSystemSet {
set: ModSystemSet::new_sandboxed(ctx.entity),
schedules,
});
}
}
#[derive(Component, Default, Debug, PartialEq, Eq)]
#[relationship_target(relationship = Sandboxed)]
pub struct SandboxedEntities(EntityHashSet);
#[derive(Component, Clone, PartialEq, Eq, Debug)]
#[component(immutable, clone_behavior = Ignore)]
#[component(on_insert = Self::on_insert, on_replace = Self::on_replace)]
pub struct Sandboxed(Entity);
impl Relationship for Sandboxed {
type RelationshipTarget = SandboxedEntities;
fn get(&self) -> Entity {
self.0
}
fn from(entity: Entity) -> Self {
Self(entity)
}
fn set_risky(&mut self, entity: Entity) {
self.0 = entity;
}
}
impl Sandboxed {
fn propagate(add: On<Insert, ChildOf>, mut world: DeferredWorld) {
let mut entity = add.entity;
while let Some(ChildOf(parent)) = world.get(entity) {
entity = *parent;
if world.get::<Sandbox>(entity).is_some() {
Self::add_children(add.entity, entity, &mut world);
break;
}
}
}
fn on_insert(mut world: DeferredWorld, ctx: HookContext) {
let Self(sandbox) = world.entity(ctx.entity).get().expect("Component was added");
if let Some(sandbox) = world.entity(*sandbox).get::<Sandbox>() {
let component_id = sandbox.component_id;
unsafe {
world
.commands()
.entity(ctx.entity)
.insert_by_id(component_id, SandboxedMarker);
}
} else {
world.commands().entity(ctx.entity).remove::<Self>();
}
<Self as Relationship>::on_insert(world, ctx);
}
fn on_replace(mut world: DeferredWorld, ctx: HookContext) {
let Self(sandbox) = world.entity(ctx.entity).get().expect("Component was added");
if let Some(sandbox) = world.entity(*sandbox).get::<Sandbox>() {
let component_id = sandbox.component_id;
world
.commands()
.entity(ctx.entity)
.remove_by_id(component_id);
}
<Self as Relationship>::on_insert(world, ctx);
}
fn add_children(entity: Entity, sandbox: Entity, world: &mut DeferredWorld) {
if entity != sandbox {
world.commands().entity(entity).insert(Sandboxed(sandbox));
if world.get::<Sandbox>(entity).is_some() {
return;
}
}
if let Some(children) = world.get::<Children>(entity) {
let children: Vec<Entity> = children.iter().collect();
for child in children {
Self::add_children(child, sandbox, world);
}
}
}
}
struct SandboxedMarker;
#[derive(Resource, Default)]
struct SandboxCount(pub usize);
#[cfg(test)]
mod tests {
use bevy_ecs::{prelude::*, relationship::RelationshipSourceCollection};
use super::*;
use crate::schedule::ModSchedules;
fn setup() -> World {
let mut world = World::new();
world.register_component::<Sandboxed>();
world
}
#[test]
fn sandboxed_propagate_marker() {
let mut world = setup();
let component = Sandbox::new(&mut world, ModSchedules::empty());
let marker = component.component_id;
let sandbox = world.spawn(component).id();
let child = world.spawn_empty().insert(ChildOf(sandbox)).id();
assert!(
world.get_by_id(sandbox, marker).is_none(),
"Sandbox should not have a marker"
);
assert!(
world.get_by_id(child, marker).is_some(),
"Child has the SandboxMarker"
);
}
#[test]
fn simple_sandboxed_propagate() {
let mut world = setup();
let component = Sandbox::new(&mut world, ModSchedules::empty());
let sandbox = world.spawn(component).id();
let child = world.spawn_empty().insert(ChildOf(sandbox)).id();
let nested_child = world.spawn_empty().insert(ChildOf(child)).id();
let mut set = EntityHashSet::new();
set.add(child);
set.add(nested_child);
assert_eq!(
world.entity(sandbox).get(),
Some(&SandboxedEntities(set)),
"All Children have the sandbox relation and were added to the SandboxedEntities"
);
}
#[test]
fn reparent_sandboxed() {
let mut world = setup();
let component = Sandbox::new(&mut world, ModSchedules::empty());
let marker1 = component.component_id;
let sandbox1 = world.spawn(component).id();
let component = Sandbox::new(&mut world, ModSchedules::empty());
let marker2 = component.component_id;
let sandbox2 = world.spawn(component).id();
let child = world.spawn_empty().insert(ChildOf(sandbox1)).id();
let nested_child = world.spawn_empty().insert(ChildOf(child)).id();
world.entity_mut(child).insert(ChildOf(sandbox2));
assert_eq!(
world.get(child),
Some(&Sandboxed(sandbox2)),
"A reparented sandboxed entity should be updated"
);
assert_eq!(
world.get(nested_child),
Some(&Sandboxed(sandbox2)),
"The child of a reparented sandboxed entity should be updated"
);
assert!(
world.get_by_id(nested_child, marker1).is_none()
&& world.get_by_id(nested_child, marker2).is_some(),
"A reparented child should have the correct marker trait"
);
}
#[test]
fn replace_sandbox() {
let mut world = setup();
let component = Sandbox::new(&mut world, ModSchedules::empty());
let marker1 = component.component_id;
let sandbox = world.spawn(component).id();
let child = world.spawn_empty().insert(ChildOf(sandbox)).id();
let component = Sandbox::new(&mut world, ModSchedules::empty());
let marker2 = component.component_id;
world.entity_mut(sandbox).insert(component);
assert!(
world.get_by_id(child, marker1).is_none() && world.get_by_id(child, marker2).is_some(),
"Child should have the correct marker trait"
);
assert!(
world.get::<SandboxedEntities>(sandbox).is_some(),
"SandboxedEntities should not be removed"
);
}
#[test]
fn remove_sandbox() {
let mut world = setup();
let component = Sandbox::new(&mut world, ModSchedules::empty());
let marker = component.component_id;
let sandbox = world.spawn(component).id();
let child = world.spawn_empty().insert(ChildOf(sandbox)).id();
world.entity_mut(sandbox).remove::<Sandbox>();
assert!(
world.get::<Sandboxed>(child).is_none(),
"A child of a removed sandbox should have no more markers"
);
assert!(
world.get_by_id(child, marker).is_none(),
"A child of a removed sandbox should have no more markers"
);
}
#[test]
fn nested_sandbox_propagate() {
let mut world = setup();
let component = Sandbox::new(&mut world, ModSchedules::empty());
let sandbox1 = world.spawn(component).id();
let child1 = world.spawn(ChildOf(sandbox1)).id();
let component = Sandbox::new(&mut world, ModSchedules::empty());
let sandbox2 = world.spawn((component, ChildOf(child1))).id();
let child2 = world.spawn(ChildOf(sandbox2)).id();
assert_eq!(
world.get(sandbox2),
Some(&Sandboxed(sandbox1)),
"Sandbox is sandboxed"
);
assert_eq!(
world.get(child2),
Some(&Sandboxed(sandbox2)),
"Nested sandbox children belong to their own sandbox"
);
}
#[test]
fn panic_world_mismatch() {
let result = std::panic::catch_unwind(move || {
let mut world = setup();
let mut other_world = setup();
let component = Sandbox::new(&mut other_world, ModSchedules::empty());
world.spawn(component);
});
assert!(
result.is_err(),
"Should panic when Sandbox was created in different world"
);
assert_eq!(
result.unwrap_err().downcast_ref::<&str>(),
Some(&"Sandbox was created from one world, but spawned in another")
);
}
}