This commit is contained in:
Ukendio 2025-06-10 14:28:00 +02:00
parent 0fea5a259d
commit d9308a91e9
43 changed files with 2437 additions and 12768 deletions

1
.luacheckrc Normal file
View file

@ -0,0 +1 @@
std="love+luajit"

View file

@ -1,6 +1,6 @@
{
"aliases": {
"jecs": "jecs",
"jecs": "jecs.luau",
"testkit": "tools/testkit",
"mirror": "mirror",
"tools": "tools",

View file

@ -1,268 +0,0 @@
local jecs = require("@jecs")
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
removed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id) -> ()) -> () -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
observer: (
PatchedWorld,
any,
(jecs.Entity) -> ()
) -> (),
monitor: (
PatchedWorld,
any,
(jecs.Entity, jecs.Id) -> ()
) -> ()
}
local function observers_new(world, query, callback)
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity, id, value)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity)
end
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
end
local function join(world, component)
local sparse_array = {}
local dense_array = {}
local values = {}
local max_id = 0
world:added(component, function(entity, id, value)
max_id += 1
sparse_array[entity] = max_id
dense_array[max_id] = entity
values[max_id] = value
end)
world:removed(component, function(entity, id)
local e_swap = dense_array[max_id]
local v_swap = values[max_id]
local dense = sparse_array[entity]
dense_array[dense] = e_swap
values[dense] = v_swap
sparse_array[entity] = nil
dense_array[max_id] = nil
values[max_id] = nil
max_id -= 1
end)
world:changed(component, function(entity, id, value)
values[sparse_array[entity]] = value
end)
return function()
local i = max_id
return function(): ...any
i -= 1
if i == 0 then
return nil
end
local e = dense_array[i]
return e, values[i]
end
end
end
local function monitors_new(world, query, callback)
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnAdd)
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
local EcsOnRemove = jecs.OnRemove :: jecs.Id
callback(entity, EcsOnRemove)
end
end
for _, term in terms do
world:added(term, emplaced)
world:removed(term, removed)
end
end
local function observers_add(world: jecs.World): PatchedWorld
type Signal = { [jecs.Entity]: { (...any) -> () } }
local world_mut = world :: jecs.World & {[string]: any}
local signals = {
added = {} :: Signal,
emplaced = {} :: Signal,
removed = {} :: Signal
}
world_mut.added = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local function on_add(entity, id, value)
for _, listener in listeners :: any do
listener(entity, id, value)
end
end
local existing_hook = world:get(component, jecs.OnAdd)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.hooks.on_add = on_add
else
world:set(component, jecs.OnAdd, on_add)
end
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world_mut.changed = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local function on_change(entity, id, value: any)
for _, listener in listeners :: any do
listener(entity, id, value)
end
end
local existing_hook = world:get(component, jecs.OnChange)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.hooks.on_change = on_change
else
world:set(component, jecs.OnChange, on_change)
end
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world_mut.removed = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id) -> ()
)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local function on_remove(entity, id)
for _, listener in listeners :: any do
listener(entity, id)
end
end
local existing_hook = world:get(component, jecs.OnRemove)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.hooks.on_remove = on_remove
else
world:set(component, jecs.OnRemove, on_remove)
end
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world_mut.signals = signals
world_mut.observer = observers_new
world_mut.monitor = monitors_new
world_mut.trackers = {}
return world_mut :: PatchedWorld
end
return observers_add

View file

@ -1,160 +0,0 @@
local jecs = require("@jecs")
local mirror = require("@mirror")
type i53 = number
do
TITLE(testkit.color.white_underline("Jecs query"))
local ecs = jecs.World.new()
do
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)
BENCH("4 component", function()
for _ in world:query(D, C, B, A) do
end
end)
end
local D1 = ecs:component()
local D2 = ecs:component()
local D3 = ecs:component()
local D4 = ecs:component()
local D5 = ecs:component()
local D6 = ecs:component()
local D7 = ecs:component()
local D8 = ecs:component()
local function flip()
return math.random() >= 0.15
end
local added = 0
local archetypes = {}
for i = 1, 2 ^ 16 - 2 do
local entity = ecs:entity()
local combination = ""
if flip() then
combination ..= "B"
ecs:set(entity, D2, { value = true })
end
if flip() then
combination ..= "C"
ecs:set(entity, D3, { value = true })
end
if flip() then
combination ..= "D"
ecs:set(entity, D4, { value = true })
end
if flip() then
combination ..= "E"
ecs:set(entity, D5, { value = true })
end
if flip() then
combination ..= "F"
ecs:set(entity, D6, { value = true })
end
if flip() then
combination ..= "G"
ecs:set(entity, D7, { value = true })
end
if flip() then
combination ..= "H"
ecs:set(entity, D8, { value = true })
end
if #combination == 7 then
added += 1
ecs:set(entity, D1, { value = true })
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end
do
TITLE(testkit.color.white_underline("Mirror query"))
local ecs = mirror.World.new()
do
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)
BENCH("4 component", function()
for _ in world:query(D, C, B, A) do
end
end)
end
local D1 = ecs:component()
local D2 = ecs:component()
local D3 = ecs:component()
local D4 = ecs:component()
local D5 = ecs:component()
local D6 = ecs:component()
local D7 = ecs:component()
local D8 = ecs:component()
local function flip()
return math.random() >= 0.15
end
local added = 0
local archetypes = {}
for i = 1, 2 ^ 16 - 2 do
local entity = ecs:entity()
local combination = ""
if flip() then
combination ..= "B"
ecs:set(entity, D2, { value = true })
end
if flip() then
combination ..= "C"
ecs:set(entity, D3, { value = true })
end
if flip() then
combination ..= "D"
ecs:set(entity, D4, { value = true })
end
if flip() then
combination ..= "E"
ecs:set(entity, D5, { value = true })
end
if flip() then
combination ..= "F"
ecs:set(entity, D6, { value = true })
end
if flip() then
combination ..= "G"
ecs:set(entity, D7, { value = true })
end
if flip() then
combination ..= "H"
ecs:set(entity, D8, { value = true })
end
if #combination == 7 then
added += 1
ecs:set(entity, D1, { value = true })
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end

View file

@ -1,244 +0,0 @@
local jecs = require("@jecs")
local testkit = require("@testkit")
local BENCH, START = testkit.benchmark()
local function TITLE(s: string)
print()
print(testkit.color.white(s))
end
local N = 2 ^ 17
local pair = jecs.pair
do
TITLE("create")
local world = jecs.World.new()
BENCH("entity", function()
for i = 1, START(N) do
world:entity()
end
end)
local A = world:component()
local B = world:component()
BENCH("pair", function()
for i = 1, START(N) do
jecs.pair(A, B)
end
end)
end
do
TITLE("set")
local world = jecs.World.new()
local A = world:component()
local entities = table.create(N)
for i = 1, N do
entities[i] = world:entity()
end
BENCH("add", function()
for i = 1, START(N) do
world:set(entities[i], A, 1)
end
end)
BENCH("set", function()
for i = 1, START(N) do
world:set(entities[i], A, 2)
end
end)
BENCH("remove", function()
for i = 1, START(N) do
world:remove(entities[i], A)
end
end)
end
-- we have a separate benchmark for relationships.
-- this is due to that relationships have a very high id compared to normal
-- components, which cause them to get added into the hashmap portion.
do
TITLE("set relationship")
local world = jecs.World.new()
local A = world:component()
local entities = table.create(N)
for i = 1, N do
entities[i] = world:entity()
world:set(entities[i], A, 1)
end
local pair = jecs.pair(A, world:entity())
BENCH("add", function()
for i = 1, START(N) do
world:set(entities[i], pair, 1)
end
end)
BENCH("set", function()
for i = 1, START(N) do
world:set(entities[i], pair, 2)
end
end)
BENCH("remove", function()
for i = 1, START(N) do
world:remove(entities[i], pair)
end
end)
end
do
TITLE("get")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local entities = table.create(N)
for i = 1, N do
entities[i] = world:entity()
world:set(entities[i], A, 1)
world:set(entities[i], B, 1)
world:set(entities[i], C, 1)
world:set(entities[i], D, 1)
end
BENCH("1", function()
for i = 1, START(N) do
world:get(entities[i], A)
end
end)
BENCH("2", function()
for i = 1, START(N) do
world:get(entities[i], A, B)
end
end)
BENCH("3", function()
for i = 1, START(N) do
world:get(entities[i], A, B, C)
end
end)
BENCH("4", function()
for i = 1, START(N) do
world:get(entities[i], A, B, C, D)
end
end)
end
do
TITLE("target")
BENCH("1st target", function()
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local entities = table.create(N)
for i = 1, N do
local ent = world:entity()
entities[i] = ent
world:set(ent, pair(A, A))
world:set(ent, pair(A, B))
world:set(ent, pair(A, C))
world:set(ent, pair(A, D))
end
for i = 1, START(N) do
world:target(entities[i], A, 0)
end
end)
end
--- this benchmark is used to view how fragmentation affects query performance
--- we use this by determining how many entities should fit per arcehtype, instead
--- of creating x amount of archetypes. this would scale better with any amount of
--- entities.
do
TITLE(`query {N} entities`)
local function view_bench(n: number)
BENCH(`{n} entities per archetype`, function()
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
for i = 1, N, n do
local ct = world:entity()
for j = 1, n do
local id = world:entity()
world:set(id, A, true)
world:set(id, B, true)
world:set(id, C, true)
world:set(id, D, true)
world:add(id, ct)
end
end
local q = world:query(A, B, C, D)
START()
for id in q do
end
end)
BENCH(`inlined query`, function()
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
for i = 1, N, n do
local ct = world:entity()
for j = 1, n do
local id = world:entity()
world:set(id, A, true)
world:set(id, B, true)
world:set(id, C, true)
world:set(id, D, true)
world:add(id, ct)
end
end
local archetypes = world:query(A, B, C, D):archetypes()
START()
for _, archetype in archetypes do
local columns, records = archetype.columns, archetype.records
local a = columns[records[A]]
local b = columns[records[B]]
local c = columns[records[C]]
local d = columns[records[D]]
for row in archetype.entities do
local _1, _2, _3, _4 = a[row], b[row], c[row], d[row]
end
end
end)
end
for i = 13, 0, -1 do
view_bench(2 ^ i)
end
end

