use concinnity_core::components::SkeletonPose;
use concinnity_core::components::{
DespawnRequest, EntityTarget, ReparentRequest, SpawnRequest, VisibilityRequest,
};
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_core::ecs::{
Entity, EventCursor, FrameTime, MenuActive, PipelineContext, StepResult, System,
};
use concinnity_core::render::ops::RenderOps;
use concinnity_core::transform::propagation;
use crate::ecs::ActiveRenderQueues;
use crate::gfx::render_slots::RenderSlots;
fn resolve_target(ctx: &PipelineContext, target: EntityTarget) -> Option<Entity> {
match target {
EntityTarget::Name(name) => resolve_name(ctx, name),
EntityTarget::Entity(entity) => Some(entity),
}
}
fn resolve_name(ctx: &PipelineContext, name: AssetId) -> Option<Entity> {
ctx.entity_of(name)
}
mod despawn;
mod visibility;
use concinnity_core::spawn as template;
fn record_clone_static(
ops: &mut RenderOps,
slots: &mut RenderSlots,
src: usize,
model: [[f32; 4]; 4],
) -> Option<usize> {
use concinnity_core::render::draw_slot::SlotAlloc;
let dst = slots.allocate_draw();
let idx = match dst {
SlotAlloc::Reuse(i) | SlotAlloc::Append(i) => i,
};
ops.record(move |backend| {
if let Err(e) = backend.clone_static_draw_object(src, model, dst) {
tracing::warn!("SpawnSystem: draw-slot clone of {} failed: {}", src, e);
}
});
Some(idx)
}
fn record_skinned_claim(
ops: &mut RenderOps,
slots: &mut RenderSlots,
template: usize,
model: [[f32; 4]; 4],
) -> Option<usize> {
let instance = slots.claim_skinned(template)?;
ops.record(move |backend| backend.reveal_skinned_instance(instance, model));
Some(instance)
}
#[derive(Debug, Default)]
pub(crate) struct SpawnSystem {
despawn_cmd_cursor: EventCursor,
reparent_cmd_cursor: EventCursor,
spawn_cmd_cursor: EventCursor,
visibility_cmd_cursor: EventCursor,
}
impl SpawnSystem {
pub(crate) fn new() -> Self {
Self::default()
}
}
impl System for SpawnSystem {
fn step(&mut self, ctx: &mut PipelineContext) -> StepResult {
let Some(mut queues) = ActiveRenderQueues::take(ctx.resources) else {
return StepResult::Continue;
};
self.drain(ctx, &mut queues.ops, &mut queues.slots);
ActiveRenderQueues::put(ctx.resources, queues);
StepResult::Continue
}
}
impl SpawnSystem {
fn drain(&mut self, ctx: &mut PipelineContext, ops: &mut RenderOps, slots: &mut RenderSlots) {
let dt = ctx
.resource::<FrameTime>()
.copied()
.unwrap_or_default()
.dt
.max(0.0);
let frame = ctx.frame;
let menu_active = ctx.resource::<MenuActive>().map(|m| m.0).unwrap_or(false);
if !menu_active {
let expired = template::tick_lifetimes(ctx, dt);
for &entity in expired.iter() {
despawn::despawn_subtree(ctx, ops, slots, entity);
}
}
let despawn_targets = match ctx.events::<DespawnRequest>() {
Some(events) => {
frame.collect(events.read(&mut self.despawn_cmd_cursor).map(|r| r.target))
}
None => frame.collect([]),
};
for &target in &despawn_targets {
if let Some(entity) = resolve_target(ctx, target) {
despawn::despawn_subtree(ctx, ops, slots, entity);
}
}
let vis_reqs = match ctx.events::<VisibilityRequest>() {
Some(events) => frame.collect(events.read(&mut self.visibility_cmd_cursor).copied()),
None => frame.collect([]),
};
for &req in &vis_reqs {
if let Some(entity) = resolve_target(ctx, req.target) {
visibility::set_subtree_visibility(ctx, ops, entity, req.visible);
}
}
let reparents = match ctx.events::<ReparentRequest>() {
Some(events) => frame.collect(events.read(&mut self.reparent_cmd_cursor).copied()),
None => frame.collect([]),
};
for &req in &reparents {
let Some(child) = resolve_target(ctx, req.child) else {
continue;
};
let parent = req.parent.and_then(|p| resolve_target(ctx, p));
if req.parent.is_some() && parent.is_none() {
continue;
}
propagation::reparent(ctx, child, parent);
}
let spawn_reqs = match ctx.events::<SpawnRequest>() {
Some(events) => frame.collect(events.read(&mut self.spawn_cmd_cursor).copied()),
None => frame.collect([]),
};
for &req in spawn_reqs.iter() {
let Some(template) = resolve_name(ctx, req.template) else {
continue;
};
if ctx.get::<SkeletonPose>(template).is_some() {
template::spawn_skinned_from_template(
ctx,
template,
req.name,
req.transform,
req.lifetime_secs,
|tmpl, model| record_skinned_claim(ops, slots, tmpl, model),
);
} else {
template::spawn_from_template(
ctx,
template,
req.name,
req.transform,
req.lifetime_secs,
|src, model| record_clone_static(ops, slots, src, model),
);
}
}
let due_spawns = if menu_active {
frame.collect([])
} else {
template::tick_spawners(ctx, dt)
};
for &due in due_spawns.iter() {
let Some(template) = resolve_name(ctx, due.template) else {
continue;
};
if ctx.get::<SkeletonPose>(template).is_some() {
template::spawn_skinned_from_template(
ctx,
template,
None,
due.transform,
due.lifetime,
|tmpl, model| record_skinned_claim(ops, slots, tmpl, model),
);
} else {
template::spawn_from_template(
ctx,
template,
None,
due.transform,
due.lifetime,
|src, model| record_clone_static(ops, slots, src, model),
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_core::ecs::{Arena, ComponentStorage, FrameContext, Resources};
use concinnity_core::profile::FrameProfile;
use concinnity_host::store::blob::BlobData;
fn run<R>(body: impl FnOnce(&mut PipelineContext) -> R) -> R {
let mut components = ComponentStorage::default();
let mut blob = BlobData::empty();
let mut profile = FrameProfile::default();
let mut resources = Resources::new();
let scratch = Arena::with_capacity(64 * 1024);
let mut ctx = PipelineContext {
components: &mut components,
blob: &mut blob,
profile: &mut profile,
resources: &mut resources,
frame: FrameContext::new(&scratch),
};
body(&mut ctx)
}
#[test]
fn an_entity_target_resolves_to_itself_without_a_name_index() {
run(|ctx| {
let entity = ctx.components.spawn();
assert_eq!(
resolve_target(ctx, EntityTarget::Entity(entity)),
Some(entity)
);
});
}
#[test]
fn a_named_target_resolves_through_the_index() {
run(|ctx| {
let entity = ctx.components.spawn();
ctx.identify(entity, AssetId(7));
assert_eq!(
resolve_target(ctx, EntityTarget::Name(AssetId(7))),
Some(entity)
);
assert_eq!(resolve_target(ctx, EntityTarget::Name(AssetId(8))), None);
});
}
#[test]
fn a_named_target_without_a_name_index_resolves_to_nothing() {
run(|ctx| {
assert_eq!(resolve_target(ctx, EntityTarget::Name(AssetId(7))), None);
});
}
}