Compare commits

...

5 commits

Author SHA1 Message Date
Ukendio
af093713b4 Add examples
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-08-30 16:12:14 +02:00
Ukendio
549fe97622 Inline world_remove 2025-08-30 14:43:52 +02:00
Ukendio
b0e73857b9 Cleanup tests 2025-08-30 14:43:34 +02:00
Ukendio
917c951d55 Remove eagerly 2025-08-30 13:47:08 +02:00
Ukendio
037035a9a1 Revert :clear to previous behaviour 2025-08-29 17:01:36 +02:00
11 changed files with 609 additions and 420 deletions

View file

@ -199,6 +199,7 @@ do
end end
local q = world:query(A, B, C, D) local q = world:query(A, B, C, D)
q:archetypes()
START() START()
for id in q do for id in q do
end end

View file

@ -11,15 +11,22 @@ end
local jecs = require("@jecs") local jecs = require("@jecs")
local mirror = require("@mirror") local mirror = require("@mirror")
type i53 = number
do do
TITLE(testkit.color.white_underline("Jecs query")) TITLE(testkit.color.white_underline("Jecs query"))
local ecs = jecs.world() local ecs = jecs.world() :: jecs.World
do do
TITLE("one component in common") TITLE("one component in common")
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) local function view_bench(world: jecs.World,
A: jecs.Id,
B: jecs.Id,
C: jecs.Id,
D: jecs.Id,
E: jecs.Id,
F: jecs.Id,
G: jecs.Id,
H: jecs.Id
)
BENCH("1 component", function() BENCH("1 component", function()
for _ in world:query(A) do for _ in world:query(A) do
end end
@ -131,11 +138,21 @@ end
do do
TITLE(testkit.color.white_underline("Mirror query")) TITLE(testkit.color.white_underline("Mirror query"))
local ecs = mirror.World.new() local ecs = mirror.World.new() :: jecs.World
do do
TITLE("one component in common") TITLE("one component in common")
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) local function view_bench(
world: mirror.World,
A: jecs.Id,
B: jecs.Id,
C: jecs.Id,
D: jecs.Id,
E: jecs.Id,
F: jecs.Id,
G: jecs.Id,
H: jecs.Id
)
BENCH("1 component", function() BENCH("1 component", function()
for _ in world:query(A) do for _ in world:query(A) do
end end

View file

@ -1,15 +1,19 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local world = jecs.World.new() local world = jecs.world()
local Position = world:component() local Position = world:component() :: jecs.Id<vector>
local Walking = world:component() local Walking = world:component()
local Name = world:component() local Name = world:component() :: jecs.Id<string>
local function name(e: jecs.Entity): string
return assert(world:get(e, Name))
end
-- Create an entity with name Bob -- Create an entity with name Bob
local bob = world:entity() local bob = world:entity()
-- The set operation finds or creates a component, and sets it. -- The set operation finds or creates a component, and sets it.
world:set(bob, Position, Vector3.new(10, 20, 30)) world:set(bob, Position, vector.create(10, 20, 30))
-- Name the entity Bob -- Name the entity Bob
world:set(bob, Name, "Bob") world:set(bob, Name, "Bob")
-- The add operation adds a component without setting a value. This is -- The add operation adds a component without setting a value. This is
@ -18,15 +22,16 @@ world:add(bob, Walking)
-- Get the value for the Position component -- Get the value for the Position component
local pos = world:get(bob, Position) local pos = world:get(bob, Position)
print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`) assert(pos)
print(`\{{pos.x}, {pos.y}, {pos.z}\}`)
-- Overwrite the value of the Position component -- Overwrite the value of the Position component
world:set(bob, Position, Vector3.new(40, 50, 60)) world:set(bob, Position, vector.create(40, 50, 60))
local alice = world:entity() local alice = world:entity()
-- Create another named entity -- Create another named entity
world:set(alice, Name, "Alice") world:set(alice, Name, "Alice")
world:set(alice, Position, Vector3.new(10, 20, 30)) world:set(alice, Position, vector.create(10, 20, 30))
world:add(alice, Walking) world:add(alice, Walking)
-- Remove tag -- Remove tag
@ -34,7 +39,7 @@ world:remove(alice, Walking)
-- Iterate all entities with Position -- Iterate all entities with Position
for entity, p in world:query(Position) do for entity, p in world:query(Position) do
print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`) print(`{name(entity)}: \{{p.x}, {p.y}, {p.z}\}`)
end end
-- Output: -- Output:

View file

@ -0,0 +1,60 @@
-- Using world:target is the recommended way to grab the target in a wildcard
-- query. However the random access can add up in very hot paths. Accessing its
-- column can drastically improve performance, especially when there are
-- multiple adjacent pairs.
local jecs = require("@jecs")
local pair = jecs.pair
local __ = jecs.Wildcard
local world = jecs.world()
local Likes = world:entity()
local function name(e, name: string): string
if name then
world:set(e, jecs.Name, name)
return name
end
return assert(world:get(e, jecs.Name))
end
local e1 = world:entity()
name(e1, "e1")
local e2 = world:entity()
name(e2, "e2")
local e3 = world:entity()
name(e3, "e3")
world:add(e1, pair(Likes, e2))
world:add(e1, pair(Likes, e3))
local likes = jecs.component_record(world, pair(Likes, __))
assert(likes)
local likes_cr = likes.records
local likes_counts = likes.counts
local archetypes = world:query(pair(Likes, __)):archetypes()
for _, archetype in archetypes do
local types = archetype.types
-- Get the starting index which is what the (R, *) alias is at
local wc = likes_cr[archetype.id]
local count = likes_counts[archetype.id]
local entities = archetype.entities
for i = #entities, 1, -1 do
-- It is generally a good idea to iterate backwards on arrays if you
-- need to delete the iterated entity to prevent iterator invalidation
local entity = entities[i]
for cr = wc, wc + count - 1 do
local person = jecs.pair_second(world, types[cr])
print(`entity ${entity} likes ${person}`)
end
end
end
-- Output:
-- entity $273 likes $275
-- entity $273 likes $274

View file

@ -0,0 +1,44 @@
-- To get the most out of performance, you can lift the inner loop of queries to
-- the system in which you can do archetype-specific optimizations like finding
-- the parent once per archetype rather than per entity.
local jecs = require("@jecs")
local pair = jecs.pair
local ChildOf = jecs.ChildOf
local __ = jecs.Wildcard
local world = jecs.world()
local Position = world:component() :: jecs.Id<vector>
local Visible = world:entity()
local parent = world:entity()
world:set(parent, Position, vector.zero)
world:add(parent, Visible)
local child = world:entity()
world:set(child, Position, vector.one)
world:add(child, pair(ChildOf, parent))
local parents = jecs.component_record(world, pair(ChildOf, __))
assert(parents)
local parent_cr = parents.records
local archetypes = world:query(Position, pair(ChildOf, __)):archetypes()
for _, archetype in archetypes do
local types = archetype.types
local p = jecs.pair_second(world, types[parent_cr[archetype.id]])
if world:has(p, Visible) then
local columns = archetype.columns_map
local positions = columns[Position]
for row, entity in archetype.entities do
local pos = positions[row]
print(`Child ${entity} of ${p} is visible at {pos}`)
end
end
end
-- Output:
-- Child $274 of $273 is visibile at 1,1,1

View file

@ -1,5 +1,5 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local world = jecs.World.new() local world = jecs.world()
local Position = world:component() local Position = world:component()
local Velocity = world:component() local Velocity = world:component()

View file

@ -1,19 +1,16 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local pair = jecs.pair local pair = jecs.pair
local world = jecs.World.new() local world = jecs.world()
local Name = world:component() local Name = world:component() :: jecs.Id<string>
local function named(ctr, name)
local e = ctr(world) local function name(e: jecs.Entity): string
world:set(e, Name, name) return assert(world:get(e, Name))
return e
end
local function name(e)
return world:get(e, Name)
end end
local Position = named(world.component, "Position") :: jecs.Entity<vector> local Position = world:component() :: jecs.Id<vector>
world:set(Position, Name, "Position")
local Previous = jecs.Rest local Previous = jecs.Rest
local added = world local added = world
@ -29,10 +26,14 @@ local removed = world
:cached() :cached()
local e1 = named(world.entity, "e1") local e1 = world:entity()
world:set(e1, Name, "e1")
world:set(e1, Position, vector.create(10, 20, 30)) world:set(e1, Position, vector.create(10, 20, 30))
local e2 = named(world.entity, "e2")
local e2 = world:entity()
world:set(e2, Name, "e2")
world:set(e2, Position, vector.create(10, 20, 30)) world:set(e2, Position, vector.create(10, 20, 30))
for entity, p in added do for entity, p in added do
print(`Added {name(entity)}: \{{p.x}, {p.y}, {p.z}}`) print(`Added {name(entity)}: \{{p.x}, {p.y}, {p.z}}`)
world:set(entity, pair(Previous, Position), p) world:set(entity, pair(Previous, Position), p)
@ -40,10 +41,10 @@ end
world:set(e1, Position, vector.create(999, 999, 1998)) world:set(e1, Position, vector.create(999, 999, 1998))
for _, archetype in changed:archetypes() do for entity, new, old in changed do
if new ~= old then if new ~= old then
print(`{name(e)}'s Position changed from \{{old.x}, {old.y}, {old.z}\} to \{{new.x}, {new.y}, {new.z}\}`) print(`{name(entity)}'s Position changed from \{{old.x}, {old.y}, {old.z}\} to \{{new.x}, {new.y}, {new.z}\}`)
world:set(e, pair(Previous, Position), new) world:set(entity, pair(Previous, Position), new)
end end
end end

View file

@ -3,32 +3,47 @@ local pair = jecs.pair
local ChildOf = jecs.ChildOf local ChildOf = jecs.ChildOf
local __ = jecs.Wildcard local __ = jecs.Wildcard
local Name = jecs.Name local Name = jecs.Name
local world = jecs.World.new() local world = jecs.world()
type Id<T = nil> = number & { __T: T } local Voxel = world:component() :: jecs.Id
local Voxel = world:component() :: Id local Position = world:component() :: jecs.Id<vector>
local Position = world:component() :: Id<Vector3> local Perception = world:component() :: jecs.Id<{
local Perception = world:component() :: Id<{
range: number, range: number,
fov: number, fov: number,
dir: Vector3, dir: vector,
}> }>
local PrimaryPart = world:component() :: Id<Part> type part = {
Position: vector
}
local PrimaryPart = world:component() :: jecs.Id<part>
local local_player = game:GetService("Players").LocalPlayer local local_player = {
Character = {
PrimaryPart = {
Position = vector.create(50, 0, 30)
}
}
}
local workspace = {
CurrentCamera = {
CFrame = {
LookVector = vector.create(0, 0, -1)
}
}
}
local function distance(a: Vector3, b: Vector3) local function distance(a: vector, b: vector)
return (b - a).Magnitude return vector.magnitude((b - a))
end end
local function is_in_fov(a: Vector3, b: Vector3, forward_dir: Vector3, fov_angle: number) local function is_in_fov(a: vector, b: vector, forward_dir: vector, fov_angle: number)
local to_target = b - a local to_target = b - a
local forward_xz = Vector3.new(forward_dir.X, 0, forward_dir.Z).Unit local forward_xz = vector.normalize(vector.create(forward_dir.x, 0, forward_dir.z))
local to_target_xz = Vector3.new(to_target.X, 0, to_target.Z).Unit local to_target_xz = vector.normalize(vector.create(to_target.x, 0, to_target.z))
local angle_to_target = math.deg(math.atan2(to_target_xz.Z, to_target_xz.X)) local angle_to_target = math.deg(math.atan2(to_target_xz.z, to_target_xz.x))
local forward_angle = math.deg(math.atan2(forward_xz.Z, forward_xz.X)) local forward_angle = math.deg(math.atan2(forward_xz.z, forward_xz.z))
local angle_difference = math.abs(forward_angle - angle_to_target) local angle_difference = math.abs(forward_angle - angle_to_target)
@ -42,7 +57,7 @@ end
local map = {} local map = {}
local grid = 50 local grid = 50
local function add_to_voxel(source: number, position: Vector3, prev_voxel_id: number?) local function add_to_voxel(source: jecs.Entity, position: vector, prev_voxel_id: jecs.Entity?)
local hash = position // grid local hash = position // grid
local voxel_id = map[hash] local voxel_id = map[hash]
if not voxel_id then if not voxel_id then
@ -79,7 +94,7 @@ local function update_camera_direction(dt: number)
end end
local function perceive_enemies(dt: number) local function perceive_enemies(dt: number)
local it = world:query(Perception, Position, PrimaryPart) local it = world:query(Perception, Position, PrimaryPart):iter()
-- There is only going to be one entity matching the query -- There is only going to be one entity matching the query
local e, self_perception, self_position, self_primary_part = it() local e, self_perception, self_position, self_primary_part = it()
@ -93,28 +108,28 @@ local function perceive_enemies(dt: number)
if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then
local p = target_position local p = target_position
print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.X}, {p.Y}, {p.Z})`) print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.x}, {p.y}, {p.z})`)
end end
end end
end end
local player = world:entity() local player = world:entity()
world:set(player, Perception, { world:set(player, Perception, {
range = 100, range = 200,
fov = 90, fov = 90,
dir = Vector3.new(1, 0, 0), dir = vector.create(1, 0, 0),
}) })
world:set(player, Name, "LocalPlayer") world:set(player, Name, "LocalPlayer")
local primary_part = (local_player.Character :: Model).PrimaryPart :: Part local primary_part = local_player.Character.PrimaryPart
world:set(player, PrimaryPart, primary_part) world:set(player, PrimaryPart, primary_part)
world:set(player, Position, Vector3.zero) world:set(player, Position, vector.zero)
local enemy = world:entity() local enemy = world:entity()
world:set(enemy, Name, "Enemy $1") world:set(enemy, Name, "Enemy $1")
world:set(enemy, Position, Vector3.new(50, 0, 20)) world:set(enemy, Position, vector.create(50, 0, 20))
add_to_voxel(player, primary_part.Position) add_to_voxel(player, primary_part.Position)
add_to_voxel(enemy, world) add_to_voxel(enemy, assert(world:get(enemy, Position)))
local dt = 1 / 60 local dt = 1 / 60
reconcile_client_owned_assembly_to_voxel(dt) reconcile_client_owned_assembly_to_voxel(dt)