View file

@ -1,246 +0,0 @@
--!optimize 2
--!native
local testkit = require("@testkit")
local BENCH, START = testkit.benchmark()
local function TITLE(title: string)
print()
print(testkit.color.white(title))
end
local jecs = require("@jecs")
local mirror = require("@mirror")
type i53 = number
do
TITLE(testkit.color.white_underline("Jecs query"))
local ecs = jecs.World.new()
do
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)
BENCH("1 component", function()
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(B, A) do
end
end)
BENCH("4 component", function()
for _ in world:query(D, C, B, A) do
end
end)
BENCH("8 component", function()
for _ in world:query(H, G, F, E, D, C, B, A) do
end
end)
local e = world:entity()
world:set(e, A, true)
world:set(e, B, true)
world:set(e, C, true)
world:set(e, D, true)
world:set(e, E, true)
world:set(e, F, true)
world:set(e, G, true)
world:set(e, H, true)
BENCH("Update Data", function()
for _ = 1, 100 do
world:set(e, A, false)
world:set(e, B, false)
world:set(e, C, false)
world:set(e, D, false)
world:set(e, E, false)
world:set(e, F, false)
world:set(e, G, false)
world:set(e, H, false)
end
end)
end
local D1 = ecs:component()
local D2 = ecs:component()
local D3 = ecs:component()
local D4 = ecs:component()
local D5 = ecs:component()
local D6 = ecs:component()
local D7 = ecs:component()
local D8 = ecs:component()
local function flip()
return math.random() >= 0.15
end
local added = 0
local archetypes = {}
for i = 1, 2 ^ 16 - 2 do
local entity = ecs:entity()
local combination = ""
if flip() then
combination ..= "B"
ecs:set(entity, D2, { value = true })
end
if flip() then
combination ..= "C"
ecs:set(entity, D3, { value = true })
end
if flip() then
combination ..= "D"
ecs:set(entity, D4, { value = true })
end
if flip() then
combination ..= "E"
ecs:set(entity, D5, { value = true })
end
if flip() then
combination ..= "F"
ecs:set(entity, D6, { value = true })
end
if flip() then
combination ..= "G"
ecs:set(entity, D7, { value = true })
end
if flip() then
combination ..= "H"
ecs:set(entity, D8, { value = true })
end
if #combination == 7 then
added += 1
ecs:set(entity, D1, { value = true })
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end
do
TITLE(testkit.color.white_underline("Mirror query"))
local ecs = mirror.World.new()
do
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)
BENCH("1 component", function()
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(B, A) do
end
end)
BENCH("4 component", function()
for _ in world:query(D, C, B, A) do
end
end)
BENCH("8 component", function()
for _ in world:query(H, G, F, E, D, C, B, A) do
end
end)
local e = world:entity()
world:set(e, A, true)
world:set(e, B, true)
world:set(e, C, true)
world:set(e, D, true)
world:set(e, E, true)
world:set(e, F, true)
world:set(e, G, true)
world:set(e, H, true)
BENCH("Update Data", function()
for _ = 1, 100 do
world:set(e, A, false)
world:set(e, B, false)
world:set(e, C, false)
world:set(e, D, false)
world:set(e, E, false)
world:set(e, F, false)
world:set(e, G, false)
world:set(e, H, false)
end
end)
end
local D1 = ecs:component()
local D2 = ecs:component()
local D3 = ecs:component()
local D4 = ecs:component()
local D5 = ecs:component()
local D6 = ecs:component()
local D7 = ecs:component()
local D8 = ecs:component()
local function flip()
return math.random() >= 0.15
end
local added = 0
local archetypes = {}
for i = 1, 2 ^ 16 - 2 do
local entity = ecs:entity()
local combination = ""
if flip() then
combination ..= "B"
ecs:set(entity, D2, { value = true })
end
if flip() then
combination ..= "C"
ecs:set(entity, D3, { value = true })
end
if flip() then
combination ..= "D"
ecs:set(entity, D4, { value = true })
end
if flip() then
combination ..= "E"
ecs:set(entity, D5, { value = true })
end
if flip() then
combination ..= "F"
ecs:set(entity, D6, { value = true })
end
if flip() then
combination ..= "G"
ecs:set(entity, D7, { value = true })
end
if flip() then
combination ..= "H"
ecs:set(entity, D8, { value = true })
end
if #combination == 7 then
added += 1
ecs:set(entity, D1, { value = true })
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end

View file

@ -1,64 +0,0 @@
--!optimize 2
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local pair = jecs.pair
local newWorld = Matter.World.new()
local ecs = jecs.World.new()
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local C1 = ecs:component()
local C2 = ecs:entity()
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
local C3 = ecs:entity()
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
local C4 = ecs:entity()
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
local E1 = mcs:component()
local E2 = mcs:entity()
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
local E3 = mcs:entity()
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
local E4 = mcs:entity()
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
local registry2 = ecr.registry()
return {
ParameterGenerator = function()
local j = ecs:entity()
ecs:set(j, C1, true)
local m = mcs:entity()
mcs:set(m, E1, true)
for i = 1, 1000 do
local friend1 = ecs:entity()
local friend2 = mcs:entity()
ecs:add(friend1, pair(C2, j))
ecs:add(friend1, pair(C3, j))
ecs:add(friend1, pair(C4, j))
mcs:add(friend2, pair(E2, m))
mcs:add(friend2, pair(E3, m))
mcs:add(friend2, pair(E4, m))
end
return {
m = m,
j = j,
}
end,
Functions = {
Mirror = function(_, a)
mcs:delete(a.m)
end,
Jecs = function(_, a)
ecs:delete(a.j)
end,
},
}

View file

@ -1,81 +0,0 @@
--!optimize 2
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib:Clone())
local ecs = jecs.World.new()
local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.World.new()
local A1 = Matter.component()
local A2 = Matter.component()
local A3 = Matter.component()
local A4 = Matter.component()
local A5 = Matter.component()
local A6 = Matter.component()
local A7 = Matter.component()
local A8 = Matter.component()
local B1 = ecr.component()
local B2 = ecr.component()
local B3 = ecr.component()
local B4 = ecr.component()
local B5 = ecr.component()
local B6 = ecr.component()
local B7 = ecr.component()
local B8 = ecr.component()
local C1 = ecs:component()
local C2 = ecs:component()
local C3 = ecs:component()
local C4 = ecs:component()
local C5 = ecs:component()
local C6 = ecs:component()
local C7 = ecs:component()
local C8 = ecs:component()
local E1 = mcs:component()
local E2 = mcs:component()
local E3 = mcs:component()
local E4 = mcs:component()
local E5 = mcs:component()
local E6 = mcs:component()
local E7 = mcs:component()
local E8 = mcs:component()
local registry2 = ecr.registry()
return {
ParameterGenerator = function()
return
end,
Functions = {
Mirror = function()
local e = mcs:entity()
for i = 1, 5000 do
mcs:set(e, E1, false)
mcs:set(e, E2, false)
mcs:set(e, E3, false)
mcs:set(e, E4, false)
mcs:set(e, E5, false)
mcs:set(e, E6, false)
mcs:set(e, E7, false)
mcs:set(e, E8, false)
end
end,
Jecs = function()
local e = ecs:entity()
for i = 1, 5000 do
ecs:set(e, C1, false)
ecs:set(e, C2, false)
ecs:set(e, C3, false)
ecs:set(e, C4, false)
ecs:set(e, C5, false)
ecs:set(e, C6, false)
ecs:set(e, C7, false)
ecs:set(e, C8, false)
end
end,
},
}

View file

