[](LICENSE-APACHE)
diff --git a/aftman.toml b/aftman.toml
index 73e1123..ace6bc1 100644
--- a/aftman.toml
+++ b/aftman.toml
@@ -3,4 +3,4 @@ wally = "upliftgames/wally@0.3.1"
rojo = "rojo-rbx/rojo@7.4.1"
stylua = "johnnymorganz/stylua@0.19.1"
selene = "kampfkarren/selene@0.26.1"
-wally-patch-package="Barocena/wally-patch-package@1.2.1"
\ No newline at end of file
+wally-patch-package = "Barocena/wally-patch-package@1.2.1"
diff --git a/benches/exhaustive.lua b/benches/exhaustive.lua
new file mode 100644
index 0000000..3095c70
--- /dev/null
+++ b/benches/exhaustive.lua
@@ -0,0 +1,372 @@
+local testkit = require("../testkit")
+local jecs = require("../lib/init")
+local ecr = require("../DevPackages/_Index/centau_ecr@0.8.0/ecr/src/ecr")
+
+
+local BENCH, START = testkit.benchmark()
+
+local function TITLE(title: string)
+ print()
+ print(testkit.color.white(title))
+end
+
+local N = 2^16-2
+
+type i53 = number
+
+do TITLE "create"
+ BENCH("entity", function()
+ local world = jecs.World.new()
+ for i = 1, START(N) do
+ world:entity()
+ end
+ end)
+end
+
+--- component benchmarks
+
+--todo: perform the same benchmarks for multiple components.?
+-- these kind of operations only support 1 component at a time, which is
+-- a shame, especially for archetypes where moving components is expensive.
+
+do TITLE "set"
+ BENCH("add 1 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+
+ for i = 1, N do
+ entities[i] = world:entity()
+ end
+
+ for i = 1, START(N) do
+ world:set(entities[i], A, i)
+ end
+ end)
+
+ BENCH("change 1 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+ local e = world:entity()
+ world:set(e, A, 1)
+
+ for i = 1, START(N) do
+ world:set(e, A, 2)
+ end
+ end)
+
+end
+
+do TITLE "remove"
+ BENCH("1 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ end
+
+ for i = 1, START(N) do
+ world:remove(entities[i], A)
+ end
+
+ end)
+end
+
+do TITLE "get"
+ BENCH("1 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ end
+
+ for i = 1, START(N) do
+ -- ? curious why the overhead is roughly 80 ns.
+ world:get(entities[i], A)
+ end
+
+ end)
+
+ BENCH("2 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+ local B = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ world:set(id, B, true)
+ end
+
+ for i = 1, START(N) do
+ world:get(entities[i], A, B)
+ end
+
+ end)
+
+ BENCH("3 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+ local B = world:component()
+ local C = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ world:set(id, B, true)
+ world:set(id, C, true)
+ end
+
+ for i = 1, START(N) do
+ world:get(entities[i], A, B, C)
+ end
+
+ end)
+
+ BENCH("4 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+ local B = world:component()
+ local C = world:component()
+ local D = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ world:set(id, B, true)
+ world:set(id, C, true)
+ world:set(id, D, true)
+ end
+
+ for i = 1, START(N) do
+ world:get(entities[i], A, B, C, D)
+ end
+
+ end)
+end
+
+do TITLE (testkit.color.white_underline("Jecs query"))
+
+ local function count(query: () -> ())
+ local n = 0
+ for _ in query do
+ n += 1
+ end
+ return n
+ end
+
+ local function flip()
+ return math.random() > 0.5
+ end
+
+ local function view_bench(
+ world: jecs.World,
+ A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53
+ )
+
+ BENCH("1 component", function()
+ START(count(world:query(A)))
+ for _ in world:query(A) do end
+ end)
+
+ BENCH("2 component", function()
+ START(count(world:query(A, B)))
+ for _ in world:query(A, B) do end
+ end)
+
+ BENCH("4 component", function()
+ START(count(world:query(A, B, C, D)))
+ for _ in world:query(A, B, C, D) do end
+ end)
+
+ BENCH("8 component", function()
+ START(count(world:query(A, B, C, D, E, F, G, H)))
+ for _ in world:query(A, B, C, D, E, F, G, H) do end
+ end)
+ end
+
+ do TITLE "random components"
+
+ local world = jecs.World.new()
+
+ local A = world:component()
+ local B = world:component()
+ local C = world:component()
+ local D = world:component()
+ local E = world:component()
+ local F = world:component()
+ local G = world:component()
+ local H = world:component()
+ local I = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ if flip() then world:set(id, A, true) end
+ if flip() then world:set(id, B, true) end
+ if flip() then world:set(id, C, true) end
+ if flip() then world:set(id, D, true) end
+ if flip() then world:set(id, E, true) end
+ if flip() then world:set(id, F, true) end
+ if flip() then world:set(id, G, true) end
+ if flip() then world:set(id, H, true) end
+ if flip() then world:set(id, I, true) end
+
+ end
+
+ view_bench(world, A, B, C, D, E, F, G, H, I)
+
+ end
+
+ do TITLE "one component in common"
+
+ local world = jecs.World.new()
+
+ local A = world:component()
+ local B = world:component()
+ local C = world:component()
+ local D = world:component()
+ local E = world:component()
+ local F = world:component()
+ local G = world:component()
+ local H = world:component()
+ local I = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ local a = true
+ if flip() then world:set(id, B, true) else a = false end
+ if flip() then world:set(id, C, true) else a = false end
+ if flip() then world:set(id, D, true) else a = false end
+ if flip() then world:set(id, E, true) else a = false end
+ if flip() then world:set(id, F, true) else a = false end
+ if flip() then world:set(id, G, true) else a = false end
+ if flip() then world:set(id, H, true) else a = false end
+ if flip() then world:set(id, I, true) else a = false end
+ if a then world:set(id, A, true) end
+
+ end
+
+ view_bench(world, A, B, C, D, E, F, G, H, I)
+
+ end
+
+end
+
+do TITLE (testkit.color.white_underline("ECR query"))
+
+ local A = ecr.component()
+ local B = ecr.component()
+ local C = ecr.component()
+ local D = ecr.component()
+ local E = ecr.component()
+ local F = ecr.component()
+ local G = ecr.component()
+ local H = ecr.component()
+ local I = ecr.component()
+
+ local function count(query: () -> ())
+ local n = 0
+ for _ in query do
+ n += 1
+ end
+ return n
+ end
+
+ local function flip()
+ return math.random() > 0.5
+ end
+
+ local function view_bench(
+ world: ecr.Registry,
+ A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53
+ )
+
+ BENCH("1 component", function()
+ START(count(world:view(A)))
+ for _ in world:view(A) do end
+ end)
+
+ BENCH("2 component", function()
+ START(count(world:view(A, B)))
+ for _ in world:view(A, B) do end
+ end)
+
+ BENCH("4 component", function()
+ START(count(world:view(A, B, C, D)))
+ for _ in world:view(A, B, C, D) do end
+ end)
+
+ BENCH("8 component", function()
+ START(count(world:view(A, B, C, D, E, F, G, H)))
+ for _ in world:view(A, B, C, D, E, F, G, H) do end
+ end)
+ end
+
+
+ do TITLE "random components"
+ local world = ecr.registry()
+
+ for i = 1, N do
+ local id = world.create()
+ if flip() then world:set(id, A, true) end
+ if flip() then world:set(id, B, true) end
+ if flip() then world:set(id, C, true) end
+ if flip() then world:set(id, D, true) end
+ if flip() then world:set(id, E, true) end
+ if flip() then world:set(id, F, true) end
+ if flip() then world:set(id, G, true) end
+ if flip() then world:set(id, H, true) end
+ if flip() then world:set(id, I, true) end
+
+ end
+
+ view_bench(world, A, B, C, D, E, F, G, H, I)
+
+ end
+
+ do TITLE "one component in common"
+
+ local world = ecr.registry()
+
+ for i = 1, N do
+ local id = world.create()
+ local a = true
+ if flip() then world:set(id, B, true) else a = false end
+ if flip() then world:set(id, C, true) else a = false end
+ if flip() then world:set(id, D, true) else a = false end
+ if flip() then world:set(id, E, true) else a = false end
+ if flip() then world:set(id, F, true) else a = false end
+ if flip() then world:set(id, G, true) else a = false end
+ if flip() then world:set(id, H, true) else a = false end
+ if flip() then world:set(id, I, true) else a = false end
+ if a then world:set(id, A, true) end
+
+ end
+
+ view_bench(world, A, B, C, D, E, F, G, H, I)
+
+ end
+
+end
\ No newline at end of file
diff --git a/benches/query.lua b/benches/query.lua
index de60944..195e9c6 100644
--- a/benches/query.lua
+++ b/benches/query.lua
@@ -1,299 +1,200 @@
--!optimize 2
--!native
-local testkit = require('../testkit')
+local testkit = require("../testkit")
local BENCH, START = testkit.benchmark()
local function TITLE(title: string)
- print()
- print(testkit.color.white(title))
+ print()
+ print(testkit.color.white(title))
end
-local jecs = require("../mirror/init")
+local jecs = require("../lib/init")
+local mirror = require("../mirror/init")
-local oldMatter = require("../oldMatter")
-
-local newMatter = require("../newMatter")
type i53 = number
-do TITLE (testkit.color.white_underline("Jecs query"))
- local ecs = jecs.World.new()
- do TITLE "one component in common"
+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
- )
+ 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("1 component", function()
- for _ in world:query(A) do end
- end)
+ BENCH("2 component", function()
+ for _ in world:query(A, B) do
+ end
+ end)
- BENCH("2 component", function()
- for _ in world:query(A, B) do end
- end)
+ BENCH("4 component", function()
+ for _ in world:query(A, B, C, D) do
+ end
+ end)
- BENCH("4 component", function()
- for _ in world:query(A, B, C, D) do
- end
- end)
+ BENCH("8 component", function()
+ for _ in world:query(A, B, C, D, E, F, G, H) do
+ end
+ end)
+ end
- BENCH("8 component", function()
- for _ in world:query(A, B, C, D, E, F, G, H) 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 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 function flip()
- return math.random() >= 0.15
- end
-
- local added = 0
+ local added = 0
local archetypes = {}
- for i = 1, 2^16-2 do
- local entity = ecs:entity()
+ for i = 1, 2 ^ 16 - 2 do
+ local entity = ecs:entity()
- local combination = ""
+ 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})
+ 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
- end
-
- if #combination == 7 then
- added += 1
- ecs:set(entity, D1, { value = true})
- end
+ if #combination == 7 then
+ added += 1
+ ecs:set(entity, D1, {value = true})
+ end
archetypes[combination] = true
- end
+ end
local a = 0
- for _ in archetypes do a+= 1 end
+ for _ in archetypes do
+ a += 1
+ end
- view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
- end
+ view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
+ end
end
-do TITLE(testkit.color.white_underline("OldMatter query"))
+do
+ TITLE(testkit.color.white_underline("Mirror query"))
+ local ecs = mirror.World.new()
+ do
+ TITLE("one component in common")
- local ecs = oldMatter.World.new()
- local component = oldMatter.component
+ 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)
- 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("2 component", function()
+ for _ in world:query(A, B) do
+ end
+ end)
- BENCH("1 component", function()
- for _ in world:query(A) do end
- end)
+ BENCH("4 component", function()
+ for _ in world:query(A, B, C, D) do
+ end
+ end)
- BENCH("2 component", function()
- for _ in world:query(A, B) do end
- end)
+ BENCH("8 component", function()
+ for _ in world:query(A, B, C, D, E, F, G, H) do
+ end
+ end)
+ end
- BENCH("4 component", function()
- for _ in world:query(A, B, C, D) do
- 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()
- BENCH("8 component", function()
- for _ in world:query(A, B, C, D, E, F, G, H) do end
- end)
- end
+ local function flip()
+ return math.random() >= 0.15
+ end
- local D1 = component()
- local D2 = component()
- local D3 = component()
- local D4 = component()
- local D5 = component()
- local D6 = component()
- local D7 = component()
- local D8 = component()
-
- local function flip()
- return math.random() >= 0.15
- end
-
- local added = 0
+ local added = 0
local archetypes = {}
- for i = 1, 2^16-2 do
- local entity = ecs:spawn()
+ for i = 1, 2 ^ 16 - 2 do
+ local entity = ecs:entity()
- local combination = ""
+ local combination = ""
- if flip() then
- combination ..= "B"
- ecs:insert(entity, D2({value = true}))
- end
- if flip() then
- combination ..= "C"
- ecs:insert(entity, D3({value = true}))
- end
- if flip() then
- combination ..= "D"
- ecs:insert(entity, D4({value = true}))
- end
- if flip() then
- combination ..= "E"
- ecs:insert(entity, D5({value = true}))
- end
- if flip() then
- combination ..= "F"
- ecs:insert(entity, D6({value = true}))
+ 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
- end
- if flip() then
- combination ..= "G"
- ecs:insert(entity, D7({value = true}))
-
- end
- if flip() then
- combination ..= "H"
- ecs:insert(entity, D8({value = true}))
- end
-
- if #combination == 7 then
- added += 1
- ecs:insert(entity, D1({value = true}))
-
- end
+ if #combination == 7 then
+ added += 1
+ ecs:set(entity, D1, {value = true})
+ end
archetypes[combination] = true
- end
+ 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("NewMatter query"))
-
- local ecs = newMatter.World.new()
- local component = newMatter.component
-
- 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(A, B) do end
- end)
-
- BENCH("4 component", function()
- for _ in world:query(A, B, C, D) do
- end
- end)
-
- BENCH("8 component", function()
- for _ in world:query(A, B, C, D, E, F, G, H) do end
- end)
- end
-
- local D1 = component()
- local D2 = component()
- local D3 = component()
- local D4 = component()
- local D5 = component()
- local D6 = component()
- local D7 = component()
- local D8 = 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:spawn()
-
- local combination = ""
-
- if flip() then
- combination ..= "B"
- ecs:insert(entity, D2({value = true}))
- end
- if flip() then
- combination ..= "C"
- ecs:insert(entity, D3({value = true}))
- end
- if flip() then
- combination ..= "D"
- ecs:insert(entity, D4({value = true}))
- end
- if flip() then
- combination ..= "E"
- ecs:insert(entity, D5({value = true}))
- end
- if flip() then
- combination ..= "F"
- ecs:insert(entity, D6({value = true}))
-
- end
- if flip() then
- combination ..= "G"
- ecs:insert(entity, D7({value = true}))
-
- end
- if flip() then
- combination ..= "H"
- ecs:insert(entity, D8({value = true}))
- end
-
- if #combination == 7 then
- added += 1
- ecs:insert(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
+ for _ in archetypes do
+ a += 1
+ end
+ view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
+ end
end
\ No newline at end of file
diff --git a/benches/visual/spawn.bench.lua b/benches/visual/spawn.bench.lua
index 962064e..c5e6aef 100644
--- a/benches/visual/spawn.bench.lua
+++ b/benches/visual/spawn.bench.lua
@@ -2,41 +2,37 @@
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-local rgb = require(ReplicatedStorage.rgb)
local Matter = require(ReplicatedStorage.DevPackages.Matter)
-local jecs = require(ReplicatedStorage.Lib)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
+local jecs = require(ReplicatedStorage.Lib)
+local rgb = require(ReplicatedStorage.rgb)
local newWorld = Matter.World.new()
local ecs = jecs.World.new()
-
return {
ParameterGenerator = function()
- local registry2 = ecr.registry()
+ local registry2 = ecr.registry()
return registry2
- end,
+ end;
Functions = {
- Matter = function()
- for i = 1, 1000 do
- newWorld:spawn()
- end
- end,
+ Matter = function()
+ for i = 1, 1000 do
+ newWorld:spawn()
+ end
+ end;
+ ECR = function(_, registry2)
+ for i = 1, 1000 do
+ registry2.create()
+ end
+ end;
- ECR = function(_, registry2)
- for i = 1, 1000 do
- registry2.create()
- end
- end,
-
-
- Jecs = function()
- for i = 1, 1000 do
- ecs:entity()
- end
- end
-
- },
+ Jecs = function()
+ for i = 1, 1000 do
+ ecs:entity()
+ end
+ end;
+ };
}
diff --git a/jecs_darkmode.svg b/jecs_darkmode.svg
new file mode 100644
index 0000000..f64b173
--- /dev/null
+++ b/jecs_darkmode.svg
@@ -0,0 +1,6 @@
+
diff --git a/jecs_lightmode.svg b/jecs_lightmode.svg
new file mode 100644
index 0000000..dbcd08c
--- /dev/null
+++ b/jecs_lightmode.svg
@@ -0,0 +1,6 @@
+
diff --git a/lib/init.lua b/lib/init.lua
index ccf3a12..defc1b3 100644
--- a/lib/init.lua
+++ b/lib/init.lua
@@ -6,10 +6,10 @@
type i53 = number
type i24 = number
-type Ty = { i53 }
+type Ty = {i53}
type ArchetypeId = number
-type Column = { any }
+type Column = {any}
type Archetype = {
id: number,
@@ -20,9 +20,9 @@ type Archetype = {
},
},
types: Ty,
- type: string | number,
- entities: { number },
- columns: { Column },
+ type: string | number,
+ entities: {number},
+ columns: {Column},
records: {},
}
@@ -31,13 +31,13 @@ type Record = {
row: number,
}
-type EntityIndex = { [i24]: Record }
-type ComponentIndex = { [i24]: ArchetypeMap}
+type EntityIndex = {[i24]: Record}
+type ComponentIndex = {[i24]: ArchetypeMap}
type ArchetypeRecord = number
-type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number }
-type Archetypes = { [ArchetypeId]: Archetype }
-
+type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
+type Archetypes = {[ArchetypeId]: Archetype}
+
type ArchetypeDiff = {
added: Ty,
removed: Ty,
@@ -64,17 +64,17 @@ local function transitionArchetype(
local types = from.types
for i, column in columns do
- -- Retrieves the new column index from the source archetype's record from each component
+ -- 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
+ 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
+ if sourceRow ~= last then
-- Swap rempves columns to ensure there are no holes in the archetype.
column[sourceRow] = column[last]
end
@@ -82,24 +82,27 @@ local function transitionArchetype(
end
-- Move the entity from the source to the destination archetype.
- destinationEntities[destinationRow] = sourceEntities[sourceRow]
- entityIndex[sourceEntities[sourceRow]].row = destinationRow
+ 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
- sourceEntities[sourceRow] = sourceEntities[movedAway]
- entityIndex[sourceEntities[movedAway]].row = sourceRow
+ if sourceRow ~= movedAway then
+ local atMovedAway = sourceEntities[movedAway]
+ sourceEntities[sourceRow] = atMovedAway
+ entityIndex[atMovedAway].row = sourceRow
end
-
+
sourceEntities[movedAway] = nil
end
-local function archetypeAppend(entity: i53, archetype: Archetype): i24
+local function archetypeAppend(entity: number, archetype: Archetype): number
local entities = archetype.entities
- table.insert(entities, entity)
- return #entities
+ local length = #entities + 1
+ entities[length] = entity
+ return length
end
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
@@ -122,47 +125,49 @@ local function hash(arr): string | number
return table.concat(arr, "_")
end
-local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
- local destinationCount = #to.types
+local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
local destinationIds = to.types
+ local records = to.records
+ local id = to.id
- for i = 1, destinationCount do
- local destinationId = destinationIds[i]
+ for i, destinationId in destinationIds do
+ local archetypesMap = componentIndex[destinationId]
- if not componentIndex[destinationId] then
- componentIndex[destinationId] = { size = 0, sparse = {} }
+ if not archetypesMap then
+ archetypesMap = {size = 0, sparse = {}}
+ componentIndex[destinationId] = archetypesMap
end
- local archetypesMap = componentIndex[destinationId]
- archetypesMap.sparse[to.id] = i
- to.records[destinationId] = i
+ archetypesMap.sparse[id] = i
+ records[destinationId] = i
end
end
-local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype
+local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
local ty = hash(types)
- world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
- local id = world.nextArchetypeId
+ local id = world.nextArchetypeId + 1
+ world.nextArchetypeId = id
- local columns = {} :: { any }
+ local length = #types
+ local columns = table.create(length) :: {any}
- for _ in types do
- table.insert(columns, {})
+ for index in types do
+ columns[index] = {}
end
local archetype = {
- id = id,
- types = types,
- type = ty,
- columns = columns,
- entities = {},
- edges = {},
- records = {},
+ columns = columns;
+ edges = {};
+ entities = {};
+ id = id;
+ records = {};
+ type = ty;
+ types = types;
}
world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype
- if #types > 0 then
+ if length > 0 then
createArchetypeRecords(world.componentIndex, archetype, prev)
end
@@ -171,42 +176,43 @@ end
local World = {}
World.__index = World
-function World.new()
+function World.new()
local self = setmetatable({
- entityIndex = {},
- componentIndex = {},
- archetypes = {},
- archetypeIndex = {},
- ROOT_ARCHETYPE = (nil :: any) :: Archetype,
- nextEntityId = 0,
- nextComponentId = 0,
- nextArchetypeId = 0,
+ archetypeIndex = {};
+ archetypes = {};
+ componentIndex = {};
+ entityIndex = {};
hooks = {
- [ON_ADD] = {}
- }
+ [ON_ADD] = {};
+ };
+ nextArchetypeId = 0;
+ nextComponentId = 0;
+ nextEntityId = 0;
+ ROOT_ARCHETYPE = (nil :: any) :: Archetype;
}, World)
- return self
+ self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
+ return self
end
-local function emit(world, eventDescription)
+local function emit(world, eventDescription)
local event = eventDescription.event
table.insert(world.hooks[event], {
- ids = eventDescription.ids,
- archetype = eventDescription.archetype,
- otherArchetype = eventDescription.otherArchetype,
- offset = eventDescription.offset
+ 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
+local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
+ if #added > 0 then
emit(world, {
- event = ON_ADD,
- ids = added,
- archetype = archetype,
- otherArchetype = otherArchetype,
- offset = row,
+ archetype = archetype;
+ event = ON_ADD;
+ ids = added;
+ offset = row;
+ otherArchetype = otherArchetype;
})
end
end
@@ -217,7 +223,7 @@ 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
@@ -227,10 +233,8 @@ local function ensureArchetype(world: World, types, prev)
return archetypeOf(world, types, prev)
end
-local function findInsert(types: { i53 }, toAdd: i53)
- local count = #types
- for i = 1, count do
- local id = types[i]
+local function findInsert(types: {i53}, toAdd: i53)
+ for i, id in types do
if id == toAdd then
return -1
end
@@ -238,13 +242,13 @@ local function findInsert(types: { i53 }, toAdd: i53)
return i
end
end
- return count + 1
+ 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
+ -- 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
@@ -259,48 +263,65 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
end
local function ensureEdge(archetype: Archetype, componentId: i53)
- if not archetype.edges[componentId] then
- archetype.edges[componentId] = {} :: any
+ local edges = archetype.edges
+ local edge = edges[componentId]
+ if not edge then
+ edge = {} :: any
+ edges[componentId] = edge
end
- return archetype.edges[componentId]
+ 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
- if not world.ROOT_ARCHETYPE then
- local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
- world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
- end
- from = world.ROOT_ARCHETYPE
- end
+ from = from or world.ROOT_ARCHETYPE
+
local edge = ensureEdge(from, componentId)
-
- if not edge.add then
- -- Save an edge using the component ID to the archetype to allow
+ 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.
- edge.add = findArchetypeWith(world, from, componentId)
+ add = findArchetypeWith(world, from, componentId)
+ edge.add = add :: never
end
- return edge.add
+ return add
end
local function ensureRecord(entityIndex, entityId: i53): Record
- local id = entityId
- if not entityIndex[id] then
- entityIndex[id] = {}
+ local record = entityIndex[entityId]
+
+ if not record then
+ record = {}
+ entityIndex[entityId] = record
end
- return entityIndex[id] :: Record
+
+ return record :: Record
end
+
+function World.add(world: World, entityId: i53, componentId: i53)
+ local record = ensureRecord(world.entityIndex, entityId)
+ local from = record.archetype
+ local to = archetypeTraverseAdd(world, componentId, from)
+ if from then
+ moveEntity(world.entityIndex, entityId, record, to)
+ else
+ if #to.types > 0 then
+ newEntity(entityId, record, to)
+ onNotifyAdd(world, to, from, record.row, { componentId })
+ end
+ end
+end
+
+-- Symmetric like `World.add` but idempotent
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.
+ 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.
@@ -308,16 +329,16 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
end
if from then
- -- If there was a previous archetype, then the entity needs to move the archetype
+ -- If there was a previous archetype, then the entity needs to move the archetype
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 })
+ onNotifyAdd(world, to, from, record.row, {componentId})
end
end
-
+
local archetypeRecord = to.records[componentId]
to.columns[archetypeRecord][record.row] = data
end
@@ -326,28 +347,30 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
local edge = ensureEdge(from, componentId)
-
- if not edge.remove then
- local to = table.clone(from.types)
+ local remove = edge.remove
+ if not remove then
+ local to = table.clone(from.types)
table.remove(to, table.find(to, componentId))
- edge.remove = ensureArchetype(world, to, from)
+ remove = ensureArchetype(world, to, from)
+ edge.remove = remove :: never
end
- return edge.remove
+ return remove
end
-function World.remove(world: World, entityId: i53, componentId: i53)
- local record = ensureRecord(world.entityIndex, entityId)
+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(world.entityIndex, entityId, record, destinationArchetype)
+ 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(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
+local function get(record: Record, componentId: i24)
local archetype = record.archetype
local archetypeRecord = archetype.records[componentId]
@@ -360,35 +383,35 @@ end
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
local id = entityId
- local componentIndex = world.componentIndex
local record = world.entityIndex[id]
if not record then
return nil
end
- local va = get(componentIndex, record, a)
+ local va = get(record, a)
if b == nil then
return va
elseif c == nil then
- return va, get(componentIndex, record, b)
+ return va, get(record, b)
elseif d == nil then
- return va, get(componentIndex, record, b), get(componentIndex, record, c)
+ return va, get(record, b), get(record, c)
elseif e == nil then
- return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d)
+ return va, get(record, b), get(record, c), get(record, d)
else
error("args exceeded")
end
end
-local function noop(self: Query, ...: i53): () -> (number, ...any)
- return function()
- end :: any
+-- 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
+ __iter = noop;
+ without = noop;
}
EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery)
@@ -396,25 +419,28 @@ setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query
- local compatibleArchetypes = {}
- local components = { ... }
- local archetypes = world.archetypes
- local queryLength = #components
-
- if queryLength == 0 then
+ -- 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 i, componentId in components do
+ 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
+ if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
firstArchetypeMap = map
end
end
@@ -422,110 +448,107 @@ function World.query(world: World, ...: i53): Query
for id in firstArchetypeMap.sparse do
local archetype = archetypes[id]
local archetypeRecords = archetype.records
- local indices = {}
+ local indices = {}
local skip = false
-
- for i, componentId in components do
+
+ for i, componentId in components do
local index = archetypeRecords[componentId]
- if not index then
+ if not index then
skip = true
break
end
- indices[i] = archetypeRecords[componentId]
+ indices[i] = index
end
- if skip then
+ if skip then
continue
end
- table.insert(compatibleArchetypes, { archetype, indices })
+
+ length += 1
+ compatibleArchetypes[length] = {archetype, indices}
end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
- if not lastArchetype then
+ if not lastArchetype then
return EmptyQuery
end
-
+
local preparedQuery = {}
preparedQuery.__index = preparedQuery
- function preparedQuery:without(...)
- local components = { ... }
- for i = #compatibleArchetypes, 1, -1 do
+ 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 components do
- if archetype.records[componentId] then
+
+ for _, componentId in withoutComponents do
+ if records[componentId] then
shouldRemove = true
break
end
end
- if shouldRemove then
+
+ if shouldRemove then
table.remove(compatibleArchetypes, i)
end
- end
+ end
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
- if not lastArchetype then
+ if not lastArchetype then
return EmptyQuery
end
-
+
return self
end
local lastRow
local queryOutput = {}
-
- function preparedQuery:__iter()
- return function()
+ function preparedQuery:__iter()
+ return function()
local archetype = compatibleArchetype[1]
local row = next(archetype.entities, lastRow)
- while row == nil do
+ while row == nil do
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
- if lastArchetype == nil then
- return
+ 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
+
+ if queryLength == 1 then
return entityId, columns[tr[1]][row]
- elseif queryLength == 2 then
+ 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,
+ 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,
+ 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,
+ elseif queryLength == 7 then
+ return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
@@ -533,8 +556,8 @@ function World.query(world: World, ...: i53): Query
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row]
- elseif queryLength == 8 then
- return entityId,
+ elseif queryLength == 8 then
+ return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
@@ -545,8 +568,8 @@ function World.query(world: World, ...: i53): Query
columns[tr[8]][row]
end
- for i in components do
- queryOutput[i] = tr[i][row]
+ for i in components do
+ queryOutput[i] = columns[tr[i]][row]
end
return entityId, unpack(queryOutput, 1, queryLength)
@@ -556,20 +579,21 @@ function World.query(world: World, ...: i53): Query
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,
+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.")
+ error("Too many components, consider using world:entity() instead to create components.")
end
world.nextComponentId = componentId
return componentId
end
function World.entity(world: World)
- world.nextEntityId += 1
- return world.nextEntityId + REST
+ local nextEntityId = world.nextEntityId + 1
+ world.nextEntityId = nextEntityId
+ return nextEntityId + REST
end
-- should reuse this logic in World.set instead of swap removing in transition archetype
@@ -620,57 +644,92 @@ function World.delete(world: World, entityId: i53)
end
function World.observer(world: World, ...)
- local componentIds = { ... }
-
+ local componentIds = {...}
+ local idsCount = #componentIds
+ local hooks = world.hooks
+
return {
- event = function(event)
- local hook = world.hooks[event]
- world.hooks[event] = nil
+ event = function(event)
+ local hook = hooks[event]
+ hooks[event] = nil
local last, change
- return function()
+ return function()
last, change = next(hook, last)
- if not last then
+ if not last then
return
end
local matched = false
-
- while not matched do
+ local ids = change.ids
+
+ while not matched do
local skip = false
- for _, id in change.ids do
- if not table.find(componentIds, id) then
+ for _, id in ids do
+ if not table.find(componentIds, id) then
skip = true
break
end
end
-
- if skip then
+
+ if skip then
last, change = next(hook, last)
+ ids = change.ids
continue
end
matched = true
end
-
- local queryOutput = {}
+
+ local queryOutput = table.create(idsCount)
local row = change.offset
local archetype = change.archetype
local columns = archetype.columns
local archetypeRecords = archetype.records
- for _, id in componentIds do
- table.insert(queryOutput, columns[archetypeRecords[id]][row])
+ for index, id in componentIds do
+ queryOutput[index] = columns[archetypeRecords[id]][row]
end
- return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
+ return archetype.entities[row], unpack(queryOutput, 1, idsCount)
end
- end
+ end;
}
end
+function World.__iter(world: World): () -> (number?, unknown?)
+ local entityIndex = world.entityIndex
+ local last
+
+ return function()
+ local entity, record = next(entityIndex, last)
+ if not entity then
+ return
+ end
+ last = entity
+
+ local archetype = record.archetype
+ if not archetype then
+ -- Returns only the entity id as an entity without data should not return
+ -- data and allow the user to get an error if they don't handle the case.
+ return entity
+ end
+
+ local row = record.row
+ local types = archetype.types
+ local columns = archetype.columns
+ local entityData = {}
+ for i, column in columns do
+ -- We use types because the key should be the component ID not the column index
+ entityData[types[i]] = column[row]
+ end
+
+ return entity, entityData
+ end
+end
+
return table.freeze({
- World = World,
- ON_ADD = ON_ADD,
- ON_REMOVE = ON_REMOVE,
- ON_SET = ON_SET
+ World = World;
+ ON_ADD = ON_ADD;
+ ON_REMOVE = ON_REMOVE;
+ ON_SET = ON_SET;
})
diff --git a/lib/init.spec.lua b/lib/init.spec.lua
index fdc8331..98f485b 100644
--- a/lib/init.spec.lua
+++ b/lib/init.spec.lua
@@ -299,5 +299,38 @@ return function()
expect(world:get(id, Poison)).to.never.be.ok()
expect(world:get(id, Health)).to.never.be.ok()
end)
+
+ it("should allow iterating the whole world", function()
+ local world = jecs.World.new()
+
+ local A, B = world:entity(), world:entity()
+
+ local eA = world:entity()
+ world:set(eA, A, true)
+ local eB = world:entity()
+ world:set(eB, B, true)
+ local eAB = world:entity()
+ world:set(eAB, A, true)
+ world:set(eAB, B, true)
+
+ local count = 0
+ for id, data in world do
+ count += 1
+ if id == eA then
+ expect(data[A]).to.be.ok()
+ expect(data[B]).to.never.be.ok()
+ elseif id == eB then
+ expect(data[B]).to.be.ok()
+ expect(data[A]).to.never.be.ok()
+ elseif id == eAB then
+ expect(data[A]).to.be.ok()
+ expect(data[B]).to.be.ok()
+ else
+ error("unknown entity", id)
+ end
+ end
+
+ expect(count).to.equal(3)
+ end)
end)
end
\ No newline at end of file
diff --git a/logo.png b/logo_old.png
similarity index 100%
rename from logo.png
rename to logo_old.png
diff --git a/mirror/init.lua b/mirror/init.lua
index e10d9de..6d9c1fe 100644
--- a/mirror/init.lua
+++ b/mirror/init.lua
@@ -6,10 +6,10 @@
type i53 = number
type i24 = number
-type Ty = { i53 }
+type Ty = {i53}
type ArchetypeId = number
-type Column = { any }
+type Column = {any}
type Archetype = {
id: number,
@@ -20,9 +20,9 @@ type Archetype = {
},
},
types: Ty,
- type: string | number,
- entities: { number },
- columns: { Column },
+ type: string | number,
+ entities: {number},
+ columns: {Column},
records: {},
}
@@ -31,13 +31,13 @@ type Record = {
row: number,
}
-type EntityIndex = { [i24]: Record }
-type ComponentIndex = { [i24]: ArchetypeMap}
+type EntityIndex = {[i24]: Record}
+type ComponentIndex = {[i24]: ArchetypeMap}
type ArchetypeRecord = number
-type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number }
-type Archetypes = { [ArchetypeId]: Archetype }
-
+type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
+type Archetypes = {[ArchetypeId]: Archetype}
+
type ArchetypeDiff = {
added: Ty,
removed: Ty,
@@ -51,38 +51,58 @@ local REST = HI_COMPONENT_ID + 4
local function transitionArchetype(
entityIndex: EntityIndex,
- destinationArchetype: Archetype,
+ to: Archetype,
destinationRow: i24,
- sourceArchetype: Archetype,
+ from: Archetype,
sourceRow: i24
)
- local columns = sourceArchetype.columns
- local sourceEntities = sourceArchetype.entities
- local destinationEntities = destinationArchetype.entities
- local destinationColumns = destinationArchetype.columns
+ 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 componentId, column in columns do
- local targetColumn = destinationColumns[componentId]
- if targetColumn then
+ 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
- column[sourceRow] = column[#column]
- column[#column] = nil
+ -- 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
- destinationEntities[destinationRow] = sourceEntities[sourceRow]
- entityIndex[sourceEntities[sourceRow]].row = destinationRow
+ -- 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
- sourceEntities[sourceRow] = sourceEntities[movedAway]
- entityIndex[sourceEntities[movedAway]].row = sourceRow
+ if sourceRow ~= movedAway then
+ local atMovedAway = sourceEntities[movedAway]
+ sourceEntities[sourceRow] = atMovedAway
+ entityIndex[atMovedAway].row = sourceRow
+ end
+
sourceEntities[movedAway] = nil
end
-local function archetypeAppend(entity: i53, archetype: Archetype): i24
+local function archetypeAppend(entity: number, archetype: Archetype): number
local entities = archetype.entities
- table.insert(entities, entity)
- return #entities
+ local length = #entities + 1
+ entities[length] = entity
+ return length
end
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
@@ -105,102 +125,104 @@ local function hash(arr): string | number
return table.concat(arr, "_")
end
-local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
- local destinationCount = #to.types
+local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
local destinationIds = to.types
+ local records = to.records
+ local id = to.id
- for i = 1, destinationCount do
- local destinationId = destinationIds[i]
+ for i, destinationId in destinationIds do
+ local archetypesMap = componentIndex[destinationId]
- if not componentIndex[destinationId] then
- componentIndex[destinationId] = { size = 0, sparse = {} }
+ if not archetypesMap then
+ archetypesMap = {size = 0, sparse = {}}
+ componentIndex[destinationId] = archetypesMap
end
- local archetypesMap = componentIndex[destinationId]
- archetypesMap.sparse[to.id] = i
- to.records[destinationId] = i
+ archetypesMap.sparse[id] = i
+ records[destinationId] = i
end
end
-local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype
+local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
local ty = hash(types)
- world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
- local id = world.nextArchetypeId
+ local id = world.nextArchetypeId + 1
+ world.nextArchetypeId = id
- local columns = {} :: { any }
+ local length = #types
+ local columns = table.create(length) :: {any}
- for _ in types do
- table.insert(columns, {})
+ for index in types do
+ columns[index] = {}
end
local archetype = {
- id = id,
- types = types,
- type = ty,
- columns = columns,
- entities = {},
- edges = {},
- records = {},
+ columns = columns;
+ edges = {};
+ entities = {};
+ id = id;
+ records = {};
+ type = ty;
+ types = types;
}
world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype
- createArchetypeRecords(world.componentIndex, archetype, prev)
+ if length > 0 then
+ createArchetypeRecords(world.componentIndex, archetype, prev)
+ end
return archetype
end
local World = {}
World.__index = World
-function World.new()
+function World.new()
local self = setmetatable({
- entityIndex = {},
- componentIndex = {},
- archetypes = {},
- archetypeIndex = {},
- ROOT_ARCHETYPE = (nil :: any) :: Archetype,
- nextEntityId = 0,
- nextComponentId = 0,
- nextArchetypeId = 0,
+ archetypeIndex = {};
+ archetypes = {};
+ componentIndex = {};
+ entityIndex = {};
hooks = {
- [ON_ADD] = {}
- }
+ [ON_ADD] = {};
+ };
+ nextArchetypeId = 0;
+ nextComponentId = 0;
+ nextEntityId = 0;
+ ROOT_ARCHETYPE = (nil :: any) :: Archetype;
}, World)
- return self
+ return self
end
-local function emit(world, eventDescription)
+local function emit(world, eventDescription)
local event = eventDescription.event
table.insert(world.hooks[event], {
- ids = eventDescription.ids,
- archetype = eventDescription.archetype,
- otherArchetype = eventDescription.otherArchetype,
- offset = eventDescription.offset
+ 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
+local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
+ if #added > 0 then
emit(world, {
- event = ON_ADD,
- ids = added,
- archetype = archetype,
- otherArchetype = otherArchetype,
- offset = row,
+ 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
@@ -210,10 +232,8 @@ local function ensureArchetype(world: World, types, prev)
return archetypeOf(world, types, prev)
end
-local function findInsert(types: { i53 }, toAdd: i53)
- local count = #types
- for i = 1, count do
- local id = types[i]
+local function findInsert(types: {i53}, toAdd: i53)
+ for i, id in types do
if id == toAdd then
return -1
end
@@ -221,13 +241,18 @@ local function findInsert(types: { i53 }, toAdd: i53)
return i
end
end
- return count + 1
+ 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
@@ -237,88 +262,108 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
end
local function ensureEdge(archetype: Archetype, componentId: i53)
- if not archetype.edges[componentId] then
- archetype.edges[componentId] = {} :: any
+ local edges = archetype.edges
+ local edge = edges[componentId]
+ if not edge then
+ edge = {} :: any
+ edges[componentId] = edge
end
- return archetype.edges[componentId]
+ return edge
end
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
- if not from then
- if not world.ROOT_ARCHETYPE then
- local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
- world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
- end
- from = world.ROOT_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)
-
- if not edge.add then
- edge.add = findArchetypeWith(world, 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 edge.add
+ return add
end
local function ensureRecord(entityIndex, entityId: i53): Record
- local id = entityId
- if not entityIndex[id] then
- entityIndex[id] = {}
+ local record = entityIndex[entityId]
+
+ if not record then
+ record = {}
+ entityIndex[entityId] = record
end
- return entityIndex[id] :: Record
+
+ return record :: Record
end
-function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
+function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
local record = ensureRecord(world.entityIndex, entityId)
- local sourceArchetype = record.archetype
- local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype)
+ local from = record.archetype
+ local to = archetypeTraverseAdd(world, componentId, from)
- if sourceArchetype == destinationArchetype then
- local archetypeRecord = destinationArchetype.records[componentId]
- destinationArchetype.columns[archetypeRecord][record.row] = data
+ 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 sourceArchetype then
- moveEntity(world.entityIndex, entityId, record, destinationArchetype)
+ 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 #destinationArchetype.types > 0 then
- newEntity(entityId, record, destinationArchetype)
- onNotifyAdd(world, destinationArchetype, sourceArchetype, record.row, { componentId })
+ 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 = destinationArchetype.records[componentId]
- destinationArchetype.columns[archetypeRecord][record.row] = data
+ 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)
-
- if not edge.remove then
- local to = table.clone(from.types)
+ local remove = edge.remove
+ if not remove then
+ local to = table.clone(from.types)
table.remove(to, table.find(to, componentId))
- edge.remove = ensureArchetype(world, to, from)
+ remove = ensureArchetype(world, to, from)
+ edge.remove = remove :: never
end
- return edge.remove
+ return remove
end
-function World.remove(world: World, entityId: i53, componentId: i53)
- local record = ensureRecord(world.entityIndex, entityId)
+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(world.entityIndex, entityId, record, destinationArchetype)
+ if sourceArchetype and not (sourceArchetype == destinationArchetype) then
+ moveEntity(entityIndex, entityId, record, destinationArchetype)
end
end
-local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
+-- Keeping the function as small as possible to enable inlining
+local function get(record: Record, componentId: i24)
local archetype = record.archetype
- local archetypeRecord = componentIndex[componentId].sparse[archetype.id]
+ local archetypeRecord = archetype.records[componentId]
if not archetypeRecord then
return nil
@@ -329,35 +374,35 @@ end
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
local id = entityId
- local componentIndex = world.componentIndex
local record = world.entityIndex[id]
if not record then
return nil
end
- local va = get(componentIndex, record, a)
+ local va = get(record, a)
if b == nil then
return va
elseif c == nil then
- return va, get(componentIndex, record, b)
+ return va, get(record, b)
elseif d == nil then
- return va, get(componentIndex, record, b), get(componentIndex, record, c)
+ return va, get(record, b), get(record, c)
elseif e == nil then
- return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d)
+ return va, get(record, b), get(record, c), get(record, d)
else
error("args exceeded")
end
end
-local function noop(self: Query, ...: i53): () -> (number, ...any)
- return function()
- end :: any
+-- 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
+ __iter = noop;
+ without = noop;
}
EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery)
@@ -365,138 +410,136 @@ setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query
- local compatibleArchetypes = {}
- local components = { ... }
- local archetypes = world.archetypes
- local queryLength = #components
-
- if queryLength == 0 then
+ -- 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 i, componentId in components do
+ 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
+ if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
firstArchetypeMap = map
end
end
- local i = 0
for id in firstArchetypeMap.sparse do
local archetype = archetypes[id]
local archetypeRecords = archetype.records
- local indices = {}
+ local indices = {}
local skip = false
-
- for j, componentId in components do
+
+ for i, componentId in components do
local index = archetypeRecords[componentId]
- if not index then
+ if not index then
skip = true
break
end
- indices[j] = archetypeRecords[componentId]
+ indices[i] = index
end
- if skip then
+ if skip then
continue
end
- i += 1
- table.insert(compatibleArchetypes, { archetype, indices })
+
+ length += 1
+ compatibleArchetypes[length] = {archetype, indices}
end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
- if not lastArchetype then
+ if not lastArchetype then
return EmptyQuery
end
-
+
local preparedQuery = {}
preparedQuery.__index = preparedQuery
- function preparedQuery:without(...)
- local components = { ... }
- for i = #compatibleArchetypes, 1, -1 do
+ 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 components do
- if archetype.records[componentId] then
+
+ for _, componentId in withoutComponents do
+ if records[componentId] then
shouldRemove = true
break
end
end
- if shouldRemove then
+
+ if shouldRemove then
table.remove(compatibleArchetypes, i)
end
- end
+ end
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
- if not lastArchetype then
+ if not lastArchetype then
return EmptyQuery
end
-
+
return self
end
local lastRow
local queryOutput = {}
-
- function preparedQuery:__iter()
- return function()
+ function preparedQuery:__iter()
+ return function()
local archetype = compatibleArchetype[1]
local row = next(archetype.entities, lastRow)
- while row == nil do
+ while row == nil do
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
- if lastArchetype == nil then
- return
+ 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
+ if queryLength == 1 then
return entityId, columns[tr[1]][row]
- elseif queryLength == 2 then
+ 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,
+ 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,
+ 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,
+ elseif queryLength == 7 then
+ return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
@@ -504,8 +547,8 @@ function World.query(world: World, ...: i53): Query
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row]
- elseif queryLength == 8 then
- return entityId,
+ elseif queryLength == 8 then
+ return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
@@ -516,8 +559,8 @@ function World.query(world: World, ...: i53): Query
columns[tr[8]][row]
end
- for i in components do
- queryOutput[i] = tr[i][row]
+ for i in components do
+ queryOutput[i] = columns[tr[i]][row]
end
return entityId, unpack(queryOutput, 1, queryLength)
@@ -527,72 +570,90 @@ function World.query(world: World, ...: i53): Query
return setmetatable({}, preparedQuery) :: any
end
-function World.component(world: World)
- local componentId = world.nextComponentId + 1
- if componentId > HI_COMPONENT_ID then
- error("Too many components")
+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)
- world.nextEntityId += 1
- return world.nextEntityId + REST
+ 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 componentIds = {...}
+ local idsCount = #componentIds
+ local hooks = world.hooks
+
return {
- event = function(event)
- local hook = world.hooks[event]
- world.hooks[event] = nil
+ event = function(event)
+ local hook = hooks[event]
+ hooks[event] = nil
local last, change
- return function()
+ return function()
last, change = next(hook, last)
- if not last then
+ if not last then
return
end
local matched = false
-
- while not matched do
+ local ids = change.ids
+
+ while not matched do
local skip = false
- for _, id in change.ids do
- if not table.find(componentIds, id) then
+ for _, id in ids do
+ if not table.find(componentIds, id) then
skip = true
break
end
end
-
- if skip then
+
+ if skip then
last, change = next(hook, last)
+ ids = change.ids
continue
end
matched = true
end
-
- local queryOutput = {}
+
+ local queryOutput = table.create(idsCount)
local row = change.offset
local archetype = change.archetype
local columns = archetype.columns
local archetypeRecords = archetype.records
- for _, id in componentIds do
- table.insert(queryOutput, columns[archetypeRecords[id]][row])
+ for index, id in componentIds do
+ queryOutput[index] = columns[archetypeRecords[id]][row]
end
- return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
+ return archetype.entities[row], unpack(queryOutput, 1, idsCount)
end
- end
+ end;
}
end
return table.freeze({
- World = World,
- ON_ADD = ON_ADD,
- ON_REMOVE = ON_REMOVE,
- ON_SET = ON_SET
+ World = World;
+ ON_ADD = ON_ADD;
+ ON_REMOVE = ON_REMOVE;
+ ON_SET = ON_SET;
})
diff --git a/newMatter.lua b/newMatter.lua
deleted file mode 100644
index 4a50791..0000000
--- a/newMatter.lua
+++ /dev/null
@@ -1,1499 +0,0 @@
---!optimize 2
---!native
---!strict
-
-local None = {}
-
-local function merge(one, two)
- local new = table.clone(one)
-
- for key, value in two do
- if value == None then
- new[key] = nil
- else
- new[key] = value
- end
- end
-
- return new
-end
-
--- https://github.com/freddylist/llama/blob/master/src/List/toSet.lua
-local function toSet(list)
- local set = {}
-
- for _, v in ipairs(list) do
- set[v] = true
- end
-
- return set
-end
-
--- https://github.com/freddylist/llama/blob/master/src/Dictionary/values.lua
-local function values(dictionary)
- local valuesList = {}
-
- local index = 1
-
- for _, value in pairs(dictionary) do
- valuesList[index] = value
- index = index + 1
- end
-
- return valuesList
-end
-
-local stack = {}
-
-local function newStackFrame(node)
- return {
- node = node,
- accessedKeys = {},
- }
-end
-
-local function cleanup()
- local currentFrame = stack[#stack]
-
- for baseKey, state in pairs(currentFrame.node.system) do
- for key, value in pairs(state.storage) do
- if not currentFrame.accessedKeys[baseKey] or not currentFrame.accessedKeys[baseKey][key] then
- local cleanupCallback = state.cleanupCallback
-
- if cleanupCallback then
- local shouldAbortCleanup = cleanupCallback(value)
-
- if shouldAbortCleanup then
- continue
- end
- end
-
- state.storage[key] = nil
- end
- end
- end
-end
-
-local function start(node, fn)
- table.insert(stack, newStackFrame(node))
- fn()
- cleanup()
- table.remove(stack, #stack)
-end
-
-local function withinTopoContext()
- return #stack ~= 0
-end
-
-local function useFrameState()
- return stack[#stack].node.frame
-end
-
-local function useCurrentSystem()
- if #stack == 0 then
- return
- end
-
- return stack[#stack].node.currentSystem
-end
-
-
---[=[
- @within Matter
-
- :::tip
- **Don't use this function directly in your systems.**
-
- This function is used for implementing your own topologically-aware functions. It should not be used in your
- systems directly. You should use this function to implement your own utilities, similar to `useEvent` and
- `useThrottle`.
- :::
-
- `useHookState` does one thing: it returns a table. An empty, pristine table. Here's the cool thing though:
- it always returns the *same* table, based on the script and line where *your function* (the function calling
- `useHookState`) was called.
-
- ### Uniqueness
-
- If your function is called multiple times from the same line, perhaps within a loop, the default behavior of
- `useHookState` is to uniquely identify these by call count, and will return a unique table for each call.
-
- However, you can override this behavior: you can choose to key by any other value. This means that in addition to
- script and line number, the storage will also only return the same table if the unique value (otherwise known as the
- "discriminator") is the same.
-
- ### Cleaning up
- As a second optional parameter, you can pass a function that is automatically invoked when your storage is about
- to be cleaned up. This happens when your function (and by extension, `useHookState`) ceases to be called again
- next frame (keyed by script, line number, and discriminator).
-
- Your cleanup callback is passed the storage table that's about to be cleaned up. You can then perform cleanup work,
- like disconnecting events.
-
- *Or*, you could return `true`, and abort cleaning up altogether. If you abort cleanup, your storage will stick
- around another frame (even if your function wasn't called again). This can be used when you know that the user will
- (or might) eventually call your function again, even if they didn't this frame. (For example, caching a value for
- a number of seconds).
-
- If cleanup is aborted, your cleanup function will continue to be called every frame, until you don't abort cleanup,
- or the user actually calls your function again.
-
- ### Example: useThrottle
-
- This is the entire implementation of the built-in `useThrottle` function:
-
- ```lua
- local function cleanup(storage)
- return os.clock() < storage.expiry
- end
-
- local function useThrottle(seconds, discriminator)
- local storage = useHookState(discriminator, cleanup)
-
- if storage.time == nil or os.clock() - storage.time >= seconds then
- storage.time = os.clock()
- storage.expiry = os.clock() + seconds
- return true
- end
-
- return false
- end
- ```
-
- A lot of talk for something so simple, right?
-
- @param discriminator? any -- A unique value to additionally key by
- @param cleanupCallback (storage: {}) -> boolean? -- A function to run when the storage for this hook is cleaned up
-]=]
-local function useHookState(discriminator, cleanupCallback): {}
- local file, line = debug.info(3, "sl")
- local fn = debug.info(2, "f")
-
- local baseKey = string.format("%s:%s:%d", tostring(fn), file, line)
-
- local currentFrame = stack[#stack]
-
- if currentFrame == nil then
- error("Attempt to access topologically-aware storage outside of a Loop-system context.", 3)
- end
-
- if not currentFrame.accessedKeys[baseKey] then
- currentFrame.accessedKeys[baseKey] = {}
- end
-
- local accessedKeys = currentFrame.accessedKeys[baseKey]
-
- local key = #accessedKeys
-
- if discriminator ~= nil then
- if type(discriminator) == "number" then
- discriminator = tostring(discriminator)
- end
-
- key = discriminator
- end
-
- accessedKeys[key] = true
-
- if not currentFrame.node.system[baseKey] then
- currentFrame.node.system[baseKey] = {
- storage = {},
- cleanupCallback = cleanupCallback,
- }
- end
-
- local storage = currentFrame.node.system[baseKey].storage
-
- if not storage[key] then
- storage[key] = {}
- end
-
- return storage[key]
-end
-
-local topoRuntime = {
- start = start,
- useHookState = useHookState,
- useFrameState = useFrameState,
- useCurrentSystem = useCurrentSystem,
- withinTopoContext = withinTopoContext,
-}
-
-
---[=[
- @class Component
-
- A component is a named piece of data that exists on an entity.
- Components are created and removed in the [World](/api/World).
-
- In the docs, the terms "Component" and "ComponentInstance" are used:
- - **"Component"** refers to the base class of a specific type of component you've created.
- This is what [`Matter.component`](/api/Matter#component) returns.
- - **"Component Instance"** refers to an actual piece of data that can exist on an entity.
- The metatable of a component instance table is its respective Component table.
-
- Component instances are *plain-old data*: they do not contain behaviors or methods.
-
- Since component instances are immutable, one helper function exists on all component instances, `patch`,
- which allows reusing data from an existing component instance to make up for the ergonomic loss of mutations.
-]=]
-
---[=[
- @within Component
- @type ComponentInstance {}
-
- The `ComponentInstance` type refers to an actual piece of data that can exist on an entity.
- The metatable of the component instance table is set to its particular Component table.
-
- A component instance can be created by calling the Component table:
-
- ```lua
- -- Component:
- local MyComponent = Matter.component("My component")
-
- -- component instance:
- local myComponentInstance = MyComponent({
- some = "data"
- })
-
- print(getmetatable(myComponentInstance) == MyComponent) --> true
- ```
-]=]
-
--- This is a special value we set inside the component's metatable that will allow us to detect when
--- a Component is accidentally inserted as a Component Instance.
--- It should not be accessible through indexing into a component instance directly.
-local DIAGNOSTIC_COMPONENT_MARKER = {}
-
-local nextId = 0
-local function newComponent(name, defaultData)
- name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l")
- assert(
- defaultData == nil or type(defaultData) == "table",
- "if component default data is specified, it must be a table"
- )
-
- local component = {}
- component.__index = component
-
- function component.new(data)
- data = data or {}
-
- if defaultData then
- data = merge(defaultData, data)
- end
-
- return table.freeze(setmetatable(data, component))
- end
-
- --[=[
- @within Component
-
- ```lua
- for id, target in world:query(Target) do
- if shouldChangeTarget(target) then
- world:insert(id, target:patch({ -- modify the existing component
- currentTarget = getNewTarget()
- }))
- end
- end
- ```
-
- A utility function used to immutably modify an existing component instance. Key/value pairs from the passed table
- will override those of the existing component instance.
-
- As all components are immutable and frozen, it is not possible to modify the existing component directly.
-
- You can use the `Matter.None` constant to remove a value from the component instance:
-
- ```lua
- target:patch({
- currentTarget = Matter.None -- sets currentTarget to nil
- })
- ```
-
- @param partialNewData {} -- The table to be merged with the existing component data.
- @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values.
- ]=]
- function component:patch(partialNewData)
- local patch = getmetatable(self).new(merge(self, partialNewData))
- return patch
- end
-
- nextId += 1
- local id = nextId
-
- setmetatable(component, {
- __call = function(_, ...)
- return component.new(...)
- end,
- __tostring = function()
- return name
- end,
- __len = function()
- return id
- end,
- [DIAGNOSTIC_COMPONENT_MARKER] = true,
- })
-
- return component
-end
-
-local function assertValidType(value, position)
- if typeof(value) ~= "table" then
- error(string.format("Component #%d is invalid: not a table", position), 3)
- end
-
- local metatable = getmetatable(value)
-
- if metatable == nil then
- error(string.format("Component #%d is invalid: has no metatable", position), 3)
- end
-end
-
-local function assertValidComponent(value, position)
- assertValidType(value, position)
-
- local metatable = getmetatable(value)
-
- if getmetatable(metatable) ~= nil and getmetatable(metatable)[DIAGNOSTIC_COMPONENT_MARKER] then
- error(
- string.format(
- "Component #%d is invalid: Component Instance %s was passed instead of the Component itself!",
- position,
- tostring(metatable)
- ),
- 3
- )
- end
-end
-
-local function assertValidComponentInstance(value, position)
- assertValidType(value, position)
-
- if getmetatable(value)[DIAGNOSTIC_COMPONENT_MARKER] ~= nil then
- error(
- string.format(
- "Component #%d is invalid: passed a Component instead of a Component instance; "
- .. "did you forget to call it as a function?",
- position
- ),
- 3
- )
- end
-end
-
-local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed"
-local ERROR_DUPLICATE_ENTITY =
- "The world already contains an entity with ID %d. Use World:replace instead if this is intentional."
-local ERROR_NO_COMPONENTS = "Missing components"
-
-type i53 = number
-type i24 = number
-
-type Component = { [any]: any }
-type ComponentInstance = Component
-
-type Ty = { i53 }
-type ArchetypeId = number
-
-type Column = { any }
-
-type Archetype = {
- -- Unique identifier of this 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 }
-
-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 componentId, column in columns do
- local targetColumn = destinationColumns[tr[types[componentId]]]
- if targetColumn then
- targetColumn[destinationRow] = column[sourceRow]
- end
-
- if sourceRow ~= #column then
- column[sourceRow] = column[#column]
- column[#column] = nil
- end
- end
-
- destinationEntities[destinationRow] = sourceEntities[sourceRow]
- entityIndex[sourceEntities[sourceRow]].row = destinationRow
-
- local movedAway = #sourceEntities
- if sourceRow ~= movedAway then
- sourceEntities[sourceRow] = sourceEntities[movedAway]
- entityIndex[sourceEntities[movedAway]].row = sourceRow
- end
-
- sourceEntities[movedAway] = nil
-end
-
-local function archetypeAppend(entity: i53, archetype: Archetype): i24
- local entities = archetype.entities
- table.insert(entities, entity)
- return #entities
-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)
- local destinationCount = #to.types
- local destinationIds = to.types
-
- for i = 1, destinationCount do
- local destinationId = destinationIds[i]
-
- if not componentIndex[destinationId] then
- componentIndex[destinationId] = { sparse = {}, size = 0 }
- end
- componentIndex[destinationId].sparse[to.id] = i
- to.records[destinationId] = i
- end
-end
-
-local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype
- local ty = hash(types)
-
- world.nextArchetypeId = (world.nextArchetypeId :: number) + 1
- local id = world.nextArchetypeId
-
- local columns = {} :: { any }
-
- for _ in types do
- table.insert(columns, {})
- end
-
- local archetype = {
- id = id,
- types = types,
- type = ty,
- columns = columns,
- entities = {},
- edges = {},
- records = {},
- }
-
- world.archetypeIndex[ty] = archetype
- world.archetypes[id] = archetype
-
- if #types > 0 then
- createArchetypeRecords(world.componentIndex, archetype, prev)
- end
-
- return archetype
-end
-
-local World = {}
-World.__index = World
-
-function World.new()
- local self = setmetatable({
- entityIndex = {},
- componentIndex = {},
- componentIdToComponent = {},
- archetypes = {},
- archetypeIndex = {},
- nextId = 0,
- nextArchetypeId = 0,
- _size = 0,
- _changedStorage = {},
- }, World)
-
- self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
- return self
-end
-
-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)
- local count = #types
- for i = 1, count do
- local id = types[i]
- if id == toAdd then
- return -1
- end
- if id > toAdd then
- return i
- end
- end
- return count + 1
-end
-
-local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
- local types = node.types
- local at = findInsert(types, componentId)
- if at == -1 then
- 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)
- if not archetype.edges[componentId] then
- archetype.edges[componentId] = {} :: any
- end
- return archetype.edges[componentId]
-end
-
-local function archetypeTraverseAdd(world: World, componentId: i53, archetype: Archetype?): Archetype
- local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
- local edge = ensureEdge(from, componentId)
-
- if not edge.add then
- edge.add = findArchetypeWith(world, from, componentId)
- end
-
- return edge.add
-end
-
-local function componentAdd(world: World, entityId: i53, componentInstance)
- local component = getmetatable(componentInstance)
- local componentId = #component
-
- -- TODO:
- -- This never gets cleaned up
- world.componentIdToComponent[componentId] = component
-
- local record = world:ensureRecord(entityId)
- local sourceArchetype = record.archetype
- local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype)
-
- if sourceArchetype == destinationArchetype then
- local archetypeRecord = destinationArchetype.records[componentId]
- destinationArchetype.columns[archetypeRecord][record.row] = componentInstance
- return
- end
-
- if sourceArchetype then
- moveEntity(world.entityIndex, entityId, record, destinationArchetype)
- else
- -- if it has any components, then it wont be the root archetype
- if #destinationArchetype.types > 0 then
- newEntity(entityId, record, destinationArchetype)
- end
- end
-
- local archetypeRecord = destinationArchetype.records[componentId]
- destinationArchetype.columns[archetypeRecord][record.row] = componentInstance
-end
-
-function World.ensureRecord(world: World, entityId: i53)
- local entityIndex = world.entityIndex
- local id = entityId
- if not entityIndex[id] then
- entityIndex[id] = {} :: Record
- end
- return entityIndex[id]
-end
-
-local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
- local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
- local edge = ensureEdge(from, componentId)
-
- if not edge.remove then
- local to = table.clone(from.types)
- table.remove(to, table.find(to, componentId))
- edge.remove = ensureArchetype(world, to, from)
- end
-
- return edge.remove
-end
-
-local function get(componentIndex: ComponentIndex, record: Record, componentId: i24): ComponentInstance?
- local archetype = record.archetype
- if archetype == nil then
- return nil
- end
-
- local archetypeRecord = archetype.records[componentId]
- if not archetypeRecord then
- return nil
- end
-
- return archetype.columns[archetypeRecord][record.row]
-end
-
-local function componentRemove(world: World, entityId: i53, component: Component)
- local componentId = #component
- local record = world:ensureRecord(entityId)
- local sourceArchetype = record.archetype
- local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
-
- -- TODO:
- -- There is a better way to get the component for returning
- local componentInstance = get(world.componentIndex, record, componentId)
- if sourceArchetype and not (sourceArchetype == destinationArchetype) then
- moveEntity(world.entityIndex, entityId, record, destinationArchetype)
- end
-
- return componentInstance
-end
-
---[=[
- Removes a component (or set of components) from an existing entity.
-
- ```lua
- local removedA, removedB = world:remove(entityId, ComponentA, ComponentB)
- ```
-
- @param entityId number -- The entity ID
- @param ... Component -- The components to remove
- @return ...ComponentInstance -- Returns the component instance values that were removed in the order they were passed.
-]=]
-function World.remove(world: World, entityId: i53, ...)
- if not world:contains(entityId) then
- error(ERROR_NO_ENTITY, 2)
- end
-
- local length = select("#", ...)
- local removed = {}
- for i = 1, length do
- table.insert(removed, componentRemove(world, entityId, select(i, ...)))
- end
-
- return unpack(removed, 1, length)
-end
-
-function World.get(
- world: World,
- entityId: i53,
- a: Component,
- b: Component?,
- c: Component?,
- d: Component?,
- e: Component?
-): any
- local componentIndex = world.componentIndex
- local record = world.entityIndex[entityId]
- if not record then
- return nil
- end
-
- local va = get(componentIndex, record, #a)
-
- if b == nil then
- return va
- elseif c == nil then
- return va, get(componentIndex, record, #b)
- elseif d == nil then
- return va, get(componentIndex, record, #b), get(componentIndex, record, #c)
- elseif e == nil then
- return va, get(componentIndex, record, #b), get(componentIndex, record, #c), get(componentIndex, record, #d)
- else
- error("args exceeded")
- end
-end
-
-function World.insert(world: World, entityId: i53, ...)
- if not world:contains(entityId) then
- error(ERROR_NO_ENTITY, 2)
- end
-
- for i = 1, select("#", ...) do
- local newComponent = select(i, ...)
- assertValidComponentInstance(newComponent, i)
-
- local metatable = getmetatable(newComponent)
- local oldComponent = world:get(entityId, metatable)
- componentAdd(world, entityId, newComponent)
-
- world:_trackChanged(metatable, entityId, oldComponent, newComponent)
- end
-end
-
-function World.replace(world: World, entityId: i53, ...: ComponentInstance)
- error("Replace is unimplemented")
-
- if not world:contains(entityId) then
- error(ERROR_NO_ENTITY, 2)
- end
-
- --moveEntity(entityId, record, world.ROOT_ARCHETYPE)
- for i = 1, select("#", ...) do
- local newComponent = select(i, ...)
- assertValidComponentInstance(newComponent, i)
- end
-end
-
-function World.entity(world: World)
- world.nextId += 1
- return world.nextId
-end
-
-function World:__iter()
- local previous = nil
- return function()
- local entityId, data = next(self.entityIndex, previous)
- previous = entityId
-
- if entityId == nil then
- return nil
- end
-
- local archetype = data.archetype
- if not archetype then
- return entityId, {}
- end
-
- local columns = archetype.columns
- local components = {}
- for i, map in columns do
- local componentId = archetype.types[i]
- components[self.componentIdToComponent[componentId]] = map[data.row]
- end
-
- return entityId, components
- end
-end
-
-function World._trackChanged(world: World, metatable, id, old, new)
- if not world._changedStorage[metatable] then
- return
- end
-
- if old == new then
- return
- end
-
- local record = table.freeze({
- old = old,
- new = new,
- })
-
- for _, storage in ipairs(world._changedStorage[metatable]) do
- -- If this entity has changed since the last time this system read it,
- -- we ensure that the "old" value is whatever the system saw it as last, instead of the
- -- "old" value we have here.
- if storage[id] then
- storage[id] = table.freeze({ old = storage[id].old, new = new })
- else
- storage[id] = record
- end
- end
-end
-
---[=[
- Spawns a new entity in the world with a specific entity ID and given components.
-
- The next ID generated from [World:spawn] will be increased as needed to never collide with a manually specified ID.
-
- @param entityId number -- The entity ID to spawn with
- @param ... ComponentInstance -- The component values to spawn the entity with.
- @return number -- The same entity ID that was passed in
-]=]
-function World.spawnAt(world: World, entityId: i53, ...: ComponentInstance)
- if world:contains(entityId) then
- error(string.format(ERROR_DUPLICATE_ENTITY, entityId), 2)
- end
-
- if entityId >= world.nextId then
- world.nextId = entityId + 1
- end
-
- world._size += 1
- world:ensureRecord(entityId)
-
- local components = {}
- for i = 1, select("#", ...) do
- local component = select(i, ...)
- assertValidComponentInstance(component, i)
-
- local metatable = getmetatable(component)
- if components[metatable] then
- error(("Duplicate component type at index %d"):format(i), 2)
- end
-
- world:_trackChanged(metatable, entityId, nil, component)
-
- components[metatable] = component
- componentAdd(world, entityId, component)
- end
-
- return entityId
-end
-
---[=[
- Spawns a new entity in the world with the given components.
-
- @param ... ComponentInstance -- The component values to spawn the entity with.
- @return number -- The new entity ID.
-]=]
-function World.spawn(world: World, ...: ComponentInstance)
- return world:spawnAt(world.nextId, ...)
-end
-
-function World.despawn(world: World, entityId: i53)
- local entityIndex = world.entityIndex
- local record = entityIndex[entityId]
- moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
- world.ROOT_ARCHETYPE.entities[record.row] = nil
- entityIndex[entityId] = nil
- world._size -= 1
-end
-
-function World.clear(world: World)
- world.entityIndex = {}
- world.componentIndex = {}
- world.archetypes = {}
- world.archetypeIndex = {}
- world._size = 0
- world.ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
-end
-
-function World.size(world: World)
- return world._size
-end
-
-function World.contains(world: World, entityId: i53)
- return world.entityIndex[entityId] ~= nil
-end
-
-local function noop(): any
- return function() end
-end
-
-local emptyQueryResult = setmetatable({
- next = function() end,
- snapshot = function()
- return {}
- end,
- without = function(self)
- return self
- end,
- view = function()
- return {
- get = function() end,
- contains = function() end,
- }
- end,
-}, {
- __iter = noop,
- __call = noop,
-})
-
-local function queryResult(compatibleArchetypes, components: { number }, queryLength, ...): any
- local a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any = ...
- local lastArchetype, archetype = next(compatibleArchetypes)
- if not lastArchetype then
- return emptyQueryResult
- end
-
- local lastRow
- local queryOutput = {}
- local function iterate()
- local row = next(archetype.entities, lastRow)
- while row == nil do
- lastArchetype, archetype = next(compatibleArchetypes, lastArchetype)
- if lastArchetype == nil then
- return
- end
- row = next(archetype.entities, row)
- end
-
- lastRow = row
-
- local columns = archetype.columns
- local entityId = archetype.entities[row :: number]
- local archetypeRecords = archetype.records
-
- if queryLength == 1 then
- return entityId, columns[archetypeRecords[a]][row]
- elseif queryLength == 2 then
- return entityId, columns[archetypeRecords[a]][row], columns[archetypeRecords[b]][row]
- elseif queryLength == 3 then
- return entityId,
- columns[archetypeRecords[a]][row],
- columns[archetypeRecords[b]][row],
- columns[archetypeRecords[c]][row]
- elseif queryLength == 4 then
- return entityId,
- columns[archetypeRecords[a]][row],
- columns[archetypeRecords[b]][row],
- columns[archetypeRecords[c]][row],
- columns[archetypeRecords[d]][row]
- elseif queryLength == 5 then
- return entityId,
- columns[archetypeRecords[a]][row],
- columns[archetypeRecords[b]][row],
- columns[archetypeRecords[c]][row],
- columns[archetypeRecords[d]][row],
- columns[archetypeRecords[e]][row]
- elseif queryLength == 6 then
- return entityId,
- columns[archetypeRecords[a]][row],
- columns[archetypeRecords[b]][row],
- columns[archetypeRecords[c]][row],
- columns[archetypeRecords[d]][row],
- columns[archetypeRecords[e]][row],
- columns[archetypeRecords[f]][row]
- elseif queryLength == 7 then
- return columns[archetypeRecords[a]][row],
- columns[archetypeRecords[b]][row],
- columns[archetypeRecords[c]][row],
- columns[archetypeRecords[d]][row],
- columns[archetypeRecords[e]][row],
- columns[archetypeRecords[f]][row],
- columns[archetypeRecords[g]][row]
- elseif queryLength == 8 then
- return columns[archetypeRecords[a]][row],
- columns[archetypeRecords[b]][row],
- columns[archetypeRecords[c]][row],
- columns[archetypeRecords[d]][row],
- columns[archetypeRecords[e]][row],
- columns[archetypeRecords[f]][row],
- columns[archetypeRecords[g]][row],
- columns[archetypeRecords[h]][row]
- end
-
- for i, componentId in components do
- queryOutput[i] = columns[archetypeRecords[componentId]][row]
- end
-
- return entityId, unpack(queryOutput, 1, queryLength)
- end
- --[=[
- @class QueryResult
-
- A result from the [`World:query`](/api/World#query) function.
-
- Calling the table or the `next` method allows iteration over the results. Once all results have been returned, the
- QueryResult is exhausted and is no longer useful.
-
- ```lua
- for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
- -- Do something
- end
- ```
- ]=]
- local QueryResult = {}
- QueryResult.__index = QueryResult
-
- -- TODO:
- -- remove in matter 1.0
- function QueryResult:__call()
- return iterate()
- end
-
- function QueryResult:__iter()
- return function()
- return iterate()
- end
- end
-
- --[=[
- Returns an iterator that will skip any entities that also have the given components.
-
- @param ... Component -- The component types to filter against.
- @return () -> (id, ...ComponentInstance) -- Iterator of entity ID followed by the requested component values
-
- ```lua
- for id in world:query(Target):without(Model) do
- -- Do something
- end
- ```
- ]=]
- function QueryResult:without(...)
- local components = { ... }
- for i, component in components do
- components[i] = #component
- end
-
- local compatibleArchetypes = compatibleArchetypes
- for i = #compatibleArchetypes, 1, -1 do
- local archetype = compatibleArchetypes[i]
- local shouldRemove = false
- for _, componentId in components do
- if archetype.records[componentId] then
- shouldRemove = true
- break
- end
- end
-
- if shouldRemove then
- table.remove(compatibleArchetypes, i)
- end
- end
-
- lastArchetype, archetype = next(compatibleArchetypes)
- if not lastArchetype then
- return emptyQueryResult
- end
-
- return self
- end
-
- --[=[
- Returns the next set of values from the query result. Once all results have been returned, the
- QueryResult is exhausted and is no longer useful.
-
- :::info
- This function is equivalent to calling the QueryResult as a function. When used in a for loop, this is implicitly
- done by the language itself.
- :::
-
- ```lua
- -- Using world:query in this position will make Lua invoke the table as a function. This is conventional.
- for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
- -- Do something
- end
- ```
-
- If you wanted to iterate over the QueryResult without a for loop, it's recommended that you call `next` directly
- instead of calling the QueryResult as a function.
- ```lua
- local id, enemy, charge, model = world:query(Enemy, Charge, Model):next()
- local id, enemy, charge, model = world:query(Enemy, Charge, Model)() -- Possible, but unconventional
- ```
-
- @return id -- Entity ID
- @return ...ComponentInstance -- The requested component values
- ]=]
- function QueryResult:next()
- return iterate()
- end
-
- local function drain()
- local entry = table.pack(iterate())
- return if entry.n > 0 then entry else nil
- end
-
- local Snapshot = {
- __iter = function(self): any
- local i = 0
- return function()
- i += 1
-
- local data = self[i] :: any
-
- if data then
- return unpack(data, 1, data.n)
- end
-
- return
- end
- end,
- }
-
- function QueryResult:snapshot()
- local list = setmetatable({}, Snapshot) :: any
- for entry in drain do
- table.insert(list, entry)
- end
-
- return list
- end
-
- --[=[
- Creates a View of the query and does all of the iterator tasks at once at an amortized cost.
- This is used for many repeated random access to an entity. If you only need to iterate, just use a query.
-
- ```lua
- local inflicting = world:query(Damage, Hitting, Player):view()
- for _, source in world:query(DamagedBy) do
- local damage = inflicting:get(source.from)
- end
-
- for _ in world:query(Damage):view() do end -- You can still iterate views if you want!
- ```
-
- @return View See [View](/api/View) docs.
- ]=]
- function QueryResult:view()
- local fetches = {}
- local list = {} :: any
-
- local View = {}
- View.__index = View
-
- function View:__iter()
- local current = list.head
- return function()
- if not current then
- return
- end
- local entity = current.entity
- local fetch = fetches[entity]
- current = current.next
-
- return entity, unpack(fetch, 1, fetch.n)
- end
- end
-
- --[=[
- @within View
- Retrieve the query results to corresponding `entity`
- @param entity number - the entity ID
- @return ...ComponentInstance
- ]=]
- function View:get(entity)
- if not self:contains(entity) then
- return
- end
-
- local fetch = fetches[entity]
- local queryLength = fetch.n
-
- if queryLength == 1 then
- return fetch[1]
- elseif queryLength == 2 then
- return fetch[1], fetch[2]
- elseif queryLength == 3 then
- return fetch[1], fetch[2], fetch[3]
- elseif queryLength == 4 then
- return fetch[1], fetch[2], fetch[3], fetch[4]
- elseif queryLength == 5 then
- return fetch[1], fetch[2], fetch[3], fetch[4], fetch[5]
- end
-
- return unpack(fetch, 1, fetch.n)
- end
-
- --[=[
- @within View
- Equivalent to `world:contains()`
- @param entity number - the entity ID
- @return boolean
- ]=]
- function View:contains(entity)
- return fetches[entity] ~= nil
- end
-
- for entry in drain do
- local entityId = entry[1]
- local fetch = table.pack(select(2, unpack(entry)))
- local node = { entity = entityId, next = nil }
- fetches[entityId] = fetch
-
- if not list.head then
- list.head = node
- else
- local current = list.head
- while current.next do
- current = current.next
- end
- current.next = node
- end
- end
-
- return setmetatable({}, View)
- end
-
- return setmetatable({}, QueryResult)
-end
-
---[=[
- Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over
- the results of the query.
-
- Order of iteration is not guaranteed.
-
- ```lua
- for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
- -- Do something
- end
-
- for id in world:query(Target):without(Model) do
- -- Again, with feeling
- end
- ```
-
- @param ... Component -- The component types to query. Only entities with *all* of these components will be returned.
- @return QueryResult -- See [QueryResult](/api/QueryResult) docs.
-]=]
-function World.query(world: World, ...: Component): any
- local compatibleArchetypes = {}
- local components = { ... }
- local archetypes = world.archetypes
- local queryLength = select("#", ...)
- local a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any = ...
-
- if queryLength == 0 then
- return emptyQueryResult
- end
-
- if queryLength == 1 then
- a = #a
- components = { a }
- -- local archetypesMap = world.componentIndex[a]
- -- components = { a }
- -- local function single()
- -- local id = next(archetypesMap)
- -- local archetype = archetypes[id :: number]
- -- local lastRow
-
- -- return function(): any
- -- local row, entity = next(archetype.entities, lastRow)
- -- while row == nil do
- -- id = next(archetypesMap, id)
- -- if id == nil then
- -- return
- -- end
- -- archetype = archetypes[id]
- -- row = next(archetype.entities, row)
- -- end
- -- lastRow = row
-
- -- return entity, archetype.columns[archetype.records[a]]
- -- end
- -- end
- -- return single()
- elseif queryLength == 2 then
- --print("iter double")
- a = #a
- b = #b
- components = { a, b }
-
- -- --print(a, b, world.componentIndex)
- -- --[[local archetypesMap = world.componentIndex[a]
- -- for id in archetypesMap do
- -- local archetype = archetypes[id]
- -- if archetype.records[b] then
- -- table.insert(compatibleArchetypes, archetype)
- -- end
- -- end
-
- -- local function double(): () -> (number, any, any)
- -- local lastArchetype, archetype = next(compatibleArchetypes)
- -- local lastRow
-
- -- return function()
- -- local row = next(archetype.entities, lastRow)
- -- while row == nil do
- -- lastArchetype, archetype = next(compatibleArchetypes, lastArchetype)
- -- if lastArchetype == nil then
- -- return
- -- end
-
- -- row = next(archetype.entities, row)
- -- end
- -- lastRow = row
-
- -- local entity = archetype.entities[row :: number]
- -- local columns = archetype.columns
- -- local archetypeRecords = archetype.records
- -- return entity, columns[archetypeRecords[a]], columns[archetypeRecords[b]]
- -- end
- -- end
- -- return double()
- elseif queryLength == 3 then
- a = #a
- b = #b
- c = #c
- components = { a, b, c }
- elseif queryLength == 4 then
- a = #a
- b = #b
- c = #c
- d = #d
-
- components = { a, b, c, d }
- elseif queryLength == 5 then
- a = #a
- b = #b
- c = #c
- d = #d
- e = #e
-
- components = { a, b, c, d, e }
- elseif queryLength == 6 then
- a = #a
- b = #b
- c = #c
- d = #d
- e = #e
- f = #f
-
- components = { a, b, c, d, e, f }
- elseif queryLength == 7 then
- a = #a
- b = #b
- c = #c
- d = #d
- e = #e
- f = #f
- g = #g
-
- components = { a, b, c, d, e, f, g }
- elseif queryLength == 8 then
- a = #a
- b = #b
- c = #c
- d = #d
- e = #e
- f = #f
- g = #g
- h = #h
-
- components = { a, b, c, d, e, f, g, h }
- else
- for i, component in components do
- components[i] = (#component) :: any
- end
- end
-
- local firstArchetypeMap
- local componentIndex = world.componentIndex
- for _, componentId in (components :: any) :: { number } do
- local map = componentIndex[componentId]
- if not map then
- return emptyQueryResult
- 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 matched = true
- for _, componentId in components do
- if not archetypeRecords[componentId] then
- matched = false
- break
- end
- end
-
- if matched then
- table.insert(compatibleArchetypes, archetype)
- end
- end
-
- return queryResult(compatibleArchetypes, components :: any, queryLength, a, b, c, d, e, f, g, h)
-end
-
-local function cleanupQueryChanged(hookState)
- local world = hookState.world
- local componentToTrack = hookState.componentToTrack
-
- for index, object in world._changedStorage[componentToTrack] do
- if object == hookState.storage then
- table.remove(world._changedStorage[componentToTrack], index)
- break
- end
- end
-
- if next(world._changedStorage[componentToTrack]) == nil then
- world._changedStorage[componentToTrack] = nil
- end
-end
-
-function World.queryChanged(world: World, componentToTrack, ...: nil)
- if ... then
- error("World:queryChanged does not take any additional parameters", 2)
- end
-
- local hookState = topoRuntime.useHookState(componentToTrack, cleanupQueryChanged) :: any
- if hookState.storage then
- return function(): any
- local entityId, record = next(hookState.storage)
-
- if entityId then
- hookState.storage[entityId] = nil
-
- return entityId, record
- end
- return
- end
- end
-
- if not world._changedStorage[componentToTrack] then
- world._changedStorage[componentToTrack] = {}
- end
-
- local storage = {}
- hookState.storage = storage
- hookState.world = world
- hookState.componentToTrack = componentToTrack
-
- table.insert(world._changedStorage[componentToTrack], storage)
-
- local queryResult = world:query(componentToTrack)
-
- return function(): any
- local entityId, component = queryResult:next()
-
- if entityId then
- return entityId, table.freeze({ new = component })
- end
- return
- end
-end
-
-return {
- World = World,
- component = newComponent
-}
diff --git a/oldMatter.lua b/oldMatter.lua
deleted file mode 100644
index 0baf7a7..0000000
--- a/oldMatter.lua
+++ /dev/null
@@ -1,1567 +0,0 @@
-
-local None = {}
-
-local function merge(one, two)
- local new = table.clone(one)
-
- for key, value in two do
- if value == None then
- new[key] = nil
- else
- new[key] = value
- end
- end
-
- return new
-end
-
--- https://github.com/freddylist/llama/blob/master/src/List/toSet.lua
-local function toSet(list)
- local set = {}
-
- for _, v in ipairs(list) do
- set[v] = true
- end
-
- return set
-end
-
--- https://github.com/freddylist/llama/blob/master/src/Dictionary/values.lua
-local function values(dictionary)
- local valuesList = {}
-
- local index = 1
-
- for _, value in pairs(dictionary) do
- valuesList[index] = value
- index = index + 1
- end
-
- return valuesList
-end
-
-local valueIds = {}
-local nextValueId = 0
-local compatibilityCache = {}
-local archetypeCache = {}
-
-local function getValueId(value)
- local valueId = valueIds[value]
- if valueId == nil then
- valueIds[value] = nextValueId
- valueId = nextValueId
- nextValueId += 1
- end
-
- return valueId
-end
-
-function archetypeOf(...)
- local length = select("#", ...)
-
- local currentNode = archetypeCache
-
- for i = 1, length do
- local nextNode = currentNode[select(i, ...)]
-
- if not nextNode then
- nextNode = {}
- currentNode[select(i, ...)] = nextNode
- end
-
- currentNode = nextNode
- end
-
- if currentNode._archetype then
- return currentNode._archetype
- end
-
- local list = table.create(length)
-
- for i = 1, length do
- list[i] = getValueId(select(i, ...))
- end
-
- table.sort(list)
-
- local archetype = table.concat(list, "_")
-
- currentNode._archetype = archetype
-
- return archetype
-end
-
-function negateArchetypeOf(...)
- return string.gsub(archetypeOf(...), "_", "x")
-end
-
-function areArchetypesCompatible(queryArchetype, targetArchetype)
- local archetypes = string.split(queryArchetype, "x")
- local baseArchetype = table.remove(archetypes, 1)
-
- local cachedCompatibility = compatibilityCache[queryArchetype .. "-" .. targetArchetype]
- if cachedCompatibility ~= nil then
- return cachedCompatibility
- end
-
- local queryIds = string.split(baseArchetype, "_")
- local targetIds = toSet(string.split(targetArchetype, "_"))
- local excludeIds = toSet(archetypes)
-
- for _, queryId in ipairs(queryIds) do
- if targetIds[queryId] == nil then
- compatibilityCache[queryArchetype .. "-" .. targetArchetype] = false
- return false
- end
- end
-
- for excludeId in excludeIds do
- if targetIds[excludeId] then
- compatibilityCache[queryArchetype .. "-" .. targetArchetype] = false
- return false
- end
- end
-
- compatibilityCache[queryArchetype .. "-" .. targetArchetype] = true
-
- return true
-
-end
-
-local stack = {}
-
-local function newStackFrame(node)
- return {
- node = node,
- accessedKeys = {},
- }
-end
-
-local function cleanup()
- local currentFrame = stack[#stack]
-
- for baseKey, state in pairs(currentFrame.node.system) do
- for key, value in pairs(state.storage) do
- if not currentFrame.accessedKeys[baseKey] or not currentFrame.accessedKeys[baseKey][key] then
- local cleanupCallback = state.cleanupCallback
-
- if cleanupCallback then
- local shouldAbortCleanup = cleanupCallback(value)
-
- if shouldAbortCleanup then
- continue
- end
- end
-
- state.storage[key] = nil
- end
- end
- end
-end
-
-local function start(node, fn)
- table.insert(stack, newStackFrame(node))
- fn()
- cleanup()
- table.remove(stack, #stack)
-end
-
-local function withinTopoContext()
- return #stack ~= 0
-end
-
-local function useFrameState()
- return stack[#stack].node.frame
-end
-
-local function useCurrentSystem()
- if #stack == 0 then
- return
- end
-
- return stack[#stack].node.currentSystem
-end
-
-
---[=[
- @within Matter
-
- :::tip
- **Don't use this function directly in your systems.**
-
- This function is used for implementing your own topologically-aware functions. It should not be used in your
- systems directly. You should use this function to implement your own utilities, similar to `useEvent` and
- `useThrottle`.
- :::
-
- `useHookState` does one thing: it returns a table. An empty, pristine table. Here's the cool thing though:
- it always returns the *same* table, based on the script and line where *your function* (the function calling
- `useHookState`) was called.
-
- ### Uniqueness
-
- If your function is called multiple times from the same line, perhaps within a loop, the default behavior of
- `useHookState` is to uniquely identify these by call count, and will return a unique table for each call.
-
- However, you can override this behavior: you can choose to key by any other value. This means that in addition to
- script and line number, the storage will also only return the same table if the unique value (otherwise known as the
- "discriminator") is the same.
-
- ### Cleaning up
- As a second optional parameter, you can pass a function that is automatically invoked when your storage is about
- to be cleaned up. This happens when your function (and by extension, `useHookState`) ceases to be called again
- next frame (keyed by script, line number, and discriminator).
-
- Your cleanup callback is passed the storage table that's about to be cleaned up. You can then perform cleanup work,
- like disconnecting events.
-
- *Or*, you could return `true`, and abort cleaning up altogether. If you abort cleanup, your storage will stick
- around another frame (even if your function wasn't called again). This can be used when you know that the user will
- (or might) eventually call your function again, even if they didn't this frame. (For example, caching a value for
- a number of seconds).
-
- If cleanup is aborted, your cleanup function will continue to be called every frame, until you don't abort cleanup,
- or the user actually calls your function again.
-
- ### Example: useThrottle
-
- This is the entire implementation of the built-in `useThrottle` function:
-
- ```lua
- local function cleanup(storage)
- return os.clock() < storage.expiry
- end
-
- local function useThrottle(seconds, discriminator)
- local storage = useHookState(discriminator, cleanup)
-
- if storage.time == nil or os.clock() - storage.time >= seconds then
- storage.time = os.clock()
- storage.expiry = os.clock() + seconds
- return true
- end
-
- return false
- end
- ```
-
- A lot of talk for something so simple, right?
-
- @param discriminator? any -- A unique value to additionally key by
- @param cleanupCallback (storage: {}) -> boolean? -- A function to run when the storage for this hook is cleaned up
-]=]
-local function useHookState(discriminator, cleanupCallback): {}
- local file, line = debug.info(3, "sl")
- local fn = debug.info(2, "f")
-
- local baseKey = string.format("%s:%s:%d", tostring(fn), file, line)
-
- local currentFrame = stack[#stack]
-
- if currentFrame == nil then
- error("Attempt to access topologically-aware storage outside of a Loop-system context.", 3)
- end
-
- if not currentFrame.accessedKeys[baseKey] then
- currentFrame.accessedKeys[baseKey] = {}
- end
-
- local accessedKeys = currentFrame.accessedKeys[baseKey]
-
- local key = #accessedKeys
-
- if discriminator ~= nil then
- if type(discriminator) == "number" then
- discriminator = tostring(discriminator)
- end
-
- key = discriminator
- end
-
- accessedKeys[key] = true
-
- if not currentFrame.node.system[baseKey] then
- currentFrame.node.system[baseKey] = {
- storage = {},
- cleanupCallback = cleanupCallback,
- }
- end
-
- local storage = currentFrame.node.system[baseKey].storage
-
- if not storage[key] then
- storage[key] = {}
- end
-
- return storage[key]
-end
-
-local topoRuntime = {
- start = start,
- useHookState = useHookState,
- useFrameState = useFrameState,
- useCurrentSystem = useCurrentSystem,
- withinTopoContext = withinTopoContext,
-}
-
-
-
-local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed"
-
---[=[
- @class World
-
- A World contains entities which have components.
- The World is queryable and can be used to get entities with a specific set of components.
- Entities are simply ever-increasing integers.
-]=]
-local World = {}
-World.__index = World
-
---[=[
- Creates a new World.
-]=]
-function World.new()
- local firstStorage = {}
-
- return setmetatable({
- -- List of maps from archetype string --> entity ID --> entity data
- _storages = { firstStorage },
- -- The most recent storage that has not been dirtied by an iterator
- _pristineStorage = firstStorage,
-
- -- Map from entity ID -> archetype string
- _entityArchetypes = {},
-
- -- Cache of the component metatables on each entity. Used for generating archetype.
- -- Map of entity ID -> array
- _entityMetatablesCache = {},
-
- -- Cache of what query archetypes are compatible with what component archetypes
- _queryCache = {},
-
- -- Cache of what entity archetypes have ever existed in the game. This is used for knowing
- -- when to update the queryCache.
- _entityArchetypeCache = {},
-
- -- The next ID that will be assigned with World:spawn
- _nextId = 1,
-
- -- The total number of active entities in the world
- _size = 0,
-
- -- Storage for `queryChanged`
- _changedStorage = {},
- }, World)
-end
-
--- Searches all archetype storages for the entity with the given archetype
--- Returns the storage that the entity is in if it exists, otherwise nil
-function World:_getStorageWithEntity(archetype, id)
- for _, storage in self._storages do
- local archetypeStorage = storage[archetype]
- if archetypeStorage then
- if archetypeStorage[id] then
- return storage
- end
- end
- end
- return nil
-end
-
-function World:_markStorageDirty()
- local newStorage = {}
- table.insert(self._storages, newStorage)
- self._pristineStorage = newStorage
-
- if topoRuntime.withinTopoContext() then
- local frameState = topoRuntime.useFrameState()
-
- frameState.dirtyWorlds[self] = true
- end
-end
-
-function World:_getEntity(id)
- local archetype = self._entityArchetypes[id]
- local storage = self:_getStorageWithEntity(archetype, id)
-
- return storage[archetype][id]
-end
-
-function World:_next(last)
- local entityId, archetype = next(self._entityArchetypes, last)
-
- if entityId == nil then
- return nil
- end
-
- local storage = self:_getStorageWithEntity(archetype, entityId)
-
- return entityId, storage[archetype][entityId]
-end
-
---[=[
- Iterates over all entities in this World. Iteration returns entity ID followed by a dictionary mapping
- Component to Component Instance.
-
- **Usage:**
-
- ```lua
- for entityId, entityData in world do
- print(entityId, entityData[Components.Example])
- end
- ```
-
- @return number
- @return {[Component]: ComponentInstance}
-]=]
-function World:__iter()
- return World._next, self
-end
-
---[=[
- Spawns a new entity in the world with the given components.
-
- @param ... ComponentInstance -- The component values to spawn the entity with.
- @return number -- The new entity ID.
-]=]
-function World:spawn(...)
- return self:spawnAt(self._nextId, ...)
-end
-
---[=[
- @class Component
-
- A component is a named piece of data that exists on an entity.
- Components are created and removed in the [World](/api/World).
-
- In the docs, the terms "Component" and "ComponentInstance" are used:
- - **"Component"** refers to the base class of a specific type of component you've created.
- This is what [`Matter.component`](/api/Matter#component) returns.
- - **"Component Instance"** refers to an actual piece of data that can exist on an entity.
- The metatable of a component instance table is its respective Component table.
-
- Component instances are *plain-old data*: they do not contain behaviors or methods.
-
- Since component instances are immutable, one helper function exists on all component instances, `patch`,
- which allows reusing data from an existing component instance to make up for the ergonomic loss of mutations.
-]=]
-
---[=[
- @within Component
- @type ComponentInstance {}
-
- The `ComponentInstance` type refers to an actual piece of data that can exist on an entity.
- The metatable of the component instance table is set to its particular Component table.
-
- A component instance can be created by calling the Component table:
-
- ```lua
- -- Component:
- local MyComponent = Matter.component("My component")
-
- -- component instance:
- local myComponentInstance = MyComponent({
- some = "data"
- })
-
- print(getmetatable(myComponentInstance) == MyComponent) --> true
- ```
-]=]
-
--- This is a special value we set inside the component's metatable that will allow us to detect when
--- a Component is accidentally inserted as a Component Instance.
--- It should not be accessible through indexing into a component instance directly.
-local DIAGNOSTIC_COMPONENT_MARKER = {}
-
-local function newComponent(name, defaultData)
- name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l")
-
- assert(
- defaultData == nil or type(defaultData) == "table",
- "if component default data is specified, it must be a table"
- )
-
- local component = {}
- component.__index = component
-
- function component.new(data)
- data = data or {}
-
- if defaultData then
- data = merge(defaultData, data)
- end
-
- return table.freeze(setmetatable(data, component))
- end
-
- --[=[
- @within Component
-
- ```lua
- for id, target in world:query(Target) do
- if shouldChangeTarget(target) then
- world:insert(id, target:patch({ -- modify the existing component
- currentTarget = getNewTarget()
- }))
- end
- end
- ```
-
- A utility function used to immutably modify an existing component instance. Key/value pairs from the passed table
- will override those of the existing component instance.
-
- As all components are immutable and frozen, it is not possible to modify the existing component directly.
-
- You can use the `Matter.None` constant to remove a value from the component instance:
-
- ```lua
- target:patch({
- currentTarget = Matter.None -- sets currentTarget to nil
- })
- ```
-
- @param partialNewData {} -- The table to be merged with the existing component data.
- @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values.
- ]=]
- function component:patch(partialNewData)
- local patch = getmetatable(self).new(merge(self, partialNewData))
- return patch
- end
-
- setmetatable(component, {
- __call = function(_, ...)
- return component.new(...)
- end,
- __tostring = function()
- return name
- end,
- [DIAGNOSTIC_COMPONENT_MARKER] = true,
- })
-
- return component
-end
-
-local function assertValidType(value, position)
- if typeof(value) ~= "table" then
- error(string.format("Component #%d is invalid: not a table", position), 3)
- end
-
- local metatable = getmetatable(value)
-
- if metatable == nil then
- error(string.format("Component #%d is invalid: has no metatable", position), 3)
- end
-end
-
-local function assertValidComponent(value, position)
- assertValidType(value, position)
-
- local metatable = getmetatable(value)
-
- if getmetatable(metatable) ~= nil and getmetatable(metatable)[DIAGNOSTIC_COMPONENT_MARKER] then
- error(
- string.format(
- "Component #%d is invalid: Component Instance %s was passed instead of the Component itself!",
- position,
- tostring(metatable)
- ),
- 3
- )
- end
-end
-
-local function assertValidComponentInstance(value, position)
- assertValidType(value, position)
-
- if getmetatable(value)[DIAGNOSTIC_COMPONENT_MARKER] ~= nil then
- error(
- string.format(
- "Component #%d is invalid: passed a Component instead of a Component instance; "
- .. "did you forget to call it as a function?",
- position
- ),
- 3
- )
- end
-end
-
---[=[
- Spawns a new entity in the world with a specific entity ID and given components.
-
- The next ID generated from [World:spawn] will be increased as needed to never collide with a manually specified ID.
-
- @param id number -- The entity ID to spawn with
- @param ... ComponentInstance -- The component values to spawn the entity with.
- @return number -- The same entity ID that was passed in
-]=]
-function World:spawnAt(id, ...)
- if self:contains(id) then
- error(
- string.format(
- "The world already contains an entity with ID %d. Use World:replace instead if this is intentional.",
- id
- ),
- 2
- )
- end
-
- self._size += 1
-
- if id >= self._nextId then
- self._nextId = id + 1
- end
-
- local components = {}
- local metatables = {}
-
- for i = 1, select("#", ...) do
- local newComponent = select(i, ...)
-
- assertValidComponentInstance(newComponent, i)
-
- local metatable = getmetatable(newComponent)
-
- if components[metatable] then
- error(("Duplicate component type at index %d"):format(i), 2)
- end
-
- self:_trackChanged(metatable, id, nil, newComponent)
-
- components[metatable] = newComponent
- table.insert(metatables, metatable)
- end
-
- self._entityMetatablesCache[id] = metatables
-
- self:_transitionArchetype(id, components)
-
- return id
-end
-
-function World:_newQueryArchetype(queryArchetype)
- if self._queryCache[queryArchetype] == nil then
- self._queryCache[queryArchetype] = {}
- else
- return -- Archetype isn't actually new
- end
-
- for _, storage in self._storages do
- for entityArchetype in storage do
- if areArchetypesCompatible(queryArchetype, entityArchetype) then
- self._queryCache[queryArchetype][entityArchetype] = true
- end
- end
- end
-end
-
-function World:_updateQueryCache(entityArchetype)
- for queryArchetype, compatibleArchetypes in pairs(self._queryCache) do
- if areArchetypesCompatible(queryArchetype, entityArchetype) then
- compatibleArchetypes[entityArchetype] = true
- end
- end
-end
-
-function World:_transitionArchetype(id, components)
- local newArchetype = nil
- local oldArchetype = self._entityArchetypes[id]
- local oldStorage
-
- if oldArchetype then
- oldStorage = self:_getStorageWithEntity(oldArchetype, id)
-
- if not components then
- oldStorage[oldArchetype][id] = nil
- end
- end
-
- if components then
- newArchetype = archetypeOf(unpack(self._entityMetatablesCache[id]))
-
- if oldArchetype ~= newArchetype then
- if oldStorage then
- oldStorage[oldArchetype][id] = nil
- end
-
- if self._pristineStorage[newArchetype] == nil then
- self._pristineStorage[newArchetype] = {}
- end
-
- if self._entityArchetypeCache[newArchetype] == nil then
- self._entityArchetypeCache[newArchetype] = true
- self:_updateQueryCache(newArchetype)
- end
- self._pristineStorage[newArchetype][id] = components
- else
- oldStorage[newArchetype][id] = components
- end
- end
-
- self._entityArchetypes[id] = newArchetype
-end
-
---[=[
- Replaces a given entity by ID with an entirely new set of components.
- Equivalent to removing all components from an entity, and then adding these ones.
-
- @param id number -- The entity ID
- @param ... ComponentInstance -- The component values to spawn the entity with.
-]=]
-function World:replace(id, ...)
- if not self:contains(id) then
- error(ERROR_NO_ENTITY, 2)
- end
-
- local components = {}
- local metatables = {}
- local entity = self:_getEntity(id)
-
- for i = 1, select("#", ...) do
- local newComponent = select(i, ...)
-
- assertValidComponentInstance(newComponent, i)
-
- local metatable = getmetatable(newComponent)
-
- if components[metatable] then
- error(("Duplicate component type at index %d"):format(i), 2)
- end
-
- self:_trackChanged(metatable, id, entity[metatable], newComponent)
-
- components[metatable] = newComponent
- table.insert(metatables, metatable)
- end
-
- for metatable, component in pairs(entity) do
- if not components[metatable] then
- self:_trackChanged(metatable, id, component, nil)
- end
- end
-
- self._entityMetatablesCache[id] = metatables
-
- self:_transitionArchetype(id, components)
-end
-
---[=[
- Despawns a given entity by ID, removing it and all its components from the world entirely.
-
- @param id number -- The entity ID
-]=]
-function World:despawn(id)
- local entity = self:_getEntity(id)
-
- for metatable, component in pairs(entity) do
- self:_trackChanged(metatable, id, component, nil)
- end
-
- self._entityMetatablesCache[id] = nil
- self:_transitionArchetype(id, nil)
-
- self._size -= 1
-end
-
---[=[
- Removes all entities from the world.
-
- :::caution
- Removing entities in this way is not reported by `queryChanged`.
- :::
-]=]
-function World:clear()
- local firstStorage = {}
- self._storages = { firstStorage }
- self._pristineStorage = firstStorage
- self._entityArchetypes = {}
- self._entityMetatablesCache = {}
- self._size = 0
- self._changedStorage = {}
-end
-
---[=[
- Checks if the given entity ID is currently spawned in this world.
-
- @param id number -- The entity ID
- @return bool -- `true` if the entity exists
-]=]
-function World:contains(id)
- return self._entityArchetypes[id] ~= nil
-end
-
---[=[
- Gets a specific component (or set of components) from a specific entity in this world.
-
- @param id number -- The entity ID
- @param ... Component -- The components to fetch
- @return ... -- Returns the component values in the same order they were passed in
-]=]
-function World:get(id, ...)
- if not self:contains(id) then
- error(ERROR_NO_ENTITY, 2)
- end
-
- local entity = self:_getEntity(id)
-
- local length = select("#", ...)
-
- if length == 1 then
- assertValidComponent((...), 1)
- return entity[...]
- end
-
- local components = {}
- for i = 1, length do
- local metatable = select(i, ...)
- assertValidComponent(metatable, i)
- components[i] = entity[metatable]
- end
-
- return unpack(components, 1, length)
-end
-
-local function noop() end
-
-local noopQuery = setmetatable({
- next = noop,
- snapshot = noop,
- without = function(self)
- return self
- end,
- view = {
- get = noop,
- contains = noop,
- },
-}, {
- __iter = function()
- return noop
- end,
-})
-
---[=[
- @class QueryResult
-
- A result from the [`World:query`](/api/World#query) function.
-
- Calling the table or the `next` method allows iteration over the results. Once all results have been returned, the
- QueryResult is exhausted and is no longer useful.
-
- ```lua
- for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
- -- Do something
- end
- ```
-]=]
-
-local QueryResult = {}
-QueryResult.__index = QueryResult
-
-function QueryResult.new(world, expand, queryArchetype, compatibleArchetypes)
- return setmetatable({
- world = world,
- seenEntities = {},
- currentCompatibleArchetype = next(compatibleArchetypes),
- compatibleArchetypes = compatibleArchetypes,
- storageIndex = 1,
- _expand = expand,
- _queryArchetype = queryArchetype,
- }, QueryResult)
-end
-
-local function nextItem(query)
- local world = query.world
- local currentCompatibleArchetype = query.currentCompatibleArchetype
- local seenEntities = query.seenEntities
- local compatibleArchetypes = query.compatibleArchetypes
-
- local entityId, entityData
-
- local storages = world._storages
- repeat
- local nextStorage = storages[query.storageIndex]
- local currently = nextStorage[currentCompatibleArchetype]
- if currently then
- entityId, entityData = next(currently, query.lastEntityId)
- end
-
- while entityId == nil do
- currentCompatibleArchetype = next(compatibleArchetypes, currentCompatibleArchetype)
-
- if currentCompatibleArchetype == nil then
- query.storageIndex += 1
-
- nextStorage = storages[query.storageIndex]
-
- if nextStorage == nil or next(nextStorage) == nil then
- return
- end
-
- currentCompatibleArchetype = nil
-
- if world._pristineStorage == nextStorage then
- world:_markStorageDirty()
- end
-
- continue
- elseif nextStorage[currentCompatibleArchetype] == nil then
- continue
- end
-
- entityId, entityData = next(nextStorage[currentCompatibleArchetype])
- end
-
- query.lastEntityId = entityId
-
- until seenEntities[entityId] == nil
-
- query.currentCompatibleArchetype = currentCompatibleArchetype
-
- seenEntities[entityId] = true
-
- return entityId, entityData
-end
-
-function QueryResult:__iter()
- return function()
- return self._expand(nextItem(self))
- end
-end
-
-function QueryResult:__call()
- return self._expand(nextItem(self))
-end
-
---[=[
- Returns the next set of values from the query result. Once all results have been returned, the
- QueryResult is exhausted and is no longer useful.
-
- :::info
- This function is equivalent to calling the QueryResult as a function. When used in a for loop, this is implicitly
- done by the language itself.
- :::
-
- ```lua
- -- Using world:query in this position will make Lua invoke the table as a function. This is conventional.
- for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
- -- Do something
- end
- ```
-
- If you wanted to iterate over the QueryResult without a for loop, it's recommended that you call `next` directly
- instead of calling the QueryResult as a function.
- ```lua
- local id, enemy, charge, model = world:query(Enemy, Charge, Model):next()
- local id, enemy, charge, model = world:query(Enemy, Charge, Model)() -- Possible, but unconventional
- ```
-
- @return id -- Entity ID
- @return ...ComponentInstance -- The requested component values
-]=]
-function QueryResult:next()
- return self._expand(nextItem(self))
-end
-
-local snapshot = {
- __iter = function(self): any
- local i = 0
- return function()
- i += 1
-
- local data = self[i]
-
- if data then
- return unpack(data, 1, data.n)
- end
- return
- end
- end,
-}
-
---[=[
- Creates a "snapshot" of this query, draining this QueryResult and returning a list containing all of its results.
-
- By default, iterating over a QueryResult happens in "real time": it iterates over the actual data in the ECS, so
- changes that occur during the iteration will affect future results.
-
- By contrast, `QueryResult:snapshot()` creates a list of all of the results of this query at the moment it is called,
- so changes made while iterating over the result of `QueryResult:snapshot` do not affect future results of the
- iteration.
-
- Of course, this comes with a cost: we must allocate a new list and iterate over everything returned from the
- QueryResult in advance, so using this method is slower than iterating over a QueryResult directly.
-
- The table returned from this method has a custom `__iter` method, which lets you use it as you would use QueryResult
- directly:
-
- ```lua
- for entityId, health, player in world:query(Health, Player):snapshot() do
-
- end
- ```
-
- However, the table itself is just a list of sub-tables structured like `{entityId, component1, component2, ...etc}`.
-
- @return {{entityId: number, component: ComponentInstance, component: ComponentInstance, component: ComponentInstance, ...}}
-]=]
-function QueryResult:snapshot()
- local list = setmetatable({}, snapshot)
-
- local function iter()
- return nextItem(self)
- end
-
- for entityId, entityData in iter do
- if entityId then
- table.insert(list, table.pack(self._expand(entityId, entityData)))
- end
- end
-
- return list
-end
-
---[=[
- Returns an iterator that will skip any entities that also have the given components.
-
- :::tip
- This is essentially equivalent to querying normally, using `World:get` to check if a component is present,
- and using Lua's `continue` keyword to skip this iteration (though, using `:without` is faster).
-
- This means that you should avoid queries that return a very large amount of results only to filter them down
- to a few with `:without`. If you can, always prefer adding components and making your query more specific.
- :::
-
- @param ... Component -- The component types to filter against.
- @return () -> (id, ...ComponentInstance) -- Iterator of entity ID followed by the requested component values
-
- ```lua
- for id in world:query(Target):without(Model) do
- -- Do something
- end
- ```
-]=]
-
-function QueryResult:without(...)
- local world = self.world
- local filter = negateArchetypeOf(...)
-
- local negativeArchetype = `{self._queryArchetype}x{filter}`
-
- if world._queryCache[negativeArchetype] == nil then
- world:_newQueryArchetype(negativeArchetype)
- end
-
- local compatibleArchetypes = world._queryCache[negativeArchetype]
-
- self.compatibleArchetypes = compatibleArchetypes
- self.currentCompatibleArchetype = next(compatibleArchetypes)
- return self
-end
-
---[=[
- @class View
-
- Provides random access to the results of a query.
-
- Calling the View is equivalent to iterating a query.
-
- ```lua
- for id, player, health, poison in world:query(Player, Health, Poison):view() do
- -- Do something
- end
- ```
-]=]
-
---[=[
- Creates a View of the query and does all of the iterator tasks at once at an amortized cost.
- This is used for many repeated random access to an entity. If you only need to iterate, just use a query.
-
- ```lua
- local inflicting = world:query(Damage, Hitting, Player):view()
- for _, source in world:query(DamagedBy) do
- local damage = inflicting:get(source.from)
- end
-
- for _ in world:query(Damage):view() do end -- You can still iterate views if you want!
- ```
-
- @return View See [View](/api/View) docs.
-]=]
-
-function QueryResult:view()
- local function iter()
- return nextItem(self)
- end
-
- local fetches = {}
- local list = {} :: any
-
- local View = {}
- View.__index = View
-
- function View:__iter()
- local current = list.head
- return function()
- if not current then
- return
- end
- local entity = current.entity
- local fetch = fetches[entity]
- current = current.next
-
- return entity, unpack(fetch, 1, fetch.n)
- end
- end
-
- --[=[
- @within View
- Retrieve the query results to corresponding `entity`
- @param entity number - the entity ID
- @return ...ComponentInstance
- ]=]
- function View:get(entity)
- if not self:contains(entity) then
- return
- end
-
- local fetch = fetches[entity]
- local queryLength = fetch.n
-
- if queryLength == 1 then
- return fetch[1]
- elseif queryLength == 2 then
- return fetch[1], fetch[2]
- elseif queryLength == 3 then
- return fetch[1], fetch[2], fetch[3]
- elseif queryLength == 4 then
- return fetch[1], fetch[2], fetch[3], fetch[4]
- elseif queryLength == 5 then
- return fetch[1], fetch[2], fetch[3], fetch[4], fetch[5]
- end
-
- return unpack(fetch, 1, fetch.n)
- end
-
- --[=[
- @within View
- Equivalent to `world:contains()`
- @param entity number - the entity ID
- @return boolean
- ]=]
-
- function View:contains(entity)
- return fetches[entity] ~= nil
- end
-
- for entityId, entityData in iter do
- if entityId then
- -- We start at 2 on Select since we don't need want to pack the entity id.
- local fetch = table.pack(select(2, self._expand(entityId, entityData)))
- local node = { entity = entityId, next = nil }
-
- fetches[entityId] = fetch
-
- if not list.head then
- list.head = node
- else
- local current = list.head
- while current.next do
- current = current.next
- end
- current.next = node
- end
- end
- end
-
- return setmetatable({}, View)
-end
-
---[=[
- Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over
- the results of the query.
-
- Order of iteration is not guaranteed.
-
- ```lua
- for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
- -- Do something
- end
-
- for id in world:query(Target):without(Model) do
- -- Again, with feeling
- end
- ```
-
- @param ... Component -- The component types to query. Only entities with *all* of these components will be returned.
- @return QueryResult -- See [QueryResult](/api/QueryResult) docs.
-]=]
-
-function World:query(...)
- assertValidComponent((...), 1)
-
- local metatables = { ... }
- local queryLength = select("#", ...)
-
- local archetype = archetypeOf(...)
-
- if self._queryCache[archetype] == nil then
- self:_newQueryArchetype(archetype)
- end
-
- local compatibleArchetypes = self._queryCache[archetype]
-
- if next(compatibleArchetypes) == nil then
- -- If there are no compatible storages avoid creating our complicated iterator
- return noopQuery
- end
-
- local queryOutput = table.create(queryLength)
-
- local function expand(entityId, entityData)
- if not entityId then
- return
- end
-
- if queryLength == 1 then
- return entityId, entityData[metatables[1]]
- elseif queryLength == 2 then
- return entityId, entityData[metatables[1]], entityData[metatables[2]]
- elseif queryLength == 3 then
- return entityId, entityData[metatables[1]], entityData[metatables[2]], entityData[metatables[3]]
- elseif queryLength == 4 then
- return entityId,
- entityData[metatables[1]],
- entityData[metatables[2]],
- entityData[metatables[3]],
- entityData[metatables[4]]
- elseif queryLength == 5 then
- return entityId,
- entityData[metatables[1]],
- entityData[metatables[2]],
- entityData[metatables[3]],
- entityData[metatables[4]],
- entityData[metatables[5]]
- end
-
- for i, metatable in ipairs(metatables) do
- queryOutput[i] = entityData[metatable]
- end
-
- return entityId, unpack(queryOutput, 1, queryLength)
- end
-
- if self._pristineStorage == self._storages[1] then
- self:_markStorageDirty()
- end
-
- return QueryResult.new(self, expand, archetype, compatibleArchetypes)
-end
-
-local function cleanupQueryChanged(hookState)
- local world = hookState.world
- local componentToTrack = hookState.componentToTrack
-
- for index, object in world._changedStorage[componentToTrack] do
- if object == hookState.storage then
- table.remove(world._changedStorage[componentToTrack], index)
- break
- end
- end
-
- if next(world._changedStorage[componentToTrack]) == nil then
- world._changedStorage[componentToTrack] = nil
- end
-end
-
---[=[
- @interface ChangeRecord
- @within World
- .new? ComponentInstance -- The new value of the component. Nil if just removed.
- .old? ComponentInstance -- The former value of the component. Nil if just added.
-]=]
-
---[=[
- :::info Topologically-aware function
- This function is only usable if called within the context of [`Loop:begin`](/api/Loop#begin).
- :::
-
- Queries for components that have changed **since the last time your system ran `queryChanged`**.
-
- Only one changed record is returned per entity, even if the same entity changed multiple times. The order
- in which changed records are returned is not guaranteed to be the order that the changes occurred in.
-
- It should be noted that `queryChanged` does not have the same iterator invalidation concerns as `World:query`.
-
- :::tip
- The first time your system runs (i.e., on the first frame), all existing entities in the world that match your query
- are returned as "new" change records.
- :::
-
- :::info
- Calling this function from your system creates storage internally for your system. Then, changes meeting your
- criteria are pushed into your storage. Calling `queryChanged` again each frame drains this storage.
-
- If your system isn't called every frame, the storage will continually fill up and does not empty unless you drain
- it.
-
- If you stop calling `queryChanged` in your system, changes will stop being tracked.
- :::
-
- ### Returns
- `queryChanged` returns an iterator function, so you call it in a for loop just like `World:query`.
-
- The iterator returns the entity ID, followed by a [`ChangeRecord`](#ChangeRecord).
-
- The `ChangeRecord` type is a table that contains two fields, `new` and `old`, respectively containing the new
- component instance, and the old component instance. `new` and `old` will never be the same value.
-
- `new` will be nil if the component was removed (or the entity was despawned), and `old` will be nil if the
- component was just added.
-
- The `old` field will be the value of the component the last time this system observed it, not
- necessarily the value it changed from most recently.
-
- The `ChangeRecord` table is potentially shared with multiple systems tracking changes for this component, so it
- cannot be modified.
-
- ```lua
- for id, record in world:queryChanged(Model) do
- if record.new == nil then
- -- Model was removed
-
- if enemy.type == "this is a made up example" then
- world:remove(id, Enemy)
- end
- end
- end
- ```
-
- @param componentToTrack Component -- The component you want to listen to changes for.
- @return () -> (id, ChangeRecord) -- Iterator of entity ID and change record
-]=]
-function World:queryChanged(componentToTrack, ...: nil)
- if ... then
- error("World:queryChanged does not take any additional parameters", 2)
- end
-
- local hookState = topoRuntime.useHookState(componentToTrack, cleanupQueryChanged)
-
- if hookState.storage then
- return function(): any
- local entityId, record = next(hookState.storage)
-
- if entityId then
- hookState.storage[entityId] = nil
-
- return entityId, record
- end
- return
- end
- end
-
- if not self._changedStorage[componentToTrack] then
- self._changedStorage[componentToTrack] = {}
- end
-
- local storage = {}
- hookState.storage = storage
- hookState.world = self
- hookState.componentToTrack = componentToTrack
-
- table.insert(self._changedStorage[componentToTrack], storage)
-
- local queryResult = self:query(componentToTrack)
-
- return function(): any
- local entityId, component = queryResult:next()
-
- if entityId then
- return entityId, table.freeze({ new = component })
- end
- return
- end
-end
-
-function World:_trackChanged(metatable, id, old, new)
- if not self._changedStorage[metatable] then
- return
- end
-
- if old == new then
- return
- end
-
- local record = table.freeze({
- old = old,
- new = new,
- })
-
- for _, storage in ipairs(self._changedStorage[metatable]) do
- -- If this entity has changed since the last time this system read it,
- -- we ensure that the "old" value is whatever the system saw it as last, instead of the
- -- "old" value we have here.
- if storage[id] then
- storage[id] = table.freeze({ old = storage[id].old, new = new })
- else
- storage[id] = record
- end
- end
-end
-
---[=[
- Inserts a component (or set of components) into an existing entity.
-
- If another instance of a given component already exists on this entity, it is replaced.
-
- ```lua
- world:insert(
- entityId,
- ComponentA({
- foo = "bar"
- }),
- ComponentB({
- baz = "qux"
- })
- )
- ```
-
- @param id number -- The entity ID
- @param ... ComponentInstance -- The component values to insert
-]=]
-function World:insert(id, ...)
- if not self:contains(id) then
- error(ERROR_NO_ENTITY, 2)
- end
-
- local entity = self:_getEntity(id)
-
- local wasNew = false
- for i = 1, select("#", ...) do
- local newComponent = select(i, ...)
-
- assertValidComponentInstance(newComponent, i)
-
- local metatable = getmetatable(newComponent)
-
- local oldComponent = entity[metatable]
-
- if not oldComponent then
- wasNew = true
-
- table.insert(self._entityMetatablesCache[id], metatable)
- end
-
- self:_trackChanged(metatable, id, oldComponent, newComponent)
-
- entity[metatable] = newComponent
- end
-
- if wasNew then -- wasNew
- self:_transitionArchetype(id, entity)
- end
-end
-
---[=[
- Removes a component (or set of components) from an existing entity.
-
- ```lua
- local removedA, removedB = world:remove(entityId, ComponentA, ComponentB)
- ```
-
- @param id number -- The entity ID
- @param ... Component -- The components to remove
- @return ...ComponentInstance -- Returns the component instance values that were removed in the order they were passed.
-]=]
-function World:remove(id, ...)
- if not self:contains(id) then
- error(ERROR_NO_ENTITY, 2)
- end
-
- local entity = self:_getEntity(id)
-
- local length = select("#", ...)
- local removed = {}
-
- for i = 1, length do
- local metatable = select(i, ...)
-
- assertValidComponent(metatable, i)
-
- local oldComponent = entity[metatable]
-
- removed[i] = oldComponent
-
- self:_trackChanged(metatable, id, oldComponent, nil)
-
- entity[metatable] = nil
- end
-
- -- Rebuild entity metatable cache
- local metatables = {}
-
- for metatable in pairs(entity) do
- table.insert(metatables, metatable)
- end
-
- self._entityMetatablesCache[id] = metatables
-
- self:_transitionArchetype(id, entity)
-
- return unpack(removed, 1, length)
-end
-
---[=[
- Returns the number of entities currently spawned in the world.
-]=]
-function World:size()
- return self._size
-end
-
---[=[
- :::tip
- [Loop] automatically calls this function on your World(s), so there is no need to call it yourself if you're using
- a Loop.
- :::
-
- If you are not using a Loop, you should call this function at a regular interval (i.e., once per frame) to optimize
- the internal storage for queries.
-
- This is part of a strategy to eliminate iterator invalidation when modifying the World while inside a query from
- [World:query]. While inside a query, any changes to the World are stored in a separate location from the rest of
- the World. Calling this function combines the separate storage back into the main storage, which speeds things up
- again.
-]=]
-function World:optimizeQueries()
- if #self._storages == 1 then
- return
- end
-
- local firstStorage = self._storages[1]
-
- for i = 2, #self._storages do
- local storage = self._storages[i]
-
- for archetype, entities in storage do
- if firstStorage[archetype] == nil then
- firstStorage[archetype] = entities
- else
- for entityId, entityData in entities do
- if firstStorage[archetype][entityId] then
- error("Entity ID already exists in first storage...")
- end
- firstStorage[archetype][entityId] = entityData
- end
- end
- end
- end
-
- table.clear(self._storages)
-
- self._storages[1] = firstStorage
- self._pristineStorage = firstStorage
-end
-
-return {
- World = World,
- component = newComponent
-}
diff --git a/selene.toml b/selene.toml
new file mode 100644
index 0000000..54227d9
--- /dev/null
+++ b/selene.toml
@@ -0,0 +1,4 @@
+std = "roblox"
+
+[lints]
+global_usage = "allow"
diff --git a/stylua.toml b/stylua.toml
new file mode 100644
index 0000000..83e5807
--- /dev/null
+++ b/stylua.toml
@@ -0,0 +1,5 @@
+column_width = 120
+quote_style = "ForceDouble"
+
+[sort_requires]
+enabled = true
diff --git a/testez-companion.toml b/testez-companion.toml
new file mode 100644
index 0000000..4de0c23
--- /dev/null
+++ b/testez-companion.toml
@@ -0,0 +1,3 @@
+roots = ["ServerStorage"]
+
+[extraOptions]
diff --git a/tests/test1.lua b/tests/test1.lua
index 0b031d3..7ff3b5a 100644
--- a/tests/test1.lua
+++ b/tests/test1.lua
@@ -110,6 +110,39 @@ TEST("world:query", function()
CHECK(world:get(id, Health) == nil)
end
+ do CASE "Should allow iterating the whole world"
+ local world = jecs.World.new()
+
+ local A, B = world:entity(), world:entity()
+
+ local eA = world:entity()
+ world:set(eA, A, true)
+ local eB = world:entity()
+ world:set(eB, B, true)
+ local eAB = world:entity()
+ world:set(eAB, A, true)
+ world:set(eAB, B, true)
+
+ local count = 0
+ for id, data in world do
+ count += 1
+ if id == eA then
+ CHECK(data[A] == true)
+ CHECK(data[B] == nil)
+ elseif id == eB then
+ CHECK(data[B] == true)
+ CHECK(data[A] == nil)
+ elseif id == eAB then
+ CHECK(data[A] == true)
+ CHECK(data[B] == true)
+ else
+ error("unknown entity", id)
+ end
+ end
+
+ CHECK(count == 3)
+ end
+
end)
FINISH()
\ No newline at end of file
diff --git a/wally.toml b/wally.toml
index c4b5be7..5885799 100644
--- a/wally.toml
+++ b/wally.toml
@@ -10,6 +10,3 @@ include = ["default.project.json", "lib", "wally.toml", "README.md"]
TestEZ = "roblox/testez@0.4.1"
Matter = "matter-ecs/matter@0.8.0"
ecr = "centau/ecr@0.8.0"
-
-
-