5
jecs.d.ts vendored
View file

@ -184,6 +184,11 @@ export class World {
*/ */
cleanup(): void; cleanup(): void;
/**
* Removes all instances of specified component
*/
// purge<T>(component: Id<T>): void
/** /**
* Clears all components and relationships from the given entity, but * Clears all components and relationships from the given entity, but
* does not delete the entity from the world. * does not delete the entity from the world.

468
jecs.luau
View file

@ -54,6 +54,8 @@ export type Query<T...> = typeof(setmetatable(
archetypes: (self: Query<T...>) -> { Archetype }, archetypes: (self: Query<T...>) -> { Archetype },
cached: (self: Query<T...>) -> Query<T...>, cached: (self: Query<T...>) -> Query<T...>,
ids: { Id<any> }, ids: { Id<any> },
filter_with: { Id<any> }?,
filter_without: { Id<any> }?
-- world: World -- world: World
}, },
{} :: { {} :: {
@ -92,13 +94,14 @@ type archetype = {
} }
type componentrecord = { type componentrecord = {
component: i53,
records: { [number]: number }, records: { [number]: number },
counts: { [i53]: number }, counts: { [i53]: number },
flags: number, flags: number,
size: number, size: number,
on_add: ((entity: i53, id: i53, value: any?) -> ())?, on_add: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
on_change: ((entity: i53, id: i53, value: any) -> ())?, on_change: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
on_remove: ((entity: i53, id: i53) -> ())?, on_remove: ((entity: i53, id: i53) -> ())?,
wildcard_pairs: { [number]: componentrecord }, wildcard_pairs: { [number]: componentrecord },
@ -137,7 +140,7 @@ type world = {
add: (self: world, id: i53, component: i53) -> (), add: (self: world, id: i53, component: i53) -> (),
set: (self: world, id: i53, component: i53, data: any) -> (), set: (self: world, id: i53, component: i53, data: any) -> (),
cleanup: (self: world) -> (), cleanup: (self: world) -> (),
clear: (self: world, id: i53) -> (), clear: (self: world, entity: i53) -> (),
remove: (self: world, id: i53, component: i53) -> (), remove: (self: world, id: i53, component: i53) -> (),
get: (world, ...i53) -> (), get: (world, ...i53) -> (),
has: (world, ...i53) -> boolean, has: (world, ...i53) -> boolean,
@ -166,9 +169,9 @@ export type World = {
observable: Map<Id, Map<Id, { Observer }>>, observable: Map<Id, Map<Id, { Observer }>>,
added: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T) -> ()) -> () -> (), added: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T, oldarchetype: Archetype) -> ()) -> () -> (),
removed: <T>(World, Entity<T>, (e: Entity, id: Id<T>) -> ()) -> () -> (), removed: <T>(World, Entity<T>, (e: Entity, id: Id<T>) -> ()) -> () -> (),
changed: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T) -> ()) -> () -> (), changed: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T, oldarchetype: Archetype) -> ()) -> () -> (),
--- Enforce a check on entities to be created within desired range --- Enforce a check on entities to be created within desired range
range: (self: World, range_begin: number, range_end: number?) -> (), range: (self: World, range_begin: number, range_end: number?) -> (),
@ -190,8 +193,9 @@ export type World = {
set: <T, a>(self: World, id: Entity<T>, component: Id<a>, data: a) -> (), set: <T, a>(self: World, id: Entity<T>, component: Id<a>, data: a) -> (),
cleanup: (self: World) -> (), cleanup: (self: World) -> (),
-- Clears an entity from the world
clear: <a>(self: World, id: Id<a>) -> (), -- Removes all components from the entity
clear: (self: World, entity: Entity) -> (),
--- Removes a component from the given entity --- Removes a component from the given entity
remove: <T, a>(self: World, id: Entity<T>, component: Id<a>) -> (), remove: <T, a>(self: World, id: Entity<T>, component: Id<a>) -> (),
--- Retrieves the value of up to 4 components. These values may be nil. --- Retrieves the value of up to 4 components. These values may be nil.
@ -261,8 +265,8 @@ export type ComponentRecord = {
flags: number, flags: number,
size: number, size: number,
on_add: (<T>(entity: Entity, id: Entity<T>, value: T?) -> ())?, on_add: (<T>(entity: Entity, id: Entity<T>, value: T, oldarchetype: Archetype) -> ())?,
on_change: (<T>(entity: Entity, id: Entity<T>, value: T) -> ())?, on_change: (<T>(entity: Entity, id: Entity<T>, value: T, oldArchetype: Archetype) -> ())?,
on_remove: ((entity: Entity, id: Entity) -> ())?, on_remove: ((entity: Entity, id: Entity) -> ())?,
} }
export type ComponentIndex = Map<Id, ComponentRecord> export type ComponentIndex = Map<Id, ComponentRecord>
@ -484,7 +488,7 @@ end
local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range" local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range"
local function entity_index_new_id(entity_index: entityindex): i53 local function ENTITY_INDEX_NEW_ID(entity_index: entityindex): i53
local dense_array = entity_index.dense_array local dense_array = entity_index.dense_array
local alive_count = entity_index.alive_count local alive_count = entity_index.alive_count
local sparse_array = entity_index.sparse_array local sparse_array = entity_index.sparse_array
@ -669,7 +673,7 @@ local function fetch(id: i53, columns_map: { [i53]: Column }, row: number): any
return column[row] return column[row]
end end
local function world_get(world: world, entity: i53, local function WORLD_GET(world: world, entity: i53,
a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any
local record = entity_index_try_get(world.entity_index, entity) local record = entity_index_try_get(world.entity_index, entity)
if not record then if not record then
@ -699,7 +703,7 @@ local function world_get(world: world, entity: i53,
end end
end end
local function world_has_one_inline(world: world, entity: i53, id: i53): boolean local function WORLD_HAS(world: world, entity: i53, id: i53): boolean
local record = entity_index_try_get(world.entity_index, entity) local record = entity_index_try_get(world.entity_index, entity)
if not record then if not record then
return false return false
@ -713,7 +717,7 @@ local function world_has_one_inline(world: world, entity: i53, id: i53): boolean
return archetype.columns_map[id] ~= nil return archetype.columns_map[id] ~= nil
end end
local function world_target(world: world, entity: i53, relation: i53, index: number?): i53? local function WORLD_TARGET(world: world, entity: i53, relation: i53, index: number?): i53?
local entity_index = world.entity_index local entity_index = world.entity_index
local record = entity_index_try_get(entity_index, entity) local record = entity_index_try_get(entity_index, entity)
if not record then if not record then
@ -771,15 +775,12 @@ local function id_record_get(world: World, id: Entity): ComponentRecord?
return nil return nil
end end
local function id_record_ensure(world: world, id: i53): componentrecord local function id_record_create(
local component_index = world.component_index world: world,
component_index: Map<i53, componentrecord>,
id: i53
): componentrecord
local entity_index = world.entity_index local entity_index = world.entity_index
local idr: componentrecord? = component_index[id]
if idr then
return idr
end
local flags = ECS_ID_MASK local flags = ECS_ID_MASK
local relation = id local relation = id
local target = 0 local target = 0
@ -796,31 +797,31 @@ local function id_record_ensure(world: world, id: i53): componentrecord
ecs_assert(target and entity_index_is_alive( ecs_assert(target and entity_index_is_alive(
entity_index, target), ECS_INTERNAL_ERROR) entity_index, target), ECS_INTERNAL_ERROR)
local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) local cleanup_policy_target = WORLD_TARGET(world, relation, EcsOnDeleteTarget, 0)
if cleanup_policy_target == EcsDelete then if cleanup_policy_target == EcsDelete then
has_delete = true has_delete = true
end end
if world_has_one_inline(world, relation, EcsExclusive) then if WORLD_HAS(world, relation, EcsExclusive) then
is_exclusive = true is_exclusive = true
end end
end end
local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) local cleanup_policy = WORLD_TARGET(world, relation, EcsOnDelete, 0)
if cleanup_policy == EcsDelete then if cleanup_policy == EcsDelete then
has_delete = true has_delete = true
end end
local on_add, on_change, on_remove = world_get(world, local on_add, on_change, on_remove = WORLD_GET(world,
relation, EcsOnAdd, EcsOnChange, EcsOnRemove) relation, EcsOnAdd, EcsOnChange, EcsOnRemove)
local is_tag = not world_has_one_inline(world, local is_tag = not WORLD_HAS(world,
relation, EcsComponent) relation, EcsComponent)
if is_tag and is_pair then if is_tag and is_pair then
is_tag = not world_has_one_inline(world, target, EcsComponent) is_tag = not WORLD_HAS(world, target, EcsComponent)
end end
flags = bit32.bor( flags = bit32.bor(
@ -830,7 +831,7 @@ local function id_record_ensure(world: world, id: i53): componentrecord
if is_exclusive then ECS_ID_IS_EXCLUSIVE else 0 if is_exclusive then ECS_ID_IS_EXCLUSIVE else 0
) )
idr = { local idr = {
size = 0, size = 0,
records = {}, records = {},
counts = {}, counts = {},
@ -846,6 +847,17 @@ local function id_record_ensure(world: world, id: i53): componentrecord
return idr return idr
end end
local function id_record_ensure(world: world, id: i53): componentrecord
local component_index = world.component_index
local idr: componentrecord? = component_index[id]
if idr then
return idr
end
return id_record_create(world, component_index, id)
end
local function archetype_append_to_records( local function archetype_append_to_records(
idr: componentrecord, idr: componentrecord,
archetype_id: number, archetype_id: number,
@ -915,14 +927,6 @@ local function archetype_create(world: world, id_types: { i53 }, ty, prev: i53?)
idr_t.size += 1 idr_t.size += 1
archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column) archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column)
-- Hypothetically this should only capture leaf component records
local idr_t_wc_pairs = idr_t.wildcard_pairs
if not idr_t_wc_pairs then
idr_t_wc_pairs = {} :: {[i53]: componentrecord }
idr_t.wildcard_pairs = idr_t_wc_pairs
end
idr_t_wc_pairs[component_id] = idr
end end
end end
@ -2049,6 +2053,7 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
local dst_types = ids local dst_types = ids
local to = archetype_ensure(world, dst_types) local to = archetype_ensure(world, dst_types)
new_entity(entity, r, to) new_entity(entity, r, to)
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
for i, id in ids do for i, id in ids do
local value = values[i] local value = values[i]
local cdr = component_index[id] local cdr = component_index[id]
@ -2057,11 +2062,11 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
if value then if value then
r.archetype.columns_map[id][r.row] = value r.archetype.columns_map[id][r.row] = value
if on_add then if on_add then
on_add(entity, id, value :: any) on_add(entity, id, value, ROOT_ARCHETYPE)
end end
else else
if on_add then if on_add then
on_add(entity, id) on_add(entity, id, nil, ROOT_ARCHETYPE)
end end
end end
end end
@ -2103,10 +2108,10 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
local on_change = idr.on_change local on_change = idr.on_change
local hook = if set then on_change else on_add local hook = if set then on_change else on_add
if hook then if hook then
hook(entity, id, value :: any) hook(entity, id, value :: any, from)
end end
elseif on_add then elseif on_add then
on_add(entity, id) on_add(entity, id, nil, from)
end end
end end
end end
@ -2208,11 +2213,34 @@ local function world_new()
} :: world } :: world
local function entity_index_new_id(entity_index: entityindex): i53
local alive_count = entity_index.alive_count
local max_id = entity_index.max_id
if alive_count < max_id then
alive_count += 1
entity_index.alive_count = alive_count
local id = eindex_dense_array[alive_count]
return id
end
local id = max_id + 1
local range_end = entity_index.range_end
ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY)
entity_index.max_id = id
alive_count += 1
entity_index.alive_count = alive_count
eindex_dense_array[alive_count] = id
eindex_sparse_array[id] = { dense = alive_count } :: record
return id
end
local ROOT_ARCHETYPE = archetype_create(world, {}, "") local ROOT_ARCHETYPE = archetype_create(world, {}, "")
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
local function inner_entity_index_try_get_any(entity: i53): record? local function entity_index_try_get_any(entity: i53): record?
local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)]
return r return r
end end
@ -2274,7 +2302,6 @@ local function world_new()
end end
local function inner_entity_move( local function inner_entity_move(
entity_index: entityindex,
entity: i53, entity: i53,
record: record, record: record,
to: archetype to: archetype
@ -2287,8 +2314,8 @@ local function world_new()
record.row = dst_row record.row = dst_row
end end
-- local function inner_entity_index_try_get(entity: number): Record? -- local function entity_index_try_get(entity: number): Record?
-- local r = inner_entity_index_try_get_any(entity) -- local r = entity_index_try_get_any(entity)
-- if r then -- if r then
-- local r_dense = r.dense -- local r_dense = r.dense
-- if r_dense > entity_index.alive_count then -- if r_dense > entity_index.alive_count then
@ -2301,7 +2328,7 @@ local function world_new()
-- return r -- return r
-- end -- end
local function inner_entity_index_try_get_unsafe(entity: i53): record? local function entity_index_try_get_unsafe(entity: i53): record?
local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)]
if r then if r then
local r_dense = r.dense local r_dense = r.dense
@ -2331,8 +2358,8 @@ local function world_new()
return to return to
end end
local function inner_world_set(world: world, entity: i53, id: i53, data): () local function world_set(world: world, entity: i53, id: i53, data): ()
local record = inner_entity_index_try_get_unsafe(entity) local record = entity_index_try_get_unsafe(entity)
if not record then if not record then
return return
end end
@ -2348,7 +2375,7 @@ local function world_new()
-- and just set the data directly. -- and just set the data directly.
local on_change = idr.on_change local on_change = idr.on_change
if on_change then if on_change then
on_change(entity, id, data) on_change(entity, id, data, src)
end end
else else
local to: archetype local to: archetype
@ -2418,7 +2445,7 @@ local function world_new()
if from then if from then
-- If there was a previous archetype, then the entity needs to move the archetype -- If there was a previous archetype, then the entity needs to move the archetype
inner_entity_move(entity_index, entity, record, to) inner_entity_move(entity, record, to)
else else
new_entity(entity, record, to) new_entity(entity, record, to)
end end
@ -2428,18 +2455,17 @@ local function world_new()
local on_add = idr.on_add local on_add = idr.on_add
if on_add then if on_add then
on_add(entity, id, data) on_add(entity, id, data, src)
end end
end end
end end
local function inner_world_add( local function world_add(
world: world, world: world,
entity: i53, entity: i53,
id: i53 id: i53
): () ): ()
local entity_index = world.entity_index local record = entity_index_try_get_unsafe(entity :: number)
local record = inner_entity_index_try_get_unsafe(entity :: number)
if not record then if not record then
return return
end end
@ -2517,7 +2543,7 @@ local function world_new()
end end
if from then if from then
inner_entity_move(entity_index, entity, record, to) inner_entity_move(entity, record, to)
else else
if #to.types > 0 then if #to.types > 0 then
new_entity(entity, record, to) new_entity(entity, record, to)
@ -2527,13 +2553,13 @@ local function world_new()
local on_add = idr.on_add local on_add = idr.on_add
if on_add then if on_add then
on_add(entity, id) on_add(entity, id, nil, src)
end end
end end
local function inner_world_get(world: world, entity: i53, local function world_get(world: world, entity: i53,
a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any
local record = inner_entity_index_try_get_unsafe(entity) local record = entity_index_try_get_unsafe(entity)
if not record then if not record then
return nil return nil
end end
@ -2561,7 +2587,7 @@ local function world_new()
end end
end end
type Listener<T> = (e: i53, id: i53, value: T?) -> () type Listener<T> = (e: i53, id: i53, value: T, oldarchetype: archetype) -> ()
world.added = function<T>(_: world, component: i53, fn: Listener<T>) world.added = function<T>(_: world, component: i53, fn: Listener<T>)
local listeners = signals.added[component] local listeners = signals.added[component]
@ -2569,12 +2595,12 @@ local function world_new()
listeners = {} listeners = {}
signals.added[component] = listeners signals.added[component] = listeners
local function on_add(entity, id, value) local function on_add(entity, id, value, oldarchetype)
for _, listener in listeners :: { Listener<T> } do for _, listener in listeners :: { Listener<T> } do
listener(entity, id, value) listener(entity, id, value, oldarchetype)
end end
end end
local existing_hook = inner_world_get(world, component, EcsOnAdd) :: Listener<T> local existing_hook = world_get(world, component, EcsOnAdd) :: Listener<T>
if existing_hook then if existing_hook then
table.insert(listeners, existing_hook) table.insert(listeners, existing_hook)
end end
@ -2592,7 +2618,7 @@ local function world_new()
idr.on_add = on_add idr.on_add = on_add
end end
end end
inner_world_set(world, component, EcsOnAdd, on_add) world_set(world, component, EcsOnAdd, on_add)
end end
table.insert(listeners, fn) table.insert(listeners, fn)
return function() return function()
@ -2612,12 +2638,12 @@ local function world_new()
if not listeners then if not listeners then
listeners = {} listeners = {}
signals.changed[component] = listeners signals.changed[component] = listeners
local function on_change(entity, id, value: any) local function on_change(entity, id, value, oldarchetype)
for _, listener in listeners :: { Listener<T> } do for _, listener in listeners :: { Listener<T> } do
listener(entity, id, value) listener(entity, id, value, oldarchetype)
end end
end end
local existing_hook = inner_world_get(world, component, EcsOnChange) :: Listener<T> local existing_hook = world_get(world, component, EcsOnChange) :: Listener<T>
if existing_hook then if existing_hook then
table.insert(listeners, existing_hook) table.insert(listeners, existing_hook)
end end
@ -2637,7 +2663,7 @@ local function world_new()
end end
end end
inner_world_set(world, component, EcsOnChange, on_change) world_set(world, component, EcsOnChange, on_change)
end end
table.insert(listeners, fn) table.insert(listeners, fn)
return function() return function()
@ -2654,12 +2680,12 @@ local function world_new()
listeners = {} listeners = {}
signals.removed[component] = listeners signals.removed[component] = listeners
local function on_remove(entity, id) local function on_remove(entity, id)
for _, listener in listeners :: { Listener<T> } do for _, listener in listeners :: { (...any) -> () } do
listener(entity, id) listener(entity, id)
end end
end end
local existing_hook = inner_world_get(world, component, EcsOnRemove) :: Listener<T> local existing_hook = world_get(world, component, EcsOnRemove) :: Listener<T>
if existing_hook then if existing_hook then
table.insert(listeners, existing_hook) table.insert(listeners, existing_hook)
end end
@ -2679,7 +2705,7 @@ local function world_new()
end end
end end
inner_world_set(world, component, EcsOnRemove, on_remove) world_set(world, component, EcsOnRemove, on_remove)
end end
table.insert(listeners, fn) table.insert(listeners, fn)
@ -2692,10 +2718,10 @@ local function world_new()
end end
end end
local function inner_world_has(world: World, entity: i53, local function world_has(world: World, entity: i53,
a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean
local record = inner_entity_index_try_get_unsafe(entity) local record = entity_index_try_get_unsafe(entity)
if not record then if not record then
return false return false
end end
@ -2714,8 +2740,8 @@ local function world_new()
(e == nil or error("args exceeded")) (e == nil or error("args exceeded"))
end end
local function inner_world_target(world: world, entity: i53, relation: i53, index: number?): i53? local function world_target(world: world, entity: i53, relation: i53, index: number?): i53?
local record = inner_entity_index_try_get_unsafe(entity) local record = entity_index_try_get_unsafe(entity)
if not record then if not record then
return nil return nil
end end
@ -2754,11 +2780,11 @@ local function world_new()
ECS_PAIR_SECOND(nth)) ECS_PAIR_SECOND(nth))
end end
local function inner_world_parent(world: world, entity: i53): i53? local function world_parent(world: world, entity: i53): i53?
return inner_world_target(world, entity, EcsChildOf, 0) return world_target(world, entity, EcsChildOf, 0)
end end
local function inner_world_entity(world: world, entity: i53?): i53 local function world_entity(world: world, entity: i53?): i53
if entity then if entity then
local index = ECS_ID(entity) local index = ECS_ID(entity)
local alive_count = entity_index.alive_count local alive_count = entity_index.alive_count
@ -2770,7 +2796,7 @@ local function world_new()
r.dense = index r.dense = index
dense = index dense = index
local e_swap = eindex_dense_array[dense] local e_swap = eindex_dense_array[dense]
local r_swap = inner_entity_index_try_get_any(e_swap) :: record local r_swap = entity_index_try_get_any(e_swap) :: record
r_swap.dense = dense r_swap.dense = dense
alive_count += 1 alive_count += 1
@ -2786,7 +2812,7 @@ local function world_new()
if any ~= entity then if any ~= entity then
if alive_count <= dense then if alive_count <= dense then
local e_swap = eindex_dense_array[dense] local e_swap = eindex_dense_array[dense]
local r_swap = inner_entity_index_try_get_any(e_swap) :: record local r_swap = entity_index_try_get_any(e_swap) :: record
r_swap.dense = dense r_swap.dense = dense
alive_count += 1 alive_count += 1
@ -2828,8 +2854,8 @@ local function world_new()
return entity_index_new_id(entity_index) return entity_index_new_id(entity_index)
end end
local function inner_world_remove(world: world, entity: i53, id: i53) local function world_remove(world: world, entity: i53, id: i53)
local record = inner_entity_index_try_get_unsafe(entity) local record = entity_index_try_get_unsafe(entity)
if not record then if not record then
return return
end end
@ -2849,99 +2875,12 @@ local function world_new()
local to = archetype_traverse_remove(world, id, record.archetype) local to = archetype_traverse_remove(world, id, record.archetype)
inner_entity_move(entity_index, entity, record, to) inner_entity_move(entity, record, to)
end end
end end
local function inner_world_clear(world: world, entity: i53) local function world_delete(world: world, entity: i53)
local tgt = ECS_PAIR(EcsWildcard, entity) local record = entity_index_try_get_unsafe(entity)
local idr_t = component_index[tgt]
local idr = component_index[entity]
local rel = ECS_PAIR(entity, EcsWildcard)
local idr_r = component_index[rel]
if idr then
local count = 0
local queue = {}
for archetype_id in idr.records do
local idr_archetype = archetypes[archetype_id]
local entities = idr_archetype.entities
local n = #entities
table.move(entities, 1, n, count + 1, queue)
count += n
end
for _, e in queue do
inner_world_remove(world, e, entity)
end
end
if idr_t then
local archetype_ids = idr_t.records
for archetype_id in archetype_ids do
local idr_t_archetype = archetypes[archetype_id]
local idr_t_types = idr_t_archetype.types
local entities = idr_t_archetype.entities
local node = idr_t_archetype
for _, id in idr_t_types do
if not ECS_IS_PAIR(id) then
continue
end
local object = entity_index_get_alive(
entity_index, ECS_PAIR_SECOND(id))
if object ~= entity then
continue
end
node = archetype_traverse_remove(world, id, node)
local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end
end
end
for i = #entities, 1, -1 do
local e = entities[i]
local r = inner_entity_index_try_get_unsafe(e) :: record
inner_entity_move(entity_index, e, r, node)
end
end
end
if idr_r then
local archetype_ids = idr_r.records
local records = idr_r.records
local counts = idr_r.counts
for archetype_id in archetype_ids do
local idr_r_archetype = archetypes[archetype_id]
local node = idr_r_archetype
local entities = idr_r_archetype.entities
local tr = records[archetype_id]
local tr_count = counts[archetype_id]
local types = idr_r_archetype.types
for i = tr, tr + tr_count - 1 do
local id = types[i]
node = archetype_traverse_remove(world, id, idr_r_archetype)
local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end
end
end
for i = #entities, 1, -1 do
local e = entities[i]
local r = inner_entity_index_try_get_unsafe(e) :: record
inner_entity_move(entity_index, e, r, node)
end
end
end
end
local function inner_world_delete(world: world, entity: i53)
local record = inner_entity_index_try_get_unsafe(entity)
if not record then if not record then
return return
end end
@ -2977,7 +2916,7 @@ local function world_new()
local entities = idr_archetype.entities local entities = idr_archetype.entities
local n = #entities local n = #entities
for i = n, 1, -1 do for i = n, 1, -1 do
inner_world_delete(world, entities[i]) world_delete(world, entities[i])
end end
archetype_destroy(world, idr_archetype) archetype_destroy(world, idr_archetype)
@ -3000,7 +2939,7 @@ local function world_new()
-- this is hypothetically not that expensive of an operation anyways -- this is hypothetically not that expensive of an operation anyways
to = archetype_traverse_remove(world, entity, from) to = archetype_traverse_remove(world, entity, from)
end end
inner_entity_move(entity_index, e, r, to) inner_entity_move(e, r, to)
end end
archetype_destroy(world, idr_archetype) archetype_destroy(world, idr_archetype)
@ -3022,53 +2961,49 @@ local function world_new()
end end
end end
if idr_t then if idr_t then
for id, cr in idr_t.wildcard_pairs do local archetype_ids = idr_t.records
local flags = cr.flags for archetype_id in archetype_ids do
local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE) local idr_t_archetype = archetypes[archetype_id]
local on_remove = cr.on_remove local idr_t_types = idr_t_archetype.types
if flags_delete_mask then local entities = idr_t_archetype.entities
for archetype_id in cr.records do
local idr_t_archetype = archetypes[archetype_id] for _, id in idr_t_types do
local entities = idr_t_archetype.entities if not ECS_IS_PAIR(id) then
continue
end
local object = entity_index_get_alive(
entity_index, ECS_PAIR_SECOND(id))
if object ~= entity then
continue
end
local id_record = component_index[id]
local flags = id_record.flags
local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE)
if flags_delete_mask then
for i = #entities, 1, -1 do for i = #entities, 1, -1 do
local child = entities[i] local child = entities[i]
inner_world_delete(world, child) world_delete(world, child)
end end
end break
else else
for archetype_id in cr.records do local on_remove = id_record.on_remove
local idr_t_archetype = archetypes[archetype_id]
local entities = idr_t_archetype.entities
-- archetype_traverse_remove is not idempotent meaning
-- this access is actually unsafe because it can
-- incorrectly cache an edge despite a node of the
-- component id on the archetype does not exist. This
-- requires careful testing to ensure correct values are
-- being passed to the arguments.
local to = archetype_traverse_remove(world, id, idr_t_archetype)
for i = #entities, 1, -1 do for i = #entities, 1, -1 do
local e = entities[i] local child = entities[i]
local r = eindex_sparse_array[ECS_ID(e :: number)]
if on_remove then if on_remove then
on_remove(e, id) on_remove(child, id)
local from = r.archetype
if from ~= idr_t_archetype then
-- unfortunately the on_remove hook allows a window where `e` can have changed archetype
-- this is hypothetically not that expensive of an operation anyways
to = archetype_traverse_remove(world, id, from)
end
end end
inner_entity_move(entity_index, e, r, to) local r = entity_index_try_get_unsafe(child) :: record
local to = archetype_traverse_remove(world, id, r.archetype)
inner_entity_move(child, r, to)
end end
end end
end end
end
for archetype_id in cr.records do for archetype_id in archetype_ids do
archetype_destroy(world, archetypes[archetype_id]) archetype_destroy(world, archetypes[archetype_id])
end
end end
end end
@ -3082,7 +3017,7 @@ local function world_new()
local entities = idr_r_archetype.entities local entities = idr_r_archetype.entities
local n = #entities local n = #entities
for i = n, 1, -1 do for i = n, 1, -1 do
inner_world_delete(world, entities[i]) world_delete(world, entities[i])
end end
archetype_destroy(world, idr_r_archetype) archetype_destroy(world, idr_r_archetype)
end end
@ -3109,8 +3044,8 @@ local function world_new()
for i = #entities, 1, -1 do for i = #entities, 1, -1 do
local e = entities[i] local e = entities[i]
local r = inner_entity_index_try_get_unsafe(e) :: record local r = entity_index_try_get_unsafe(e) :: record
inner_entity_move(entity_index, e, r, node) inner_entity_move(e, r, node)
end end
end end
@ -3120,12 +3055,13 @@ local function world_new()
end end
end end
local dense = record.dense local dense = record.dense
local i_swap = entity_index.alive_count local i_swap = entity_index.alive_count
entity_index.alive_count = i_swap - 1 entity_index.alive_count = i_swap - 1
local e_swap = eindex_dense_array[i_swap] local e_swap = eindex_dense_array[i_swap]
local r_swap = inner_entity_index_try_get_any(e_swap) :: record local r_swap = entity_index_try_get_any(e_swap) :: record
r_swap.dense = dense r_swap.dense = dense
record.archetype = nil :: any record.archetype = nil :: any
record.row = nil :: any record.row = nil :: any
@ -3135,15 +3071,34 @@ local function world_new()
eindex_dense_array[i_swap] = ECS_GENERATION_INC(entity) eindex_dense_array[i_swap] = ECS_GENERATION_INC(entity)
end end
local function inner_world_exists(world: world, entity: i53): boolean local function world_clear(world: world, entity: i53)
return inner_entity_index_try_get_any(entity) ~= nil local record = entity_index_try_get_unsafe(entity)
if not record then
return
end
local archetype = record.archetype
for _, id in archetype.types do
local idr = component_index[id]
local on_remove = idr.on_remove
if on_remove then
on_remove(entity, id)
end
end
archetype_delete(world, record.archetype, record.row)
record.archetype = nil :: any
record.row = nil :: any
end end
local function inner_world_contains(world: world, entity: i53): boolean local function world_exists(world: world, entity: i53): boolean
return entity_index_try_get_any(entity) ~= nil
end
local function world_contains(world: world, entity: i53): boolean
return entity_index_is_alive(entity_index, entity) return entity_index_is_alive(entity_index, entity)
end end
local function inner_world_cleanup(world: world) local function world_cleanup(world: world)
for _, archetype in archetypes do for _, archetype in archetypes do
if #archetype.entities == 0 then if #archetype.entities == 0 then
archetype_destroy(world, archetype) archetype_destroy(world, archetype)
@ -3173,26 +3128,27 @@ local function world_new()
error("Too many components, consider using world:entity() instead to create components.") error("Too many components, consider using world:entity() instead to create components.")
end end
world.max_component_id = max_component_id world.max_component_id = max_component_id
inner_world_add(world, max_component_id, EcsComponent) world_add(world, max_component_id, EcsComponent)
return max_component_id return max_component_id
end end
world.entity = inner_world_entity world.entity = world_entity
world.query = world_query :: any world.query = world_query :: any
world.remove = inner_world_remove world.remove = world_remove
world.clear = inner_world_clear world.clear = world_clear
world.delete = inner_world_delete -- world.purge = world_purge
world.delete = world_delete
world.component = world_component world.component = world_component
world.add = inner_world_add world.add = world_add
world.set = inner_world_set world.set = world_set
world.get = inner_world_get :: any world.get = world_get :: any
world.has = inner_world_has :: any world.has = world_has :: any
world.target = inner_world_target world.target = world_target
world.parent = inner_world_parent world.parent = world_parent
world.contains = inner_world_contains world.contains = world_contains
world.exists = inner_world_exists world.exists = world_exists
world.cleanup = inner_world_cleanup world.cleanup = world_cleanup
world.each = world_each world.each = world_each
world.children = world_children world.children = world_children
world.range = world_range world.range = world_range
@ -3202,36 +3158,36 @@ local function world_new()
end end
for i = 1, max_component_id do for i = 1, max_component_id do
inner_world_add(world, i, EcsComponent) world_add(world, i, EcsComponent)
end end
inner_world_add(world, EcsName, EcsComponent) world_add(world, EcsName, EcsComponent)
inner_world_add(world, EcsOnChange, EcsComponent) world_add(world, EcsOnChange, EcsComponent)
inner_world_add(world, EcsOnAdd, EcsComponent) world_add(world, EcsOnAdd, EcsComponent)
inner_world_add(world, EcsOnRemove, EcsComponent) world_add(world, EcsOnRemove, EcsComponent)
inner_world_add(world, EcsWildcard, EcsComponent) world_add(world, EcsWildcard, EcsComponent)
inner_world_add(world, EcsRest, EcsComponent) world_add(world, EcsRest, EcsComponent)
inner_world_set(world, EcsOnAdd, EcsName, "jecs.OnAdd") world_set(world, EcsOnAdd, EcsName, "jecs.OnAdd")
inner_world_set(world, EcsOnRemove, EcsName, "jecs.OnRemove") world_set(world, EcsOnRemove, EcsName, "jecs.OnRemove")
inner_world_set(world, EcsOnChange, EcsName, "jecs.OnChange") world_set(world, EcsOnChange, EcsName, "jecs.OnChange")
inner_world_set(world, EcsWildcard, EcsName, "jecs.Wildcard") world_set(world, EcsWildcard, EcsName, "jecs.Wildcard")
inner_world_set(world, EcsChildOf, EcsName, "jecs.ChildOf") world_set(world, EcsChildOf, EcsName, "jecs.ChildOf")
inner_world_set(world, EcsComponent, EcsName, "jecs.Component") world_set(world, EcsComponent, EcsName, "jecs.Component")
inner_world_set(world, EcsOnDelete, EcsName, "jecs.OnDelete") world_set(world, EcsOnDelete, EcsName, "jecs.OnDelete")
inner_world_set(world, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") world_set(world, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget")
inner_world_set(world, EcsDelete, EcsName, "jecs.Delete") world_set(world, EcsDelete, EcsName, "jecs.Delete")
inner_world_set(world, EcsRemove, EcsName, "jecs.Remove") world_set(world, EcsRemove, EcsName, "jecs.Remove")
inner_world_set(world, EcsName, EcsName, "jecs.Name") world_set(world, EcsName, EcsName, "jecs.Name")
inner_world_set(world, EcsRest, EcsRest, "jecs.Rest") world_set(world, EcsRest, EcsRest, "jecs.Rest")
inner_world_add(world, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) world_add(world, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete))
inner_world_add(world, EcsChildOf, EcsExclusive) world_add(world, EcsChildOf, EcsExclusive)
inner_world_add(world, EcsOnDelete, EcsExclusive) world_add(world, EcsOnDelete, EcsExclusive)
inner_world_add(world, EcsOnDeleteTarget, EcsExclusive) world_add(world, EcsOnDeleteTarget, EcsExclusive)
for i = EcsRest + 1, ecs_max_tag_id do for i = EcsRest + 1, ecs_max_tag_id do
entity_index_new_id(entity_index) entity_index_new_id(entity_index)
@ -3240,9 +3196,9 @@ local function world_new()
for i, bundle in ecs_metadata do for i, bundle in ecs_metadata do
for ty, value in bundle do for ty, value in bundle do
if value == NULL then if value == NULL then
inner_world_add(world, i, ty) world_add(world, i, ty)
else else
inner_world_set(world, i, ty, value) world_set(world, i, ty, value)
end end
end end
end end
@ -3273,7 +3229,7 @@ local function ecs_is_tag(world: world, entity: i53): boolean
if idr then if idr then
return bit32.btest(idr.flags, ECS_ID_IS_TAG) return bit32.btest(idr.flags, ECS_ID_IS_TAG)
end end
return not world_has_one_inline(world, entity, EcsComponent) return not WORLD_HAS(world, entity, EcsComponent)
end end
local function ecs_entity_record(world: world, entity: i53) local function ecs_entity_record(world: world, entity: i53)
@ -3338,7 +3294,7 @@ return {
entity_index_try_get_fast = entity_index_try_get_fast :: (EntityIndex, Entity) -> Record?, entity_index_try_get_fast = entity_index_try_get_fast :: (EntityIndex, Entity) -> Record?,
entity_index_try_get_any = entity_index_try_get_any :: (EntityIndex, Entity) -> Record, entity_index_try_get_any = entity_index_try_get_any :: (EntityIndex, Entity) -> Record,
entity_index_is_alive = entity_index_is_alive :: (EntityIndex, Entity) -> boolean, entity_index_is_alive = entity_index_is_alive :: (EntityIndex, Entity) -> boolean,
entity_index_new_id = entity_index_new_id :: (EntityIndex) -> Entity, entity_index_new_id = ENTITY_INDEX_NEW_ID :: (EntityIndex) -> Entity,
Query = Query, Query = Query,

View file

@ -24,39 +24,6 @@ type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser") local entity_visualiser = require("@tools/entity_visualiser")
local dwi = entity_visualiser.stringify local dwi = entity_visualiser.stringify
TEST("ardi", function()
local world = jecs.world()
local r = world:entity()
world:add(r, jecs.pair(jecs.OnDelete, jecs.Delete))
local e = world:entity()
local e1 = world:entity()
world:add(e, jecs.pair(r, e1))
world:delete(r)
CHECK(not world:contains(e))
end)
TEST("dai", function()
local world = jecs.world()
local C = world:component()
world:set(C, jecs.Name, "C")
CHECK(world:get(C, jecs.Name) == "C")
world:entity(2000)
CHECK(world:get(C, jecs.Name) == "C")
end)
TEST("another axen banger", function()
-- taken from jecs.luau
local world = jecs.world()
world:range(2000, 3000)
local e0v1_id = jecs.ECS_COMBINE(1000, 1) -- id can be both within or outside the world's range
local e0v1 = world:entity(e0v1_id)
assert(world:contains(e0v1)) -- fails
end)
TEST("Ensure archetype edges get cleaned", function() TEST("Ensure archetype edges get cleaned", function()
local A = jecs.component() local A = jecs.component()
local B = jecs.component() local B = jecs.component()
@ -91,6 +58,7 @@ TEST("Ensure archetype edges get cleaned", function()
CHECK(false) CHECK(false)
end end
end) end)
TEST("repeated entity cached query", function() TEST("repeated entity cached query", function()
local pair = jecs.pair local pair = jecs.pair
local world = jecs.world() local world = jecs.world()
@ -591,100 +559,146 @@ TEST("world:children()", function()
jecs.ECS_META_RESET() jecs.ECS_META_RESET()
end) end)
TEST("world:clear()", function() -- TEST("world:purge()", function()
do CASE "should remove its components" -- do CASE "should remove all instances of specified component"
-- local world = jecs.world()
-- local A = world:component()
-- local B = world:component()
-- local e = world:entity()
-- local e1 = world:entity()
-- local _e2 = world:entity()
-- world:set(e, A, true)
-- world:set(e, B, true)
-- world:set(e1, A, true)
-- world:set(e1, B, true)
-- CHECK(world:get(e, A))
-- CHECK(world:get(e, B))
-- world:purge(A)
-- CHECK(world:get(e, A) == nil)
-- CHECK(world:get(e, B))
-- CHECK(world:get(e1, A) == nil)
-- CHECK(world:get(e1, B))
-- end
-- do CASE "remove purged component from entities"
-- local world = jecs.world()
-- local A = world:component()
-- local B = world:component()
-- local C = world:component()
-- do
-- local id1 = world:entity()
-- local id2 = world:entity()
-- local id3 = world:entity()
-- world:set(id1, A, true)
-- world:set(id2, A, true)
-- world:set(id2, B, true)
-- world:set(id3, A, true)
-- world:set(id3, B, true)
-- world:set(id3, C, true)
-- world:purge(A)
-- CHECK(not world:has(id1, A))
-- CHECK(not world:has(id2, A))
-- CHECK(not world:has(id3, A))
-- CHECK(world:has(id2, B))
-- CHECK(world:has(id3, B, C))
-- world:purge(C)
-- CHECK(world:has(id2, B))
-- CHECK(world:has(id3, B))
-- CHECK(world:contains(A))
-- CHECK(world:contains(C))
-- CHECK(world:has(A, jecs.Component))
-- CHECK(world:has(B, jecs.Component))
-- end
-- do
-- local id1 = world:entity()
-- local id2 = world:entity()
-- local id3 = world:entity()
-- local tgt = world:entity()
-- world:add(id1, pair(A, tgt))
-- world:add(id1, pair(B, tgt))
-- world:add(id1, pair(C, tgt))
-- world:add(id2, pair(A, tgt))
-- world:add(id2, pair(B, tgt))
-- world:add(id2, pair(C, tgt))
-- world:add(id3, pair(A, tgt))
-- world:add(id3, pair(B, tgt))
-- world:add(id3, pair(C, tgt))
-- world:purge(B)
-- CHECK(world:has(id1, pair(A, tgt), pair(C, tgt)))
-- CHECK(not world:has(id1, pair(B, tgt)))
-- CHECK(world:has(id2, pair(A, tgt), pair(C, tgt)))
-- CHECK(not world:has(id1, pair(B, tgt)))
-- CHECK(world:has(id3, pair(A, tgt), pair(C, tgt)))
-- end
-- end
-- end)
TEST("world:clear", function()
do CASE "remove all components on entity"
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
local e = world:entity() local e = world:entity()
local e1 = world:entity()
local _e2 = world:entity()
world:set(e, A, true) world:set(e, A, true)
world:set(e, B, true) world:set(e, B, true)
world:set(e1, A, true) world:clear(e)
world:set(e1, B, true)
CHECK(world:get(e, A)) CHECK(world:contains(e))
CHECK(world:get(e, B)) CHECK(not world:has(e, A))
CHECK(not world:has(e, B))
world:clear(A) print(jecs.record(world, e).archetype == nil::any)
CHECK(world:get(e, A) == nil)
CHECK(world:get(e, B))
CHECK(world:get(e1, A) == nil)
CHECK(world:get(e1, B))
end end
do CASE "remove cleared ID from entities" do CASE "should invoke hooks"
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
local called = 0
world:set(A, jecs.OnRemove, function()
called += 1
end)
local B = world:component() local B = world:component()
local C = world:component() world:set(B, jecs.OnRemove, function()
called += 1
end)
do local e = world:entity()
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(id1, A, true) world:set(e, A, true)
world:set(e, B, true)
world:set(id2, A, true) world:clear(e)
world:set(id2, B, true)
world:set(id3, A, true)
world:set(id3, B, true)
world:set(id3, C, true)
world:clear(A)
CHECK(not world:has(id1, A))
CHECK(not world:has(id2, A))
CHECK(not world:has(id3, A))
CHECK(world:has(id2, B))
CHECK(world:has(id3, B, C))
world:clear(C)
CHECK(world:has(id2, B))
CHECK(world:has(id3, B))
CHECK(world:contains(A))
CHECK(world:contains(C))
CHECK(world:has(A, jecs.Component))
CHECK(world:has(B, jecs.Component))
end
do
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
local tgt = world:entity()
world:add(id1, pair(A, tgt))
world:add(id1, pair(B, tgt))
world:add(id1, pair(C, tgt))
world:add(id2, pair(A, tgt))
world:add(id2, pair(B, tgt))
world:add(id2, pair(C, tgt))
world:add(id3, pair(A, tgt))
world:add(id3, pair(B, tgt))
world:add(id3, pair(C, tgt))
world:clear(B)
CHECK(world:has(id1, pair(A, tgt), pair(C, tgt)))
CHECK(not world:has(id1, pair(B, tgt)))
CHECK(world:has(id2, pair(A, tgt), pair(C, tgt)))
CHECK(not world:has(id1, pair(B, tgt)))
CHECK(world:has(id3, pair(A, tgt), pair(C, tgt)))
end
CHECK(world:contains(e))
CHECK(not world:has(e, A))
CHECK(not world:has(e, B))
CHECK(called == 2)
end end
end) end)
@ -756,6 +770,60 @@ TEST("world:contains()", function()
end) end)
TEST("world:delete()", function() TEST("world:delete()", function()
do CASE "OnDelete cleanup policy cascades deletion to entites with idr_r pairs"
local world = jecs.world()
local r = world:entity()
world:add(r, jecs.pair(jecs.OnDelete, jecs.Delete))
local e = world:entity()
local e1 = world:entity()
world:add(e, jecs.pair(r, e1))
world:delete(r)
CHECK(not world:contains(e))
end
do CASE "OnDeleteTarget works correctly regardless of adjacent archetype iteration order"
local world = jecs.world()
local t = world:entity()
local c = world:component()
world:add(c, t)
local component = world:component()
local lifetime = world:component()
local tag = world:entity()
local rel1 = world:entity()
local rel2 = world:entity()
local rel3 = world:entity()
local destroyed = false
world:removed(lifetime, function(e)
destroyed = true
end)
local parent = world:entity()
world:set(parent, component, "foo")
world:add(parent, jecs.pair(rel1, component))
local other1 = world:entity()
world:add(other1, tag)
world:add(other1, jecs.pair(jecs.ChildOf, parent))
world:add(other1, jecs.pair(rel1, component))
local child = world:entity()
world:set(child, lifetime, "")
world:add(child, jecs.pair(jecs.ChildOf, parent))
world:add(child, jecs.pair(rel3, parent))
world:add(child, jecs.pair(rel2, other1))
world:delete(parent)
CHECK(destroyed)
CHECK(not world:contains(child))
end
do CASE "Should delete children in different archetypes if they have the same parent" do CASE "Should delete children in different archetypes if they have the same parent"
local world = jecs.world() local world = jecs.world()
@ -810,7 +878,6 @@ TEST("world:delete()", function()
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
local C = world:component()
world:set(A, jecs.OnRemove, function(entity, id) world:set(A, jecs.OnRemove, function(entity, id)
world:set(entity, B, true) world:set(entity, B, true)
end) end)
@ -1380,7 +1447,7 @@ TEST("world:range()", function()
CHECK(world.entity_index.alive_count == 400) CHECK(world.entity_index.alive_count == 400)
CHECK(e) CHECK(e)
end end
do CASE "axen" do CASE "entity ID reuse works correctly across different world ranges"
local base = jecs.world() local base = jecs.world()
base:range(1_000, 2_000) base:range(1_000, 2_000)
@ -1492,6 +1559,24 @@ TEST("world:range()", function()
end) end)
TEST("world:entity()", function() TEST("world:entity()", function()
do CASE "entity mirroring preserves IDs across world ranges"
local world = jecs.world()
world:range(2000, 3000)
local e0v1_id = jecs.ECS_COMBINE(1000, 1) -- id can be both within or outside the world's range
local e0v1 = world:entity(e0v1_id)
CHECK(world:contains(e0v1)) -- fails
end
do CASE "component names persist after entity creation"
local world = jecs.world()
local C = world:component()
world:set(C, jecs.Name, "C")
CHECK(world:get(C, jecs.Name) == "C")
world:entity(2000)
CHECK(world:get(C, jecs.Name) == "C")
end
do CASE "desired id" do CASE "desired id"
local world = jecs.world() local world = jecs.world()
local id = world:entity() local id = world:entity()