@ -1,164 +0,0 @@
--!optimize 2
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matter@0.8.1"].matter)
local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr)
local newWorld = Matter.World.new()
local jecs = require(ReplicatedStorage.Lib)
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local ecs = jecs.World.new()
local A1 = Matter.component()
local A2 = Matter.component()
local A3 = Matter.component()
local A4 = Matter.component()
local A5 = Matter.component()
local A6 = Matter.component()
local A7 = Matter.component()
local A8 = Matter.component()
local B1 = ecr.component()
local B2 = ecr.component()
local B3 = ecr.component()
local B4 = ecr.component()
local B5 = ecr.component()
local B6 = ecr.component()
local B7 = ecr.component()
local B8 = ecr.component()
local D1 = ecs:component()
local D2 = ecs:component()
local D3 = ecs:component()
local D4 = ecs:component()
local D5 = ecs:component()
local D6 = ecs:component()
local D7 = ecs:component()
local D8 = ecs:component()
local E1 = mcs:entity()
local E2 = mcs:entity()
local E3 = mcs:entity()
local E4 = mcs:entity()
local E5 = mcs:entity()
local E6 = mcs:entity()
local E7 = mcs:entity()
local E8 = mcs:entity()
local registry2 = ecr.registry()
local function flip()
return math.random() >= 0.25
end
local N = 2 ^ 16 - 2
local archetypes = {}
local hm = 0
for i = 1, N do
local id = registry2.create()
local combination = ""
local n = newWorld:spawn()
local entity = ecs:entity()
local m = mcs:entity()
if flip() then
registry2:set(id, B1, { value = true })
ecs:set(entity, D1, { value = true })
newWorld:insert(n, A1({ value = true }))
mcs:set(m, E1, { value = 2 })
end
if flip() then
combination ..= "B"
registry2:set(id, B2, { value = true })
ecs:set(entity, D2, { value = true })
mcs:set(m, E2, { value = 2 })
newWorld:insert(n, A2({ value = true }))
end
if flip() then
combination ..= "C"
registry2:set(id, B3, { value = true })
ecs:set(entity, D3, { value = true })
mcs:set(m, E3, { value = 2 })
newWorld:insert(n, A3({ value = true }))
end
if flip() then
combination ..= "D"
registry2:set(id, B4, { value = true })
ecs:set(entity, D4, { value = true })
mcs:set(m, E4, { value = 2 })
newWorld:insert(n, A4({ value = true }))
end
if flip() then
combination ..= "E"
registry2:set(id, B5, { value = true })
ecs:set(entity, D5, { value = true })
mcs:set(m, E5, { value = 2 })
newWorld:insert(n, A5({ value = true }))
end
if flip() then
combination ..= "F"
registry2:set(id, B6, { value = true })
ecs:set(entity, D6, { value = true })
mcs:set(m, E6, { value = 2 })
newWorld:insert(n, A6({ value = true }))
end
if flip() then
combination ..= "G"
registry2:set(id, B7, { value = true })
ecs:set(entity, D7, { value = true })
mcs:set(m, E7, { value = 2 })
newWorld:insert(n, A7({ value = true }))
end
if flip() then
combination ..= "H"
registry2:set(id, B8, { value = true })
newWorld:insert(n, A8({ value = true }))
ecs:set(entity, D8, { value = true })
mcs:set(m, E8, { value = 2 })
end
if combination:find("BCDF") then
if not archetypes[combination] then
print(combination)
end
hm += 1
end
archetypes[combination] = true
end
print("TEST", hm)
local count = 0
for _, archetype in ecs:query(D2, D4, D6, D8):archetypes() do
count += #archetype.entities
end
print(count)
return {
ParameterGenerator = function()
return
end,
Functions = {
Matter = function()
for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do
end
end,
ECR = function()
for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do
end
end,
Jecs = function()
for entityId, firstComponent in ecs:query(D2, D4, D6, D8) do
end
end,
},
}

View file

@ -1,49 +0,0 @@
--!optimize 2
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local pair = jecs.pair
local ecs = jecs.World.new()
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local C1 = ecs:component()
local C2 = ecs:entity()
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
local C3 = ecs:entity()
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
local C4 = ecs:entity()
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
local E1 = mcs:component()
local E2 = mcs:entity()
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
local E3 = mcs:entity()
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
local E4 = mcs:entity()
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
return {
ParameterGenerator = function()
end,
Functions = {
Mirror = function()
local m = mcs:entity()
for i = 1, 100 do
mcs:add(m, E3)
mcs:remove(m, E3)
end
end,
Jecs = function()
local j = ecs:entity()
for i = 1, 100 do
ecs:add(j, C3)
ecs:remove(j, C3)
end
end,
},
}

View file

@ -1,33 +0,0 @@
--!optimize 2
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.Lib:Clone())
local mirror = require(ReplicatedStorage.mirror:Clone())
return {
ParameterGenerator = function()
local ecs = jecs.world()
ecs:range(1000, 20000)
local mcs = mirror.World.new()
return ecs, mcs
end,
Functions = {
Mirror = function(_, ecs, mcs)
for i = 1, 100 do
mcs:entity()
end
end,
Jecs = function(_, ecs, mcs)
for i = 1, 100 do
ecs:entity()
end
end,
},
}

View file

@ -1,11 +0,0 @@
[package]
name = "private/private"
version = "0.1.0-rc.6"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
exclude = ["**"]
[dev-dependencies]
Matter = "matter-ecs/matter@0.8.0"
ecr = "centau/ecr@0.8.0"

6
demo/.gitignore vendored
View file

@ -1,6 +0,0 @@
# Project place file
/example.rbxlx
# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock

View file

@ -1,15 +0,0 @@
# Demo
## Build with Rojo
To build the place, run the following commands from the root of the repository:
```bash
cd demo
rojo build -o "demo.rbxl"
```
Next, open `demo.rbxl` in Roblox Studio and start the Rojo server:
```bash
rojo serve
```

View file

@ -1,55 +0,0 @@
{
"name": "demo",
"emitLegacyScripts": false,
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "src/ReplicatedStorage",
"ecs": {
"$path": "../jecs.luau"
},
"Packages": {
"$path": "Packages"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"$path": "src/ServerScriptService"
},
"Workspace": {
"$properties": {
"FilteringEnabled": true
},
"Baseplate": {
"$className": "Part",
"$properties": {
"Anchored": true,
"Color": [0.38823, 0.37254, 0.38823],
"Locked": true,
"Position": [0, -10, 0],
"Size": [512, 20, 512]
}
}
},
"Lighting": {
"$properties": {
"Ambient": [0, 0, 0],
"Brightness": 2,
"GlobalShadows": true,
"Outlines": false,
"Technology": "Voxel"
}
},
"SoundService": {
"$properties": {
"RespectFilteringEnabled": true
}
},
"StarterPlayer": {
"StarterPlayerScripts": {
"$path": "src/StarterPlayer/StarterPlayerScripts"
}
}
}
}

View file

@ -1,28 +0,0 @@
local function collect<T...>(
signal: {
Connect: (RBXScriptSignal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
): () -> (T...)
local enqueued = {}
local i = 0
local connection = (signal :: any):Connect(function(...)
table.insert(enqueued, { ... })
i += 1
end)
return function(): any
if i == 0 then
return
end
i -= 1
local args: any = table.remove(enqueued, 1)
return unpack(args)
end, connection
end
return collect

View file

@ -1,36 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local types = require("./types")
local Networked = jecs.tag()
local NetworkedPair = jecs.tag()
local Renderable = jecs.component() :: jecs.Id<Instance>
jecs.meta(Renderable, Networked)
local Poison = jecs.component() :: jecs.Id<number>
jecs.meta(Poison, Networked)
local Health = jecs.component() :: jecs.Id<number>
jecs.meta(Health, Networked)
local Player = jecs.component() :: jecs.Id<Player>
jecs.meta(Player, Networked)
local components = {
Renderable = Renderable,
Player = Player,
Poison = Poison,
Health = Health,
Networked = Networked,
NetworkedPair = NetworkedPair,
}
for name, component in components do
jecs.meta(component, jecs.Name, name)
end
return components

View file

@ -1,13 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN
require(ReplicatedStorage.components)
local world = observers_add(jecs.world())
local systems = ReplicatedStorage.systems
SYSTEM(world, systems.receive_replication)
RUN(world)

View file

@ -1,190 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
type Observer<T...> = {
callback: (jecs.Entity) -> (),
query: jecs.Query<T...>,
}
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
-- deleted: (PatchedWorld, () -> ()) -> () -> (),
observer: (PatchedWorld, Observer<any>) -> (),
monitor: (PatchedWorld, Observer<any>) -> (),
}
local function observers_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity)
end
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
end
local function monitors_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnAdd)
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnRemove)
end
end
for _, term in terms do
world:added(term, emplaced)
world:removed(term, removed)
end
end
local function observers_add(world: jecs.World): PatchedWorld
local signals = {
added = {},
emplaced = {},
removed = {},
deleted = {}
}
world = world :: jecs.World & {[string]: any}
world.added = function(_, component, fn)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_add(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnAdd, on_add)
idr.hooks.on_add = on_add :: any
idr_r.hooks.on_add = on_add :: any
end
table.insert(listeners, fn)
end
world.changed = function(_, component, fn)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_change(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnChange, on_change)
idr.hooks.on_change = on_change :: any
idr_r.hooks.on_change = on_change :: any
end
table.insert(listeners, fn)
end
world.removed = function(_, component, fn)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_remove(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnRemove, on_remove)
idr.hooks.on_remove = on_remove :: any
idr_r.hooks.on_remove = on_remove :: any
end
table.insert(listeners, fn)
end
world.signals = signals
world.observer = observers_new
world.monitor = monitors_new
-- local world_delete = world.delete
-- world.deleted = function(_, fn)
-- local listeners = signals.deleted
-- table.insert(listeners, fn)
-- end
-- world.delete = function(world, entity)
-- world_delete(world, entity)
-- for _, fn in signals.deleted do
-- fn(entity)
-- end
-- end
return world :: PatchedWorld
end
return observers_add

