mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-24 17:10:03 +00:00
Compare commits
7 commits
5a6357b8da
...
cba11653d2
Author | SHA1 | Date | |
---|---|---|---|
|
cba11653d2 | ||
|
ee9bc6a775 | ||
|
18928302c8 | ||
|
0ec1d55c36 | ||
|
7c025a3782 | ||
|
02cb4ad7a2 | ||
|
4841915af3 |
4 changed files with 196 additions and 676 deletions
20
.github/workflows/styling.yaml
vendored
20
.github/workflows/styling.yaml
vendored
|
@ -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
|
138
jecs.luau
138
jecs.luau
|
@ -38,7 +38,7 @@ export type Archetype = {
|
|||
records: { ArchetypeRecord },
|
||||
} & GraphNode
|
||||
|
||||
type Record = {
|
||||
export type Record = {
|
||||
archetype: Archetype,
|
||||
row: number,
|
||||
dense: i24,
|
||||
|
@ -1554,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
|
||||
|
||||
|
@ -1571,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
|
||||
|
@ -1594,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
|
||||
|
@ -1626,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)
|
||||
|
@ -1645,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
|
||||
|
||||
|
@ -1733,6 +1777,10 @@ end
|
|||
|
||||
export type Id<T = unknown> = Entity<T>
|
||||
|
||||
type function ecs_entity_t(entity)
|
||||
return entity:components()[2]:readproperty(types.singleton("__T"))
|
||||
end
|
||||
|
||||
export type function Pair(first, second)
|
||||
local thing = first:components()[2]
|
||||
|
||||
|
@ -1745,7 +1793,7 @@ 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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
712
test/tests.luau
712
test/tests.luau
|
@ -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,7 +159,7 @@ 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
|
||||
|
@ -213,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)
|
||||
|
@ -368,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
|
||||
|
@ -874,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")
|
||||
|
@ -914,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)
|
||||
|
||||
|
@ -981,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))
|
||||
|
@ -1253,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()
|
||||
|
@ -1532,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)
|
||||
|
@ -1551,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()
|
||||
|
@ -1585,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?)
|
||||
|
@ -1634,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 = {}
|
||||
|
||||
|
@ -1647,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
|
||||
|
@ -1687,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()
|
||||
|
|
Loading…
Reference in a new issue