use crate::abi::Side;
use crate::raw::{read_at, read_ptr_at, write_at};
use crate::{Entity, Host, Load, Off};
type IteratorInit = unsafe extern "system" fn(ctx: *mut u64, iterator: *mut u64, size: u64);
type IteratorNext = unsafe extern "system" fn(ctx: *mut u64, iterator: *mut u64, size: u64) -> u64;
type MarkChanged = unsafe extern "system" fn(
tracker: u64,
stamp_a: u64,
stamp_b: u64,
entity: u64,
kind: u16,
flags: u8,
);
#[derive(Clone, Copy, Debug)]
pub struct Ecs {
init: IteratorInit,
next: IteratorNext,
mark: MarkChanged,
context_row: usize,
entity_records: usize,
entity_record_size: usize,
}
unsafe fn function<F: crate::FnPtr>(address: u64) -> F {
let address = usize::try_from(address).unwrap_or(0);
unsafe { core::mem::transmute_copy(&address) }
}
impl Ecs {
pub fn resolve(load: &Load<'_>) -> Result<Self, Off> {
let size = |name| {
load.constant(name)
.map(|v| usize::try_from(v).unwrap_or(usize::MAX))
};
let (init, next, mark) = (
load.symbol("ecs.iterator_init")?,
load.symbol("ecs.iterator_next")?,
load.symbol("ecs.mark_changed")?,
);
let init = unsafe { function::<IteratorInit>(init) };
let next = unsafe { function::<IteratorNext>(next) };
let mark = unsafe { function::<MarkChanged>(mark) };
Ok(Self {
init,
next,
mark,
context_row: size("ecs.context_row")?,
entity_records: size("ecs.descriptor_entity_records")?,
entity_record_size: size("ecs.entity_record_size")?,
})
}
#[must_use]
pub unsafe fn query<const N: usize>(&self, ctx: *mut u64) -> Query<'_, N> {
let mut words = [0u64; N];
unsafe { (self.init)(ctx, words.as_mut_ptr(), (N * 8) as u64) };
Query {
ecs: self,
ctx,
words,
}
}
#[must_use]
pub unsafe fn row(&self, ctx: *mut u64) -> u32 {
unsafe { read_at(ctx.cast(), self.context_row) }
}
pub unsafe fn set_row(&self, ctx: *mut u64, row: u32) {
unsafe { write_at(ctx.cast(), self.context_row, row) };
}
#[must_use]
pub unsafe fn entity(&self, ctx: *mut u64) -> Option<Entity> {
let row = unsafe { self.row(ctx) };
if row == 0 {
return None;
}
let descriptor = unsafe { read_ptr_at::<u8>(ctx.cast(), 0) }.cast_const();
let records = unsafe { read_ptr_at::<u8>(descriptor, self.entity_records) }.cast_const();
let offset = (row as usize - 1) * self.entity_record_size;
Entity::from_raw(unsafe { read_at(records, offset) })
}
pub unsafe fn mark_changed(
&self,
tracker: u64,
stamp_a: u64,
stamp_b: u64,
entity: u64,
kind: u64,
) {
if tracker == 0 {
return;
}
#[expect(
clippy::cast_possible_truncation,
reason = "the engine packs the type and flags"
)]
let (type_index, flags) = (kind as u16, (kind >> 16) as u8);
unsafe { (self.mark)(tracker, stamp_a, stamp_b, entity, type_index, flags) };
}
}
#[derive(Debug)]
pub struct Query<'e, const N: usize> {
ecs: &'e Ecs,
ctx: *mut u64,
words: [u64; N],
}
impl<const N: usize> Query<'_, N> {
#[expect(
clippy::should_implement_trait,
reason = "rows borrow the query, so this is a lending walk"
)]
pub fn next(&mut self) -> Option<&[u64; N]> {
let more = unsafe { (self.ecs.next)(self.ctx, self.words.as_mut_ptr(), (N * 8) as u64) };
(more & 0xff != 0).then_some(&self.words)
}
}
type TypeIndex = unsafe extern "system" fn(map: *const u8, hash: *const u32) -> *const u16;
type ReadComponent = unsafe extern "system" fn(
out: *mut [usize; 2],
world: *const u8,
index: *const u16,
entity: u32,
);
type SlotRoot = unsafe extern "system" fn(
out: *mut [u64; 2],
slot: u64,
owner_index: u64,
ctx: *mut u64,
) -> *mut [u64; 2];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ComponentType(u32);
impl ComponentType {
pub fn resolve(load: &Load<'_>, name: &str) -> Result<Self, Off> {
Self::named(load.host(), name)
}
pub(crate) fn named(host: Host, name: &str) -> Result<Self, Off> {
let mut hash = 0;
let status = (host.api().type_hash)(crate::abi::Str::new(name), &raw mut hash);
crate::load::ok(status, Self(hash), || format!("component type {name}"))
}
#[cfg(feature = "unstable")]
pub(crate) const fn from_hash(hash: u32) -> Self {
Self(hash)
}
#[must_use]
pub const fn hash(self) -> u32 {
self.0
}
pub(crate) fn read(self, host: Host, entity: Entity) -> Option<*const core::ffi::c_void> {
let mut out: *const core::ffi::c_void = core::ptr::null();
let status = (host.api().component)(host.handle(), entity, self.0, &raw mut out);
(status == crate::abi::Status::OK && !out.is_null()).then_some(out)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Components {
type_index: TypeIndex,
read: ReadComponent,
slot_root: SlotRoot,
entity_world: usize,
registry_maps: [usize; 2],
}
impl Components {
pub fn resolve(load: &Load<'_>) -> Result<Self, Off> {
let size = |name| {
load.constant(name)
.map(|v| usize::try_from(v).unwrap_or(usize::MAX))
};
let (type_index, read, slot_root) = (
load.symbol("ecs.type_index")?,
load.symbol("ecs.component_read_raw")?,
load.symbol("ecs.slot_root")?,
);
let type_index = unsafe { function::<TypeIndex>(type_index) };
let read = unsafe { function::<ReadComponent>(read) };
let slot_root = unsafe { function::<SlotRoot>(slot_root) };
Ok(Self {
type_index,
read,
slot_root,
entity_world: size("ecs.context_entity_world")?,
registry_maps: [
size("ecs.registry_type_map")?,
size("ecs.registry_type_map_fallback")?,
],
})
}
#[must_use]
pub unsafe fn component(
&self,
ctx: *mut u64,
entity: Entity,
ty: ComponentType,
) -> Option<*mut u8> {
if ctx.is_null() || entity.is_none() {
return None;
}
let hash = ty.0;
let descriptor = unsafe { read_ptr_at::<u8>(ctx.cast(), 0) }.cast_const();
let world = unsafe { read_ptr_at::<u8>(descriptor, self.entity_world) }.cast_const();
if world.is_null() {
return None;
}
let registry = unsafe { read_ptr_at::<u8>(world, 0) }.cast_const();
let index = self.registry_maps.iter().find_map(|&map| {
let index = unsafe { (self.type_index)(registry.wrapping_add(map), &raw const hash) };
(!index.is_null()).then_some(index)
})?;
let mut out = [0usize; 2];
unsafe { (self.read)(&raw mut out, world, index, entity.0) };
(out[0] != 0).then(|| core::ptr::with_exposed_provenance_mut(out[0]))
}
#[must_use]
pub unsafe fn slot_root(&self, ctx: *mut u64, slot: u64, owner_index: u64) -> Option<Entity> {
let mut out = [0u64; 2];
unsafe { (self.slot_root)(&raw mut out, slot, owner_index, ctx) };
#[expect(
clippy::cast_possible_truncation,
reason = "the error is the low byte, the root a u32"
)]
let (error, root) = (out[0] as u8, out[1] as u32);
(error == 0 && root != 0).then_some(Entity(root))
}
}
#[must_use]
pub fn server_role(process: Side, context_world: u64, server_world: u64) -> bool {
process == Side::SERVER || (server_world != 0 && context_world == server_world)
}
#[derive(Clone, Copy, Debug)]
pub struct Worlds {
host: Host,
side: Side,
entity_world: usize,
}
impl Worlds {
pub fn resolve(load: &Load<'_>) -> Result<Self, Off> {
let host = load.host();
let entity_world = load.constant("ecs.context_entity_world")?;
Ok(Self {
host,
side: host.build().side,
entity_world: usize::try_from(entity_world).unwrap_or(usize::MAX),
})
}
#[must_use]
pub fn hosting(&self) -> bool {
self.side == Side::SERVER || self.host.hosting()
}
#[must_use]
pub unsafe fn is_server(&self, ctx: *mut u64) -> bool {
if self.side == Side::SERVER {
return true;
}
let server_world = self.host.server_world();
if server_world == 0 || ctx.is_null() {
return false;
}
let descriptor = unsafe { read_ptr_at::<u8>(ctx.cast(), 0) }.cast_const();
if descriptor.is_null() {
return false;
}
let world: u64 = unsafe { read_at(descriptor, self.entity_world) };
server_role(self.side, world, server_world)
}
}
pub const FIXED_PER_METRE: f64 = 4_294_967_296.0;
#[must_use]
#[expect(clippy::cast_precision_loss, reason = "world coordinates fit an f64")]
pub fn metres(fixed: [i64; 3]) -> [f64; 3] {
fixed.map(|v| v as f64 / FIXED_PER_METRE)
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "a float-to-int cast saturates, as documented"
)]
pub fn fixed(metres: [f64; 3]) -> [i64; 3] {
metres.map(|m| (m * FIXED_PER_METRE) as i64)
}
#[must_use]
pub unsafe fn transform_position(transform: *const u8) -> [f64; 3] {
metres(unsafe { read_at(transform, 0) })
}
type AttributeFind = unsafe extern "system" fn(out: *mut u64, attribute: u32, registry: u64) -> u64;
#[derive(Clone, Copy, Debug)]
pub struct Attributes {
find: AttributeFind,
flags: usize,
data_offset: usize,
slot: usize,
}
impl Attributes {
pub fn resolve(load: &Load<'_>) -> Result<Self, Off> {
let size = |name| {
load.constant(name)
.map(|v| usize::try_from(v).unwrap_or(usize::MAX))
};
let find = load.symbol("ecs.attribute_find")?;
Ok(Self {
find: unsafe { function(find) },
flags: size("ecs.level_flags")?,
data_offset: size("ecs.level_data_offset")?,
slot: size("ecs.attribute_slot")?,
})
}
#[must_use]
pub unsafe fn value(&self, registry: u64, set: *const u8, attribute: u32) -> Option<u32> {
let word = unsafe { self.word(registry, set, attribute) }?;
Some(unsafe { read_at(word, 0) })
}
pub unsafe fn set(&self, registry: u64, set: *mut u8, attribute: u32, value: u32) -> bool {
let Some(word) = (unsafe { self.word(registry, set, attribute) }) else {
return false;
};
unsafe { write_at(word.cast_mut(), 0, value) };
true
}
unsafe fn word(&self, registry: u64, set: *const u8, attribute: u32) -> Option<*const u8> {
if set.is_null() {
return None;
}
let flags: u8 = unsafe { read_at(set, self.flags) };
if flags & 1 == 0 {
return None;
}
let mut found = [0u64; 2];
unsafe { (self.find)(found.as_mut_ptr(), attribute, registry) };
let definition =
core::ptr::with_exposed_provenance::<u8>(usize::try_from(found[1]).unwrap_or(0));
if found[0] & 0xff != 0 || definition.is_null() {
return None;
}
let definition_set: u32 = unsafe { read_at(definition, 0) };
let set_id: u32 = unsafe { read_at(set, 0) };
if definition_set != set_id {
return None;
}
let slot = usize::from(unsafe { read_at::<u16>(definition, self.slot) });
let data = usize::from(unsafe { read_at::<u16>(set, self.data_offset) });
Some(set.wrapping_add(data + slot * 4))
}
}