View file

@ -1,50 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../ReplicatedStorage/types")
type Signal<T...> = {
Connect: (Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
type Remote<T...> = {
FireClient: (Remote<T...>, T...) -> (),
FireAllClients: (Remote<T...>, T...) -> (),
FireServer: (Remote<T...>) -> (),
OnServerEvent: {
Connect: (any, fn: (Player, T...) -> () ) -> ()
},
OnClientEvent: {
Connect: (any, fn: (T...) -> () ) -> ()
}
}
local function stream_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("RemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
local function datagram_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("UnreliableRemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
return {
input = datagram_ensure("input") :: Remote<string>,
replication = stream_ensure("replication") :: Remote<{
[string]: {
set: { types.Entity }?,
values: { any }?,
removed: { types.Entity }?
}
}>,
}

View file

@ -1,136 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jabby = require(ReplicatedStorage.Packages.jabby)
local jecs = require(ReplicatedStorage.ecs)
jabby.set_check_function(function() return true end)
local scheduler = jabby.scheduler.create("jabby scheduler")
jabby.register({
applet = jabby.applets.scheduler,
name = "Scheduler",
configuration = {
scheduler = scheduler,
},
})
local ContextActionService = game:GetService("ContextActionService")
local function create_widget(_, state: Enum.UserInputState)
local client = jabby.obtain_client()
if state ~= Enum.UserInputState.Begin then return end
client.spawn_app(client.apps.home, nil)
end
local RunService = game:GetService("RunService")
local System = jecs.component() :: jecs.Id<{
fn: () -> (),
name: string,
}>
local DependsOn = jecs.component()
local Phase = jecs.tag()
local Event = jecs.component() :: jecs.Id<RBXScriptSignal>
local pair = jecs.pair
local types = require(ReplicatedStorage.types)
local function ECS_PHASE(world, after: types.Entity)
local phase = world:entity()
world:add(phase, Phase)
if after then
local dependency = pair(DependsOn, after)
world:add(phase, dependency)
end
return phase
end
local Heartbeat = jecs.tag()
jecs.meta(Heartbeat, Phase)
jecs.meta(Heartbeat, Event, RunService.Heartbeat)
local PreSimulation = jecs.tag()
jecs.meta(PreSimulation, Phase)
jecs.meta(PreSimulation, Event, RunService.PreSimulation)
local PreAnimation = jecs.tag()
jecs.meta(PreAnimation, Phase)
jecs.meta(PreAnimation, Event, RunService.PreAnimation)
local PreRender = jecs.tag()
jecs.meta(PreRender, Phase)
jecs.meta(PreRender, Event, RunService.PreRender)
local function ECS_SYSTEM(world: types.World, mod: ModuleScript, phase: types.Entity?)
local system = world:entity()
local p = phase or Heartbeat
local fn = require(mod) :: (...any) -> ()
world:set(system, System, {
fn = fn(world, 0) or fn,
name = mod.Name,
})
local depends_on = DependsOn :: jecs.Entity
world:add(system, pair(depends_on, p))
end
local function find_systems_w_phase(world: types.World, systems, phase: types.Entity)
local phase_name = world:get(phase, jecs.Name) :: string
for _, s in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, {
id = scheduler:register_system({
phase = phase_name,
name = s.name,
}),
fn = s.fn
})
end
for after in world:query(Phase, pair(DependsOn, phase)) do
find_systems_w_phase(world, systems, after)
end
return systems
end
local function ECS_RUN(world: types.World)
jabby.register({
applet = jabby.applets.world,
name = "MyWorld",
configuration = {
world = world,
},
})
if RunService:IsClient() then
ContextActionService:BindAction("Open Jabby Home", create_widget, false, Enum.KeyCode.F4)
end
for phase, event in world:query(Event, Phase) do
local systems = find_systems_w_phase(world, {}, phase)
event:Connect(function(...)
for _, system in systems do
scheduler:run(system.id, system.fn, world, ...)
end
end)
end
end
return {
PHASE = ECS_PHASE,
SYSTEM = ECS_SYSTEM,
RUN = ECS_RUN,
phases = {
Heartbeat = Heartbeat,
PreSimulation = PreSimulation,
PreAnimation = PreAnimation,
PreRender = PreRender
},
components = {
System = System,
DependsOn = DependsOn,
Phase = Phase,
Event = Event,
}
}

View file

@ -1,86 +0,0 @@
local types = require("../types")
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local remotes = require("../remotes")
local collect = require("../collect")
local client_ids = {}
local function ecs_map_get(world, id)
local deserialised_id = client_ids[id]
if not deserialised_id then
if world:has(id, jecs.Name) then
deserialised_id = world:entity(id)
else
deserialised_id = world:entity()
end
client_ids[id] = deserialised_id
end
-- local deserialised_id = client_ids[id]
-- if not deserialised_id then
-- if world:has(id, jecs.Name) then
-- deserialised_id = world:entity(id)
-- else
-- if world:exists(id) then
-- deserialised_id = world:entity()
-- else
-- deserialised_id = world:entity(id)
-- end
-- end
-- client_ids[id] = deserialised_id
-- end
return deserialised_id
end
local function ecs_make_alive_id(world, id)
local rel = jecs.ECS_PAIR_FIRST(id)
local tgt = jecs.ECS_PAIR_SECOND(id)
rel = ecs_map_get(world, rel)
tgt = ecs_map_get(world, tgt)
return jecs.pair(rel, tgt)
end
local snapshots = collect(remotes.replication.OnClientEvent)
return function(world: types.World)
for snapshot in snapshots do
for id, map in snapshot do
id = tonumber(id)
if jecs.IS_PAIR(id) then
id = ecs_make_alive_id(world, id)
end
local set = map.set
if set then
if jecs.is_tag(world, id) then
for _, entity in set do
entity = ecs_map_get(world, entity)
world:add(entity, id)
end
else
local values = map.values
for i, entity in set do
entity = ecs_map_get(world, entity)
world:set(entity, id, values[i])
end
end
end
local removed = map.removed
if removed then
for i, e in removed do
if not world:contains(e) then
continue
end
world:remove(e, id)
end
end
end
end
end

View file

@ -1,15 +0,0 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local observers_add = require("../ReplicatedStorage/observers_add")
export type World = typeof(observers_add(jecs.world()))
export type Entity = jecs.Entity
export type Id<T> = jecs.Id<T>
export type Snapshot = {
[string]: {
set: { jecs.Entity }?,
values: { any }?,
removed: { jecs.Entity }?
}
}
return {}

View file

@ -1,19 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN
require(ReplicatedStorage.components)
local world = observers_add(jecs.world())
local systems = ServerScriptService.systems
SYSTEM(world, systems.replication)
SYSTEM(world, systems.players_added)
SYSTEM(world, systems.poison_hurts)
SYSTEM(world, systems.life_is_painful)
RUN(world, 0)

View file

@ -1,12 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
local types = require(ReplicatedStorage.types)
return function(world: types.World, dt: number)
for e in world:query(ct.Player):without(ct.Health) do
world:set(e, ct.Health, 100)
end
for e in world:query(ct.Player, ct.Health):without(ct.Poison) do
world:set(e, ct.Poison, 10)
end
end

View file

@ -1,20 +0,0 @@
local collect = require("../../ReplicatedStorage/collect")
local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components")
local Players = game:GetService("Players")
local player_added = collect(Players.PlayerAdded)
return function(world: types.World, dt: number)
for player in player_added do
local entity = world:entity()
world:set(entity, ct.Player, player)
end
for entity, player in world:query(ct.Player):without(ct.Renderable) do
local character = player.Character
if character then
if not character.Parent then
world:set(entity, ct.Renderable, character)
end
end
end

View file

@ -1,12 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
return function(world, dt)
for e, poison, health in world:query(ct.Poison, ct.Health) do
local health_after_tick = health - poison * dt * 0.05
if health_after_tick < 0 then
world:remove(e, ct.Health)
continue
end
world:set(e, ct.Health, health_after_tick)
end
end

View file

@ -1,190 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components")
local jecs = require(ReplicatedStorage.ecs)
local remotes = require("../../ReplicatedStorage/remotes")
local components = ct :: {[string]: jecs.Entity }
return function(world: ty.World)
--- integration test
-- for _ = 1, 10 do
-- local e = world:entity()
-- world:set(e, ct.TestA, true)
-- end
local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }}
local networked_components = {}
local networked_pairs = {}
for component in world:each(ct.Networked) do
local name = world:get(component, jecs.Name) :: string
if components[name] == nil then
continue
end
storages[component] = {}
table.insert(networked_components, component)
end
for relation in world:each(ct.NetworkedPair) do
local name = world:get(relation, jecs.Name) :: string
if not components[name] then
continue
end
table.insert(networked_pairs, relation)
end
for _, component in networked_components do
local name = world:get(component, jecs.Name) :: string
if not components[name] then
error(`Networked Component (%id{component}%name{name})`)
end
local is_tag = jecs.is_tag(world, component)
local storage = storages[component]
if is_tag then
world:added(component, function(entity)
storage[entity] = true
end)
else
world:added(component, function(entity, _, value)
storage[entity] = value
end)
world:changed(component, function(entity, _, value)
storage[entity] = value
end)
end
world:removed(component, function(entity)
storage[entity] = "jecs.Remove"
end)
end
for _, relation in networked_pairs do
world:added(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
if is_tag then
storage[entity] = true
else
storage[entity] = value
end
end)
world:changed(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
if is_tag then
return
end
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = value
end)
world:removed(relation, function(entity, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = "jecs.Remove"
end)
end
local players_added = collect(Players.PlayerAdded)
return function()
local snapshot_lazy: ty.Snapshot
local set_ids_lazy: { jecs.Entity }
for player in players_added do
if not snapshot_lazy then
snapshot_lazy, set_ids_lazy = {}, {}
for component, storage in storages do
local set_values = {}
local set_n = 0
local q = world:query(component)
local is_tag = jecs.is_tag(world, component)
for _, archetype in q:archetypes() do
local entities = archetype.entities
local entities_len = #entities
table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy)
if is_tag then
set_values = table.create(entities_len, true)
else
local column = archetype.columns[archetype.records[component]]
table.move(column, 1, entities_len, set_n + 1, set_values)
end
set_n += entities_len
end
local set = table.move(set_ids_lazy, 1, set_n, 1, {})
snapshot_lazy[tostring(component)] = {
set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil,
}
end
end
remotes.replication:FireClient(player, snapshot_lazy)
end
local snapshot = {} :: ty.Snapshot
local set_ids = {}
local removed_ids = {}
for component, storage in storages do
local set_values = {} :: { any }
local set_n = 0
local removed_n = 0
for e, v in storage do
if v ~= "jecs.Remove" then
set_n += 1
set_ids[set_n] = e
set_values[set_n] = v or true
elseif not world:contains(e) then
removed_n += 1
removed_ids[removed_n] = e
end
end
table.clear(storage)
local dirty = false
if set_n > 0 or removed_n > 0 then
dirty = true
end
if dirty then
local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity }
local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity }
snapshot[tostring(component)] = {
set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil,
removed = if removed_n > 0 then removed else nil
}
end
end
if next(snapshot) ~= nil then
remotes.replication:FireAllClients(snapshot)
end
end
end

