// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// ECS foundation (dependency-free), aligned with Bevy concepts:
/// - Entity uses (id, generation) to avoid ABA after despawn/reuse.
/// - SparseSet[T] provides a cache-friendly component storage.
///|
pub struct Entity {
id : Int
generation : Int
}
///|
pub fn Entity::new(id : Int, generation : Int) -> Entity {
Entity::{ id, generation }
}
///|
pub fn Entity::placeholder() -> Entity {
Entity::{ id: -1, generation: 0 }
}
///|
pub fn Entity::is_placeholder(self : Entity) -> Bool {
self.id < 0
}
///|
pub(all) suberror EntityError {
InvalidId(Int)
NotSpawned(Entity)
GenerationMismatch(Entity, Int)
}
///|
struct Entities {
generations : Array[Int]
alive : Array[Bool]
free : Array[Int]
}
///|
const FIRST_GENERATION : Int = 1
///|
pub fn Entities::new() -> Entities {
{ generations: [], alive: [], free: [] }
}
///|
pub fn Entities::len(self : Entities) -> Int {
self.generations.length()
}
///|
pub fn Entities::spawn(self : Entities) -> Entity {
match self.free.pop() {
Some(id) => {
self.alive[id] = true
Entity::{ id, generation: self.generations[id] }
}
None => {
let id = self.generations.length()
self.generations.push(FIRST_GENERATION)
self.alive.push(true)
Entity::{ id, generation: FIRST_GENERATION }
}
}
}
///|
pub fn Entities::is_alive(self : Entities, entity : Entity) -> Bool {
if entity.id < 0 {
return false
}
match (self.alive.get(entity.id), self.generations.get(entity.id)) {
(Some(is_alive), Some(gen)) => is_alive && gen == entity.generation
_ => false
}
}
///|
pub fn Entities::despawn(
self : Entities,
entity : Entity,
) -> Unit raise EntityError {
if entity.id < 0 {
raise InvalidId(entity.id)
}
guard self.alive.get(entity.id) is Some(is_alive) else {
raise NotSpawned(entity)
}
guard self.generations.get(entity.id) is Some(gen) else {
raise NotSpawned(entity)
}
if !is_alive {
raise NotSpawned(entity)
}
if gen != entity.generation {
raise GenerationMismatch(entity, gen)
}
self.alive[entity.id] = false
self.generations[entity.id] = gen + 1
self.free.push(entity.id)
}
///|
struct World {
entities : Entities
}
///|
pub fn World::new() -> World {
{ entities: Entities::new() }
}
///|
pub fn World::spawn_empty(self : World) -> Entity {
self.entities.spawn()
}
///|
pub fn World::despawn(self : World, entity : Entity) -> Unit raise EntityError {
self.entities.despawn(entity)
}
///|
pub fn World::is_alive(self : World, entity : Entity) -> Bool {
self.entities.is_alive(entity)
}
///|
/// Minimal trait for worlds that can spawn/despawn entities.
///
/// This is used by engine helpers that are generic over a generated World type,
/// without depending on a concrete `ecs_world` package (avoids cycles).
pub(open) trait SpawnWorld {
spawn_empty(self : Self) -> Entity
despawn(self : Self, entity : Entity) -> Unit raise EntityError
}
///|
struct SparseSet[T] {
sparse : Array[Int]
dense_entities : Array[Entity]
dense_values : Array[T]
}
///|
pub fn[T] SparseSet::new() -> SparseSet[T] {
{ sparse: [], dense_entities: [], dense_values: [] }
}
///|
pub fn[T] SparseSet::len(self : SparseSet[T]) -> Int {
self.dense_entities.length()
}
///|
pub fn[T] SparseSet::is_empty(self : SparseSet[T]) -> Bool {
self.len() == 0
}
///|
pub fn[T] SparseSet::contains(self : SparseSet[T], entity : Entity) -> Bool {
self.index_of(entity) is Some(_)
}
///|
fn[T] SparseSet::ensure_sparse_len(self : SparseSet[T], len : Int) -> Unit {
while self.sparse.length() < len {
self.sparse.push(-1)
}
}
///|
pub fn[T] SparseSet::index_of(self : SparseSet[T], entity : Entity) -> Int? {
if entity.id < 0 {
return None
}
match self.sparse.get(entity.id) {
Some(i) if i >= 0 && self.dense_entities.get(i) is Some(e) =>
if e.generation == entity.generation {
Some(i)
} else {
None
}
_ => None
}
}
///|
pub fn[T] SparseSet::get(self : SparseSet[T], entity : Entity) -> T? {
match self.index_of(entity) {
Some(i) => self.dense_values.get(i)
None => None
}
}
///|
pub fn[T] SparseSet::set(
self : SparseSet[T],
entity : Entity,
value : T,
) -> Bool {
match self.index_of(entity) {
Some(i) => {
self.dense_values[i] = value
true
}
None => false
}
}
///|
pub fn[T] SparseSet::insert(
self : SparseSet[T],
entity : Entity,
value : T,
) -> Unit {
if entity.id < 0 {
return
}
self.ensure_sparse_len(entity.id + 1)
match self.index_of(entity) {
Some(i) => self.dense_values[i] = value
None => {
let i = self.dense_entities.length()
self.dense_entities.push(entity)
self.dense_values.push(value)
self.sparse[entity.id] = i
}
}
}
///|
pub fn[T] SparseSet::remove(self : SparseSet[T], entity : Entity) -> T? {
match self.index_of(entity) {
None => None
Some(i) => {
let last = self.dense_entities.length() - 1
let removed_value = self.dense_values.get(i)
// Remove sparse mapping for this id.
self.sparse[entity.id] = -1
if i != last {
// Swap-remove to keep dense storage packed.
let moved_entity = self.dense_entities[last]
let moved_value = self.dense_values[last]
self.dense_entities[i] = moved_entity
self.dense_values[i] = moved_value
self.sparse[moved_entity.id] = i
}
self.dense_entities.pop() |> ignore
self.dense_values.pop() |> ignore
removed_value
}
}
}
///|
pub fn[T] SparseSet::entity_at(
self : SparseSet[T],
dense_index : Int,
) -> Entity {
self.dense_entities[dense_index]
}
///|
pub fn[T] SparseSet::value_at(self : SparseSet[T], dense_index : Int) -> T {
self.dense_values[dense_index]
}