use std::any::TypeId;
use std::sync::OnceLock;
use crate::ecs::{Access, ComponentId, EventStore};
macro_rules! define_access_ids {
(
resources: [ $( $res:path, )* ],
events: [ $( $ev:path, )* ] $(,)?
) => {
fn table() -> &'static [(TypeId, &'static str, u8)] {
static TABLE: OnceLock<Vec<(TypeId, &'static str, u8)>> = OnceLock::new();
TABLE.get_or_init(|| {
let types = [
$( (TypeId::of::<$res>(), stringify!($res)), )*
$( (TypeId::of::<$ev>(), stringify!($ev)), )*
];
assert!(types.len() <= 128, "access-id registry exceeds the 128-bit mask");
types
.into_iter()
.enumerate()
.map(|(id, (type_id, name))| (type_id, name, id as u8))
.collect()
})
}
pub(crate) fn ensure_event_queues(store: &mut EventStore, access: Access) {
if access.is_exclusive() {
return;
}
$(
if let Some(id) = id_of::<$ev>() {
if access.may_write_resource(id) || access.may_read_resource(id) {
store.get_mut_or_create::<$ev>();
}
}
)*
}
};
}
define_access_ids! {
resources: [
crate::components::FrameInput,
crate::ecs::MenuActive,
crate::ecs::SimTiming,
crate::ecs::MenuOverride,
crate::ecs::DesiredCursor,
crate::ecs::HudLayers,
crate::ecs::ScreenStack,
crate::ecs::FlyCam,
crate::ecs::CursorState,
crate::ecs::HudPrefs,
crate::ecs::OpenDropdown,
crate::ecs::DisabledSettingRows,
crate::ecs::DisplayModes,
crate::ecs::InputMailbox,
crate::ecs::ScheduleMode,
crate::ecs::ActiveSceneFlow,
crate::ecs::SceneResidencyStatus,
crate::ecs::decompose::EntityByName,
crate::app::budget::MemoryBudget,
crate::app::budget::ThreadBudget,
crate::gfx::overlay::OverlayFrame,
crate::gfx::overlay::OverlayAssets,
crate::gfx::overlay::OverlayRecycle,
],
events: [
crate::components::ControlsCommand,
crate::components::InteractEvent,
crate::components::RootMotionEvent,
crate::components::ScreenCommand,
crate::components::ScreenShown,
crate::components::SettingCommand,
crate::components::SceneCommand,
crate::components::StoryCommand,
crate::components::StoryReload,
crate::components::PlayCue,
crate::components::AudioCommand,
],
}
pub(crate) fn id_of<T: 'static>() -> Option<ComponentId> {
resolve(TypeId::of::<T>())
}
fn resolve(type_id: TypeId) -> Option<ComponentId> {
table()
.iter()
.find(|(t, _, _)| *t == type_id)
.map(|&(_, _, id)| ComponentId::new(id))
}
macro_rules! component_mask {
( $( $ty:ty ),* $(,)? ) => {{
let mut m = $crate::ecs::ComponentMask::EMPTY;
$( m.insert($crate::ecs::ComponentId::new(
<$ty as $crate::ecs::ComponentSlot>::DISCRIMINANT,
)); )*
m
}};
}
macro_rules! resource_mask {
( $( $ty:ty ),* $(,)? ) => {{
let mut m = $crate::ecs::ComponentMask::EMPTY;
$( m.insert(
$crate::ecs::access_ids::id_of::<$ty>()
.unwrap_or_else(|| panic!(
"not in the access-id registry: {}",
stringify!($ty),
)),
); )*
m
}};
}
pub(crate) use {component_mask, resource_mask};
#[cfg(debug_assertions)]
mod validate {
use super::{resolve, table};
use crate::ecs::Access;
use concinnity_core::ecs::access_check::{self, Touch};
std::thread_local! {
static ACTIVE: std::cell::Cell<Option<(Access, &'static str)>> =
const { std::cell::Cell::new(None) };
}
fn set_active(active: Option<(Access, &'static str)>) {
ACTIVE.with(|a| a.set(active));
}
pub(crate) fn install_hook() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
access_check::install(check);
access_check::install_active(set_active);
});
}
fn check(touch: &Touch) {
let Some((access, system)) = ACTIVE.with(|a| a.get()) else {
return;
};
if access.is_exclusive() {
return;
}
match touch {
Touch::ComponentRead { id, type_name } => assert!(
access.may_read_component(crate::ecs::ComponentId::new(*id)),
"{system} reads {type_name} without declaring it",
),
Touch::ComponentWrite { id, type_name } => assert!(
access.may_write_component(crate::ecs::ComponentId::new(*id)),
"{system} writes {type_name} without declaring it",
),
Touch::Structural { op } => {
panic!("{system} performs structural change ({op}) without exclusive access")
}
Touch::Blob { op } => {
panic!("{system} touches the blob store ({op}) without exclusive access")
}
Touch::Resource {
type_id,
type_name,
write,
} => match resolve(*type_id) {
Some(id) if *write => assert!(
access.may_write_resource(id),
"{system} writes resource {type_name} without declaring it",
),
Some(id) => assert!(
access.may_read_resource(id),
"{system} reads resource {type_name} without declaring it",
),
None => panic!(
"{system} touches resource {type_name}, which is not in the \
access-id registry; register it or keep the system exclusive \
(registry holds {} entries)",
table().len(),
),
},
}
}
}
#[cfg(debug_assertions)]
pub(crate) use validate::install_hook;
#[cfg(test)]
mod tests {
use super::*;
use crate::ecs::Access;
#[test]
fn registered_types_resolve_to_distinct_ids() {
let a = id_of::<crate::ecs::MenuActive>().unwrap();
let b = id_of::<crate::components::ScreenCommand>().unwrap();
assert_ne!(a, b);
}
#[test]
fn unregistered_types_do_not_resolve() {
struct NotRegistered;
assert!(id_of::<NotRegistered>().is_none());
}
#[test]
fn ensure_event_queues_creates_declared_queues_only() {
let mut store = EventStore::new();
let access =
Access::new().writes_resources(resource_mask![crate::components::ScreenCommand]);
ensure_event_queues(&mut store, access);
assert!(store.get::<crate::components::ScreenCommand>().is_some());
assert!(store.get::<crate::components::PlayCue>().is_none());
let mut lazy = EventStore::new();
ensure_event_queues(&mut lazy, Access::new().exclusive());
assert!(lazy.get::<crate::components::ScreenCommand>().is_none());
}
#[cfg(debug_assertions)]
mod hook {
use super::super::*;
use crate::blob::BlobData;
use crate::ecs::{Access, ComponentStorage, FrameContext, PipelineContext, Resources};
use crate::gfx::profile::FrameProfile;
use concinnity_core::ecs::Arena;
use concinnity_core::ecs::access_check::set_active;
struct Parts {
components: ComponentStorage,
blob: BlobData,
profile: FrameProfile,
resources: Resources,
scratch: Arena,
}
fn parts() -> Parts {
Parts {
components: ComponentStorage::default(),
blob: BlobData::empty(),
profile: FrameProfile::default(),
resources: Resources::default(),
scratch: Arena::with_capacity(0),
}
}
fn ctx(p: &mut Parts) -> PipelineContext<'_> {
PipelineContext {
components: &mut p.components,
blob: &mut p.blob,
profile: &mut p.profile,
resources: &mut p.resources,
frame: FrameContext::new(&p.scratch),
}
}
#[test]
#[should_panic(expected = "Sprite without declaring it")]
fn undeclared_component_read_panics() {
install_hook();
set_active(Some((
Access::new().writes_components(component_mask![crate::components::TextLabel]),
"TestSystem",
)));
let mut p = parts();
let _ = ctx(&mut p).query::<crate::components::Sprite>().count();
}
#[test]
#[should_panic(expected = "structural change")]
fn undeclared_structural_change_panics() {
install_hook();
set_active(Some((Access::new(), "TestSystem")));
let mut p = parts();
ctx(&mut p).push(crate::components::TextLabel::default());
}
#[test]
#[should_panic(expected = "not in the access-id registry")]
fn unregistered_resource_touch_panics() {
install_hook();
set_active(Some((Access::new(), "TestSystem")));
struct Unregistered;
let mut p = parts();
let _ = ctx(&mut p).resource::<Unregistered>();
}
#[test]
fn declared_touches_pass_and_exclusive_passes_everything() {
install_hook();
let mut p = parts();
set_active(Some((
Access::new()
.writes_components(component_mask![crate::components::TextLabel])
.reads_resources(resource_mask![crate::ecs::MenuActive]),
"TestSystem",
)));
{
let mut c = ctx(&mut p);
let _ = c.query::<crate::components::TextLabel>().count();
let _ = c.query_mut::<crate::components::TextLabel>().count();
let _ = c.resource::<crate::ecs::MenuActive>();
}
set_active(Some((Access::new().exclusive(), "TestSystem")));
{
let mut c = ctx(&mut p);
c.push(crate::components::TextLabel::default());
let _ = c.query::<crate::components::Sprite>().count();
}
set_active(None);
}
}
#[test]
fn masks_build_from_registered_types() {
let m = resource_mask![crate::ecs::MenuActive, crate::ecs::ScreenStack];
assert!(m.contains(id_of::<crate::ecs::MenuActive>().unwrap()));
assert!(!m.contains(id_of::<crate::ecs::FlyCam>().unwrap()));
let c = component_mask![crate::components::TextLabel];
assert!(!c.is_empty());
}
}