View file

@ -1,8 +0,0 @@
[package]
name = "marcus/demo"
version = "0.1.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
[dependencies]
jabby = "alicesaidhi/jabby@0.2.2"

310
jecs.d.ts vendored
View file

@ -1,310 +0,0 @@
/**
* A unique identifier in the world, entity.
* The generic type T defines the data type when this entity is used as a component
*/
export type Entity<TData = unknown> = number & {
readonly __nominal_Entity: unique symbol;
readonly __type_TData: TData;
};
/**
* An entity with no associated data when used as a component
*/
export type Tag = Entity<undefined>;
/**
* A pair of entities:
* - `pred` is the type of the "predicate" entity.
* - `obj` is the type of the "object" entity.
*/
export type Pair<P = unknown, O = unknown> = number & {
readonly __nominal_Pair: unique symbol;
readonly __pred: P;
readonly __obj: O;
};
/**
* An `Id` can be either a single Entity or a Pair of Entities.
* By providing `TData`, you can specifically require an Id that yields that type.
*/
export type Id<TData = unknown> = Entity<TData> | Pair<TData, unknown> | Pair<undefined, TData>;
export type InferComponent<E> = E extends Entity<infer D>
? D
: E extends Pair<infer P, infer O>
? P extends undefined
? O
: P
: never;
type FlattenTuple<T extends unknown[]> = T extends [infer U] ? U : LuaTuple<T>;
type Nullable<T extends unknown[]> = { [K in keyof T]: T[K] | undefined };
type InferComponents<A extends Id[]> = { [K in keyof A]: InferComponent<A[K]> };
type ArchetypeId = number;
type Column = unknown[];
export type Archetype = {
id: number;
types: number[];
type: string;
entities: number[];
columns: Column[];
records: number[];
counts: number[];
};
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
export type CachedQuery<T extends unknown[]> = {
/**
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
*/
iter(): Iter<T>;
/**
* Returns the matched archetypes of the query
* @returns An array of archetypes of the query
*/
archetypes(): Archetype[];
} & Iter<T>;
export type Query<T extends unknown[]> = {
/**
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
*/
iter(): Iter<T>;
/**
* Creates and returns a cached version of this query for efficient reuse.
* Call refinement methods (with/without) on the query before caching.
* @returns A cached query
*/
cached(): CachedQuery<T>;
/**
* Modifies the query to include specified components.
* @param components The components to include.
* @returns A new Query with the inclusion applied.
*/
with(...components: Id[]): Query<T>;
/**
* Modifies the Query to exclude specified components.
* @param components The components to exclude.
* @returns A new Query with the exclusion applied.
*/
without(...components: Id[]): Query<T>;
/**
* Returns the matched archetypes of the query
* @returns An array of archetypes of the query
*/
archetypes(): Archetype[];
} & Iter<T>;
export class World {
/**
* Creates a new World.
*/
constructor();
/**
* Enforces a check for entities to be created within a desired range.
* @param range_begin The starting point
* @param range_end The end point (optional)
*/
range(range_begin: number, range_end?: number): void;
/**
* Creates a new entity.
* @returns An entity (Tag) with no data.
*/
entity(): Tag;
entity<T extends Entity>(id: T): InferComponent<T> extends undefined ? Tag : T;
/**
* Creates a new entity in the first 256 IDs, typically used for static
* components that need fast access.
* @returns A typed Entity with `TData`.
*/
component<TData = unknown>(): Entity<TData>;
/**
* Gets the target of a relationship. For example, if we say
* `world.target(entity, ChildOf)`, this returns the parent entity.
* @param entity The entity using a relationship pair.
* @param relation The "relationship" component/tag (e.g., ChildOf).
* @param index If multiple targets exist, specify an index. Defaults to 0.
*/
target(entity: Entity, relation: Entity, index?: number): Entity | undefined;
/**
* Deletes an entity (and its components/relationships) from the world entirely.
* @param entity The entity to delete.
*/
delete(entity: Entity): void;
/**
* Adds a component (with no value) to the entity.
* @param entity The target entity.
* @param component The component (or tag) to add.
*/
add<C>(entity: Entity, component: undefined extends InferComponent<C> ? C : Id<undefined>): void;
/**
* Installs a hook on the given component.
* @param component The target component.
* @param hook The hook to install.
* @param value The hook callback.
*/
set<T>(component: Entity<T>, hook: StatefulHook, value: (e: Entity<T>, id: Id<T>, data: T) => void): void;
set<T>(component: Entity<T>, hook: StatelessHook, value: (e: Entity<T>, id: Id<T>) => void): void;
/**
* Assigns a value to a component on the given entity.
* @param entity The target entity.
* @param component The component definition (could be a Pair or Entity).
* @param value The value to store with that component.
*/
set<E extends Id<unknown>>(entity: Entity, component: E, value: InferComponent<E>): void;
/**
* Cleans up the world by removing empty archetypes and rebuilding the archetype collections.
* This helps maintain memory efficiency by removing unused archetype definitions.
*/
cleanup(): void;
/**
* Clears all components and relationships from the given entity, but
* does not delete the entity from the world.
* @param entity The entity to clear.
*/
clear(entity: Entity): void;
/**
* Removes a component from the given entity.
* @param entity The target entity.
* @param component The component to remove.
*/
remove(entity: Entity, component: Id): void;
/**
* Retrieves the values of up to 4 components on a given entity. Missing
* components will return `undefined`.
* @param entity The entity to query.
* @param components Up to 4 components/tags to retrieve.
* @returns A tuple of data (or a single value), each possibly undefined.
*/
get<T extends [Id] | [Id, Id] | [Id, Id, Id] | [Id, Id, Id, Id]>(
entity: Entity,
...components: T
): FlattenTuple<Nullable<InferComponents<T>>>;
/**
* Returns `true` if the given entity has all of the specified components.
* A maximum of 4 components can be checked at once.
* @param entity The entity to check.
* @param components Upto 4 components to check for.
*/
has(entity: Entity, ...components: Id[]): boolean;
/**
* Gets the parent (the target of a `ChildOf` relationship) for an entity,
* if such a relationship exists.
* @param entity The entity whose parent is queried.
*/
parent(entity: Entity): Entity | undefined;
/**
* Checks if an entity exists in the world.
* @param entity The entity to verify.
*/
contains(entity: Entity): boolean;
/**
* Checks if an entity with the given ID is currently alive, ignoring its generation.
* @param entity The entity to verify.
* @returns boolean true if any entity with the given ID exists (ignoring generation), false otherwise
*/
exists(entity: Entity): boolean;
/**
* Returns an iterator that yields all entities that have the specified component or relationship.
* @param id The component or relationship ID to search for
* @returns An iterator function that yields entities
*/
each(id: Id): IterableFunction<Entity>;
/**
* Returns an iterator that yields all child entities of the specified parent entity.
* Uses the ChildOf relationship internally.
* @param parent The parent entity to get children for
* @returns An iterator function that yields child entities
*/
children(parent: Entity): IterableFunction<Entity>;
/**
* Searches the world for entities that match specified components.
* @param components The list of components to query.
* @returns A Query object to iterate over results.
*/
query<T extends Id[]>(...components: T): Query<InferComponents<T>>;
}
export function component<T>(): Entity<T>;
export function tag(): Tag;
// note: original types had id: Entity, id: Id<T>, which does not work with TS.
export function meta<T>(e: Entity, id: Id<T>, value?: T): Entity<T>;
export function is_tag(world: World, id: Id): boolean;
/**
* Creates a composite key (pair)
* @param pred The first entity (predicate)
* @param obj The second entity (object)
* @returns The composite key (pair)
*/
export function pair<P, O>(pred: Entity<P>, obj: Entity<O>): Pair<P, O>;
/**
* Checks if the entity is a composite key (pair)
* @param value The entity to check
* @returns If the entity is a pair
*/
export function IS_PAIR(value: Id): value is Pair;
/**
* Gets the first entity (predicate) of a pair
* @param pair The pair to get the first entity from
* @returns The first entity (predicate) of the pair
*/
export function pair_first<P, O>(world: World, p: Pair<P, O>): Entity<P>;
/**
* Gets the second entity (object) of a pair
* @param pair The pair to get the second entity from
* @returns The second entity (object) of the pair
*/
export function pair_second<P, O>(world: World, p: Pair<P, O>): Entity<O>;
type StatefulHook = Entity<<T>(e: Entity<T>, id: Id<T>, data: T) => void> & {
readonly __nominal_StatefulHook: unique symbol,
}
type StatelessHook = Entity<<T>(e: Entity<T>, id: Id<T>) => void> & {
readonly __nominal_StatelessHook: unique symbol,
}
export declare const OnAdd: StatefulHook;
export declare const OnRemove: StatelessHook;
export declare const OnChange: StatefulHook;
export declare const ChildOf: Tag;
export declare const Wildcard: Entity;
export declare const w: Entity;
export declare const OnDelete: Tag;
export declare const OnDeleteTarget: Tag;
export declare const Delete: Tag;
export declare const Remove: Tag;
export declare const Name: Entity<string>;
export declare const Rest: Entity;

