Compare commits

...

14 commits

Author SHA1 Message Date
EncodedVenom
dc3c13e2b1
Merge acc6e40aed into eaafd27280 2024-12-24 21:55:54 -06:00
Marcus
eaafd27280
Add support for components in both positions of pairs (#164)
Some checks are pending
Analysis / Run Luau Analyze (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Unit Testing / Run Luau Tests (push) Waiting to run
* Add support for components in both positions of pairs

* Export type function

* Rework query types
2024-12-24 22:39:32 +01:00
Marcus
ee9bc6a775
Remove stylua (#163)
* Add support for components in both positions of pairs

* Remove stylua workflow!

* remove file
2024-12-24 22:38:25 +01:00
Ukendio
7c025a3782 Fix linting errors from debug.info
Some checks failed
Analysis / Run Luau Analyze (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Styling / Run Stylua (push) Has been cancelled
Unit Testing / Run Luau Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2024-12-21 22:08:46 +01:00
Ukendio
02cb4ad7a2 Prevent iterator invalidation in world:each
Some checks are pending
Analysis / Run Luau Analyze (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Styling / Run Stylua (push) Waiting to run
Unit Testing / Run Luau Tests (push) Waiting to run
2024-12-21 06:51:48 +01:00
Marcus
4841915af3
Specialized method to find entities with a single ID (#165)
Some checks are pending
Analysis / Run Luau Analyze (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Styling / Run Stylua (push) Waiting to run
Unit Testing / Run Luau Tests (push) Waiting to run
* Initial commit

* Export query functions to make Michael happy

* Adding trailing commas
2024-12-20 13:08:50 +01:00
EncodedVenom
acc6e40aed Simplify script maybe? 2024-10-19 20:04:29 -04:00
EncodedVenom
9878df20ba Use bash entirely 2024-10-19 20:02:05 -04:00
EncodedVenom
b365fc8c1c Parenthesis 2024-10-19 19:58:09 -04:00
EncodedVenom
a8004011cc change to file 2024-10-19 19:55:54 -04:00
EncodedVenom
c697030ec4 debug 2024-10-19 19:53:43 -04:00
EncodedVenom
fefbd19b38 get around permissions 2024-10-19 19:52:21 -04:00
EncodedVenom
d5dc64be1b change location 2024-10-19 19:49:13 -04:00
EncodedVenom
04ef154193 Unit testing init PR 2024-10-19 19:47:26 -04:00
6 changed files with 256 additions and 717 deletions

View file

@ -16,4 +16,5 @@ jobs:
- name: Analyze
run: |
output=$(luau-analyze src || true) # Suppress errors for now.
(luau-analyze src || true) > analyze-log.txt # Suppress errors for now.
bash ./scripts/gh-warn-luau-analyze.sh analyze-log.txt

View file

@ -1,20 +0,0 @@
name: Styling
on: [push, pull_request, workflow_dispatch]
jobs:
run:
name: Run Stylua
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Run Stylua
uses: JohnnyMorganz/stylua-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: latest # NOTE: we recommend pinning to a specific version in case of formatting changes
# CLI arguments
args: --check jecs.luau

202
jecs.luau
View file

@ -38,7 +38,7 @@ export type Archetype = {
records: { ArchetypeRecord },
} & GraphNode
type Record = {
export type Record = {
archetype: Archetype,
row: number,
dense: i24,
@ -257,7 +257,7 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row:
local src_entities = from.entities
local last = #src_entities
local types = from.types
local id_types = from.types
local records = to.records
for i, column in src_columns do
@ -266,12 +266,13 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row:
end
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local tr = records[types[i]]
local tr = records[id_types[i]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if tr then
dst_columns[tr.column][dst_row] = column[src_row]
end
-- If the entity is the last row in the archetype then swapping it would be meaningless.
if src_row ~= last then
-- Swap rempves columns to ensure there are no holes in the archetype.
@ -475,7 +476,8 @@ local function id_record_ensure(world: World, id: number): IdRecord
if not idr then
local flags = ECS_ID_MASK
local relation = id
if ECS_IS_PAIR(id) then
local is_pair = ECS_IS_PAIR(id)
if is_pair then
relation = ecs_pair_first(world, id)
end
@ -492,6 +494,10 @@ local function id_record_ensure(world: World, id: number): IdRecord
local is_tag = not world_has_one_inline(world, relation, EcsComponent)
if is_tag and is_pair then
is_tag = not world_has_one_inline(world, ecs_pair_second(world, id), EcsComponent)
end
flags = bit32.bor(
flags,
if on_add then ECS_ID_HAS_ON_ADD else 0,
@ -535,15 +541,15 @@ local function archetype_append_to_records(
end
end
local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): Archetype
local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype
local archetype_id = (world.nextArchetypeId :: number) + 1
world.nextArchetypeId = archetype_id
local length = #types
local length = #id_types
local columns = (table.create(length) :: any) :: { Column }
local records: { ArchetypeRecord } = {}
for i, componentId in types do
for i, componentId in id_types do
local idr = id_record_ensure(world, componentId)
archetype_append_to_records(idr, archetype_id, records, componentId, i)
@ -572,7 +578,7 @@ local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): A
id = archetype_id,
records = records,
type = ty,
types = types,
types = id_types,
add = {},
remove = {},
@ -593,22 +599,22 @@ local function world_parent(world: World, entity: i53)
return world_target(world, entity, EcsChildOf, 0)
end
local function archetype_ensure(world: World, types): Archetype
if #types < 1 then
local function archetype_ensure(world: World, id_types): Archetype
if #id_types < 1 then
return world.ROOT_ARCHETYPE
end
local ty = hash(types)
local ty = hash(id_types)
local archetype = world.archetypeIndex[ty]
if archetype then
return archetype
end
return archetype_create(world, types, ty)
return archetype_create(world, id_types, ty)
end
local function find_insert(types: { i53 }, toAdd: i53): number
for i, id in types do
local function find_insert(id_types: { i53 }, toAdd: i53): number
for i, id in id_types do
if id == toAdd then
return -1
end
@ -616,7 +622,7 @@ local function find_insert(types: { i53 }, toAdd: i53): number
return i
end
end
return #types + 1
return #id_types + 1
end
local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype
@ -886,7 +892,7 @@ end
local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?)
local entityIndex = world.entity_index
local columns = archetype.columns
local types = archetype.types
local id_types = archetype.types
local entities = archetype.entities
local column_count = #entities
local last = #entities
@ -905,7 +911,7 @@ local function archetype_delete(world: World, archetype: Archetype, row: number,
-- TODO: if last == 0 then deactivate table
for _, id in types do
for _, id in id_types do
local on_remove: (entity: i53) -> () = world_get_one_inline(world, id, EcsOnRemove)
if on_remove then
on_remove(delete)
@ -913,9 +919,9 @@ local function archetype_delete(world: World, archetype: Archetype, row: number,
end
if row == last then
archetype_fast_delete_last(columns, column_count, types, delete)
archetype_fast_delete_last(columns, column_count, id_types, delete)
else
archetype_fast_delete(columns, column_count, row, types, delete)
archetype_fast_delete(columns, column_count, row, id_types, delete)
end
end
@ -1548,6 +1554,43 @@ local function world_query(world: World, ...)
return q
end
local function world_each(world: World, id): () -> ()
local idr = world.componentIndex[id]
if not idr then
return NOOP
end
local idr_cache = idr.cache
local archetypes = world.archetypes
local archetype_id = next(idr_cache, nil) :: number
local archetype = archetypes[archetype_id]
if not archetype then
return NOOP
end
local entities = archetype.entities
local row = #entities
return function(): any
local entity = entities[row]
while not entity do
archetype_id = next(idr_cache, archetype_id)
if not archetype_id then
return
end
archetype = archetypes[archetype_id]
entities = archetype.entities
row = #entities
end
row -= 1
return entity
end
end
local function world_children(world, parent)
return world_each(world, ECS_PAIR(EcsChildOf, parent))
end
local World = {}
World.__index = World
@ -1565,15 +1608,19 @@ World.target = world_target
World.parent = world_parent
World.contains = world_contains
World.cleanup = world_cleanup
World.each = world_each
World.children = world_children
if _G.__JECS_DEBUG then
-- taken from https://github.com/centau/ecr/blob/main/src/ecr.luau
-- error but stack trace always starts at first callsite outside of this file
local function dbg_info(n: number): any
return debug.info(n, "s")
end
local function throw(msg: string)
local s = 1
local root = dbg_info(1)
repeat
s += 1
until debug.info(s, "s") ~= debug.info(1, "s")
until dbg_info(s) ~= root
if warn then
error(msg, s)
else
@ -1588,15 +1635,18 @@ if _G.__JECS_DEBUG then
throw(msg)
end
local function get_name(world, id): string
local name: string | nil
local function get_name(world, id)
return world_get_one_inline(world, id, EcsName)
end
local function bname(world: World, id): string
local name: string
if ECS_IS_PAIR(id) then
name = `pair({get_name(world, ECS_ENTITY_T_HI(id))}, {get_name(world, ECS_ENTITY_T_LO(id))})`
local first = get_name(world, ecs_pair_first(world, id))
local second = get_name(world, ecs_pair_second(world, id))
name = `pair({first}, {second})`
else
local _1 = world_get_one_inline(world, id, EcsName)
if _1 then
name = `${_1}`
end
return get_name(world, id)
end
if name then
return name
@ -1620,14 +1670,14 @@ if _G.__JECS_DEBUG then
World.set = function(world: World, entity: i53, id: i53, value: any): ()
local is_tag = ID_IS_TAG(world, id)
if is_tag and value == nil then
local _1 = get_name(world, entity)
local _2 = get_name(world, id)
local _1 = bname(world, entity)
local _2 = bname(world, id)
local why = "cannot set component value to nil"
throw(why)
return
elseif value ~= nil and is_tag then
local _1 = get_name(world, entity)
local _2 = get_name(world, id)
local _1 = bname(world, entity)
local _2 = bname(world, id)
local why = `cannot set a component value because {_2} is a tag`
why ..= `\n[jecs] note: consider using "world:add({_1}, {_2})" instead`
throw(why)
@ -1639,8 +1689,8 @@ if _G.__JECS_DEBUG then
World.add = function(world: World, entity: i53, id: i53, value: any)
if value ~= nil then
local _1 = get_name(world, entity)
local _2 = get_name(world, id)
local _1 = bname(world, entity)
local _2 = bname(world, id)
throw("You provided a value when none was expected. " .. `Did you mean to use "world:add({_1}, {_2})"`)
end
@ -1725,27 +1775,25 @@ function World.new()
return self
end
export type Id<T = nil> = Entity<T> | Pair<Entity<T>, Entity<unknown>>
export type Id<T = unknown> = Entity<T>
export type Pair<First, Second> = number & {
__relation: First,
}
type function ecs_entity_t(entity)
return entity:components()[2]:readproperty(types.singleton("__T"))
end
-- type function _Pair(first, second)
-- local thing = first:components()[2]
export type function Pair(first, second)
local thing = first:components()[2]
-- if thing:readproperty(types.singleton("__T")):is("nil") then
-- return second
-- else
-- return first
-- end
-- end
-- type TestPair = _Pair<Entity<number>, Entity<Vector3>>
if thing:readproperty(types.singleton("__T")):is("nil") then
return second
else
return first
end
end
type Item<T...> = (self: Query<T...>) -> (Entity, T...)
export type Entity<T = nil> = number & { __T: T }
export type Entity<T = unknown> = number & { __T: T }
type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
@ -1805,43 +1853,19 @@ export type World = {
--- Checks if the world contains the given entity
contains: (self: World, entity: Entity) -> boolean,
each: (self: World, id: Id) -> () -> Entity,
children: (self: World, id: Id) -> () -> Entity,
--- Searches the world for entities that match a given query
query: (<A>(self: World, Id<A>) -> Query<A>)
& (<A, B>(self: World, Id<A>, Id<B>) -> Query<A, B>)
& (<A, B, C>(self: World, Id<A>, Id<B>, Id<C>) -> Query<A, B, C>)
& (<A, B, C, D>(self: World, Id<A>, Id<B>, Id<C>, Id<D>) -> Query<A, B, C, D>)
& (<A, B, C, D, E>(self: World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>) -> Query<A, B, C, D, E>)
& (<A, B, C, D, E, F>(
self: World,
Id<A>,
Id<B>,
Id<C>,
Id<D>,
Id<E>,
Id<F>
) -> Query<A, B, C, D, E, F>)
& (<A, B, C, D, E, F, G>(
self: World,
Id<A>,
Id<B>,
Id<C>,
Id<D>,
Id<E>,
Id<F>,
Id<G>
) -> Query<A, B, C, D, E, F, G>)
& (<A, B, C, D, E, F, G, H>(
self: World,
Id<A>,
Id<B>,
Id<C>,
Id<D>,
Id<E>,
Id<F>,
Id<G>,
Id<H>,
...Id<any>
) -> Query<A, B, C, D, E, F, G, H>),
query: (<A>(World, A) -> Query<ecs_entity_t<A>>)
& (<A, B>(World, A, B) -> Query<ecs_entity_t<A>, ecs_entity_t<B>>)
& (<A, B, C>(World, A, B, C) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>>)
& (<A, B, C, D>(World, A, B, C, D) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>>)
& (<A, B, C, D, E>(World, A, B, C, D, E) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>, ecs_entity_t<E>>)
& (<A, B, C, D, E, F>(World, A, B, C, D, E, F) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>, ecs_entity_t<E>, ecs_entity_t<F>>)
& (<A, B, C, D, E, F, G>(World, A, B, C, D, E, F, G) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>, ecs_entity_t<E>, ecs_entity_t<F>, ecs_entity_t<G>>)
& (<A, B, C, D, E, F, G, H>(World, A, B, C, D, E, F, G, H) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>, ecs_entity_t<E>, ecs_entity_t<F>, ecs_entity_t<G>, ecs_entity_t<H>>)
}
return {
@ -1895,4 +1919,10 @@ return {
entity_index_try_get_fast = entity_index_try_get_fast,
entity_index_is_alive = entity_index_is_alive,
entity_index_new_id = entity_index_new_id,
query_iter = query_iter,
query_iter_init = query_iter_init,
query_with = query_with,
query_without = query_without,
query_archetypes = query_archetypes,
}

View file

@ -0,0 +1,13 @@
# Read the input file line by line
while IFS= read -r line; do
# Use regex to capture file name, line number, column number, and message
if [[ $line =~ ^(.+)\(([0-9]+),([0-9]+)\):\ (.+)$ ]]; then
file="${BASH_REMATCH[1]}"
line_number="${BASH_REMATCH[2]}"
column_number="${BASH_REMATCH[3]}"
message="${BASH_REMATCH[4]}"
# Format output for GitHub Actions
echo "::warning file=$file,line=$line_number,col=$column_number::${message}"
fi
done < "$1"

View file

@ -289,7 +289,7 @@ local function FINISH(): boolean
return success, table.clear(tests)
end
local function SKIP(name: string)
local function SKIP()
skip = true
end

View file

@ -12,26 +12,19 @@ local ecs_pair_second = jecs.pair_second
local entity_index_try_get_any = jecs.entity_index_try_get_any
local entity_index_get_alive = jecs.entity_index_get_alive
local entity_index_is_alive = jecs.entity_index_is_alive
local ChildOf = jecs.ChildOf
local world_new = jecs.World.new
local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test()
local function CHECK_NO_ERR<T...>(s: string, fn: (T...) -> (), ...: T...)
local ok, err: string? = pcall(fn, ...)
if not CHECK(not ok, 2) then
local i = string.find(err :: string, " ")
assert(i)
local msg = string.sub(err :: string, i + 1)
CHECK(msg == s, 2)
end
end
local N = 2 ^ 8
type World = jecs.WorldShim
type World = jecs.World
type Entity<T=nil> = jecs.Entity<T>
local function debug_world_inspect(world)
local function record(e)
return entity_index_try_get_any(world.entity_index, e)
local function debug_world_inspect(world: World)
local function record(e): jecs.Record
return entity_index_try_get_any(world.entity_index, e) :: any
end
local function tbl(e)
return record(e).archetype
@ -69,10 +62,6 @@ local function debug_world_inspect(world)
}
end
local function name(world, e)
return world:get(e, jecs.Name)
end
TEST("archetype", function()
local archetype_append_to_records = jecs.archetype_append_to_records
local id_record_ensure = jecs.id_record_ensure
@ -117,6 +106,7 @@ TEST("world:cleanup()", function()
world:set(e2, A, true)
world:set(e2, B, true)
world:set(e3, A, true)
world:set(e3, B, true)
world:set(e3, C, true)
@ -135,19 +125,19 @@ TEST("world:cleanup()", function()
archetypeIndex = world.archetypeIndex
CHECK(archetypeIndex["1"] == nil)
CHECK(archetypeIndex["1_2"] == nil)
CHECK(archetypeIndex["1_2_3"] == nil)
CHECK((archetypeIndex["1"] :: jecs.Archetype?) == nil)
CHECK((archetypeIndex["1_2"] :: jecs.Archetype?) == nil)
CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil)
local e4 = world:entity()
world:set(e4, A, true)
CHECK(#archetypeIndex["1"].entities == 1)
CHECK(archetypeIndex["1_2"] == nil)
CHECK(archetypeIndex["1_2_3"] == nil)
CHECK((archetypeIndex["1_2"] :: jecs.Archetype?) == nil)
CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil)
world:set(e4, B, true)
CHECK(#archetypeIndex["1"].entities == 0)
CHECK(#archetypeIndex["1_2"].entities == 1)
CHECK(archetypeIndex["1_2_3"] == nil)
CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil)
world:set(e4, C, true)
CHECK(#archetypeIndex["1"].entities == 0)
CHECK(#archetypeIndex["1_2"].entities == 0)
@ -169,14 +159,13 @@ TEST("world:entity()", function()
CASE("generations")
local world = jecs.World.new()
local e = world:entity()
CHECK(ECS_ID(e) == 1 + jecs.Rest)
CHECK(ECS_ID(e) == 1 + jecs.Rest :: number)
CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e)
CHECK(ECS_GENERATION(e) == 1) -- 1
end
do
CASE("pairs")
do CASE("pairs")
local world = jecs.World.new()
local _e = world:entity()
local e2 = world:entity()
@ -185,11 +174,17 @@ TEST("world:entity()", function()
-- Incomplete pair, must have a bit flag that notes it is a pair
CHECK(IS_PAIR(world:entity()) == false)
local pair = pair(e2, e3)
CHECK(IS_PAIR(pair) == true)
local p = pair(e2, e3)
CHECK(IS_PAIR(p) == true)
CHECK(ecs_pair_first(world, pair) == e2)
CHECK(ecs_pair_second(world, pair) == e3)
CHECK(ecs_pair_first(world, p) == e2)
CHECK(ecs_pair_second(world, p) == e3)
world:delete(e2)
local e2v2 = world:entity()
CHECK(IS_PAIR(e2v2) == false)
CHECK(IS_PAIR(pair(e2v2, e3)) == true)
end
do CASE "Recycling"
@ -208,7 +203,7 @@ TEST("world:entity()", function()
do CASE "Recycling max generation"
local world = world_new()
local pin = jecs.Rest + 1
local pin = jecs.Rest::number + 1
for i = 1, 2^16-1 do
local e = world:entity()
world:delete(e)
@ -291,7 +286,7 @@ TEST("world:set()", function()
CHECK(world:get(e, pair(C1, C2)))
CHECK(world:get(e, pair(C1, T1)))
CHECK(not world:get(e, pair(T1, C1)))
CHECK(world:get(e, pair(T1, C1)))
CHECK(not world:get(e, pair(T1, T2)))
local e2 = world:entity()
@ -363,13 +358,12 @@ TEST("world:add()", function()
end)
TEST("world:query()", function()
do
CASE("multiple iter")
do CASE("multiple iter")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local e = world:entity()
world:add(e, A, "a")
world:add(e, A)
world:add(e, B)
local q = world:query(A, B)
local counter = 0
@ -410,7 +404,7 @@ TEST("world:query()", function()
for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) do
CHECK(a == true)
CHECK(b == true)
CHECK(c == nil)
CHECK(c == true)
CHECK(d == nil)
end
end
@ -869,6 +863,52 @@ TEST("world:query()", function()
end
end)
TEST("world:each", function()
local world = world_new()
local A = world:component()
local B = world:component()
local C = world:component()
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
world:set(e1, A, true)
world:set(e2, A, true)
world:set(e2, B, true)
world:set(e3, A, true)
world:set(e3, B, true)
world:set(e3, C, true)
for entity in world:each(A) do
if entity == e1 or entity == e2 or entity == e3 then
CHECK(true)
continue
end
CHECK(false)
end
end)
TEST("world:children", function()
local world = world_new()
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
world:add(e2, pair(ChildOf, e1))
world:add(e3, pair(ChildOf, e1))
for entity in world:children(pair(ChildOf, e1)) do
if entity == e2 or entity == e3 then
CHECK(true)
continue
end
CHECK(false)
end
end)
TEST("world:clear()", function()
do
CASE("should remove its components")
@ -909,18 +949,18 @@ TEST("world:clear()", function()
CHECK(archetype_entities[1] == _e)
CHECK(archetype_entities[2] == _e1)
local e_record = entity_index_try_get_any(
world.entity_index, e)
local e1_record = entity_index_try_get_any(
world.entity_index, e1)
local e_record: jecs.Record = entity_index_try_get_any(
world.entity_index, e) :: any
local e1_record: jecs.Record = entity_index_try_get_any(
world.entity_index, e1) :: any
CHECK(e_record.archetype == archetype)
CHECK(e1_record.archetype == archetype)
CHECK(e1_record.row == 2)
world:clear(e)
CHECK(e_record.archetype == nil)
CHECK(e_record.row == nil)
CHECK((e_record.archetype :: jecs.Archetype?) == nil)
CHECK((e_record.row :: number?) == nil)
CHECK(e1_record.archetype == archetype)
CHECK(e1_record.row == 1)
@ -976,15 +1016,14 @@ TEST("world:component()", function()
CHECK(not world:has(e, jecs.Component))
end
do
CASE("tag")
do CASE("tag")
local world = jecs.World.new() :: World
local A = world:component()
local B = world:entity()
local C = world:entity()
local e = world:entity()
world:set(e, A, "test")
world:add(e, B, "test")
world:add(e, B)
world:set(e, C, 11)
CHECK(world:has(e, A))
@ -1248,276 +1287,9 @@ TEST("world:contains", function()
CHECK(not world:contains(id))
end
end)
type Tracker<T> = {
track: (
world: World,
fn: (
changes: {
added: () -> () -> (number, T),
removed: () -> () -> number,
changed: () -> () -> (number, T, T),
}
) -> ()
) -> (),
}
type Entity<T = any> = number & { __nominal_type_dont_use: T }
local function diff(a, b)
local size = 0
for k, v in a do
if b[k] ~= v then
return true
end
size += 1
end
for k, v in b do
size -= 1
end
if size ~= 0 then
return true
end
return false
end
local function ChangeTracker<T>(world, T: Entity<T>): Tracker<T>
local PreviousT = jecs.pair(jecs.Rest, T)
local add = {}
local added
local removed
local is_trivial
local function changes_added()
added = true
local it = world:query(T):without(PreviousT):iter()
return function()
local id, data = it()
if not id then
return nil
end
is_trivial = typeof(data) ~= "table"
add[id] = data
return id, data
end
end
local function changes_changed()
local it = world:query(T, PreviousT):iter()
return function()
local id, new, old = it()
while true do
if not id then
return nil
end
if not is_trivial then
if diff(new, old) then
break
end
elseif new ~= old then
break
end
id, new, old = it()
end
add[id] = new
return id, old, new
end
end
local function changes_removed()
removed = true
local it = world:query(PreviousT):without(T):iter()
return function()
local id = it()
if id then
world:remove(id, PreviousT)
end
return id
end
end
local changes = {
added = changes_added,
changed = changes_changed,
removed = changes_removed,
}
local function track(fn)
added = false
removed = false
fn(changes)
if not added then
for _ in changes_added() do
end
end
if not removed then
for _ in changes_removed() do
end
end
for e, data in add do
world:set(e, PreviousT, if is_trivial then data else table.clone(data))
end
end
local tracker = { track = track }
return tracker
end
TEST("changetracker:track()", function()
local world = jecs.World.new()
do
CASE("added")
local Test = world:component() :: Entity<{ foo: number }>
local TestTracker = ChangeTracker(world, Test)
local e1 = world:entity()
local data = { foo = 11 }
world:set(e1, Test, data)
TestTracker.track(function(changes)
local added = 0
for e, test in changes.added() do
added += 1
CHECK(test == data)
end
for e, old, new in changes.changed() do
CHECK(false)
end
for e in changes.removed() do
CHECK(false)
end
CHECK(added == 1)
end)
end
do
CASE("changed")
local Test = world:component() :: Entity<{ foo: number }>
local TestTracker = ChangeTracker(world, Test)
local data = { foo = 11 }
local e1 = world:entity()
world:set(e1, Test, data)
TestTracker.track(function(changes) end)
data.foo += 1
TestTracker.track(function(changes)
for _ in changes.added() do
CHECK(false)
end
local changed = 0
for e, old, new in changes.changed() do
CHECK(e == e1)
CHECK(new == data)
CHECK(old ~= new)
CHECK(diff(new, old))
changed += 1
end
CHECK(changed == 1)
end)
end
do
CASE("removed")
local Test = world:component() :: Entity<{ foo: number }>
local TestTracker = ChangeTracker(world, Test)
local data = { foo = 11 }
local e1 = world:entity()
world:set(e1, Test, data)
TestTracker.track(function(changes) end)
world:remove(e1, Test)
TestTracker.track(function(changes)
for _ in changes.added() do
CHECK(false)
end
for _ in changes.changed() do
CHECK(false)
end
local removed = 0
for e in changes.removed() do
removed += 1
CHECK(e == e1)
end
CHECK(removed == 1)
end)
end
do
CASE("multiple change trackers")
local A = world:component()
local B = world:component()
local trackerA = ChangeTracker(world, A)
local trackerB = ChangeTracker(world, B)
local e1 = world:entity()
world:set(e1, A, "a1")
local e2 = world:entity()
world:set(e2, B, "b1")
trackerA.track(function() end)
trackerB.track(function() end)
world:set(e2, B, "b2")
trackerA.track(function(changes)
for _, old, new in changes.changed() do
end
end)
trackerB.track(function(changes)
for _, old, new in changes.changed() do
CHECK(new == "b2")
end
end)
end
end)
local function create_cache(hook)
local columns = setmetatable({}, {
__index = function(self, component)
local column = {}
self[component] = column
return column
end,
})
return function(world, component, fn)
local column = columns[component]
table.insert(column, fn)
world:set(component, hook, function(entity, value)
for _, callback in column do
callback(entity, value)
end
end)
end
end
local hooks = {
OnSet = create_cache(jecs.OnSet),
OnAdd = create_cache(jecs.OnAdd),
OnRemove = create_cache(jecs.OnRemove),
}
TEST("Hooks", function()
do
CASE("OnAdd")
do CASE "OnAdd"
local world = jecs.World.new()
local Transform = world:component()
local e1 = world:entity()
@ -1527,18 +1299,12 @@ TEST("Hooks", function()
world:add(e1, Transform)
end
do
CASE("OnSet")
do CASE "OnSet"
local world = jecs.World.new()
local Number = world:component()
local e1 = world:entity()
hooks.OnSet(world, Number, function(entity, data)
CHECK(e1 == entity)
CHECK(data == world:get(entity, Number))
CHECK(data == 1)
end)
hooks.OnSet(world, Number, function(entity, data)
world:set(Number, jecs.OnSet, function(entity, data)
CHECK(e1 == entity)
CHECK(data == world:get(entity, Number))
CHECK(data == 1)
@ -1546,8 +1312,7 @@ TEST("Hooks", function()
world:set(e1, Number, 1)
end
do
CASE("OnRemove")
do CASE("OnRemove")
do
-- basic
local world = jecs.World.new()
@ -1580,9 +1345,41 @@ TEST("Hooks", function()
CHECK(not world:get(e, B))
end
end
end)
do
CASE("the filip incident")
TEST("repro", function()
do CASE "#1"
local world = world_new()
local reproEntity = world:component()
local components = { Cooldown = world:component() :: jecs.Id<number> }
world:set(reproEntity, components.Cooldown, 2)
local function updateCooldowns(dt: number)
local toRemove = {}
for id, cooldown in world:query(components.Cooldown):iter() do
cooldown -= dt
if cooldown <= 0 then
table.insert(toRemove, id)
print("removing")
-- world:remove(id, components.Cooldown)
else
world:set(id, components.Cooldown, cooldown)
end
end
for _, id in toRemove do
world:remove(id, components.Cooldown)
CHECK(not world:get(id, components.Cooldown))
end
end
updateCooldowns(1.5)
updateCooldowns(1.5)
end
do CASE "#2"
local world = jecs.World.new()
export type Iterator<T> = () -> (Entity, T?, T?)
@ -1629,7 +1426,7 @@ TEST("Hooks", function()
return cachedChangeSets[component]
end
local function ChangeTracker<T>(component): (Iterator<T>, Destructor)
local function ChangeTracker<T>(component: jecs.Id): (Iterator<T>, Destructor)
local values: ValuesMap<T> = {}
local changeSet: ChangeSet = {}
@ -1642,7 +1439,7 @@ TEST("Hooks", function()
changeSets.Changed[changeSet] = true
changeSets.Removed[changeSet] = true
local id: Entity? = nil
local id: jecs.Id? = nil
local iter: Iterator<T> = function()
id = next(changeSet)
if id then
@ -1682,286 +1479,4 @@ TEST("Hooks", function()
CHECK(counter == 1)
end
end)
TEST("scheduler", function()
type System = {
callback: (world: World) -> (),
}
type Systems = { System }
type Events = {
RenderStepped: Systems,
Heartbeat: Systems,
}
local scheduler_new: (
w: World
) -> {
components: {
Disabled: Entity,
System: Entity<System>,
Phase: Entity,
DependsOn: Entity,
},
collect: {
under_event: (event: Entity) -> Systems,
all: () -> Events,
},
systems: {
run: (events: Events) -> (),
new: (callback: (world: World) -> (), phase: Entity) -> Entity,
},
phases: {
RenderStepped: Entity,
Heartbeat: Entity,
},
phase: (after: Entity) -> Entity,
}
do
local world
local Disabled
local System
local DependsOn
local Phase
local Event
local RenderStepped
local Heartbeat
local Name
local function scheduler_systems_run(events)
for _, system in events[RenderStepped] do
system.callback()
end
for _, system in events[Heartbeat] do
system.callback()
end
end
local function scheduler_collect_systems_under_phase_recursive(systems, phase)
for _, system in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, system)
end
for dependant in world:query(Phase):with(pair(DependsOn, phase)) do
scheduler_collect_systems_under_phase_recursive(systems, dependant)
end
end
local function scheduler_collect_systems_under_event(event)
local systems = {}
scheduler_collect_systems_under_phase_recursive(systems, event)
return systems
end
local function scheduler_collect_systems_all()
local systems = {}
for phase in world:query(Phase, Event) do
systems[phase] = scheduler_collect_systems_under_event(phase)
end
return systems
end
local function scheduler_phase_new(after)
local phase = world:entity()
world:add(phase, Phase)
local dependency = pair(DependsOn, after)
world:add(phase, dependency)
return phase
end
local function scheduler_systems_new(callback, phase)
local system = world:entity()
world:set(system, System, { callback = callback })
world:add(system, pair(DependsOn, phase))
return system
end
function scheduler_new(w)
world = w
Disabled = world:component()
System = world:component()
Phase = world:component()
DependsOn = world:component()
Event = world:component()
RenderStepped = world:component()
Heartbeat = world:component()
world:add(RenderStepped, Phase)
world:add(RenderStepped, Event)
world:add(Heartbeat, Phase)
world:add(Heartbeat, Event)
return {
phase = scheduler_phase_new,
phases = {
RenderStepped = RenderStepped,
Heartbeat = Heartbeat,
},
world = world,
components = {
DependsOn = DependsOn,
Disabled = Disabled,
Heartbeat = Heartbeat,
Phase = Phase,
RenderStepped = RenderStepped,
System = System,
},
collect = {
under_event = scheduler_collect_systems_under_event,
all = scheduler_collect_systems_all,
},
systems = {
new = scheduler_systems_new,
run = scheduler_systems_run,
},
}
end
end
do
CASE("event dependant phase")
local world = jecs.World.new()
local scheduler = scheduler_new(world)
local components = scheduler.components
local phases = scheduler.phases
local Heartbeat = phases.Heartbeat
local DependsOn = components.DependsOn
local Physics = scheduler.phase(Heartbeat)
CHECK(world:target(Physics, DependsOn, 0) == Heartbeat)
end
do
CASE("user-defined sub phases")
local world = jecs.World.new()
local scheduler = scheduler_new(world)
local components = scheduler.components
local phases = scheduler.phases
local DependsOn = components.DependsOn
local A = scheduler.phase(phases.Heartbeat)
local B = scheduler.phase(A)
CHECK(world:target(B, DependsOn, 0) == A)
end
do
CASE("phase order")
local world = jecs.World.new()
local scheduler = scheduler_new(world)
local phases = scheduler.phases
local Physics = scheduler.phase(phases.Heartbeat)
local Collisions = scheduler.phase(Physics)
local order = "BEGIN"
local function move()
order ..= "->move"
end
local function hit()
order ..= "->hit"
end
local createSystem = scheduler.systems.new
createSystem(hit, Collisions)
createSystem(move, Physics)
local events = scheduler.collect.all()
scheduler.systems.run(events)
order ..= "->END"
CHECK(order == "BEGIN->move->hit->END")
end
do
CASE("collect only systems under phase recursive")
local world = jecs.World.new()
local scheduler = scheduler_new(world)
local phases = scheduler.phases
local Heartbeat = phases.Heartbeat
local RenderStepped = phases.RenderStepped
local Render = scheduler.phase(RenderStepped)
local Physics = scheduler.phase(Heartbeat)
local Collisions = scheduler.phase(Physics)
local function move() end
local function hit() end
local function camera() end
local createSystem = scheduler.systems.new
createSystem(hit, Collisions)
createSystem(move, Physics)
createSystem(camera, Render)
local systems = scheduler.collect.under_event(Collisions)
CHECK(#systems == 1)
CHECK(systems[1].callback == hit)
systems = scheduler.collect.under_event(Physics)
CHECK(#systems == 2)
systems = scheduler.collect.under_event(Heartbeat)
CHECK(#systems == 2)
systems = scheduler.collect.under_event(Render)
CHECK(#systems == 1)
CHECK(systems[1].callback == camera)
end
end)
TEST("repro", function()
do
CASE("")
local world = world_new()
local reproEntity = world:component()
local components = { Cooldown = world:component() }
world:set(reproEntity, components.Cooldown, 2)
local function updateCooldowns(dt: number)
local toRemove = {}
for id, cooldown in world:query(components.Cooldown):iter() do
cooldown -= dt
if cooldown <= 0 then
table.insert(toRemove, id)
print("removing")
-- world:remove(id, components.Cooldown)
else
world:set(id, components.Cooldown, cooldown)
end
end
for _, id in toRemove do
world:remove(id, components.Cooldown)
CHECK(not world:get(id, components.Cooldown))
end
end
updateCooldowns(1.5)
updateCooldowns(1.5)
end
end)
FINISH()