File diff suppressed because it is too large Load diff

View file

@ -1,659 +0,0 @@
--!optimize 2
--!native
--!strict
--draft 4
type i53 = number
type i24 = number
type Ty = { i53 }
type ArchetypeId = number
type Column = { any }
type Archetype = {
id: number,
edges: {
[i24]: {
add: Archetype,
remove: Archetype,
},
},
types: Ty,
type: string | number,
entities: { number },
columns: { Column },
records: {},
}
type Record = {
archetype: Archetype,
row: number,
}
type EntityIndex = { [i24]: Record }
type ComponentIndex = { [i24]: ArchetypeMap }
type ArchetypeRecord = number
type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord }, size: number }
type Archetypes = { [ArchetypeId]: Archetype }
type ArchetypeDiff = {
added: Ty,
removed: Ty,
}
local HI_COMPONENT_ID = 256
local ON_ADD = HI_COMPONENT_ID + 1
local ON_REMOVE = HI_COMPONENT_ID + 2
local ON_SET = HI_COMPONENT_ID + 3
local REST = HI_COMPONENT_ID + 4
local function transitionArchetype(
entityIndex: EntityIndex,
to: Archetype,
destinationRow: i24,
from: Archetype,
sourceRow: i24
)
local columns = from.columns
local sourceEntities = from.entities
local destinationEntities = to.entities
local destinationColumns = to.columns
local tr = to.records
local types = from.types
for i, column in columns do
-- 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 targetColumn = destinationColumns[tr[types[i]]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if targetColumn then
targetColumn[destinationRow] = column[sourceRow]
end
-- If the entity is the last row in the archetype then swapping it would be meaningless.
local last = #column
if sourceRow ~= last then
-- Swap rempves columns to ensure there are no holes in the archetype.
column[sourceRow] = column[last]
end
column[last] = nil
end
-- Move the entity from the source to the destination archetype.
local atSourceRow = sourceEntities[sourceRow]
destinationEntities[destinationRow] = atSourceRow
entityIndex[atSourceRow].row = destinationRow
-- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped.
local movedAway = #sourceEntities
if sourceRow ~= movedAway then
local atMovedAway = sourceEntities[movedAway]
sourceEntities[sourceRow] = atMovedAway
entityIndex[atMovedAway].row = sourceRow
end
sourceEntities[movedAway] = nil
end
local function archetypeAppend(entity: number, archetype: Archetype): number
local entities = archetype.entities
local length = #entities + 1
entities[length] = entity
return length
end
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
local row = archetypeAppend(entityId, archetype)
record.archetype = archetype
record.row = row
return record
end
local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype)
local sourceRow = record.row
local from = record.archetype
local destinationRow = archetypeAppend(entityId, to)
transitionArchetype(entityIndex, to, destinationRow, from, sourceRow)
record.archetype = to
record.row = destinationRow
end
local function hash(arr): string | number
return table.concat(arr, "_")
end
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
local destinationIds = to.types
local records = to.records
local id = to.id
for i, destinationId in destinationIds do
local archetypesMap = componentIndex[destinationId]
if not archetypesMap then
archetypesMap = { size = 0, sparse = {} }
componentIndex[destinationId] = archetypesMap
end
archetypesMap.sparse[id] = i
records[destinationId] = i
end
end
local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype
local ty = hash(types)
local id = world.nextArchetypeId + 1
world.nextArchetypeId = id
local length = #types
local columns = table.create(length) :: { any }
for index in types do
columns[index] = {}
end
local archetype = {
columns = columns,
edges = {},
entities = {},
id = id,
records = {},
type = ty,
types = types,
}
world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype
if length > 0 then
createArchetypeRecords(world.componentIndex, archetype, prev)
end
return archetype
end
local World = {}
World.__index = World
function World.new()
local self = setmetatable({
archetypeIndex = {},
archetypes = {},
componentIndex = {},
entityIndex = {},
hooks = {
[ON_ADD] = {},
},
nextArchetypeId = 0,
nextComponentId = 0,
nextEntityId = 0,
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
}, World)
return self
end
local function emit(world, eventDescription)
local event = eventDescription.event
table.insert(world.hooks[event], {
archetype = eventDescription.archetype,
ids = eventDescription.ids,
offset = eventDescription.offset,
otherArchetype = eventDescription.otherArchetype,
})
end
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
if #added > 0 then
emit(world, {
archetype = archetype,
event = ON_ADD,
ids = added,
offset = row,
otherArchetype = otherArchetype,
})
end
end
export type World = typeof(World.new())
local function ensureArchetype(world: World, types, prev)
if #types < 1 then
return world.ROOT_ARCHETYPE
end
local ty = hash(types)
local archetype = world.archetypeIndex[ty]
if archetype then
return archetype
end
return archetypeOf(world, types, prev)
end
local function findInsert(types: { i53 }, toAdd: i53)
for i, id in types do
if id == toAdd then
return -1
end
if id > toAdd then
return i
end
end
return #types + 1
end
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
local types = node.types
-- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array.
local at = findInsert(types, componentId)
if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it
-- directly instead of needing to hash types for a lookup to the archetype.
return node
end
local destinationType = table.clone(node.types)
table.insert(destinationType, at, componentId)
return ensureArchetype(world, destinationType, node)
end
local function ensureEdge(archetype: Archetype, componentId: i53)
local edges = archetype.edges
local edge = edges[componentId]
if not edge then
edge = {} :: any
edges[componentId] = edge
end
return edge
end
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
if not from then
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
if not ROOT_ARCHETYPE then
ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
end
from = ROOT_ARCHETYPE
end
local edge = ensureEdge(from, componentId)
local add = edge.add
if not add then
-- Save an edge using the component ID to the archetype to allow
-- faster traversals to adjacent archetypes.
add = findArchetypeWith(world, from, componentId)
edge.add = add :: never
end
return add
end
local function ensureRecord(entityIndex, entityId: i53): Record
local record = entityIndex[entityId]
if not record then
record = {}
entityIndex[entityId] = record
end
return record :: Record
end
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
local record = ensureRecord(world.entityIndex, entityId)
local from = record.archetype
local to = archetypeTraverseAdd(world, componentId, from)
if from == to then
-- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly.
local archetypeRecord = to.records[componentId]
from.columns[archetypeRecord][record.row] = data
-- Should fire an OnSet event here.
return
end
if from then
-- If there was a previous archetype, then the entity needs to move the archetype
moveEntity(world.entityIndex, entityId, record, to)
else
if #to.types > 0 then
-- When there is no previous archetype it should create the archetype
newEntity(entityId, record, to)
onNotifyAdd(world, to, from, record.row, { componentId })
end
end
local archetypeRecord = to.records[componentId]
to.columns[archetypeRecord][record.row] = data
end
local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
local edge = ensureEdge(from, componentId)
local remove = edge.remove
if not remove then
local to = table.clone(from.types)
table.remove(to, table.find(to, componentId))
remove = ensureArchetype(world, to, from)
edge.remove = remove :: never
end
return remove
end
function World.remove(world: World, entityId: i53, componentId: i53)
local entityIndex = world.entityIndex
local record = ensureRecord(entityIndex, entityId)
local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
moveEntity(entityIndex, entityId, record, destinationArchetype)
end
end
-- Keeping the function as small as possible to enable inlining
local function get(record: Record, componentId: i24)
local archetype = record.archetype
local archetypeRecord = archetype.records[componentId]
if not archetypeRecord then
return nil
end
return archetype.columns[archetypeRecord][record.row]
end
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
local id = entityId
local record = world.entityIndex[id]
if not record then
return nil
end
local va = get(record, a)
if b == nil then
return va
elseif c == nil then
return va, get(record, b)
elseif d == nil then
return va, get(record, b), get(record, c)
elseif e == nil then
return va, get(record, b), get(record, c), get(record, d)
else
error("args exceeded")
end
end
-- the less creation the better
local function actualNoOperation() end
local function noop(_self: Query, ...: i53): () -> (number, ...any)
return actualNoOperation :: any
end
local EmptyQuery = {
__iter = noop,
without = noop,
}
EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query
-- breaking?
if (...) == nil then
error("Missing components")
end
local compatibleArchetypes = {}
local length = 0
local components = { ... }
local archetypes = world.archetypes
local queryLength = #components
local firstArchetypeMap
local componentIndex = world.componentIndex
for _, componentId in components do
local map = componentIndex[componentId]
if not map then
return EmptyQuery
end
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
firstArchetypeMap = map
end
end
for id in firstArchetypeMap.sparse do
local archetype = archetypes[id]
local archetypeRecords = archetype.records
local indices = {}
local skip = false
for i, componentId in components do
local index = archetypeRecords[componentId]
if not index then
skip = true
break
end
indices[i] = index
end
if skip then
continue
end
length += 1
compatibleArchetypes[length] = { archetype, indices }
end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not lastArchetype then
return EmptyQuery
end
local preparedQuery = {}
preparedQuery.__index = preparedQuery
function preparedQuery:without(...)
local withoutComponents = { ... }
for i = #compatibleArchetypes, 1, -1 do
local archetype = compatibleArchetypes[i][1]
local records = archetype.records
local shouldRemove = false
for _, componentId in withoutComponents do
if records[componentId] then
shouldRemove = true
break
end
end
if shouldRemove then
table.remove(compatibleArchetypes, i)
end
end
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not lastArchetype then
return EmptyQuery
end
return self
end
local lastRow
local queryOutput = {}
function preparedQuery:__iter()
return function()
local archetype = compatibleArchetype[1]
local row = next(archetype.entities, lastRow)
while row == nil do
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
if lastArchetype == nil then
return
end
archetype = compatibleArchetype[1]
row = next(archetype.entities, row)
end
lastRow = row
local entityId = archetype.entities[row :: number]
local columns = archetype.columns
local tr = compatibleArchetype[2]
if queryLength == 1 then
return entityId, columns[tr[1]][row]
elseif queryLength == 2 then
return entityId, columns[tr[1]][row], columns[tr[2]][row]
elseif queryLength == 3 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
elseif queryLength == 4 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
elseif queryLength == 5 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row]
elseif queryLength == 6 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row]
elseif queryLength == 7 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row]
elseif queryLength == 8 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row],
columns[tr[8]][row]
end
for i in components do
queryOutput[i] = columns[tr[i]][row]
end
return entityId, unpack(queryOutput, 1, queryLength)
end
end
return setmetatable({}, preparedQuery) :: any
end
function World.component(world: World)
local componentId = world.nextComponentId + 1
if componentId > HI_COMPONENT_ID then
-- IDs are partitioned into ranges because component IDs are not nominal,
-- so it needs to error when IDs intersect into the entity range.
error("Too many components, consider using world:entity() instead to create components.")
end
world.nextComponentId = componentId
return componentId
end
function World.entity(world: World)
local nextEntityId = world.nextEntityId + 1
world.nextEntityId = nextEntityId
return nextEntityId + REST
end
function World.delete(world: World, entityId: i53)
local entityIndex = world.entityIndex
local record = entityIndex[entityId]
moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
-- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from
-- the entities array and delete the record. We know there won't be the hole since
-- we are always removing the last row.
--world.ROOT_ARCHETYPE.entities[record.row] = nil
--entityIndex[entityId] = nil
end
function World.observer(world: World, ...)
local componentIds = { ... }
local idsCount = #componentIds
local hooks = world.hooks
return {
event = function(event)
local hook = hooks[event]
hooks[event] = nil
local last, change
return function()
last, change = next(hook, last)
if not last then
return
end
local matched = false
local ids = change.ids
while not matched do
local skip = false
for _, id in ids do
if not table.find(componentIds, id) then
skip = true
break
end
end
if skip then
last, change = next(hook, last)
ids = change.ids
continue
end
matched = true
end
local queryOutput = table.create(idsCount)
local row = change.offset
local archetype = change.archetype
local columns = archetype.columns
local archetypeRecords = archetype.records
for index, id in componentIds do
queryOutput[index] = columns[archetypeRecords[id]][row]
end
return archetype.entities[row], unpack(queryOutput, 1, idsCount)
end
end,
}
end
return table.freeze({
World = World,
ON_ADD = ON_ADD,
ON_REMOVE = ON_REMOVE,
ON_SET = ON_SET,
})

4464
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,47 +0,0 @@
{
"name": "@rbxts/jecs",
"version": "0.6.0",
"description": "Stupidly fast Entity Component System",
"main": "jecs.luau",
"repository": {
"type": "git",
"url": "git+https://github.com/ukendio/jecs.git"
},
"keywords": [],
"author": "Ukendio",
"contributors": [
"Ukendio",
"EncodedVenom"
],
"homepage": "https://github.com/ukendio/jecs",
"license": "MIT",
"types": "jecs.d.ts",
"files": [
"jecs.luau",
"jecs.d.ts",
"LICENSE.md",
"README.md"
],
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@rbxts/compiler-types": "^2.3.0-types.1",
"@rbxts/types": "^1.0.781",
"@typescript-eslint/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^5.8.0",
"eslint": "^8.5.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-roblox-ts": "^0.0.32",
"prettier": "^2.5.1",
"roblox-ts": "^3.0.0",
"typescript": "^5.4.2",
"vitepress": "^1.3.0"
},
"scripts": {
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
}
}

View file

@ -1,6 +0,0 @@
[tools]
wally = "upliftgames/wally@0.3.2"
rojo = "rojo-rbx/rojo@7.4.4"
stylua = "johnnymorganz/stylua@2.0.1"
Blink = "1Axen/Blink@0.14.1"
wally-package-types = "JohnnyMorganz/wally-package-types@1.4.2"

View file

@ -1,136 +0,0 @@
local jecs = require("@jecs")
local testkit = require("@testkit")
local test = testkit.test()
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
local observers_add = require("@addons/observers")
TEST("addons/observers", function()
local world = observers_add(jecs.world())
local Test = world:component() :: jecs.Id<number>
type Q<T...> = () -> (jecs.Entity, T...)
local query:
(<A>(jecs.World, jecs.Id<A>) -> Q<A>)
& (<A, B>(jecs.World, jecs.Id<A>, jecs.Id<B>) -> Q<A, B>)
& (<A, B, C>(jecs.World, jecs.Id<A>, jecs.Id<B>, jecs.Id<C>) -> Q<A, B, C>)
= 1 :: never
for e, a in query(world, Test) do
end
do CASE "Should work even if set after the component has been used"
local A = world:component()
world:set(world:entity(), A, 2)
local ran = true
world:added(A, function()
ran = false
end)
local entity = world:entity()
world:set(entity, A, 3)
CHECK(ran)
end
do CASE "Should not override hook"
local A = world:component()
local count = 1
local function counter()
count += 2
end
world:set(A, jecs.OnAdd, counter)
world:added(A, counter)
world:set(world:entity(), A, false)
CHECK(count == 3)
world:set(world:entity(), A, false)
CHECK(count == 5)
end
do CASE "Ensure ordering between signals and observers"
local A = world:component()
local B = world:component()
local count = 1
local function counter()
count += 2
end
world:observer(world:query(A, B), counter)
world:added(A, counter)
world:added(A, counter)
local e = world:entity()
world:add(e, A)
CHECK(count == 3)
world:add(e, B)
CHECK(count == 4)
end
do CASE "Rematch entities in observers"
local A = world:component()
local count = 1
local function counter()
count += 2
end
world:observer(world:query(A), counter)
local e = world:entity()
world:set(e, A, false)
CHECK(count == 2)
world:remove(e, A)
CHECK(count == 2)
world:set(e, A, false)
CHECK(count == 3)
world:set(e, A, false)
CHECK(count == 4)
end
do CASE "Don't report changed components in monitor"
local A = world:component()
local count = 1
local function counter()
count += 2
end
world:monitor(world:query(A), counter)
local e = world:entity()
world:set(e, A, false)
CHECK(count == 2)
world:remove(e, A)
CHECK(count == 3)
world:set(e, A, false)
CHECK(count == 4)
world:set(e, A, false)
CHECK(count == 4)
end
do CASE "Call off pairs"
local A = world:component()
local callcount = 1
world:added(A, function(entity)
callcount += 2
end)
world:added(A, function(entity)
callcount += 2
end)
local e = world:entity()
local e2 = world:entity()
world:add(e2, jecs.pair(A, e))
world:add(e, jecs.pair(A, e2))
CHECK(callcount == 5)
end
end)
return FINISH()

View file

@ -1,158 +0,0 @@
local c = {
white_underline = function(s: any)
return `\27[1;4m{s}\27[0m`
end,
white = function(s: any)
return `\27[37;1m{s}\27[0m`
end,
green = function(s: any)
return `\27[32;1m{s}\27[0m`
end,
red = function(s: any)
return `\27[31;1m{s}\27[0m`
end,
yellow = function(s: any)
return `\27[33;1m{s}\27[0m`
end,
red_highlight = function(s: any)
return `\27[41;1;30m{s}\27[0m`
end,
green_highlight = function(s: any)
return `\27[42;1;30m{s}\27[0m`
end,
gray = function(s: any)
return `\27[30;1m{s}\27[0m`
end,
}
local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
type i53 = number
type i24 = number
local function ECS_ENTITY_T_LO(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
end
local function ECS_GENERATION(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
end
local ECS_ID = ECS_ENTITY_T_LO
local function ECS_COMBINE(source: number, target: number): i53
return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
end
local function ECS_GENERATION_INC(e: i53)
if e > ECS_ENTITY_MASK then
local flags = e // ECS_ID_FLAGS_MASK
local id = flags // ECS_ENTITY_MASK
local generation = flags % ECS_GENERATION_MASK
local next_gen = generation + 1
if next_gen > ECS_GENERATION_MASK then
return id
end
return ECS_COMBINE(id, next_gen) + flags
end
return ECS_COMBINE(e, 1)
end
local function bl()
print("")
end
local function pe(e)
local gen = ECS_GENERATION(e)
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`)
end
local function dprint(tbl: { [number]: number })
bl()
print("--------")
for i, e in tbl do
print("| "..pe(e).." |")
print("--------")
end
bl()
end
local max_id = 0
local alive_count = 0
local dense = {}
local sparse = {}
local function alloc()
if alive_count ~= #dense then
alive_count += 1
print("*recycled", pe(dense[alive_count]))
return dense[alive_count]
end
max_id += 1
local id = max_id
alive_count += 1
dense[alive_count] = id
sparse[id] = {
dense = alive_count
}
print("*allocated", pe(id))
return id
end
local function remove(entity)
local id = ECS_ID(entity)
local r = sparse[id]
local index_of_deleted_entity = r.dense
local last_entity_alive_at_index = alive_count -- last entity alive
alive_count -= 1
local last_alive_entity = dense[last_entity_alive_at_index]
local r_swap = sparse[ECS_ID(last_alive_entity)]
r_swap.dense = r.dense
r.dense = last_entity_alive_at_index
dense[index_of_deleted_entity] = last_alive_entity
dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
print("*dellocated", pe(id))
end
local function alive(e)
local r = sparse[ECS_ID(e)]
return dense[r.dense] == e
end
local function pa(e)
print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`)
end
local tprint = require("@testkit").print
local e1v0 = alloc()
local e2v0 = alloc()
local e3v0 = alloc()
local e4v0 = alloc()
local e5v0 = alloc()
pa(e1v0)
pa(e4v0)
remove(e5v0)
pa(e5v0)
local e5v1 = alloc()
pa(e5v0)
pa(e5v1)
pa(e2v0)
print(ECS_ID(e2v0))
dprint(dense)
remove(e2v0)
dprint(dense)

View file

@ -1,122 +0,0 @@
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
_G.__JECS_HI_COMPONENT_ID = 300
local ecs = require(ReplicatedStorage.ecs)
-- 500 entities
-- 2-30 components on each entity
-- 300 unique components
-- 200 systems
-- 1-10 components to query per system
local startTime = os.clock()
local world = ecs.World.new()
local components = {}
for i = 1, 300 do -- 300 components
components[i] = world:component()
end
local archetypes = {}
for i = 1, 50 do -- 50 archetypes
local archetype = {}
for _ = 1, math.random(2, 30) do
local componentId = math.random(1, #components)
table.insert(archetype, components[componentId])
end
archetypes[i] = archetype
end
for _ = 1, 1000 do -- 1000 entities in the world
local componentsToAdd = {}
local archetypeId = math.random(1, #archetypes)
local e = world:entity()
for _, component in ipairs(archetypes[archetypeId]) do
world:set(e, component, {
DummyData = math.random(1, 5000),
})
end
end
local function values(t)
local array = {}
for _, v in t do
table.insert(array, v)
end
return array
end
local contiguousComponents = values(components)
local systemComponentsToQuery = {}
for _ = 1, 200 do -- 200 systems
local numComponentsToQuery = math.random(1, 10)
local componentsToQuery = {}
for _ = 1, numComponentsToQuery do
table.insert(componentsToQuery, contiguousComponents[math.random(1, #contiguousComponents)])
end
table.insert(systemComponentsToQuery, componentsToQuery)
end
local worldCreateTime = os.clock() - startTime
local results = {}
startTime = os.clock()
RunService.Heartbeat:Connect(function()
local added = 0
local systemStartTime = os.clock()
debug.profilebegin("systems")
for _, componentsToQuery in ipairs(systemComponentsToQuery) do
debug.profilebegin("system")
for entityId, firstComponent in world:query(unpack(componentsToQuery)) do
world:set(
entityId,
{
DummyData = firstComponent.DummyData + 1,
}
)
added += 1
end
debug.profileend()
end
debug.profileend()
if os.clock() - startTime < 4 then
-- discard first 4 seconds
return
end
if results == nil then
return
elseif #results < 1000 then
table.insert(results, os.clock() - systemStartTime)
else
print("added", added)
print("World created in", worldCreateTime * 1000, "ms")
local sum = 0
for _, result in ipairs(results) do
sum += result
end
print(("Average frame time: %fms"):format((sum / #results) * 1000))
results = nil
local n = #world.archetypes
print(
("X entities\n%d components\n%d systems\n%d archetypes"):format(
#components,
#systemComponentsToQuery,
n
)
)
end
end)

View file

@ -1,19 +0,0 @@
{
"name": "Stress",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "DevPackages",
"ecs": {
"$path": "jecs.luau"
}
},
"ReplicatedFirst": {
"$className": "ReplicatedFirst",
"stres": {
"$path": "stress.client.luau"
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,24 +0,0 @@
local jecs = require("@jecs")
local pair = jecs.pair
local ChildOf = jecs.ChildOf
local lifetime_tracker_add = require("@tools/lifetime_tracker")
local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false})
local FriendsWith = world:component()
world:print_snapshot()
local e1 = world:entity()
local e2 = world:entity()
world:delete(e2)
world:print_snapshot()
local e3 = world:entity()
world:add(e3, pair(ChildOf, e1))
local e4 = world:entity()
world:add(e4, pair(FriendsWith, e3))
world:print_snapshot()
world:delete(e1)
world:delete(e3)
world:print_snapshot()
world:print_entity_index()
world:entity()
world:entity()
world:print_snapshot()

View file

@ -1,24 +0,0 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"jsx": "react",
"jsxFactory": "Roact.createElement",
"jsxFragmentFactory": "Roact.Fragment",
"module": "commonjs",
"moduleResolution": "Node",
"noLib": true,
"resolveJsonModule": true,
"strict": true,
"target": "ESNext",
"typeRoots": [
"node_modules/@rbxts"
],
"rootDir": "lib",
"outDir": "out",
"baseUrl": "lib",
"incremental": true,
"tsBuildInfoFile": "out/tsconfig.tsbuildinfo",
"moduleDetection": "force"
}
}

View file

@ -1,15 +0,0 @@
[package]
name = "ukendio/jecs"
version = "0.6.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
license = "MIT"
include = [
"default.project.json",
"jecs.luau",
"wally.toml",
"README.md",
"CHANGELOG.md",
"LICENSE",
]
exclude = ["**"]