diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 13d54205..63e8180c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: - name: Run Stylua uses: JohnnyMorganz/stylua-action@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} - version: latest # NOTE: we recommend pinning to a specific version in case of formatting changes - # CLI arguments - args: ./src + token: ${{ secrets.GITHUB_TOKEN }} + version: latest # NOTE: we recommend pinning to a specific version in case of formatting changes + # CLI arguments + args: --check ./src diff --git a/benches/cached.luau b/benches/cached.luau index 2e3057fd..bdac6897 100644 --- a/benches/cached.luau +++ b/benches/cached.luau @@ -1,4 +1,3 @@ - local jecs = require("@jecs") local mirror = require("../mirror/init") @@ -39,36 +38,36 @@ do if flip() then combination ..= "B" - ecs:set(entity, D2, {value = true}) + ecs:set(entity, D2, { value = true }) end if flip() then combination ..= "C" - ecs:set(entity, D3, {value = true}) + ecs:set(entity, D3, { value = true }) end if flip() then combination ..= "D" - ecs:set(entity, D4, {value = true}) + ecs:set(entity, D4, { value = true }) end if flip() then combination ..= "E" - ecs:set(entity, D5, {value = true}) + ecs:set(entity, D5, { value = true }) end if flip() then combination ..= "F" - ecs:set(entity, D6, {value = true}) + ecs:set(entity, D6, { value = true }) end if flip() then combination ..= "G" - ecs:set(entity, D7, {value = true}) + ecs:set(entity, D7, { value = true }) end if flip() then combination ..= "H" - ecs:set(entity, D8, {value = true}) + ecs:set(entity, D8, { value = true }) end if #combination == 7 then added += 1 - ecs:set(entity, D1, {value = true}) + ecs:set(entity, D1, { value = true }) end archetypes[combination] = true end @@ -117,36 +116,36 @@ do if flip() then combination ..= "B" - ecs:set(entity, D2, {value = true}) + ecs:set(entity, D2, { value = true }) end if flip() then combination ..= "C" - ecs:set(entity, D3, {value = true}) + ecs:set(entity, D3, { value = true }) end if flip() then combination ..= "D" - ecs:set(entity, D4, {value = true}) + ecs:set(entity, D4, { value = true }) end if flip() then combination ..= "E" - ecs:set(entity, D5, {value = true}) + ecs:set(entity, D5, { value = true }) end if flip() then combination ..= "F" - ecs:set(entity, D6, {value = true}) + ecs:set(entity, D6, { value = true }) end if flip() then combination ..= "G" - ecs:set(entity, D7, {value = true}) + ecs:set(entity, D7, { value = true }) end if flip() then combination ..= "H" - ecs:set(entity, D8, {value = true}) + ecs:set(entity, D8, { value = true }) end if #combination == 7 then added += 1 - ecs:set(entity, D1, {value = true}) + ecs:set(entity, D1, { value = true }) end archetypes[combination] = true end diff --git a/benches/general.luau b/benches/general.luau index e28011b9..f19cd569 100644 --- a/benches/general.luau +++ b/benches/general.luau @@ -8,11 +8,12 @@ local function TITLE(s: string) print(testkit.color.white(s)) end -local N = 2^17 +local N = 2 ^ 17 local pair = jecs.pair -do TITLE "create" +do + TITLE("create") local world = jecs.World.new() BENCH("entity", function() @@ -22,17 +23,17 @@ do TITLE "create" end) local A = world:component() - local B = world:component() + local B = world:component() BENCH("pair", function() for i = 1, START(N) do jecs.pair(A, B) end end) - end -do TITLE "set" +do + TITLE("set") local world = jecs.World.new() local A = world:entity() @@ -65,7 +66,8 @@ end -- we have a separate benchmark for relationships. -- this is due to that relationships have a very high id compared to normal -- components, which cause them to get added into the hashmap portion. -do TITLE "set relationship" +do + TITLE("set relationship") local world = jecs.World.new() local A = world:entity() @@ -98,7 +100,8 @@ do TITLE "set relationship" end) end -do TITLE "get" +do + TITLE("get") local world = jecs.World.new() local A = world:component() @@ -140,7 +143,8 @@ do TITLE "get" end) end -do TITLE "target" +do + TITLE("target") BENCH("1st target", function() local world = jecs.World.new() @@ -164,14 +168,14 @@ do TITLE "target" world:target(entities[i], A, 0) end end) - end --- this benchmark is used to view how fragmentation affects query performance --- we use this by determining how many entities should fit per arcehtype, instead --- of creating x amount of archetypes. this would scale better with any amount of --- entities. -do TITLE(`query {N} entities`) +do + TITLE(`query {N} entities`) local function view_bench(n: number) BENCH(`{n} entities per archetype`, function() @@ -194,14 +198,13 @@ do TITLE(`query {N} entities`) end end - START() for id in world:query(A, B, C, D) do end end) BENCH(`inlined query`, function() - local world = jecs.World.new() + local world = jecs.World.new() local A = world:component() local B = world:component() local C = world:component() @@ -219,15 +222,14 @@ do TITLE(`query {N} entities`) end end - START() for _, archetype in world:query(A, B, C, D):archetypes() do - local columns, records = archetype.columns, archetype.records + local columns, records = archetype.columns, archetype.records local a = columns[records[A].column] local b = columns[records[B].column] local c = columns[records[C].column] local d = columns[records[D].column] - for row in archetype.entities do + for row in archetype.entities do local _1, _2, _3, _4 = a[row], b[row], c[row], d[row] end end @@ -235,7 +237,6 @@ do TITLE(`query {N} entities`) end for i = 13, 0, -1 do - view_bench(2^i) + view_bench(2 ^ i) end - end diff --git a/benches/query.luau b/benches/query.luau index 54cc7d41..bff674c5 100644 --- a/benches/query.luau +++ b/benches/query.luau @@ -20,7 +20,7 @@ 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() + BENCH("1 component", function() for _ in world:query(A) do end end) @@ -86,36 +86,36 @@ do if flip() then combination ..= "B" - ecs:set(entity, D2, {value = true}) + ecs:set(entity, D2, { value = true }) end if flip() then combination ..= "C" - ecs:set(entity, D3, {value = true}) + ecs:set(entity, D3, { value = true }) end if flip() then combination ..= "D" - ecs:set(entity, D4, {value = true}) + ecs:set(entity, D4, { value = true }) end if flip() then combination ..= "E" - ecs:set(entity, D5, {value = true}) + ecs:set(entity, D5, { value = true }) end if flip() then combination ..= "F" - ecs:set(entity, D6, {value = true}) + ecs:set(entity, D6, { value = true }) end if flip() then combination ..= "G" - ecs:set(entity, D7, {value = true}) + ecs:set(entity, D7, { value = true }) end if flip() then combination ..= "H" - ecs:set(entity, D8, {value = true}) + ecs:set(entity, D8, { value = true }) end if #combination == 7 then added += 1 - ecs:set(entity, D1, {value = true}) + ecs:set(entity, D1, { value = true }) end archetypes[combination] = true end @@ -202,36 +202,36 @@ do if flip() then combination ..= "B" - ecs:set(entity, D2, {value = true}) + ecs:set(entity, D2, { value = true }) end if flip() then combination ..= "C" - ecs:set(entity, D3, {value = true}) + ecs:set(entity, D3, { value = true }) end if flip() then combination ..= "D" - ecs:set(entity, D4, {value = true}) + ecs:set(entity, D4, { value = true }) end if flip() then combination ..= "E" - ecs:set(entity, D5, {value = true}) + ecs:set(entity, D5, { value = true }) end if flip() then combination ..= "F" - ecs:set(entity, D6, {value = true}) + ecs:set(entity, D6, { value = true }) end if flip() then combination ..= "G" - ecs:set(entity, D7, {value = true}) + ecs:set(entity, D7, { value = true }) end if flip() then combination ..= "H" - ecs:set(entity, D8, {value = true}) + ecs:set(entity, D8, { value = true }) end if #combination == 7 then added += 1 - ecs:set(entity, D1, {value = true}) + ecs:set(entity, D1, { value = true }) end archetypes[combination] = true end diff --git a/benches/visual/despawn.bench.luau b/benches/visual/despawn.bench.luau index e765ba33..40e033c8 100644 --- a/benches/visual/despawn.bench.luau +++ b/benches/visual/despawn.bench.luau @@ -12,37 +12,35 @@ local ecs = jecs.World.new() local A, B = Matter.component(), Matter.component() local C, D = ecs:component(), ecs:component() - return { ParameterGenerator = function() - local matter_entities = {} - local jecs_entities = {} - local entities = { - matter = matter_entities, - jecs = jecs_entities - } - for i = 1, 1000 do - table.insert(matter_entities, newWorld:spawn(A(), B())) - local e = ecs:entity() - ecs:set(e, C, {}) - ecs:set(e, D, {}) - table.insert(jecs_entities, e) - end - return entities - end; + local matter_entities = {} + local jecs_entities = {} + local entities = { + matter = matter_entities, + jecs = jecs_entities, + } + for i = 1, 1000 do + table.insert(matter_entities, newWorld:spawn(A(), B())) + local e = ecs:entity() + ecs:set(e, C, {}) + ecs:set(e, D, {}) + table.insert(jecs_entities, e) + end + return entities + end, Functions = { Matter = function(_, entities) - for _, entity in entities.matter do - newWorld:despawn(entity) - end - end; - + for _, entity in entities.matter do + newWorld:despawn(entity) + end + end, Jecs = function(_, entities) - for _, entity in entities.jecs do + for _, entity in entities.jecs do ecs:delete(entity) end - end; - }; + end, + }, } diff --git a/benches/visual/insertion.bench.luau b/benches/visual/insertion.bench.luau index 40bea46d..035f9e14 100644 --- a/benches/visual/insertion.bench.luau +++ b/benches/visual/insertion.bench.luau @@ -3,8 +3,8 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local Matter = require(ReplicatedStorage.DevPackages.Matter) -local jecs = require(ReplicatedStorage.Lib) local ecr = require(ReplicatedStorage.DevPackages.ecr) +local jecs = require(ReplicatedStorage.Lib) local newWorld = Matter.World.new() local ecs = jecs.World.new() local mirror = require(ReplicatedStorage.mirror) @@ -45,7 +45,6 @@ local E6 = mcs:entity() local E7 = mcs:entity() local E8 = mcs:entity() - local registry2 = ecr.registry() return { ParameterGenerator = function() @@ -53,51 +52,49 @@ return { end, Functions = { - Matter = function() - local e = newWorld:spawn() - for i = 1, 5000 do - newWorld:insert(e, - A1({ value = true }), - A2({ value = true }), - A3({ value = true }), - A4({ value = true }), - A5({ value = true }), - A6({ value = true }), - A7({ value = true }), - A8({ value = true }) - ) - end + Matter = function() + local e = newWorld:spawn() + for i = 1, 5000 do + newWorld:insert( + e, + A1({ value = true }), + A2({ value = true }), + A3({ value = true }), + A4({ value = true }), + A5({ value = true }), + A6({ value = true }), + A7({ value = true }), + A8({ value = true }) + ) + end end, - - ECR = function() - local e = registry2.create() - for i = 1, 5000 do - registry2:set(e, B1, {value = false}) - registry2:set(e, B2, {value = false}) - registry2:set(e, B3, {value = false}) - registry2:set(e, B4, {value = false}) - registry2:set(e, B5, {value = false}) - registry2:set(e, B6, {value = false}) - registry2:set(e, B7, {value = false}) - registry2:set(e, B8, {value = false}) - end + ECR = function() + local e = registry2.create() + for i = 1, 5000 do + registry2:set(e, B1, { value = false }) + registry2:set(e, B2, { value = false }) + registry2:set(e, B3, { value = false }) + registry2:set(e, B4, { value = false }) + registry2:set(e, B5, { value = false }) + registry2:set(e, B6, { value = false }) + registry2:set(e, B7, { value = false }) + registry2:set(e, B8, { value = false }) + end end, - - Jecs = function() - local e = ecs:entity() - for i = 1, 5000 do - ecs:set(e, C1, {value = false}) - ecs:set(e, C2, {value = false}) - ecs:set(e, C3, {value = false}) - ecs:set(e, C4, {value = false}) - ecs:set(e, C5, {value = false}) - ecs:set(e, C6, {value = false}) - ecs:set(e, C7, {value = false}) - ecs:set(e, C8, {value = false}) - - end + Jecs = function() + local e = ecs:entity() + for i = 1, 5000 do + ecs:set(e, C1, { value = false }) + ecs:set(e, C2, { value = false }) + ecs:set(e, C3, { value = false }) + ecs:set(e, C4, { value = false }) + ecs:set(e, C5, { value = false }) + ecs:set(e, C6, { value = false }) + ecs:set(e, C7, { value = false }) + ecs:set(e, C8, { value = false }) + end end, }, } diff --git a/benches/visual/query.bench.luau b/benches/visual/query.bench.luau index af9a49d9..1761eea1 100644 --- a/benches/visual/query.bench.luau +++ b/benches/visual/query.bench.luau @@ -2,9 +2,9 @@ --!native local ReplicatedStorage = game:GetService("ReplicatedStorage") -local rgb = require(ReplicatedStorage.rgb) local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matter@0.8.1"].matter) local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr) +local rgb = require(ReplicatedStorage.rgb) local newWorld = Matter.World.new() local jecs = require(ReplicatedStorage.Lib) @@ -39,7 +39,6 @@ local D6 = ecs:component() local D7 = ecs:component() local D8 = ecs:component() - local E1 = mcs:entity() local E2 = mcs:entity() local E3 = mcs:entity() @@ -49,14 +48,13 @@ local E6 = mcs:entity() local E7 = mcs:entity() local E8 = mcs:entity() - local registry2 = ecr.registry() local function flip() return math.random() >= 0.25 end -local N = 2^16-2 +local N = 2 ^ 16 - 2 local archetypes = {} local hm = 0 @@ -68,69 +66,66 @@ for i = 1, N do local m = mcs:entity() if flip() then - registry2:set(id, B1, {value = true}) - ecs:set(entity, D1, { value = true}) - newWorld:insert(n, A1({value = true})) - mcs:set(m, E1, { value = 2}) + registry2:set(id, B1, { value = true }) + ecs:set(entity, D1, { value = true }) + newWorld:insert(n, A1({ value = true })) + mcs:set(m, E1, { value = 2 }) end if flip() then combination ..= "B" - registry2:set(id, B2, {value = true}) - ecs:set(entity, D2, { value = true}) - mcs:set(m, E2, { value = 2}) - newWorld:insert(n, A2({value = true})) + registry2:set(id, B2, { value = true }) + ecs:set(entity, D2, { value = true }) + mcs:set(m, E2, { value = 2 }) + newWorld:insert(n, A2({ value = true })) end if flip() then combination ..= "C" - registry2:set(id, B3, {value = true}) - ecs:set(entity, D3, { value = true}) - mcs:set(m, E3, { value = 2}) - newWorld:insert(n, A3({value = true})) + registry2:set(id, B3, { value = true }) + ecs:set(entity, D3, { value = true }) + mcs:set(m, E3, { value = 2 }) + newWorld:insert(n, A3({ value = true })) end if flip() then combination ..= "D" - registry2:set(id, B4, {value = true}) - ecs:set(entity, D4, { value = true}) - mcs:set(m, E4, { value = 2}) + registry2:set(id, B4, { value = true }) + ecs:set(entity, D4, { value = true }) + mcs:set(m, E4, { value = 2 }) - newWorld:insert(n, A4({value = true})) + newWorld:insert(n, A4({ value = true })) end if flip() then combination ..= "E" - registry2:set(id, B5, {value = true}) - ecs:set(entity, D5, { value = true}) - mcs:set(m, E5, { value = 2}) + registry2:set(id, B5, { value = true }) + ecs:set(entity, D5, { value = true }) + mcs:set(m, E5, { value = 2 }) - newWorld:insert(n, A5({value = true})) + newWorld:insert(n, A5({ value = true })) end if flip() then combination ..= "F" - registry2:set(id, B6, {value = true}) - ecs:set(entity, D6, { value = true}) - mcs:set(m, E6, { value = 2}) - newWorld:insert(n, A6({value = true})) + registry2:set(id, B6, { value = true }) + ecs:set(entity, D6, { value = true }) + mcs:set(m, E6, { value = 2 }) + newWorld:insert(n, A6({ value = true })) end if flip() then combination ..= "G" - registry2:set(id, B7, {value = true}) - ecs:set(entity, D7, { value = true}) - mcs:set(m, E7, { value = 2}) - newWorld:insert(n, A7({value = true})) + registry2:set(id, B7, { value = true }) + ecs:set(entity, D7, { value = true }) + mcs:set(m, E7, { value = 2 }) + newWorld:insert(n, A7({ value = true })) end if flip() then combination ..= "H" - registry2:set(id, B8, {value = true}) - newWorld:insert(n, A8({value = true})) - ecs:set(entity, D8, { value = true}) - mcs:set(m, E8, { value = 2}) - + registry2:set(id, B8, { value = true }) + newWorld:insert(n, A8({ value = true })) + ecs:set(entity, D8, { value = true }) + mcs:set(m, E8, { value = 2 }) end - - if combination:find("BCDF") then if not archetypes[combination] then - print(combination) + print(combination) end hm += 1 end @@ -148,7 +143,7 @@ local WALL = gray(" │ ") local count = 0 for _, archetype in ecs:query(D2, D4, D6, D8):archetypes() do - count += #archetype.entities + count += #archetype.entities end print(count) @@ -165,13 +160,13 @@ return { end, ECR = function() - for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do - end - end, + for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do + end + end, Jecs = function() - for entityId, firstComponent in ecs:query(D2, D4, D6, D8) do - end + for entityId, firstComponent in ecs:query(D2, D4, D6, D8) do + end end, }, } diff --git a/benches/visual/spawn.bench.luau b/benches/visual/spawn.bench.luau index c5e6aef5..0948fcd9 100644 --- a/benches/visual/spawn.bench.luau +++ b/benches/visual/spawn.bench.luau @@ -14,25 +14,25 @@ return { local registry2 = ecr.registry() return registry2 - end; + end, Functions = { Matter = function() for i = 1, 1000 do newWorld:spawn() end - end; + end, ECR = function(_, registry2) for i = 1, 1000 do registry2.create() end - end; + end, Jecs = function() for i = 1, 1000 do ecs:entity() end - end; - }; + end, + }, } diff --git a/demo/net/client.luau b/demo/net/client.luau index 29b0f8bc..062dcabb 100644 --- a/demo/net/client.luau +++ b/demo/net/client.luau @@ -10,11 +10,12 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") if not RunService:IsClient() then - error("Client network module can only be required from the client.") + error("Client network module can only be required from the client.") end local Reliable: RemoteEvent = ReplicatedStorage:WaitForChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent -local Unreliable: UnreliableRemoteEvent = ReplicatedStorage:WaitForChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent +local Unreliable: UnreliableRemoteEvent = + ReplicatedStorage:WaitForChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent local Invocations = 0 @@ -31,128 +32,128 @@ local RecieveInstances = {} local RecieveInstanceCursor = 0 type Entry = { - value: any, - next: Entry? + value: any, + next: Entry?, } type Queue = { - head: Entry?, - tail: Entry? + head: Entry?, + tail: Entry?, } type BufferSave = { - Size: number, - Cursor: number, - Buffer: buffer, - Instances: {Instance} + Size: number, + Cursor: number, + Buffer: buffer, + Instances: { Instance }, } local function Read(Bytes: number) - local Offset = RecieveCursor - RecieveCursor += Bytes - return Offset + local Offset = RecieveCursor + RecieveCursor += Bytes + return Offset end local function Save(): BufferSave - return { - Size = SendSize, - Cursor = SendCursor, - Buffer = SendBuffer, - Instances = SendInstances - } + return { + Size = SendSize, + Cursor = SendCursor, + Buffer = SendBuffer, + Instances = SendInstances, + } end local function Load(Save: BufferSave?) - if Save then - SendSize = Save.Size - SendCursor = Save.Cursor - SendOffset = Save.Cursor - SendBuffer = Save.Buffer - SendInstances = Save.Instances - return - end - - SendSize = 64 - SendCursor = 0 - SendOffset = 0 - SendBuffer = buffer.create(64) - SendInstances = {} + if Save then + SendSize = Save.Size + SendCursor = Save.Cursor + SendOffset = Save.Cursor + SendBuffer = Save.Buffer + SendInstances = Save.Instances + return + end + + SendSize = 64 + SendCursor = 0 + SendOffset = 0 + SendBuffer = buffer.create(64) + SendInstances = {} end local function Invoke() - if Invocations == 255 then - Invocations = 0 - end + if Invocations == 255 then + Invocations = 0 + end - local Invocation = Invocations - Invocations += 1 - return Invocation + local Invocation = Invocations + Invocations += 1 + return Invocation end local function Allocate(Bytes: number) - local InUse = (SendCursor + Bytes) - if InUse > SendSize then - --> Avoid resizing the buffer for every write - while InUse > SendSize do - SendSize *= 1.5 - end - - local Buffer = buffer.create(SendSize) - buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) - SendBuffer = Buffer - end - - SendOffset = SendCursor - SendCursor += Bytes - - return SendOffset + local InUse = (SendCursor + Bytes) + if InUse > SendSize then + --> Avoid resizing the buffer for every write + while InUse > SendSize do + SendSize *= 1.5 + end + + local Buffer = buffer.create(SendSize) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + SendBuffer = Buffer + end + + SendOffset = SendCursor + SendCursor += Bytes + + return SendOffset end local function CreateQueue(): Queue - return { - head = nil, - tail = nil - } + return { + head = nil, + tail = nil, + } end local function Pop(queue: Queue): any - local head = queue.head - if head == nil then - return - end + local head = queue.head + if head == nil then + return + end - queue.head = head.next - return head.value + queue.head = head.next + return head.value end local function Push(queue: Queue, value: any) - local entry: Entry = { - value = value, - next = nil - } + local entry: Entry = { + value = value, + next = nil, + } - if queue.tail ~= nil then - queue.tail.next = entry - end + if queue.tail ~= nil then + queue.tail.next = entry + end - queue.tail = entry + queue.tail = entry - if queue.head == nil then - queue.head = entry - end + if queue.head == nil then + queue.head = entry + end end local Types = {} local Calls = table.create(256) local Events: any = { - Reliable = table.create(256), - Unreliable = table.create(256) + Reliable = table.create(256), + Unreliable = table.create(256), } local Queue: any = { - Reliable = table.create(256), - Unreliable = table.create(256) + Reliable = table.create(256), + Unreliable = table.create(256), } Queue.Unreliable[0] = CreateQueue() @@ -220,21 +221,20 @@ function Types.WriteEVENT_SpawnMob(Value1: number, Value2: CFrame, Value3: numbe buffer.writeu8(SendBuffer, BLOCK_START + 33, Value3) end - local function StepReplication() - if SendCursor <= 0 then - return - end - - local Buffer = buffer.create(SendCursor) - buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) - Reliable:FireServer(Buffer, SendInstances) - - SendSize = 64 - SendCursor = 0 - SendOffset = 0 - SendBuffer = buffer.create(64) - table.clear(SendInstances) + if SendCursor <= 0 then + return + end + + local Buffer = buffer.create(SendCursor) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + Reliable:FireServer(Buffer, SendInstances) + + SendSize = 64 + SendCursor = 0 + SendOffset = 0 + SendBuffer = buffer.create(64) + table.clear(SendInstances) end local Elapsed = 0 @@ -246,13 +246,13 @@ RunService.Heartbeat:Connect(function(DeltaTime: number) end end) -Reliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: {Instance}) +Reliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: { Instance }) RecieveCursor = 0 RecieveBuffer = Buffer RecieveInstances = Instances RecieveInstanceCursor = 0 local Size = buffer.len(RecieveBuffer) - while (RecieveCursor < Size) do + while RecieveCursor < Size do -- Read BLOCK: 1 bytes local BLOCK_START = Read(1) local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0) @@ -262,13 +262,13 @@ Reliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: {Instance}) end end) -Unreliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: {Instance}) +Unreliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: { Instance }) RecieveCursor = 0 RecieveBuffer = Buffer RecieveInstances = Instances RecieveInstanceCursor = 0 local Size = buffer.len(RecieveBuffer) - while (RecieveCursor < Size) do + while RecieveCursor < Size do -- Read BLOCK: 1 bytes local BLOCK_START = Read(1) local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0) @@ -285,53 +285,52 @@ return { Iter = function(): () -> (number, number, CFrame) local index = 0 local queue = Queue.Unreliable[0] - return function (): (number, number, CFrame) + return function(): (number, number, CFrame) index += 1 local arguments = Pop(queue) if arguments ~= nil then return index, unpack(arguments, 1, arguments.n) end - return + return end end, Next = function(): () -> (number, number, CFrame) local index = 0 local queue = Queue.Unreliable[0] - return function (): (number, number, CFrame) + return function(): (number, number, CFrame) index += 1 local arguments = Pop(queue) if arguments ~= nil then return index, unpack(arguments, 1, arguments.n) end - return + return end - end + end, }, SpawnMob = { Iter = function(): () -> (number, number, CFrame, number) local index = 0 local queue = Queue.Reliable[0] - return function (): (number, number, CFrame, number) + return function(): (number, number, CFrame, number) index += 1 local arguments = Pop(queue) if arguments ~= nil then return index, unpack(arguments, 1, arguments.n) end - return + return end end, Next = function(): () -> (number, number, CFrame, number) local index = 0 local queue = Queue.Reliable[0] - return function (): (number, number, CFrame, number) + return function(): (number, number, CFrame, number) index += 1 local arguments = Pop(queue) if arguments ~= nil then return index, unpack(arguments, 1, arguments.n) end - return + return end - end + end, }, - -} \ No newline at end of file +} diff --git a/demo/net/server.luau b/demo/net/server.luau index a31208b1..8a8755ce 100644 --- a/demo/net/server.luau +++ b/demo/net/server.luau @@ -11,23 +11,24 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") if not RunService:IsServer() then - error("Server network module can only be required from the server.") + error("Server network module can only be required from the server.") end local Reliable: RemoteEvent = ReplicatedStorage:FindFirstChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent if not Reliable then - local RemoteEvent = Instance.new("RemoteEvent") - RemoteEvent.Name = "BLINK_RELIABLE_REMOTE" - RemoteEvent.Parent = ReplicatedStorage - Reliable = RemoteEvent + local RemoteEvent = Instance.new("RemoteEvent") + RemoteEvent.Name = "BLINK_RELIABLE_REMOTE" + RemoteEvent.Parent = ReplicatedStorage + Reliable = RemoteEvent end -local Unreliable: UnreliableRemoteEvent = ReplicatedStorage:FindFirstChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent +local Unreliable: UnreliableRemoteEvent = + ReplicatedStorage:FindFirstChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent if not Unreliable then - local UnreliableRemoteEvent = Instance.new("UnreliableRemoteEvent") - UnreliableRemoteEvent.Name = "BLINK_UNRELIABLE_REMOTE" - UnreliableRemoteEvent.Parent = ReplicatedStorage - Unreliable = UnreliableRemoteEvent + local UnreliableRemoteEvent = Instance.new("UnreliableRemoteEvent") + UnreliableRemoteEvent.Name = "BLINK_UNRELIABLE_REMOTE" + UnreliableRemoteEvent.Parent = ReplicatedStorage + Unreliable = UnreliableRemoteEvent end local Invocations = 0 @@ -45,131 +46,130 @@ local RecieveInstances = {} local RecieveInstanceCursor = 0 type Entry = { - value: any, - next: Entry? + value: any, + next: Entry?, } type Queue = { - head: Entry?, - tail: Entry? + head: Entry?, + tail: Entry?, } type BufferSave = { - Size: number, - Cursor: number, - Buffer: buffer, - Instances: {Instance} + Size: number, + Cursor: number, + Buffer: buffer, + Instances: { Instance }, } local function Read(Bytes: number) - local Offset = RecieveCursor - RecieveCursor += Bytes - return Offset + local Offset = RecieveCursor + RecieveCursor += Bytes + return Offset end local function Save(): BufferSave - return { - Size = SendSize, - Cursor = SendCursor, - Buffer = SendBuffer, - Instances = SendInstances - } + return { + Size = SendSize, + Cursor = SendCursor, + Buffer = SendBuffer, + Instances = SendInstances, + } end local function Load(Save: BufferSave?) - if Save then - SendSize = Save.Size - SendCursor = Save.Cursor - SendOffset = Save.Cursor - SendBuffer = Save.Buffer - SendInstances = Save.Instances - return - end - - SendSize = 64 - SendCursor = 0 - SendOffset = 0 - SendBuffer = buffer.create(64) - SendInstances = {} + if Save then + SendSize = Save.Size + SendCursor = Save.Cursor + SendOffset = Save.Cursor + SendBuffer = Save.Buffer + SendInstances = Save.Instances + return + end + + SendSize = 64 + SendCursor = 0 + SendOffset = 0 + SendBuffer = buffer.create(64) + SendInstances = {} end local function Invoke() - if Invocations == 255 then - Invocations = 0 - end + if Invocations == 255 then + Invocations = 0 + end - local Invocation = Invocations - Invocations += 1 - return Invocation + local Invocation = Invocations + Invocations += 1 + return Invocation end local function Allocate(Bytes: number) - local InUse = (SendCursor + Bytes) - if InUse > SendSize then - --> Avoid resizing the buffer for every write - while InUse > SendSize do - SendSize *= 1.5 - end - - local Buffer = buffer.create(SendSize) - buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) - SendBuffer = Buffer - end - - SendOffset = SendCursor - SendCursor += Bytes - - return SendOffset + local InUse = (SendCursor + Bytes) + if InUse > SendSize then + --> Avoid resizing the buffer for every write + while InUse > SendSize do + SendSize *= 1.5 + end + + local Buffer = buffer.create(SendSize) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + SendBuffer = Buffer + end + + SendOffset = SendCursor + SendCursor += Bytes + + return SendOffset end local function CreateQueue(): Queue - return { - head = nil, - tail = nil - } + return { + head = nil, + tail = nil, + } end local function Pop(queue: Queue): any - local head = queue.head - if head == nil then - return - end + local head = queue.head + if head == nil then + return + end - queue.head = head.next - return head.value + queue.head = head.next + return head.value end local function Push(queue: Queue, value: any) - local entry: Entry = { - value = value, - next = nil - } + local entry: Entry = { + value = value, + next = nil, + } - if queue.tail ~= nil then - queue.tail.next = entry - end + if queue.tail ~= nil then + queue.tail.next = entry + end - queue.tail = entry + queue.tail = entry - if queue.head == nil then - queue.head = entry - end + if queue.head == nil then + queue.head = entry + end end local Types = {} local Calls = table.create(256) local Events: any = { - Reliable = table.create(256), - Unreliable = table.create(256) + Reliable = table.create(256), + Unreliable = table.create(256), } local Queue: any = { - Reliable = table.create(256), - Unreliable = table.create(256) + Reliable = table.create(256), + Unreliable = table.create(256), } - function Types.ReadEVENT_UpdateTransform(): (number, CFrame) -- Read BLOCK: 32 bytes local BLOCK_START = Read(32) @@ -232,52 +232,51 @@ function Types.WriteEVENT_SpawnMob(Value1: number, Value2: CFrame, Value3: numbe buffer.writeu8(SendBuffer, BLOCK_START + 33, Value3) end - -local PlayersMap: {[Player]: BufferSave} = {} +local PlayersMap: { [Player]: BufferSave } = {} Players.PlayerRemoving:Connect(function(Player) - PlayersMap[Player] = nil + PlayersMap[Player] = nil end) local function StepReplication() - for Player, Send in PlayersMap do - if Send.Cursor <= 0 then - continue - end - - local Buffer = buffer.create(Send.Cursor) - buffer.copy(Buffer, 0, Send.Buffer, 0, Send.Cursor) - Reliable:FireClient(Player, Buffer, Send.Instances) - - Send.Size = 64 - Send.Cursor = 0 - Send.Buffer = buffer.create(64) - table.clear(Send.Instances) - end + for Player, Send in PlayersMap do + if Send.Cursor <= 0 then + continue + end + + local Buffer = buffer.create(Send.Cursor) + buffer.copy(Buffer, 0, Send.Buffer, 0, Send.Cursor) + Reliable:FireClient(Player, Buffer, Send.Instances) + + Send.Size = 64 + Send.Cursor = 0 + Send.Buffer = buffer.create(64) + table.clear(Send.Instances) + end end RunService.Heartbeat:Connect(StepReplication) -Reliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: {Instance}) +Reliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: { Instance }) RecieveCursor = 0 RecieveBuffer = Buffer RecieveInstances = Instances RecieveInstanceCursor = 0 local Size = buffer.len(RecieveBuffer) - while (RecieveCursor < Size) do + while RecieveCursor < Size do -- Read BLOCK: 1 bytes local BLOCK_START = Read(1) local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0) end end) -Unreliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: {Instance}) +Unreliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: { Instance }) RecieveCursor = 0 RecieveBuffer = Buffer RecieveInstances = Instances RecieveInstanceCursor = 0 local Size = buffer.len(RecieveBuffer) - while (RecieveCursor < Size) do + while RecieveCursor < Size do -- Read BLOCK: 1 bytes local BLOCK_START = Read(1) local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0) @@ -302,7 +301,7 @@ return { buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) Unreliable:FireAllClients(Buffer, SendInstances) end, - FireList = function(List: {Player}, Value1: number, Value2: CFrame): () + FireList = function(List: { Player }, Value1: number, Value2: CFrame): () Load() Types.WriteEVENT_UpdateTransform(Value1, Value2) local Buffer = buffer.create(SendCursor) @@ -342,7 +341,7 @@ return { PlayersMap[Player] = Save() end end, - FireList = function(List: {Player}, Value1: number, Value2: CFrame, Value3: number): () + FireList = function(List: { Player }, Value1: number, Value2: CFrame, Value3: number): () Load() Types.WriteEVENT_SpawnMob(Value1, Value2, Value3) local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances @@ -370,5 +369,4 @@ return { end end, }, - -} \ No newline at end of file +} diff --git a/demo/src/ReplicatedStorage/ecs_init.luau b/demo/src/ReplicatedStorage/ecs_init.luau index 455fb9b9..d454567d 100644 --- a/demo/src/ReplicatedStorage/ecs_init.luau +++ b/demo/src/ReplicatedStorage/ecs_init.luau @@ -1,4 +1,4 @@ -_G.JECS_DEBUG = true -_G.JECS_HI_COMPONENT_ID = 32 -require(game:GetService("ReplicatedStorage").ecs) -return +_G.JECS_DEBUG = true +_G.JECS_HI_COMPONENT_ID = 32 +require(game:GetService("ReplicatedStorage").ecs) +return diff --git a/demo/src/ReplicatedStorage/start.luau b/demo/src/ReplicatedStorage/start.luau index 59b4bca7..d74afa8f 100644 --- a/demo/src/ReplicatedStorage/start.luau +++ b/demo/src/ReplicatedStorage/start.luau @@ -1,33 +1,33 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local UserInputService = game:GetService("UserInputService") -local ReplicatedStorage = game:GetService("ReplicatedStorage") local jabby = require(ReplicatedStorage.Packages.jabby) local std = require(ReplicatedStorage.std) local Scheduler = std.Scheduler local world = std.world local function start(modules) - local scheduler = Scheduler.new(world, require(ReplicatedStorage.std.components)) - for _, module in modules do - require(module)(scheduler) - end - local events = scheduler.collect.all() - scheduler.systems.begin(events) + local scheduler = Scheduler.new(world, require(ReplicatedStorage.std.components)) + for _, module in modules do + require(module)(scheduler) + end + local events = scheduler.collect.all() + scheduler.systems.begin(events) - jabby.set_check_function(function(player) - return true - end) - if RunService:IsClient() then - local client = jabby.obtain_client() - local dtor - UserInputService.InputBegan:Connect(function(input) - if input.KeyCode == Enum.KeyCode.F4 then - if dtor then - dtor() - end - dtor = client.spawn_app(client.apps.home) - end - end) + jabby.set_check_function(function(player) + return true + end) + if RunService:IsClient() then + local client = jabby.obtain_client() + local dtor + UserInputService.InputBegan:Connect(function(input) + if input.KeyCode == Enum.KeyCode.F4 then + if dtor then + dtor() + end + dtor = client.spawn_app(client.apps.home) + end + end) end end diff --git a/demo/src/ReplicatedStorage/std/bt.luau b/demo/src/ReplicatedStorage/std/bt.luau index 9cb6d1c3..24473c2a 100644 --- a/demo/src/ReplicatedStorage/std/bt.luau +++ b/demo/src/ReplicatedStorage/std/bt.luau @@ -8,33 +8,33 @@ local FAILURE = 1 local RUNNING = 2 local function SEQUENCE(nodes) - return function(...) - for _, node in nodes do - local status = node(...) - if status == FAILURE or status == RUNNING then - return status - end - end - return SUCCESS - end + return function(...) + for _, node in nodes do + local status = node(...) + if status == FAILURE or status == RUNNING then + return status + end + end + return SUCCESS + end end local function FALLBACK(nodes) - return function(...) - for _, node in nodes do - local status = node(...) - if status == SUCCESS or status == RUNNING then - return status - end - end - return FAILURE - end + return function(...) + for _, node in nodes do + local status = node(...) + if status == SUCCESS or status == RUNNING then + return status + end + end + return FAILURE + end end local bt = { - SEQUENCE = SEQUENCE, - FALLBACK = FALLBACK, - RUNNING = RUNNING + SEQUENCE = SEQUENCE, + FALLBACK = FALLBACK, + RUNNING = RUNNING, } return bt diff --git a/demo/src/ReplicatedStorage/std/changetracker.luau b/demo/src/ReplicatedStorage/std/changetracker.luau index d6cfe58f..078baa7b 100644 --- a/demo/src/ReplicatedStorage/std/changetracker.luau +++ b/demo/src/ReplicatedStorage/std/changetracker.luau @@ -1,133 +1,139 @@ local jecs = require(game:GetService("ReplicatedStorage").ecs) type World = jecs.World -type Tracker = { track: (world: World, fn: (changes: { - added: () -> () -> (number, T), - removed: () -> () -> number, - changed: () -> () -> (number, T, T) - }) -> ()) -> () +type Tracker = { + track: ( + world: World, + fn: ( + changes: { + added: () -> () -> (number, T), + removed: () -> () -> number, + changed: () -> () -> (number, T, T), + } + ) -> () + ) -> (), } type Entity = number & { __nominal_type_dont_use: T } local function diff(a, b) - local size = 0 - for k, v in a do - if b[k] ~= v then - return true - end - size += 1 - end - for k, v in b do - size -= 1 - end - - if size ~= 0 then - return true - end - - return false + local size = 0 + for k, v in a do + if b[k] ~= v then + return true + end + size += 1 + end + for k, v in b do + size -= 1 + end + + if size ~= 0 then + return true + end + + return false end local function ChangeTracker(world: World, T: Entity): Tracker - local sparse = world.entityIndex.sparse - local PreviousT = jecs.pair(jecs.Rest, T) - local add = {} - local added - local removed - local is_trivial - - local function changes_added() - added = true - local q = world:query(T):without(PreviousT):drain() - return function() - local id, data = q.next() - if not id then - return nil - end - - is_trivial = typeof(data) ~= "table" - - add[id] = data - - return id, data - end - end - - local function changes_changed() - local q = world:query(T, PreviousT):drain() - - return function() - local id, new, old = q.next() - while true do - if not id then - return nil - end - - if not is_trivial then - if diff(new, old) then - break - end - elseif new ~= old then - break - end - - id, new, old = q.next() - end - - local record = sparse[id] - local archetype = record.archetype - local column = archetype.records[PreviousT].column - local data = if is_trivial then new else table.clone(new) - archetype.columns[column][record.row] = data - - return id, old, new - end - end - - local function changes_removed() - removed = true - - local q = world:query(PreviousT):without(T):drain() - return function() - local id = q.next() - if id then - world:remove(id, PreviousT) - end - return id - end - end - - local changes = { - added = changes_added, - changed = changes_changed, - removed = changes_removed, - } - - local function track(fn) - added = false - removed = false - - fn(changes) - - if not added then - for _ in changes_added() do - end - end - - if not removed then - for _ in changes_removed() do - end - end - - for e, data in add do - world:set(e, PreviousT, if is_trivial then data else table.clone(data)) - end - end - - local tracker = { track = track } - - return tracker + local sparse = world.entityIndex.sparse + local PreviousT = jecs.pair(jecs.Rest, T) + local add = {} + local added + local removed + local is_trivial + + local function changes_added() + added = true + local q = world:query(T):without(PreviousT):drain() + return function() + local id, data = q.next() + if not id then + return nil + end + + is_trivial = typeof(data) ~= "table" + + add[id] = data + + return id, data + end + end + + local function changes_changed() + local q = world:query(T, PreviousT):drain() + + return function() + local id, new, old = q.next() + while true do + if not id then + return nil + end + + if not is_trivial then + if diff(new, old) then + break + end + elseif new ~= old then + break + end + + id, new, old = q.next() + end + + local record = sparse[id] + local archetype = record.archetype + local column = archetype.records[PreviousT].column + local data = if is_trivial then new else table.clone(new) + archetype.columns[column][record.row] = data + + return id, old, new + end + end + + local function changes_removed() + removed = true + + local q = world:query(PreviousT):without(T):drain() + return function() + local id = q.next() + if id then + world:remove(id, PreviousT) + end + return id + end + end + + local changes = { + added = changes_added, + changed = changes_changed, + removed = changes_removed, + } + + local function track(fn) + added = false + removed = false + + fn(changes) + + if not added then + for _ in changes_added() do + end + end + + if not removed then + for _ in changes_removed() do + end + end + + for e, data in add do + world:set(e, PreviousT, if is_trivial then data else table.clone(data)) + end + end + + local tracker = { track = track } + + return tracker end return ChangeTracker diff --git a/demo/src/ReplicatedStorage/std/components.luau b/demo/src/ReplicatedStorage/std/components.luau index 8c5548d3..88e27a87 100644 --- a/demo/src/ReplicatedStorage/std/components.luau +++ b/demo/src/ReplicatedStorage/std/components.luau @@ -1,25 +1,26 @@ local jecs = require(game:GetService("ReplicatedStorage").ecs) local world = require(script.Parent.world) -type Entity = jecs.Entity +type Entity = jecs.Entity local components: { - Character: Entity, - Mob: Entity, - Model: Entity, - Player: Entity, - Target: Entity, - Transform: Entity, - Velocity: Entity, - Previous: Entity, -} = { - Character = world:component(), - Mob = world:component(), - Model = world:component(), - Player = world:component(), - Target = world:component(), - Transform = world:component(), - Velocity = world:component(), - Previous = world:component(), -} + Character: Entity, + Mob: Entity, + Model: Entity, + Player: Entity, + Target: Entity, + Transform: Entity, + Velocity: Entity, + Previous: Entity, +} = + { + Character = world:component(), + Mob = world:component(), + Model = world:component(), + Player = world:component(), + Target = world:component(), + Transform = world:component(), + Velocity = world:component(), + Previous = world:component(), + } return table.freeze(components) diff --git a/demo/src/ReplicatedStorage/std/ctx.luau b/demo/src/ReplicatedStorage/std/ctx.luau index 9a4cbf8f..91a0db68 100644 --- a/demo/src/ReplicatedStorage/std/ctx.luau +++ b/demo/src/ReplicatedStorage/std/ctx.luau @@ -1,11 +1,11 @@ -local world = require(script.Parent.world) local handle = require(script.Parent.handle) +local world = require(script.Parent.world) local singleton = world:entity() local function ctx() - -- Cannot cache handles because they will get invalidated - return handle(singleton) + -- Cannot cache handles because they will get invalidated + return handle(singleton) end return ctx diff --git a/demo/src/ReplicatedStorage/std/handle.luau b/demo/src/ReplicatedStorage/std/handle.luau index 06315845..cb4aba8f 100644 --- a/demo/src/ReplicatedStorage/std/handle.luau +++ b/demo/src/ReplicatedStorage/std/handle.luau @@ -2,55 +2,55 @@ local jecs = require(game:GetService("ReplicatedStorage").ecs) local world = require(script.Parent.world) type Handle = { - has: (self: Handle, id: jecs.Entity) -> boolean, - get: (self: Handle, id: jecs.Entity) -> T?, - add: (self: Handle, id: jecs.Entity) -> Handle, - set: (self: Handle, id: jecs.Entity, value: T) -> Handle, - id: (self: Handle?) -> jecs.Entity + has: (self: Handle, id: jecs.Entity) -> boolean, + get: (self: Handle, id: jecs.Entity) -> T?, + add: (self: Handle, id: jecs.Entity) -> Handle, + set: (self: Handle, id: jecs.Entity, value: T) -> Handle, + id: (self: Handle?) -> jecs.Entity, } local handle: (e: jecs.Entity) -> Handle do - local e - local function has(_, id) - return world:has(e, id) - end - local function get(_, id) - return world:get(e, id) - end - local function set(self, id, value) - world:set(e, id, value) - return self - end - local function add(self, id) - world:add(e, id) - return self - end - local function clear(self) - world:clear(e) - return self - end - local function delete(self) - world:delete(e) - end - local function id() - return e - end + local e + local function has(_, id) + return world:has(e, id) + end + local function get(_, id) + return world:get(e, id) + end + local function set(self, id, value) + world:set(e, id, value) + return self + end + local function add(self, id) + world:add(e, id) + return self + end + local function clear(self) + world:clear(e) + return self + end + local function delete(self) + world:delete(e) + end + local function id() + return e + end - local entity = { - has = has, - get = get, - set = set, - add = add, - clear = clear, - id = id, - } + local entity = { + has = has, + get = get, + set = set, + add = add, + clear = clear, + id = id, + } - function handle(id) - e = id - return entity - end + function handle(id) + e = id + return entity + end end return handle diff --git a/demo/src/ReplicatedStorage/std/hooks.luau b/demo/src/ReplicatedStorage/std/hooks.luau index 07adfcc5..abf66ada 100644 --- a/demo/src/ReplicatedStorage/std/hooks.luau +++ b/demo/src/ReplicatedStorage/std/hooks.luau @@ -1,32 +1,32 @@ ---!native ---!optimize 2 -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local jecs = require(ReplicatedStorage.ecs) - -local function create_cache(hook) - local columns = setmetatable({}, { - __index = function(self, component) - local column = {} - self[component] = column - return column - end - }) - - return function(world, component, fn) - local column = columns[component] - table.insert(column, fn) - world:set(component, hook, function(entity, value) - for _, callback in column do - callback(entity, value) - end - end) - end -end - -local hooks = { - OnSet = create_cache(jecs.OnSet), - OnAdd = create_cache(jecs.OnAdd), - OnRemove = create_cache(jecs.OnRemove) -} - -return hooks +--!native +--!optimize 2 +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local jecs = require(ReplicatedStorage.ecs) + +local function create_cache(hook) + local columns = setmetatable({}, { + __index = function(self, component) + local column = {} + self[component] = column + return column + end, + }) + + return function(world, component, fn) + local column = columns[component] + table.insert(column, fn) + world:set(component, hook, function(entity, value) + for _, callback in column do + callback(entity, value) + end + end) + end +end + +local hooks = { + OnSet = create_cache(jecs.OnSet), + OnAdd = create_cache(jecs.OnAdd), + OnRemove = create_cache(jecs.OnRemove), +} + +return hooks diff --git a/demo/src/ReplicatedStorage/std/init.luau b/demo/src/ReplicatedStorage/std/init.luau index ec353440..4fc23498 100644 --- a/demo/src/ReplicatedStorage/std/init.luau +++ b/demo/src/ReplicatedStorage/std/init.luau @@ -7,19 +7,19 @@ local Scheduler = require(script.scheduler) export type Scheduler = Scheduler.Scheduler local std = { - ChangeTracker = require(script.changetracker), - Scheduler = Scheduler, - bt = require(script.bt), - collect = require(script.collect), - components = require(script.components), - ctx = require(script.ctx), - handle = require(script.handle), - interval = require(script.interval), - ref = require(script.ref), - world = world :: World, - pair = jecs.pair, - __ = jecs.w, - hooks = require(script.hooks) + ChangeTracker = require(script.changetracker), + Scheduler = Scheduler, + bt = require(script.bt), + collect = require(script.collect), + components = require(script.components), + ctx = require(script.ctx), + handle = require(script.handle), + interval = require(script.interval), + ref = require(script.ref), + world = world :: World, + pair = jecs.pair, + __ = jecs.w, + hooks = require(script.hooks), } return std diff --git a/demo/src/ReplicatedStorage/std/interval.luau b/demo/src/ReplicatedStorage/std/interval.luau index fadae901..37b40538 100644 --- a/demo/src/ReplicatedStorage/std/interval.luau +++ b/demo/src/ReplicatedStorage/std/interval.luau @@ -1,19 +1,19 @@ local function interval(s) - local pin + local pin - local function throttle() - if not pin then - pin = os.clock() - end + local function throttle() + if not pin then + pin = os.clock() + end - local elapsed = os.clock() - pin > s - if elapsed then - pin = os.clock() - end + local elapsed = os.clock() - pin > s + if elapsed then + pin = os.clock() + end - return elapsed - end - return throttle + return elapsed + end + return throttle end return interval diff --git a/demo/src/ReplicatedStorage/std/ref.luau b/demo/src/ReplicatedStorage/std/ref.luau index 53017fd8..abd1b3af 100644 --- a/demo/src/ReplicatedStorage/std/ref.luau +++ b/demo/src/ReplicatedStorage/std/ref.luau @@ -1,24 +1,24 @@ -local world = require(script.Parent.world) local handle = require(script.Parent.handle) +local world = require(script.Parent.world) local refs = {} local function fini(key) - return function() - refs[key] = nil - end + return function() + refs[key] = nil + end end local function ref(key): (handle.Handle, (() -> ())?) - if not key then - return handle(world:entity()) - end - local e = refs[key] - if not e then - e = world:entity() - refs[key] = e - end - -- Cannot cache handles because they will get invalidated - return handle(e), fini(key) + if not key then + return handle(world:entity()) + end + local e = refs[key] + if not e then + e = world:entity() + refs[key] = e + end + -- Cannot cache handles because they will get invalidated + return handle(e), fini(key) end return ref diff --git a/demo/src/ReplicatedStorage/std/registry.luau b/demo/src/ReplicatedStorage/std/registry.luau index 04b55cc8..7be5d09f 100644 --- a/demo/src/ReplicatedStorage/std/registry.luau +++ b/demo/src/ReplicatedStorage/std/registry.luau @@ -1,8 +1,8 @@ local reserved = 0 local function reserve() - reserved += 1 - return reserved + reserved += 1 + return reserved end -- If you don't like passing around a world singleton @@ -19,13 +19,13 @@ end world:set(e, components.Transform, CFrame) ]] local function register(world) - for _ = 1, reserved do - world:component() - end - return world + for _ = 1, reserved do + world:component() + end + return world end return { - reserve = reserve, - register = register, + reserve = reserve, + register = register, } diff --git a/demo/src/ReplicatedStorage/std/scheduler.luau b/demo/src/ReplicatedStorage/std/scheduler.luau index 33291ebe..82c21e47 100644 --- a/demo/src/ReplicatedStorage/std/scheduler.luau +++ b/demo/src/ReplicatedStorage/std/scheduler.luau @@ -7,245 +7,250 @@ local pair = jecs.pair local Name = jecs.Name type World = jecs.World -type Entity = jecs.Entity +type Entity = jecs.Entity type System = { - callback: (world: World) -> (), - id: number, + callback: (world: World) -> (), + id: number, } type Systems = { System } - type Events = { - RenderStepped: Systems, - Heartbeat: Systems + RenderStepped: Systems, + Heartbeat: Systems, } export type Scheduler = { - components: { - Disabled: Entity, - System: Entity, - Phase: Entity, - DependsOn: Entity - }, - - collect: { - under_event: (event: Entity) -> Systems, - all: () -> Events - }, - - systems: { - begin: (events: Events) -> { [Entity]: thread }, - new: (callback: (dt: number) -> (), phase: Entity) -> Entity - }, - - phases: { - RenderStepped: Entity, - Heartbeat: Entity, - }, - - phase: (after: Entity) -> Entity, - - debugging: boolean, + components: { + Disabled: Entity, + System: Entity, + Phase: Entity, + DependsOn: Entity, + }, + + collect: { + under_event: (event: Entity) -> Systems, + all: () -> Events, + }, + + systems: { + begin: (events: Events) -> { [Entity]: thread }, + new: (callback: (dt: number) -> (), phase: Entity) -> Entity, + }, + + phases: { + RenderStepped: Entity, + Heartbeat: Entity, + }, + + phase: (after: Entity) -> Entity, + + debugging: boolean, } local scheduler_new: (w: World, components: { [string]: Entity }) -> Scheduler do - local world: World - local Disabled: Entity - local System: Entity - local DependsOn: Entity - local Phase: Entity - local Event: Entity - - local scheduler - - local RenderStepped - local Heartbeat - local PreAnimation - local PreSimulation - - local sys: System - local dt - - local function run() - local id = sys.id - local system_data = scheduler.system_data[id] - if system_data.paused then return end - - scheduler:mark_system_frame_start(id) - sys.callback(dt) - scheduler:mark_system_frame_end(id) - end - - local function panic(str) - -- We don't want to interrupt the loop when we error - task.spawn(error, str) - end - local function begin(events: { Systems }) - local threads = {} - for event, systems in events do - if not event then continue end - local event_name = tostring(event) - threads[event] = task.spawn(function() - while true do - dt = event:Wait() - - debug.profilebegin(event_name) - for _, s in systems do - sys = s - local didNotYield, why = xpcall(function() - for _ in run do break end - end, debug.traceback) - - if didNotYield then - continue - end - - if string.find(why, "thread is not yieldable") then - panic("Not allowed to yield in the systems." - .. "\n" - .. "System: " - .. debug.info(s.callback, "n") - .. " has been ejected" - ) - continue - end - panic(why) - end - debug.profileend() - end - end) - end - return threads - end - - local function scheduler_collect_systems_under_phase_recursive(systems, phase) - local phase_name = world:get(phase, Name) - for _, s in world:query(System):with(pair(DependsOn, phase)) do - table.insert(systems, { - id = scheduler:register_system({ - name = s.name, - phase = phase_name - }), - callback = s.callback - }) - end - for after in world:query(Phase):with(pair(DependsOn, phase)) do - scheduler_collect_systems_under_phase_recursive(systems, after) - end - end - - local function scheduler_collect_systems_under_event(event) - local systems = {} - scheduler_collect_systems_under_phase_recursive(systems, event) - return systems - end - - local function scheduler_collect_systems_all() - local events = {} - for phase, event in world:query(Event):with(Phase) do - events[event] = scheduler_collect_systems_under_event(phase) - end - return events - end - - local function scheduler_phase_new(after) - local phase = world:entity() - world:add(phase, Phase) - local dependency = pair(DependsOn, after) - world:add(phase, dependency) - return phase - end - - local function scheduler_systems_new(callback, phase) - local system = world:entity() - local name = debug.info(callback, "n") - world:set(system, System, { callback = callback, name = name }) - world:add(system, pair(DependsOn, phase)) - return system - end - - function scheduler_new(w: World, components: { [string]: Entity }) - world = w - Disabled = world:component() - System = world:component() - Phase = world:component() - DependsOn = world:component() - Event = world:component() - - RenderStepped = world:component() - Heartbeat = world:component() - PreSimulation = world:component() - PreAnimation = world:component() - - local RunService = game:GetService("RunService") - if RunService:IsClient() then - world:add(RenderStepped, Phase) - world:set(RenderStepped, Event, RunService.RenderStepped) - end - - world:add(Heartbeat, Phase) - world:set(Heartbeat, Event, RunService.Heartbeat) - - world:add(PreSimulation, Phase) - world:set(PreSimulation, Event, RunService.PreSimulation) - - world:add(PreAnimation, Phase) - world:set(PreAnimation, Event, RunService.PreAnimation) - - for name, component in components do - world:set(component, Name, name) - end - - table.insert(jabby.public, { - class_name = "World", - name = "MyWorld", - world = world, - debug = Name, - entities = {} - }) - - - jabby.public.updated = true - scheduler = jabby.scheduler.create("scheduler") - - table.insert(jabby.public, scheduler) - - return { - phase = scheduler_phase_new, - - phases = { - RenderStepped = RenderStepped, - PreSimulation = PreSimulation, - Heartbeat = Heartbeat, - PreAnimation = PreAnimation - }, - - world = world, - - components = { - DependsOn = DependsOn, - Disabled = Disabled, - Phase = Phase, - System = System, - }, - - collect = { - under_event = scheduler_collect_systems_under_event, - all = scheduler_collect_systems_all - }, - - systems = { - new = scheduler_systems_new, - begin = begin - } - } - end + local world: World + local Disabled: Entity + local System: Entity + local DependsOn: Entity + local Phase: Entity + local Event: Entity + + local scheduler + + local RenderStepped + local Heartbeat + local PreAnimation + local PreSimulation + + local sys: System + local dt + + local function run() + local id = sys.id + local system_data = scheduler.system_data[id] + if system_data.paused then + return + end + + scheduler:mark_system_frame_start(id) + sys.callback(dt) + scheduler:mark_system_frame_end(id) + end + + local function panic(str) + -- We don't want to interrupt the loop when we error + task.spawn(error, str) + end + local function begin(events: { Systems }) + local threads = {} + for event, systems in events do + if not event then + continue + end + local event_name = tostring(event) + threads[event] = task.spawn(function() + while true do + dt = event:Wait() + + debug.profilebegin(event_name) + for _, s in systems do + sys = s + local didNotYield, why = xpcall(function() + for _ in run do + break + end + end, debug.traceback) + + if didNotYield then + continue + end + + if string.find(why, "thread is not yieldable") then + panic( + "Not allowed to yield in the systems." + .. "\n" + .. "System: " + .. debug.info(s.callback, "n") + .. " has been ejected" + ) + continue + end + panic(why) + end + debug.profileend() + end + end) + end + return threads + end + + local function scheduler_collect_systems_under_phase_recursive(systems, phase) + local phase_name = world:get(phase, Name) + for _, s in world:query(System):with(pair(DependsOn, phase)) do + table.insert(systems, { + id = scheduler:register_system({ + name = s.name, + phase = phase_name, + }), + callback = s.callback, + }) + end + for after in world:query(Phase):with(pair(DependsOn, phase)) do + scheduler_collect_systems_under_phase_recursive(systems, after) + end + end + + local function scheduler_collect_systems_under_event(event) + local systems = {} + scheduler_collect_systems_under_phase_recursive(systems, event) + return systems + end + + local function scheduler_collect_systems_all() + local events = {} + for phase, event in world:query(Event):with(Phase) do + events[event] = scheduler_collect_systems_under_event(phase) + end + return events + end + + local function scheduler_phase_new(after) + local phase = world:entity() + world:add(phase, Phase) + local dependency = pair(DependsOn, after) + world:add(phase, dependency) + return phase + end + + local function scheduler_systems_new(callback, phase) + local system = world:entity() + local name = debug.info(callback, "n") + world:set(system, System, { callback = callback, name = name }) + world:add(system, pair(DependsOn, phase)) + return system + end + + function scheduler_new(w: World, components: { [string]: Entity }) + world = w + Disabled = world:component() + System = world:component() + Phase = world:component() + DependsOn = world:component() + Event = world:component() + + RenderStepped = world:component() + Heartbeat = world:component() + PreSimulation = world:component() + PreAnimation = world:component() + + local RunService = game:GetService("RunService") + if RunService:IsClient() then + world:add(RenderStepped, Phase) + world:set(RenderStepped, Event, RunService.RenderStepped) + end + + world:add(Heartbeat, Phase) + world:set(Heartbeat, Event, RunService.Heartbeat) + + world:add(PreSimulation, Phase) + world:set(PreSimulation, Event, RunService.PreSimulation) + + world:add(PreAnimation, Phase) + world:set(PreAnimation, Event, RunService.PreAnimation) + + for name, component in components do + world:set(component, Name, name) + end + + table.insert(jabby.public, { + class_name = "World", + name = "MyWorld", + world = world, + debug = Name, + entities = {}, + }) + + jabby.public.updated = true + scheduler = jabby.scheduler.create("scheduler") + + table.insert(jabby.public, scheduler) + + return { + phase = scheduler_phase_new, + + phases = { + RenderStepped = RenderStepped, + PreSimulation = PreSimulation, + Heartbeat = Heartbeat, + PreAnimation = PreAnimation, + }, + + world = world, + + components = { + DependsOn = DependsOn, + Disabled = Disabled, + Phase = Phase, + System = System, + }, + + collect = { + under_event = scheduler_collect_systems_under_event, + all = scheduler_collect_systems_all, + }, + + systems = { + new = scheduler_systems_new, + begin = begin, + }, + } + end end return { - new = scheduler_new + new = scheduler_new, } diff --git a/demo/src/ServerScriptService/systems/mobs.luau b/demo/src/ServerScriptService/systems/mobs.luau index 411d0df4..20aab19d 100644 --- a/demo/src/ServerScriptService/systems/mobs.luau +++ b/demo/src/ServerScriptService/systems/mobs.luau @@ -21,58 +21,53 @@ local Player = cts.Player local Character = cts.Character local function mobsMove(dt: number) - local targets = {} - for _, character in world:query(Character):with(Player):iter() do - table.insert(targets, (character.PrimaryPart :: Part).Position) - end - - for mob, transform, v in world:query(Transform, Velocity):with(Mob):iter() do - local cf = transform.new - local p = cf.Position - - local target - local closest - - for _, pos in targets do - local distance = (p - pos).Magnitude - if not target or distance < closest then - target = pos - closest = distance - end - end - - if not target then - continue - end - - local moving = CFrame.new(p + (target - p).Unit * dt * v) - transform.new = moving - blink.UpdateTransform.FireAll(mob, moving) - end + local targets = {} + for _, character in world:query(Character):with(Player):iter() do + table.insert(targets, (character.PrimaryPart :: Part).Position) + end + + for mob, transform, v in world:query(Transform, Velocity):with(Mob):iter() do + local cf = transform.new + local p = cf.Position + + local target + local closest + + for _, pos in targets do + local distance = (p - pos).Magnitude + if not target or distance < closest then + target = pos + closest = distance + end + end + + if not target then + continue + end + + local moving = CFrame.new(p + (target - p).Unit * dt * v) + transform.new = moving + blink.UpdateTransform.FireAll(mob, moving) + end end local throttle = interval(5) local function spawnMobs() - if throttle() then - local p = Vector3.new(0, 5, 0) - local cf = CFrame.new(p) - local v = 5 + if throttle() then + local p = Vector3.new(0, 5, 0) + local cf = CFrame.new(p) + local v = 5 - local id = ref() - :set(Velocity, v) - :set(Transform, { new = cf }) - :add(Mob) - .id() + local id = ref():set(Velocity, v):set(Transform, { new = cf }):add(Mob).id() - blink.SpawnMob.FireAll(id, cf, v) - end + blink.SpawnMob.FireAll(id, cf, v) + end end return function(scheduler: std.Scheduler) - local phases = scheduler.phases - local system_new = scheduler.systems.new - system_new(mobsMove, phases.Heartbeat) - system_new(spawnMobs, phases.Heartbeat) - + local phases = scheduler.phases + local system_new = scheduler.systems.new + system_new(mobsMove, phases.Heartbeat) + system_new(spawnMobs, phases.Heartbeat) end diff --git a/demo/src/ServerScriptService/systems/players.luau b/demo/src/ServerScriptService/systems/players.luau index 4425ddda..0532f7ac 100644 --- a/demo/src/ServerScriptService/systems/players.luau +++ b/demo/src/ServerScriptService/systems/players.luau @@ -16,31 +16,28 @@ local world: std.World = std.world local conn = {} local function players() - for _, player in playersAdded do - world:set( - std.world:entity(), - cts.Transform - ) - - local e = ref(player.UserId):set(Player, player) - local characterAdd = player.CharacterAdded - conn[e.id()] = characterAdd:Connect(function(rig) - while rig.Parent ~= workspace do - task.wait() - end - e:set(Character, rig) - end) - end - - for _, player in playersRemoved do - local id = ref(player.UserId):clear().id() - conn[id]:Disconnect() - conn[id] = nil - end + for _, player in playersAdded do + world:set(std.world:entity(), cts.Transform) + + local e = ref(player.UserId):set(Player, player) + local characterAdd = player.CharacterAdded + conn[e.id()] = characterAdd:Connect(function(rig) + while rig.Parent ~= workspace do + task.wait() + end + e:set(Character, rig) + end) + end + + for _, player in playersRemoved do + local id = ref(player.UserId):clear().id() + conn[id]:Disconnect() + conn[id] = nil + end end return function(scheduler: std.Scheduler) - local phases = scheduler.phases - local system_new = scheduler.systems.new - system_new(players, phases.Heartbeat) + local phases = scheduler.phases + local system_new = scheduler.systems.new + system_new(players, phases.Heartbeat) end diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau index a5e9679a..0c649332 100644 --- a/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau @@ -10,30 +10,30 @@ local Model = cts.Model local Transform = cts.Transform local function move(dt: number) - for _, transform, model in world:query(Transform, Model):iter() do - local cf = transform.new - if cf ~= transform.old then - local origo = model.PrimaryPart.CFrame - model.PrimaryPart.CFrame = origo:Lerp(cf, 1) - transform.old = cf - end - end + for _, transform, model in world:query(Transform, Model):iter() do + local cf = transform.new + if cf ~= transform.old then + local origo = model.PrimaryPart.CFrame + model.PrimaryPart.CFrame = origo:Lerp(cf, 1) + transform.old = cf + end + end end local function syncTransforms() - for _, id, cf in blink.UpdateTransform.Iter() do - local e = ref("server-"..id) - local transform = e:get(cts.Transform) - if not transform then - continue - end - transform.new = cf - end + for _, id, cf in blink.UpdateTransform.Iter() do + local e = ref("server-" .. id) + local transform = e:get(cts.Transform) + if not transform then + continue + end + transform.new = cf + end end return function(scheduler: std.Scheduler) - local phases = scheduler.phases - local system_new = scheduler.systems.new - system_new(move, phases.Heartbeat) - system_new(syncTransforms, phases.RenderStepped) + local phases = scheduler.phases + local system_new = scheduler.systems.new + system_new(move, phases.Heartbeat) + system_new(syncTransforms, phases.RenderStepped) end diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau index 4647c971..94cd97db 100644 --- a/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau @@ -6,26 +6,26 @@ local world = std.world local cts = std.components local function syncMobs() - for _, id, cf, vel in blink.SpawnMob.Iter() do - local part = Instance.new("Part") - part.Size = Vector3.one * 5 - part.BrickColor = BrickColor.Red() - part.Anchored = true - local model = Instance.new("Model") - model.PrimaryPart = part - part.Parent = model - model.Parent = workspace + for _, id, cf, vel in blink.SpawnMob.Iter() do + local part = Instance.new("Part") + part.Size = Vector3.one * 5 + part.BrickColor = BrickColor.Red() + part.Anchored = true + local model = Instance.new("Model") + model.PrimaryPart = part + part.Parent = model + model.Parent = workspace - ref("server-"..id) - :set(cts.Transform, { new = cf, old = cf }) - :set(cts.Velocity, vel) - :set(cts.Model, model) - :add(cts.Mob) - end + ref("server-" .. id) + :set(cts.Transform, { new = cf, old = cf }) + :set(cts.Velocity, vel) + :set(cts.Model, model) + :add(cts.Mob) + end end return function(scheduler: std.Scheduler) - local phases = scheduler.phases - local system_new = scheduler.systems.new - system_new(syncMobs, phases.RenderStepped) + local phases = scheduler.phases + local system_new = scheduler.systems.new + system_new(syncMobs, phases.RenderStepped) end diff --git a/examples/luau/entities/basics.luau b/examples/luau/entities/basics.luau index cfebf6d8..170431e7 100644 --- a/examples/luau/entities/basics.luau +++ b/examples/luau/entities/basics.luau @@ -23,20 +23,18 @@ print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`) -- Overwrite the value of the Position component world:set(bob, Position, Vector3.new(40, 50, 60)) - local alice = world:entity() -- Create another named entity world:set(alice, Name, "Alice") world:set(alice, Position, Vector3.new(10, 20, 30)) world:add(alice, Walking) - -- Remove tag world:remove(alice, Walking) -- Iterate all entities with Position for entity, p in world:query(Position) do - print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`) + print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`) end -- Output: diff --git a/examples/luau/entities/hierarchy.luau b/examples/luau/entities/hierarchy.luau index d8725095..40e3727f 100644 --- a/examples/luau/entities/hierarchy.luau +++ b/examples/luau/entities/hierarchy.luau @@ -11,67 +11,55 @@ local Moon = world:component() local Vector3 do - Vector3 = {} - Vector3.__index = Vector3 - - function Vector3.new(x, y, z) - x = x or 0 - y = y or 0 - z = z or 0 - return setmetatable({ X = x, Y = y, Z = z }, Vector3) - end - - function Vector3.__add(left, right) - return Vector3.new( - left.X + right.X, - left.Y + right.Y, - left.Z + right.Z - ) - end - - function Vector3.__mul(left, right) - if typeof(right) == "number" then - return Vector3.new( - left.X * right, - left.Y * right, - left.Z * right - ) - end - return Vector3.new( - left.X * right.X, - left.Y * right.Y, - left.Z * right.Z - ) - end - - Vector3.one = Vector3.new(1, 1, 1) - Vector3.zero = Vector3.new() + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new(left.X + right.X, left.Y + right.Y, left.Z + right.Z) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new(left.X * right, left.Y * right, left.Z * right) + end + return Vector3.new(left.X * right.X, left.Y * right.Y, left.Z * right.Z) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() end local function path(entity) - local str = world:get(entity, Name) - local parent - while true do - parent = world:parent(entity) - if not parent then - break - end - entity = parent - str = world:get(parent, Name) .. "/" .. str - end - return str + local str = world:get(entity, Name) + local parent + while true do + parent = world:parent(entity) + if not parent then + break + end + entity = parent + str = world:get(parent, Name) .. "/" .. str + end + return str end local function iterate(entity, parent) - local p = world:get(entity, Position) - local actual = p + parent - print(path(entity)) - print(`\{{actual.X}, {actual.Y}, {actual.Z}}`) - - for child in world:query(pair(ChildOf, entity)) do - --print(world:get(child, Name)) - iterate(child, actual) - end + local p = world:get(entity, Position) + local actual = p + parent + print(path(entity)) + print(`\{{actual.X}, {actual.Y}, {actual.Z}}`) + + for child in world:query(pair(ChildOf, entity)) do + --print(world:get(child, Name)) + iterate(child, actual) + end end local sun = world:entity() @@ -79,36 +67,35 @@ world:add(sun, Star) world:set(sun, Position, Vector3.one) world:set(sun, Name, "Sun") do - local earth = world:entity() - world:set(earth, Name, "Earth") - world:add(earth, pair(ChildOf, sun)) - world:add(earth, Planet) - world:set(earth, Position, Vector3.one * 3) - - do - local moon = world:entity() - world:set(moon, Name, "Moon") - world:add(moon, pair(ChildOf, earth)) - world:add(moon, Moon) - world:set(moon, Position, Vector3.one * 0.1) - - print(`Child of Earth? {world:has(moon, pair(ChildOf, earth))}`) - end - - local venus = world:entity() - world:set(venus, Name, "Venus") - world:add(venus, pair(ChildOf, sun)) - world:add(venus, Planet) - world:set(venus, Position, Vector3.one * 2) - - local mercury = world:entity() - world:set(mercury, Name, "Mercury") - world:add(mercury, pair(ChildOf, sun)) - world:add(mercury, Planet) - world:set(mercury, Position, Vector3.one) - - - iterate(sun, Vector3.zero) + local earth = world:entity() + world:set(earth, Name, "Earth") + world:add(earth, pair(ChildOf, sun)) + world:add(earth, Planet) + world:set(earth, Position, Vector3.one * 3) + + do + local moon = world:entity() + world:set(moon, Name, "Moon") + world:add(moon, pair(ChildOf, earth)) + world:add(moon, Moon) + world:set(moon, Position, Vector3.one * 0.1) + + print(`Child of Earth? {world:has(moon, pair(ChildOf, earth))}`) + end + + local venus = world:entity() + world:set(venus, Name, "Venus") + world:add(venus, pair(ChildOf, sun)) + world:add(venus, Planet) + world:set(venus, Position, Vector3.one * 2) + + local mercury = world:entity() + world:set(mercury, Name, "Mercury") + world:add(mercury, pair(ChildOf, sun)) + world:add(mercury, Planet) + world:set(mercury, Position, Vector3.one) + + iterate(sun, Vector3.zero) end -- Output: diff --git a/examples/luau/hooks/cleanup.luau b/examples/luau/hooks/cleanup.luau index 8a40797d..9088ad25 100644 --- a/examples/luau/hooks/cleanup.luau +++ b/examples/luau/hooks/cleanup.luau @@ -1,21 +1,21 @@ -local jecs = require("@jecs") -local world = jecs.World.new() - -local Model = world:component() - --- It is important to define hooks for the component before the component is ever used --- otherwise the hooks will never invoke! -world:set(Model, jecs.OnRemove, function(entity) - -- OnRemove is invoked before the component and its value is removed - -- which provides a stable reference to the entity at deletion. - -- This means that it is safe to retrieve the data inside of a hook - local model = world:get(entity, Model) - model:Destroy() -end) - -world:set(Model, jecs.OnSet, function(entity, model) - -- OnSet is invoked after the data has been assigned. - -- It also returns the data for faster access. - -- There may be some logic to do some side effects on reassignments - model:SetAttribute("entityId", entity) -end) +local jecs = require("@jecs") +local world = jecs.World.new() + +local Model = world:component() + +-- It is important to define hooks for the component before the component is ever used +-- otherwise the hooks will never invoke! +world:set(Model, jecs.OnRemove, function(entity) + -- OnRemove is invoked before the component and its value is removed + -- which provides a stable reference to the entity at deletion. + -- This means that it is safe to retrieve the data inside of a hook + local model = world:get(entity, Model) + model:Destroy() +end) + +world:set(Model, jecs.OnSet, function(entity, model) + -- OnSet is invoked after the data has been assigned. + -- It also returns the data for faster access. + -- There may be some logic to do some side effects on reassignments + model:SetAttribute("entityId", entity) +end) diff --git a/examples/luau/queries/basics.luau b/examples/luau/queries/basics.luau index 8dd9a35f..21e7cae9 100644 --- a/examples/luau/queries/basics.luau +++ b/examples/luau/queries/basics.luau @@ -7,41 +7,29 @@ local Name = world:component() local Vector3 do - Vector3 = {} - Vector3.__index = Vector3 - - function Vector3.new(x, y, z) - x = x or 0 - y = y or 0 - z = z or 0 - return setmetatable({ X = x, Y = y, Z = z }, Vector3) - end - - function Vector3.__add(left, right) - return Vector3.new( - left.X + right.X, - left.Y + right.Y, - left.Z + right.Z - ) - end - - function Vector3.__mul(left, right) - if typeof(right) == "number" then - return Vector3.new( - left.X * right, - left.Y * right, - left.Z * right - ) - end - return Vector3.new( - left.X * right.X, - left.Y * right.Y, - left.Z * right.Z - ) - end - - Vector3.one = Vector3.new(1, 1, 1) - Vector3.zero = Vector3.new() + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new(left.X + right.X, left.Y + right.Y, left.Z + right.Z) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new(left.X * right, left.Y * right, left.Z * right) + end + return Vector3.new(left.X * right.X, left.Y * right.Y, left.Z * right.Z) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() end -- Create a few test entities for a Position, Velocity query @@ -62,12 +50,12 @@ world:set(e3, Position, Vector3.new(10, 20, 30)) -- Create an uncached query for Position, Velocity. for entity, p, v in world:query(Position, Velocity) do - -- Iterate entities matching the query - p.X += v.X - p.Y += v.Y - p.Z += v.Z + -- Iterate entities matching the query + p.X += v.X + p.Y += v.Y + p.Z += v.Z - print(`{world:get(entity, Name)}: \{{p.X}, {p.Y}, {p.Z}}`) + print(`{world:get(entity, Name)}: \{{p.X}, {p.Y}, {p.Z}}`) end -- Output: diff --git a/examples/luau/queries/changetracking.luau b/examples/luau/queries/changetracking.luau index 20ed8884..ed1de8f7 100644 --- a/examples/luau/queries/changetracking.luau +++ b/examples/luau/queries/changetracking.luau @@ -2,179 +2,173 @@ local jecs = require("@jecs") type World = jecs.WorldShim -type Tracker = { track: (world: World, fn: (changes: { - added: () -> () -> (number, T), - removed: () -> () -> number, - changed: () -> () -> (number, T, T) - }) -> ()) -> () +type Tracker = { + track: ( + world: World, + fn: ( + changes: { + added: () -> () -> (number, T), + removed: () -> () -> number, + changed: () -> () -> (number, T, T), + } + ) -> () + ) -> (), } local function diff(a, b) - local size = 0 - for k, v in a do - if b[k] ~= v then - return true - end - size += 1 - end - for k, v in b do - size -= 1 - end - - if size ~= 0 then - return true - end - - return false + local size = 0 + for k, v in a do + if b[k] ~= v then + return true + end + size += 1 + end + for k, v in b do + size -= 1 + end + + if size ~= 0 then + return true + end + + return false end type Entity = number & { __nominal_type_dont_use: T } local function ChangeTracker(world, T: Entity): Tracker - local PreviousT = jecs.pair(jecs.Rest, T) - local add = {} - local added - local removed - local is_trivial - - local function changes_added() - added = true - local q = world:query(T):without(PreviousT):drain() - return function() - local id, data = q.next() - if not id then - return nil - end - - is_trivial = typeof(data) ~= "table" - - add[id] = data - - return id, data - end - end - - local function changes_changed() - local q = world:query(T, PreviousT):drain() - - return function() - local id, new, old = q.next() - while true do - if not id then - return nil - end - - if not is_trivial then - if diff(new, old) then - break - end - elseif new ~= old then - break - end - - id, new, old = q.next() - end - - add[id] = new - - return id, old, new - end - end - - local function changes_removed() - removed = true - - local q = world:query(PreviousT):without(T):drain() - return function() - local id = q.next() - if id then - world:remove(id, PreviousT) - end - return id - end - end - - local changes = { - added = changes_added, - changed = changes_changed, - removed = changes_removed, - } - - local function track(fn) - added = false - removed = false - - fn(changes) - - if not added then - for _ in changes_added() do - end - end - - if not removed then - for _ in changes_removed() do - end - end - - for e, data in add do - world:set(e, PreviousT, if is_trivial then data else table.clone(data)) - end - end - - local tracker = { track = track } - - return tracker + local PreviousT = jecs.pair(jecs.Rest, T) + local add = {} + local added + local removed + local is_trivial + + local function changes_added() + added = true + local q = world:query(T):without(PreviousT):drain() + return function() + local id, data = q.next() + if not id then + return nil + end + + is_trivial = typeof(data) ~= "table" + + add[id] = data + + return id, data + end + end + + local function changes_changed() + local q = world:query(T, PreviousT):drain() + + return function() + local id, new, old = q.next() + while true do + if not id then + return nil + end + + if not is_trivial then + if diff(new, old) then + break + end + elseif new ~= old then + break + end + + id, new, old = q.next() + end + + add[id] = new + + return id, old, new + end + end + + local function changes_removed() + removed = true + + local q = world:query(PreviousT):without(T):drain() + return function() + local id = q.next() + if id then + world:remove(id, PreviousT) + end + return id + end + end + + local changes = { + added = changes_added, + changed = changes_changed, + removed = changes_removed, + } + + local function track(fn) + added = false + removed = false + + fn(changes) + + if not added then + for _ in changes_added() do + end + end + + if not removed then + for _ in changes_removed() do + end + end + + for e, data in add do + world:set(e, PreviousT, if is_trivial then data else table.clone(data)) + end + end + + local tracker = { track = track } + + return tracker end local Vector3 do - Vector3 = {} - Vector3.__index = Vector3 - - function Vector3.new(x, y, z) - x = x or 0 - y = y or 0 - z = z or 0 - return setmetatable({ X = x, Y = y, Z = z }, Vector3) - end - - function Vector3.__add(left, right) - return Vector3.new( - left.X + right.X, - left.Y + right.Y, - left.Z + right.Z - ) - end - - function Vector3.__mul(left, right) - if typeof(right) == "number" then - return Vector3.new( - left.X * right, - left.Y * right, - left.Z * right - ) - end - return Vector3.new( - left.X * right.X, - left.Y * right.Y, - left.Z * right.Z - ) - end - - Vector3.one = Vector3.new(1, 1, 1) - Vector3.zero = Vector3.new() + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new(left.X + right.X, left.Y + right.Y, left.Z + right.Z) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new(left.X * right, left.Y * right, left.Z * right) + end + return Vector3.new(left.X * right.X, left.Y * right.Y, left.Z * right.Z) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() end local world = jecs.World.new() local Name = world:component() local function named(ctr, name) - local e = ctr(world) - world:set(e, Name, name) - return e + local e = ctr(world) + world:set(e, Name, name) + return e end local function name(e) - return world:get(e, Name) + return world:get(e, Name) end local Position = named(world.component, "Position") @@ -189,50 +183,50 @@ local e2 = named(world.entity, "e2") world:set(e2, Position, Vector3.new(10, 20, 30)) PositionTracker.track(function(changes) - -- You can iterate over different types of changes: Added, Changed, Removed + -- You can iterate over different types of changes: Added, Changed, Removed - -- added queries for every entity with a new Position component - for e, p in changes.added() do - print(`Added {e}: \{{p.X}, {p.Y}, {p.Z}}`) - end + -- added queries for every entity with a new Position component + for e, p in changes.added() do + print(`Added {e}: \{{p.X}, {p.Y}, {p.Z}}`) + end - -- changed queries for entities who's changed their data since - -- last was it tracked - for _ in changes.changed() do - print([[This won't print because it is the first time + -- changed queries for entities who's changed their data since + -- last was it tracked + for _ in changes.changed() do + print([[This won't print because it is the first time we are tracking the Position component]]) - end + end - -- removed queries for entities who's removed their Position component - -- since last it was tracked - for _ in changes.removed() do - print([[This won't print because it is the first time + -- removed queries for entities who's removed their Position component + -- since last it was tracked + for _ in changes.removed() do + print([[This won't print because it is the first time we are tracking the Position component]]) - end + end end) world:set(e1, Position, Vector3.new(1, 1, 2) * 999) PositionTracker.track(function(changes) - for e, p in changes.added() do - print([[This won't never print no Position component was added + for e, p in changes.added() do + print([[This won't never print no Position component was added since last time we tracked]]) - end + end - for e, old, new in changes.changed() do - print(`{name(e)}'s Position changed from \{{old.X}, {old.Y}, {old.Z}\} to \{{new.X}, {new.Y}, {new.Z}\}`) - end + for e, old, new in changes.changed() do + print(`{name(e)}'s Position changed from \{{old.X}, {old.Y}, {old.Z}\} to \{{new.X}, {new.Y}, {new.Z}\}`) + end - -- If you don't call e.g. changes.removed() then it will automatically drain its iterator and stage their changes. - -- This ensures you will not have any off-by-one frame errors. + -- If you don't call e.g. changes.removed() then it will automatically drain its iterator and stage their changes. + -- This ensures you will not have any off-by-one frame errors. end) world:remove(e2, Position) PositionTracker.track(function(changes) - for e in changes.removed() do - print(`Position was removed from {name(e)}`) - end + for e in changes.removed() do + print(`Position was removed from {name(e)}`) + end end) -- Output: diff --git a/examples/luau/queries/spatial_grids.luau b/examples/luau/queries/spatial_grids.luau index 1b497a2e..6dcf72fa 100644 --- a/examples/luau/queries/spatial_grids.luau +++ b/examples/luau/queries/spatial_grids.luau @@ -1,130 +1,125 @@ -local jecs = require("@jecs") -local pair = jecs.pair -local ChildOf = jecs.ChildOf -local __ = jecs.Wildcard -local Name = jecs.Name -local world = jecs.World.new() - -type Id = number & {__T: T} -local Voxel = world:component() :: Id -local Position = world:component() :: Id -local Perception = world:component() :: Id<{ - range: number, - fov: number, - dir: Vector3 -}> -local PrimaryPart = world:component() :: Id - -local local_player = game:GetService("Players").LocalPlayer - -local function distance(a: Vector3, b: Vector3) - return (b - a).Magnitude -end - -local function is_in_fov(a: Vector3, b: Vector3, forward_dir: Vector3, fov_angle: number) - local to_target = b - a - - local forward_xz = Vector3.new(forward_dir.X, 0, forward_dir.Z).Unit - local to_target_xz = Vector3.new(to_target.X, 0, to_target.Z).Unit - - local angle_to_target = math.deg(math.atan2(to_target_xz.Z, to_target_xz.X)) - local forward_angle = math.deg(math.atan2(forward_xz.Z, forward_xz.X)) - - local angle_difference = math.abs(forward_angle - angle_to_target) - - if angle_difference > 180 then - angle_difference = 360 - angle_difference - end - - return angle_difference <= (fov_angle / 2) -end - -local map = {} -local grid = 50 - -local function add_to_voxel(source: number, position: Vector3, - prev_voxel_id: number?) - - local hash = position // grid - local voxel_id = map[hash] - if not voxel_id then - voxel_id = world:entity() - world:add(voxel_id, Voxel) - world:set(voxel_id, Position, hash) - map[hash] = voxel_id - end - if prev_voxel_id ~= nil then - world:remove(source, pair(ChildOf, prev_voxel_id)) - end - world:add(source, pair(ChildOf, voxel_id)) -end - -local function reconcile_client_owned_assembly_to_voxel(dt: number) - for e, part, position in world:query(PrimaryPart, Position) do - local p = part.Position - if p ~= position then - world:set(e, Position, p) - local voxel_id = world:target(e, ChildOf, 0) - if map[p // grid] == voxel_id then - continue - end - - add_to_voxel(e, p, voxel_id) - end - end -end - -local function update_camera_direction(dt: number) - for _, perception in world:query(Perception) do - perception.dir = workspace.CurrentCamera.CFrame.LookVector - end -end - -local function perceive_enemies(dt: number) - local it = world:query(Perception, Position, PrimaryPart) - -- There is only going to be one entity matching the query - local e, self_perception, self_position, self_primary_part = it() - - local voxel_id = map[self_primary_part.Position // grid] - local nearby_entities_query = world:query(Position, pair(ChildOf, voxel_id)) - - for enemy, target_position in nearby_entities_query do - if distance(self_position, target_position) > self_perception.range then - continue - end - - if is_in_fov(self_position, target_position, - self_perception.dir, self_perception.fov) then - - local p = target_position - print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.X}, {p.Y}, {p.Z})`) - end - end -end - -local player = world:entity() -world:set(player, Perception, { - range = 100, - fov = 90, - dir = Vector3.new(1, 0, 0) -}) -world:set(player, Name, "LocalPlayer") -local primary_part = (local_player.Character :: Model).PrimaryPart :: Part -world:set(player, PrimaryPart, primary_part) -world:set(player, Position, Vector3.zero) - -local enemy = world:entity() -world:set(enemy, Name, "Enemy $1") -world:set(enemy, Position, Vector3.new(50, 0, 20)) - - -add_to_voxel(player, primary_part.Position) -add_to_voxel(enemy, world) - -local dt = 1/60 -reconcile_client_owned_assembly_to_voxel(dt) -update_camera_direction(dt) -perceive_enemies(dt) - --- Output: --- LocalPlayer can see target Enemy $1 +local jecs = require("@jecs") +local pair = jecs.pair +local ChildOf = jecs.ChildOf +local __ = jecs.Wildcard +local Name = jecs.Name +local world = jecs.World.new() + +type Id = number & { __T: T } +local Voxel = world:component() :: Id +local Position = world:component() :: Id +local Perception = world:component() :: Id<{ + range: number, + fov: number, + dir: Vector3, +}> +local PrimaryPart = world:component() :: Id + +local local_player = game:GetService("Players").LocalPlayer + +local function distance(a: Vector3, b: Vector3) + return (b - a).Magnitude +end + +local function is_in_fov(a: Vector3, b: Vector3, forward_dir: Vector3, fov_angle: number) + local to_target = b - a + + local forward_xz = Vector3.new(forward_dir.X, 0, forward_dir.Z).Unit + local to_target_xz = Vector3.new(to_target.X, 0, to_target.Z).Unit + + local angle_to_target = math.deg(math.atan2(to_target_xz.Z, to_target_xz.X)) + local forward_angle = math.deg(math.atan2(forward_xz.Z, forward_xz.X)) + + local angle_difference = math.abs(forward_angle - angle_to_target) + + if angle_difference > 180 then + angle_difference = 360 - angle_difference + end + + return angle_difference <= (fov_angle / 2) +end + +local map = {} +local grid = 50 + +local function add_to_voxel(source: number, position: Vector3, prev_voxel_id: number?) + local hash = position // grid + local voxel_id = map[hash] + if not voxel_id then + voxel_id = world:entity() + world:add(voxel_id, Voxel) + world:set(voxel_id, Position, hash) + map[hash] = voxel_id + end + if prev_voxel_id ~= nil then + world:remove(source, pair(ChildOf, prev_voxel_id)) + end + world:add(source, pair(ChildOf, voxel_id)) +end + +local function reconcile_client_owned_assembly_to_voxel(dt: number) + for e, part, position in world:query(PrimaryPart, Position) do + local p = part.Position + if p ~= position then + world:set(e, Position, p) + local voxel_id = world:target(e, ChildOf, 0) + if map[p // grid] == voxel_id then + continue + end + + add_to_voxel(e, p, voxel_id) + end + end +end + +local function update_camera_direction(dt: number) + for _, perception in world:query(Perception) do + perception.dir = workspace.CurrentCamera.CFrame.LookVector + end +end + +local function perceive_enemies(dt: number) + local it = world:query(Perception, Position, PrimaryPart) + -- There is only going to be one entity matching the query + local e, self_perception, self_position, self_primary_part = it() + + local voxel_id = map[self_primary_part.Position // grid] + local nearby_entities_query = world:query(Position, pair(ChildOf, voxel_id)) + + for enemy, target_position in nearby_entities_query do + if distance(self_position, target_position) > self_perception.range then + continue + end + + if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then + local p = target_position + print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.X}, {p.Y}, {p.Z})`) + end + end +end + +local player = world:entity() +world:set(player, Perception, { + range = 100, + fov = 90, + dir = Vector3.new(1, 0, 0), +}) +world:set(player, Name, "LocalPlayer") +local primary_part = (local_player.Character :: Model).PrimaryPart :: Part +world:set(player, PrimaryPart, primary_part) +world:set(player, Position, Vector3.zero) + +local enemy = world:entity() +world:set(enemy, Name, "Enemy $1") +world:set(enemy, Position, Vector3.new(50, 0, 20)) + +add_to_voxel(player, primary_part.Position) +add_to_voxel(enemy, world) + +local dt = 1 / 60 +reconcile_client_owned_assembly_to_voxel(dt) +update_camera_direction(dt) +perceive_enemies(dt) + +-- Output: +-- LocalPlayer can see target Enemy $1 diff --git a/examples/luau/queries/wildcards.luau b/examples/luau/queries/wildcards.luau index 87bef3d4..5d33e3d6 100644 --- a/examples/luau/queries/wildcards.luau +++ b/examples/luau/queries/wildcards.luau @@ -4,12 +4,12 @@ local world = jecs.World.new() local Name = world:component() local function named(ctr, name) - local e = ctr(world) - world:set(e, Name, name) - return e + local e = ctr(world) + world:set(e, Name, name) + return e end local function name(e) - return world:get(e, Name) + return world:get(e, Name) end local Eats = world:component() @@ -27,9 +27,9 @@ local __ = jecs.Wildcard -- Create a query that matches edible components for entity, amount in world:query(pair(Eats, __)) do - -- Iterate the query - local food = world:target(entity, Eats) - print(`{name(entity)} eats {amount} {name(food)}`) + -- Iterate the query + local food = world:target(entity, Eats) + print(`{name(entity)} eats {amount} {name(food)}`) end -- Output: diff --git a/mirror/init.luau b/mirror/init.luau index 6d9c1fee..c2ceac60 100644 --- a/mirror/init.luau +++ b/mirror/init.luau @@ -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, @@ -21,8 +21,8 @@ type Archetype = { }, types: Ty, type: string | number, - entities: {number}, - columns: {Column}, + entities: { number }, + columns: { Column }, records: {}, } @@ -31,12 +31,12 @@ 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, @@ -134,7 +134,7 @@ local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archet local archetypesMap = componentIndex[destinationId] if not archetypesMap then - archetypesMap = {size = 0, sparse = {}} + archetypesMap = { size = 0, sparse = {} } componentIndex[destinationId] = archetypesMap end @@ -143,27 +143,27 @@ local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archet 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) local id = world.nextArchetypeId + 1 world.nextArchetypeId = id local length = #types - local columns = table.create(length) :: {any} + local columns = table.create(length) :: { any } for index in types do columns[index] = {} end local archetype = { - columns = columns; - edges = {}; - entities = {}; - id = id; - records = {}; - type = ty; - types = types; + columns = columns, + edges = {}, + entities = {}, + id = id, + records = {}, + type = ty, + types = types, } world.archetypeIndex[ty] = archetype world.archetypes[id] = archetype @@ -178,17 +178,17 @@ local World = {} World.__index = World function World.new() local self = setmetatable({ - archetypeIndex = {}; - archetypes = {}; - componentIndex = {}; - entityIndex = {}; + archetypeIndex = {}, + archetypes = {}, + componentIndex = {}, + entityIndex = {}, hooks = { - [ON_ADD] = {}; - }; - nextArchetypeId = 0; - nextComponentId = 0; - nextEntityId = 0; - ROOT_ARCHETYPE = (nil :: any) :: Archetype; + [ON_ADD] = {}, + }, + nextArchetypeId = 0, + nextComponentId = 0, + nextEntityId = 0, + ROOT_ARCHETYPE = (nil :: any) :: Archetype, }, World) return self end @@ -197,21 +197,21 @@ local function emit(world, eventDescription) local event = eventDescription.event table.insert(world.hooks[event], { - archetype = eventDescription.archetype; - ids = eventDescription.ids; - offset = eventDescription.offset; - otherArchetype = eventDescription.otherArchetype; + archetype = eventDescription.archetype, + ids = eventDescription.ids, + offset = eventDescription.offset, + otherArchetype = eventDescription.otherArchetype, }) end local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) if #added > 0 then emit(world, { - archetype = archetype; - event = ON_ADD; - ids = added; - offset = row; - otherArchetype = otherArchetype; + archetype = archetype, + event = ON_ADD, + ids = added, + offset = row, + otherArchetype = otherArchetype, }) end end @@ -232,7 +232,7 @@ local function ensureArchetype(world: World, types, prev) return archetypeOf(world, types, prev) end -local function findInsert(types: {i53}, toAdd: i53) +local function findInsert(types: { i53 }, toAdd: i53) for i, id in types do if id == toAdd then return -1 @@ -326,7 +326,7 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown) 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 @@ -401,8 +401,8 @@ local function noop(_self: Query, ...: i53): () -> (number, ...any) end local EmptyQuery = { - __iter = noop; - without = noop; + __iter = noop, + without = noop, } EmptyQuery.__index = EmptyQuery setmetatable(EmptyQuery, EmptyQuery) @@ -418,7 +418,7 @@ function World.query(world: World, ...: i53): Query local compatibleArchetypes = {} local length = 0 - local components = {...} + local components = { ... } local archetypes = world.archetypes local queryLength = #components @@ -456,7 +456,7 @@ function World.query(world: World, ...: i53): Query end length += 1 - compatibleArchetypes[length] = {archetype, indices} + compatibleArchetypes[length] = { archetype, indices } end local lastArchetype, compatibleArchetype = next(compatibleArchetypes) @@ -468,7 +468,7 @@ function World.query(world: World, ...: i53): Query preparedQuery.__index = preparedQuery function preparedQuery:without(...) - local withoutComponents = {...} + local withoutComponents = { ... } for i = #compatibleArchetypes, 1, -1 do local archetype = compatibleArchetypes[i][1] local records = archetype.records @@ -599,7 +599,7 @@ function World.delete(world: World, entityId: i53) end function World.observer(world: World, ...) - local componentIds = {...} + local componentIds = { ... } local idsCount = #componentIds local hooks = world.hooks @@ -647,13 +647,13 @@ function World.observer(world: World, ...) 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/rgb.luau b/rgb.luau index 11880c7f..a391f8eb 100644 --- a/rgb.luau +++ b/rgb.luau @@ -16,7 +16,7 @@ return { end, yellow = function(s: any) - return `\27[33;1m{s}\27[0m` + return `\27[33;1m{s}\27[0m` end, red_highlight = function(s: any) @@ -30,4 +30,4 @@ return { gray = function(s: any) return `\27[30;1m{s}\27[0m` end, -} \ No newline at end of file +} diff --git a/rokit.toml b/rokit.toml index ea906bf5..9ff23f26 100644 --- a/rokit.toml +++ b/rokit.toml @@ -1,6 +1,6 @@ [tools] wally = "upliftgames/wally@0.3.2" -rojo = "rojo-rbx/rojo@7.4.1" -stylua = "johnnymorganz/stylua@0.19.1" -selene = "kampfkarren/selene@0.26.1" +rojo = "rojo-rbx/rojo@7.4.4" +stylua = "johnnymorganz/stylua@0.20.0" +selene = "kampfkarren/selene@0.27.1" Blink = "1Axen/Blink@0.14.1" diff --git a/src/init.luau b/src/init.luau index 9ee392c9..86acb51f 100644 --- a/src/init.luau +++ b/src/init.luau @@ -972,8 +972,6 @@ local function world_cleanup(world) world.archetypeIndex = new_archetype_map end - - local world_delete: (world: World, entity: i53, destruct: boolean?) -> () do function world_delete(world: World, entity: i53, destruct: boolean?) diff --git a/stylua.toml b/stylua.toml index 83e58074..a29ce940 100644 --- a/stylua.toml +++ b/stylua.toml @@ -1,5 +1,10 @@ column_width = 120 +line_endings = "Unix" +indent_type = "Tabs" +indent_width = 4 quote_style = "ForceDouble" +call_parentheses = "Always" +collapse_simple_statement = "Never" [sort_requires] enabled = true diff --git a/test/btree.luau b/test/btree.luau index e6f1957e..f4f6fca4 100644 --- a/test/btree.luau +++ b/test/btree.luau @@ -1,152 +1,152 @@ -- original author @centauri local bt do - - local FAILURE = 0 - local SUCCESS = 1 - local RUNNING = 2 - - local function SEQUENCE(nodes) - return function(...) - for _, node in nodes do - local status = node(...) - if status == FAILURE or status == RUNNING then - return status - end - end - return SUCCESS - end - end - local function FALLBACK(nodes) - return function(...) - for _, node in nodes do - local status = node(...) - if status == SUCCESS or status == RUNNING then - return status - end - end - return FAILURE - end - end - bt = { - SEQUENCE = SEQUENCE, - FALLBACK = FALLBACK, - RUNNING = RUNNING, - SUCCESS = SUCCESS, - FAILURE = FAILURE, - } + local FAILURE = 0 + local SUCCESS = 1 + local RUNNING = 2 + + local function SEQUENCE(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == FAILURE or status == RUNNING then + return status + end + end + return SUCCESS + end + end + local function FALLBACK(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == SUCCESS or status == RUNNING then + return status + end + end + return FAILURE + end + end + bt = { + SEQUENCE = SEQUENCE, + FALLBACK = FALLBACK, + RUNNING = RUNNING, + SUCCESS = SUCCESS, + FAILURE = FAILURE, + } end local SEQUENCE, FALLBACK = bt.SEQUENCE, bt.FALLBACK local RUNNING, SUCCESS, FAILURE = bt.FAILURE, bt.SUCCESS, bt.FAILURE -local btree = FALLBACK { - SEQUENCE { - function() - return 1 - end, - - function() - return 0 - end - }, - SEQUENCE { - function() - print(3) - local start = os.clock() - local now = os.clock() - while os.clock() - now < 4 do - print("yielding") - coroutine.yield() - end - return 0 - end - }, - function() - return 1 - end -} +local btree = FALLBACK({ + SEQUENCE({ + function() + return 1 + end, + + function() + return 0 + end, + }), + SEQUENCE({ + function() + print(3) + local start = os.clock() + local now = os.clock() + while os.clock() - now < 4 do + print("yielding") + coroutine.yield() + end + return 0 + end, + }), + function() + return 1 + end, +}) function wait(seconds) - local start = os.clock() - while os.clock() - start < seconds do end - return os.clock() - start + local start = os.clock() + while os.clock() - start < seconds do + end + return os.clock() - start end local function panic(str) - -- We don't want to interrupt the loop when we error - coroutine.resume(coroutine.create(function() error(str) end)) + -- We don't want to interrupt the loop when we error + coroutine.resume(coroutine.create(function() + error(str) + end)) end local jecs = require("@jecs") local world = jecs.World.new() local function Scheduler(world, ...) - local systems = { ... } - local systemsNames = {} - local N = #systems - local system - local dt - - for i, module in systems do - local sys = if typeof(module) == "function" then module else require(module) - systems[i] = sys - local file, line = debug.info(2, "sl") - systemsNames[sys] = `{file}->::{line}::->{debug.info(sys, "n")}` - end - - local function run() - local name = systemsNames[system] - - --debug.profilebegin(name) - --debug.setmemorycategory(name) - system(world, dt) - --debug.profileend() - end - - local function loop(sinceLastFrame) - --debug.profilebegin("loop()") - local start = os.clock() - for i = N, 1, -1 do - system = systems[i] - - dt = sinceLastFrame - - local didNotYield, why = xpcall(function() - for _ in run do end - end, debug.traceback) - - if didNotYield then + local systems = { ... } + local systemsNames = {} + local N = #systems + local system + local dt + + for i, module in systems do + local sys = if typeof(module) == "function" then module else require(module) + systems[i] = sys + local file, line = debug.info(2, "sl") + systemsNames[sys] = `{file}->::{line}::->{debug.info(sys, "n")}` + end + + local function run() + local name = systemsNames[system] + + --debug.profilebegin(name) + --debug.setmemorycategory(name) + system(world, dt) + --debug.profileend() + end + + local function loop(sinceLastFrame) + --debug.profilebegin("loop()") + local start = os.clock() + for i = N, 1, -1 do + system = systems[i] + + dt = sinceLastFrame + + local didNotYield, why = xpcall(function() + for _ in run do + end + end, debug.traceback) + + if didNotYield then continue end if string.find(why, "thread is not yieldable") then N -= 1 local name = table.remove(systems, i) - panic("Not allowed to yield in the systems." - .. "\n" - .. `System: {name} has been ejected` - ) + panic("Not allowed to yield in the systems." .. "\n" .. `System: {name} has been ejected`) else - panic(why) + panic(why) end - end + end - --debug.profileend() - --debug.resetmemorycategory() - return os.clock() - start - end + --debug.profileend() + --debug.resetmemorycategory() + return os.clock() - start + end - return loop + return loop end local co = coroutine.create(btree) local function ai(world, dt) - coroutine.resume(co) + coroutine.resume(co) end local loop = Scheduler(world, ai) while wait(0.2) do - print("frame time: ", loop(0.2)) + print("frame time: ", loop(0.2)) end diff --git a/test/hooks.luau b/test/hooks.luau index bebe4a10..7e91986b 100644 --- a/test/hooks.luau +++ b/test/hooks.luau @@ -1,47 +1,47 @@ -local jecs = require("@jecs") - -local function create_cache(hook) - local columns = setmetatable({}, { - __index = function(self, component) - local column = {} - self[component] = column - return column - end - }) - - return function(world, component, fn) - local column = columns[component] - table.insert(column, fn) - world:set(component, hook, function(entity, value) - for _, callback in column do - callback(entity, value) - end - end) - end -end - -local hooks = { - OnSet = create_cache(jecs.OnSet), - OnAdd = create_cache(jecs.OnAdd), - OnRemove = create_cache(jecs.OnRemove) -} - -local world = jecs.World.new() -local Position = world:component() -local order = "" -hooks.OnSet(world, Position, function(entity, value) - print("$1", entity, `({value.x}, {value.y}, {value.z})`) - order ..= "$1" -end) -hooks.OnSet(world, Position, function(entity, value) - print("$2", entity, `\{{value.x}, {value.y}, {value.z}}`) - order ..= "-$2" -end) - -world:set(world:entity(), Position, {x=1,y=0,z=1}) - --- Output: --- $1 270 (1, 0, 1) --- $2 270 {1, 0, 1} - -assert(order == "$1".."-".."$2") +local jecs = require("@jecs") + +local function create_cache(hook) + local columns = setmetatable({}, { + __index = function(self, component) + local column = {} + self[component] = column + return column + end, + }) + + return function(world, component, fn) + local column = columns[component] + table.insert(column, fn) + world:set(component, hook, function(entity, value) + for _, callback in column do + callback(entity, value) + end + end) + end +end + +local hooks = { + OnSet = create_cache(jecs.OnSet), + OnAdd = create_cache(jecs.OnAdd), + OnRemove = create_cache(jecs.OnRemove), +} + +local world = jecs.World.new() +local Position = world:component() +local order = "" +hooks.OnSet(world, Position, function(entity, value) + print("$1", entity, `({value.x}, {value.y}, {value.z})`) + order ..= "$1" +end) +hooks.OnSet(world, Position, function(entity, value) + print("$2", entity, `\{{value.x}, {value.y}, {value.z}}`) + order ..= "-$2" +end) + +world:set(world:entity(), Position, { x = 1, y = 0, z = 1 }) + +-- Output: +-- $1 270 (1, 0, 1) +-- $2 270 {1, 0, 1} + +assert(order == "$1" .. "-" .. "$2") diff --git a/test/leaky.luau b/test/leaky.luau index ebbb1a34..5d3da619 100644 --- a/test/leaky.luau +++ b/test/leaky.luau @@ -1,56 +1,57 @@ - local function calculateAverage(times) - local sum = 0 - for _, time in ipairs(times) do - sum = sum + time - end - return sum / #times + local sum = 0 + for _, time in ipairs(times) do + sum = sum + time + end + return sum / #times end -- Main logic to time the test function local CASES = { - jecs = function(world, ...) - for i = 1, 100 do - local q = world:query(...) - for _ in q do end - end - end, - mirror = function(world, ...) - for i = 1, 100 do - local q = world:query(...) - for _ in q do end - end - end + jecs = function(world, ...) + for i = 1, 100 do + local q = world:query(...) + for _ in q do + end + end + end, + mirror = function(world, ...) + for i = 1, 100 do + local q = world:query(...) + for _ in q do + end + end + end, } for name, fn in CASES do - local times = {} - local allocations = {} - local ecs = require("@"..name) - local world = ecs.World.new() - local A, B, C = world:component(), world:component(), world:component() - - for i = 1, 5 do - local e = world:entity() - world:add(e, A) - world:add(e, B) - world:add(e, C) - end - - collectgarbage("collect") - local count = collectgarbage("count") - - for i = 1, 50000 do - local startTime = os.clock() - fn(world, A, B, C) - local allocated = collectgarbage("count") - collectgarbage("collect") - local endTime = os.clock() - table.insert(times, endTime - startTime) - table.insert(allocations, allocated) - end - - print(name, "gc cycle time", calculateAverage(times)) - print(name, "memory allocated", calculateAverage(allocations)) + local times = {} + local allocations = {} + local ecs = require("@" .. name) + local world = ecs.World.new() + local A, B, C = world:component(), world:component(), world:component() + + for i = 1, 5 do + local e = world:entity() + world:add(e, A) + world:add(e, B) + world:add(e, C) + end + + collectgarbage("collect") + local count = collectgarbage("count") + + for i = 1, 50000 do + local startTime = os.clock() + fn(world, A, B, C) + local allocated = collectgarbage("count") + collectgarbage("collect") + local endTime = os.clock() + table.insert(times, endTime - startTime) + table.insert(allocations, allocated) + end + + print(name, "gc cycle time", calculateAverage(times)) + print(name, "memory allocated", calculateAverage(allocations)) end diff --git a/test/memory.luau b/test/memory.luau index e3d964f5..1d1f2699 100644 --- a/test/memory.luau +++ b/test/memory.luau @@ -1,14 +1,14 @@ -local testkit = require("@testkit") -local jecs = require("@jecs") - -local world = jecs.World.new() - -local A = world:component() -local B = world:component() - -local e = world:entity() -world:add(e, A) -world:add(e, B) -local archetype_id = world.archetypeIndex["1_2"].id -world:delete(e) -testkit.print(world) +local jecs = require("@jecs") +local testkit = require("@testkit") + +local world = jecs.World.new() + +local A = world:component() +local B = world:component() + +local e = world:entity() +world:add(e, A) +world:add(e, B) +local archetype_id = world.archetypeIndex["1_2"].id +world:delete(e) +testkit.print(world) diff --git a/test/tests.luau b/test/tests.luau index 4b6fef14..b9375efe 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,5 +1,4 @@ - -local jecs: typeof(require("../jecs/src")) = require("@jecs"); +local jecs: typeof(require("../jecs/src")) = require("@jecs") local testkit = require("@testkit") local BENCH, START = testkit.benchmark() @@ -24,146 +23,148 @@ local function CHECK_NO_ERR(s: string, fn: (T...) -> (), ...: T...) CHECK(msg == s, 2) end end -local N = 2^8 +local N = 2 ^ 8 type World = jecs.WorldShim local function debug_world_inspect(world) - local function record(e) - return world.entityIndex.sparse[e] - end - local function tbl(e) - return record(e).archetype - end - local function archetype(e) - return tbl(e).type - end - local function records(e) - return tbl(e).records - end - local function columns(e) - return tbl(e).columns - end - local function row(e) - return record(e).row - end - - -- Important to order them in the order of their columns - local function tuple(e, ...) - for i, column in columns(e) do - if select(i, ...) ~= column[row(e)] then - return false - end - end - return true - end - - return { - record = record, - tbl = tbl, - archetype = archetype, - records = records, - row = row, - tuple = tuple, - } + local function record(e) + return world.entityIndex.sparse[e] + end + local function tbl(e) + return record(e).archetype + end + local function archetype(e) + return tbl(e).type + end + local function records(e) + return tbl(e).records + end + local function columns(e) + return tbl(e).columns + end + local function row(e) + return record(e).row + end + + -- Important to order them in the order of their columns + local function tuple(e, ...) + for i, column in columns(e) do + if select(i, ...) ~= column[row(e)] then + return false + end + end + return true + end + + return { + record = record, + tbl = tbl, + archetype = archetype, + records = records, + row = row, + tuple = tuple, + } end local function name(world, e) - return world:get(e, jecs.Name) + return world:get(e, jecs.Name) end TEST("archetype", function() - local archetype_append_to_records = jecs.archetype_append_to_records - local id_record_ensure = jecs.id_record_ensure - local archetype_create = jecs.archetype_create - local archetype_ensure = jecs.archetype_ensure - local find_insert = jecs.find_insert - local find_archetype_with = jecs.find_archetype_with - local find_archetype_without = jecs.find_archetype_without - local archetype_init_edge = jecs.archetype_init_edge - local archetype_ensure_edge = jecs.archetype_ensure_edge - local init_edge_for_add = jecs.init_edge_for_add - local init_edge_for_remove = jecs.init_edge_for_remove - local create_edge_for_add = jecs.create_edge_for_add - local create_edge_for_remove = jecs.create_edge_for_remove - local archetype_traverse_add = jecs.archetype_traverse_add - local archetype_traverse_remove = jecs.archetype_traverse_remove - - local world = world_new() - local root = world.ROOT_ARCHETYPE - local c1 = world:component() - local c2 = world:component() - local c3 = world:component() - - local a1 = archetype_traverse_add(world, c1, nil) - local a2 = archetype_traverse_remove(world, c1, a1) - CHECK(root.node.add[c1].to == a1) - CHECK(root == a2) + local archetype_append_to_records = jecs.archetype_append_to_records + local id_record_ensure = jecs.id_record_ensure + local archetype_create = jecs.archetype_create + local archetype_ensure = jecs.archetype_ensure + local find_insert = jecs.find_insert + local find_archetype_with = jecs.find_archetype_with + local find_archetype_without = jecs.find_archetype_without + local archetype_init_edge = jecs.archetype_init_edge + local archetype_ensure_edge = jecs.archetype_ensure_edge + local init_edge_for_add = jecs.init_edge_for_add + local init_edge_for_remove = jecs.init_edge_for_remove + local create_edge_for_add = jecs.create_edge_for_add + local create_edge_for_remove = jecs.create_edge_for_remove + local archetype_traverse_add = jecs.archetype_traverse_add + local archetype_traverse_remove = jecs.archetype_traverse_remove + + local world = world_new() + local root = world.ROOT_ARCHETYPE + local c1 = world:component() + local c2 = world:component() + local c3 = world:component() + + local a1 = archetype_traverse_add(world, c1, nil) + local a2 = archetype_traverse_remove(world, c1, a1) + CHECK(root.node.add[c1].to == a1) + CHECK(root == a2) end) TEST("world:cleanup()", function() - local world = world_new() - local A = world:component() - local B = world:component() - local C = world:component() - - local e1 = world:entity() - local e2 = world:entity() - local e3 = world:entity() - - world:set(e1, A, true) - - world:set(e2, A, true) - world:set(e2, B, true) - - world:set(e3, A, true) - world:set(e3, B, true) - world:set(e3, C, true) - - local archetypeIndex = world.archetypeIndex - - CHECK(#archetypeIndex["1"].entities == 1) - CHECK(#archetypeIndex["1_2"].entities == 1) - CHECK(#archetypeIndex["1_2_3"].entities == 1) - - world:delete(e1) - world:delete(e2) - world:delete(e3) - - world:cleanup() - - archetypeIndex = world.archetypeIndex - - CHECK(archetypeIndex["1"] == nil) - CHECK(archetypeIndex["1_2"] == nil) - CHECK(archetypeIndex["1_2_3"] == nil) - - local e4 = world:entity() - world:set(e4, A, true) - CHECK(#archetypeIndex["1"].entities == 1) - CHECK(archetypeIndex["1_2"] == nil) - CHECK(archetypeIndex["1_2_3"] == nil) - world:set(e4, B, true) - CHECK(#archetypeIndex["1"].entities == 0) - CHECK(#archetypeIndex["1_2"].entities == 1) - CHECK(archetypeIndex["1_2_3"] == nil) - world:set(e4, C, true) - CHECK(#archetypeIndex["1"].entities == 0) - CHECK(#archetypeIndex["1_2"].entities == 0) - CHECK(#archetypeIndex["1_2_3"].entities == 1) + local world = world_new() + local A = world:component() + local B = world:component() + local C = world:component() + + local e1 = world:entity() + local e2 = world:entity() + local e3 = world:entity() + + world:set(e1, A, true) + + world:set(e2, A, true) + world:set(e2, B, true) + + world:set(e3, A, true) + world:set(e3, B, true) + world:set(e3, C, true) + + local archetypeIndex = world.archetypeIndex + + CHECK(#archetypeIndex["1"].entities == 1) + CHECK(#archetypeIndex["1_2"].entities == 1) + CHECK(#archetypeIndex["1_2_3"].entities == 1) + + world:delete(e1) + world:delete(e2) + world:delete(e3) + + world:cleanup() + + archetypeIndex = world.archetypeIndex + + CHECK(archetypeIndex["1"] == nil) + CHECK(archetypeIndex["1_2"] == nil) + CHECK(archetypeIndex["1_2_3"] == nil) + + local e4 = world:entity() + world:set(e4, A, true) + CHECK(#archetypeIndex["1"].entities == 1) + CHECK(archetypeIndex["1_2"] == nil) + CHECK(archetypeIndex["1_2_3"] == nil) + world:set(e4, B, true) + CHECK(#archetypeIndex["1"].entities == 0) + CHECK(#archetypeIndex["1_2"].entities == 1) + CHECK(archetypeIndex["1_2_3"] == nil) + world:set(e4, C, true) + CHECK(#archetypeIndex["1"].entities == 0) + CHECK(#archetypeIndex["1_2"].entities == 0) + CHECK(#archetypeIndex["1_2_3"].entities == 1) end) TEST("world:entity()", function() - do CASE "unique IDs" - local world = jecs.World.new() - local set = {} - for i = 1, N do - local e = world:entity() - CHECK(not set[e]) - set[e] = true - end - end - do CASE "generations" + do + CASE("unique IDs") + local world = jecs.World.new() + local set = {} + for i = 1, N do + local e = world:entity() + CHECK(not set[e]) + set[e] = true + end + end + do + CASE("generations") local world = jecs.World.new() local e = world:entity() CHECK(ECS_ID(e) == 1 + jecs.Rest) @@ -173,7 +174,8 @@ TEST("world:entity()", function() CHECK(ECS_GENERATION(e) == 1) -- 1 end - do CASE "pairs" + do + CASE("pairs") local world = jecs.World.new() local _e = world:entity() local e2 = world:entity() @@ -191,82 +193,86 @@ TEST("world:entity()", function() end) TEST("world:set()", function() - do CASE "archetype move" - do - local world = jecs.World.new() - - local d = debug_world_inspect(world) - - local _1 = world:component() - local _2 = world:component() - local e = world:entity() - -- An entity starts without an archetype or row - -- should therefore not need to copy over data - CHECK(d.tbl(e) == nil) - CHECK(d.row(e) == nil) - - local archetypes = #world.archetypes - -- This should create a new archetype since it is the first - -- entity to have moved there - world:set(e, _1, 1) - local oldRow = d.row(e) - local oldArchetype = d.archetype(e) - CHECK(#world.archetypes == archetypes + 1) - CHECK(oldArchetype == "1") - CHECK(d.tbl(e)) - CHECK(oldRow == 1) - - world:set(e, _2, 2) - CHECK(d.archetype(e) == "1_2") - -- Should have tuple of fields to the next archetype and set the component data - CHECK(d.tuple(e, 1, 2)) - -- Should have moved the data from the old archetype - CHECK(world.archetypeIndex[oldArchetype].columns[_1][oldRow] == nil) - end - end - - do CASE "arbitrary order" - local world = jecs.World.new() - - local Health = world:entity() - local Poison = world:component() - - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) - - CHECK(world:get(id, Poison) == 5) - end - - do CASE "pairs" - local world = jecs.World.new() - - local C1 = world:component() - local C2 = world:component() - local T1 = world:entity() - local T2 = world:entity() - - local e = world:entity() - - world:set(e, pair(C1, C2), true) - world:set(e, pair(C1, T1), true) - world:set(e, pair(T1, C1), true) - world:set(e, pair(T1, T2), true) - - CHECK(world:get(e, pair(C1, C2))) - CHECK(world:get(e, pair(C1, T1))) - CHECK(not world:get(e, pair(T1, C1))) - CHECK(not world:get(e, pair(T1, T2))) - - local e2 = world:entity() - - world:set(e2, pair(jecs.ChildOf, e), true) - CHECK(not world:get(e2, pair(jecs.ChildOf, e))) - end + do + CASE("archetype move") + do + local world = jecs.World.new() + + local d = debug_world_inspect(world) + + local _1 = world:component() + local _2 = world:component() + local e = world:entity() + -- An entity starts without an archetype or row + -- should therefore not need to copy over data + CHECK(d.tbl(e) == nil) + CHECK(d.row(e) == nil) + + local archetypes = #world.archetypes + -- This should create a new archetype since it is the first + -- entity to have moved there + world:set(e, _1, 1) + local oldRow = d.row(e) + local oldArchetype = d.archetype(e) + CHECK(#world.archetypes == archetypes + 1) + CHECK(oldArchetype == "1") + CHECK(d.tbl(e)) + CHECK(oldRow == 1) + + world:set(e, _2, 2) + CHECK(d.archetype(e) == "1_2") + -- Should have tuple of fields to the next archetype and set the component data + CHECK(d.tuple(e, 1, 2)) + -- Should have moved the data from the old archetype + CHECK(world.archetypeIndex[oldArchetype].columns[_1][oldRow] == nil) + end + end + + do + CASE("arbitrary order") + local world = jecs.World.new() + + local Health = world:entity() + local Poison = world:component() + + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) + + CHECK(world:get(id, Poison) == 5) + end + + do + CASE("pairs") + local world = jecs.World.new() + + local C1 = world:component() + local C2 = world:component() + local T1 = world:entity() + local T2 = world:entity() + + local e = world:entity() + + world:set(e, pair(C1, C2), true) + world:set(e, pair(C1, T1), true) + world:set(e, pair(T1, C1), true) + world:set(e, pair(T1, T2), true) + + CHECK(world:get(e, pair(C1, C2))) + CHECK(world:get(e, pair(C1, T1))) + CHECK(not world:get(e, pair(T1, C1))) + CHECK(not world:get(e, pair(T1, T2))) + + local e2 = world:entity() + + world:set(e2, pair(jecs.ChildOf, e), true) + CHECK(not world:get(e2, pair(jecs.ChildOf, e))) + end end) TEST("world:remove()", function() - do CASE "should allow remove a component that doesn't exist on entity" + do + CASE("should allow remove a component that doesn't exist on entity") local world = jecs.World.new() local Health = world:component() @@ -274,7 +280,7 @@ TEST("world:remove()", function() local id = world:entity() do - world:remove(id, Poison) + world:remove(id, Poison) CHECK(true) -- Didn't error end @@ -287,222 +293,231 @@ TEST("world:remove()", function() end) TEST("world:add()", function() + do + CASE("idempotent") + local world = jecs.World.new() + local d = debug_world_inspect(world) + local _1, _2 = world:component(), world:component() + + local e = world:entity() + world:add(e, _1) + world:add(e, _2) + world:add(e, _2) -- should have 0 effects + CHECK(d.archetype(e) == "1_2") + end + + do + CASE("archetype move") + do + local world = jecs.World.new() + + local d = debug_world_inspect(world) + + local _1 = world:component() + local e = world:entity() + -- An entity starts without an archetype or row + -- should therefore not need to copy over data + CHECK(d.tbl(e) == nil) + CHECK(d.row(e) == nil) - do CASE "idempotent" - local world = jecs.World.new() - local d = debug_world_inspect(world) - local _1, _2 = world:component(), world:component() - - local e = world:entity() - world:add(e, _1) - world:add(e, _2) - world:add(e, _2) -- should have 0 effects - CHECK(d.archetype(e) == "1_2") - end - - do CASE "archetype move" - do - local world = jecs.World.new() - - local d = debug_world_inspect(world) - - local _1 = world:component() - local e = world:entity() - -- An entity starts without an archetype or row - -- should therefore not need to copy over data - CHECK(d.tbl(e) == nil) - CHECK(d.row(e) == nil) - - local archetypes = #world.archetypes - -- This should create a new archetype - world:add(e, _1) - CHECK(#world.archetypes == archetypes + 1) - - CHECK(d.archetype(e) == "1") - CHECK(d.tbl(e)) - end - end + local archetypes = #world.archetypes + -- This should create a new archetype + world:add(e, _1) + CHECK(#world.archetypes == archetypes + 1) + + CHECK(d.archetype(e) == "1") + CHECK(d.tbl(e)) + end + end end) TEST("world:query()", function() - do CASE "multiple iter" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - local e = world:entity() - world:add(e, A, "a") - world:add(e, B) - local q = world:query(A, B) - local counter = 0 - for x in q:iter() do - counter += 1 - end - for x in q:iter() do - counter += 1 - end - CHECK(counter == 2) - end - do CASE "tag" - local world = jecs.World.new() - local A = world:entity() - local e = world:entity() - world:set(e, A, "test") - for id, a in world:query(A) do - CHECK(a == nil) - end - end - do CASE "pairs" - local world = jecs.World.new() - - local C1 = world:component() - local C2 = world:component() - local T1 = world:entity() - local T2 = world:entity() - - local e = world:entity() - - world:set(e, pair(C1, C2), true) - world:set(e, pair(C1, T1), true) - world:set(e, pair(T1, C1), true) - world:set(e, pair(T1, T2), true) - - for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) do - CHECK(a == true) - CHECK(b == true) - CHECK(c == nil) - CHECK(d == nil) - end - end - do CASE "query single component" - do - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local entities = {} - for i = 1, N do - local id = world:entity() - - world:set(id, A, true) - if i > 5 then - world:set(id, B, true) - end - entities[i] = id - end - - for id in world:query(A) do - table.remove(entities, CHECK(table.find(entities, id))) - end - - CHECK(#entities == 0) - end - - do - local world = jecs.World.new() :: World - local A = world:component() - local B = world:component() - 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) - - -- Should drain the iterator - local q = world:query(A) - - local i = 0 - local j = 0 - for _ in q do - i+=1 - end - for _ in q do - j+=1 - end - CHECK(i == 2) - CHECK(j == 0) - end - end - - do CASE "query missing component" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - local C = world:component() - - local e1 = world:entity() - local e2 = world:entity() - - world:set(e1, A, "abc") - world:set(e2, A, "def") - world:set(e1, B, 123) - world:set(e2, B, 457) - - local counter = 0 - for _ in world:query(B, C) do - counter += 1 - end - CHECK(counter == 0) - end - - do CASE "query more than 8 components" - local world = jecs.World.new() - local components = {} - - for i = 1, 9 do - local id = world:component() - components[i] = id - end - local e = world:entity() - for i, id in components do - world:set(e, id, 13^i) - end - - for entity, a, b, c, d, e, f, g, h, i in world:query(unpack(components)) do - CHECK(a == 13^1) - CHECK(b == 13^2) - CHECK(c == 13^3) - CHECK(d == 13^4) - CHECK(e == 13^5) - CHECK(f == 13^6) - CHECK(g == 13^7) - CHECK(h == 13^8) - CHECK(i == 13^9) - end - end - - do CASE "should be able to get next results" - local world = jecs.World.new() :: World - world:component() - local A = world:component() - local B = world:component() - 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 it = world:query(A):iter() - - local e, data = it() - while e do - if e == eA then - CHECK(data) - elseif e == eAB then - CHECK(data) - else - CHECK(false) - end - - e, data = it() - end - CHECK(true) - end - - do CASE("should query all matching entities when irrelevant component is removed") + do + CASE("multiple iter") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local e = world:entity() + world:add(e, A, "a") + world:add(e, B) + local q = world:query(A, B) + local counter = 0 + for x in q:iter() do + counter += 1 + end + for x in q:iter() do + counter += 1 + end + CHECK(counter == 2) + end + do + CASE("tag") + local world = jecs.World.new() + local A = world:entity() + local e = world:entity() + world:set(e, A, "test") + for id, a in world:query(A) do + CHECK(a == nil) + end + end + do + CASE("pairs") + local world = jecs.World.new() + + local C1 = world:component() + local C2 = world:component() + local T1 = world:entity() + local T2 = world:entity() + + local e = world:entity() + + world:set(e, pair(C1, C2), true) + world:set(e, pair(C1, T1), true) + world:set(e, pair(T1, C1), true) + world:set(e, pair(T1, T2), true) + + for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) do + CHECK(a == true) + CHECK(b == true) + CHECK(c == nil) + CHECK(d == nil) + end + end + do + CASE("query single component") + do + local world = jecs.World.new() + local A = world:component() + local B = world:component() + + local entities = {} + for i = 1, N do + local id = world:entity() + + world:set(id, A, true) + if i > 5 then + world:set(id, B, true) + end + entities[i] = id + end + + for id in world:query(A) do + table.remove(entities, CHECK(table.find(entities, id))) + end + + CHECK(#entities == 0) + end + + do + local world = jecs.World.new() :: World + local A = world:component() + local B = world:component() + 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) + + -- Should drain the iterator + local q = world:query(A) + + local i = 0 + local j = 0 + for _ in q do + i += 1 + end + for _ in q do + j += 1 + end + CHECK(i == 2) + CHECK(j == 0) + end + end + + do + CASE("query missing component") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() + + local e1 = world:entity() + local e2 = world:entity() + + world:set(e1, A, "abc") + world:set(e2, A, "def") + world:set(e1, B, 123) + world:set(e2, B, 457) + + local counter = 0 + for _ in world:query(B, C) do + counter += 1 + end + CHECK(counter == 0) + end + + do + CASE("query more than 8 components") + local world = jecs.World.new() + local components = {} + + for i = 1, 9 do + local id = world:component() + components[i] = id + end + local e = world:entity() + for i, id in components do + world:set(e, id, 13 ^ i) + end + + for entity, a, b, c, d, e, f, g, h, i in world:query(unpack(components)) do + CHECK(a == 13 ^ 1) + CHECK(b == 13 ^ 2) + CHECK(c == 13 ^ 3) + CHECK(d == 13 ^ 4) + CHECK(e == 13 ^ 5) + CHECK(f == 13 ^ 6) + CHECK(g == 13 ^ 7) + CHECK(h == 13 ^ 8) + CHECK(i == 13 ^ 9) + end + end + + do + CASE("should be able to get next results") + local world = jecs.World.new() :: World + world:component() + local A = world:component() + local B = world:component() + 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 it = world:query(A):iter() + + local e, data = it() + while e do + if e == eA then + CHECK(data) + elseif e == eAB then + CHECK(data) + else + CHECK(false) + end + + e, data = it() + end + CHECK(true) + end + + do + CASE("should query all matching entities when irrelevant component is removed") local world = jecs.World.new() local A = world:component() local B = world:component() @@ -531,7 +546,8 @@ TEST("world:query()", function() CHECK(added == N) end - do CASE("should query all entities without B") + do + CASE("should query all entities without B") local world = jecs.World.new() local A = world:component() local B = world:component() @@ -555,7 +571,8 @@ TEST("world:query()", function() CHECK(#entities == 0) end - do CASE("should allow querying for relations") + do + CASE("should allow querying for relations") local world = jecs.World.new() local Eats = world:component() local Apples = world:component() @@ -568,7 +585,8 @@ TEST("world:query()", function() end end - do CASE("should allow wildcards in queries") + do + CASE("should allow wildcards in queries") local world = jecs.World.new() local Eats = world:component() local Apples = world:entity() @@ -587,7 +605,8 @@ TEST("world:query()", function() end end - do CASE("should match against multiple pairs") + do + CASE("should match against multiple pairs") local world = jecs.World.new() local Eats = world:component() local Apples = world:entity() @@ -619,8 +638,9 @@ TEST("world:query()", function() CHECK(count == 1) end - do CASE "should only relate alive entities" - SKIP() + do + CASE("should only relate alive entities") + SKIP() local world = jecs.World.new() local Eats = world:entity() local Apples = world:entity() @@ -646,7 +666,8 @@ TEST("world:query()", function() CHECK(world:get(bob, pair(Eats, Apples)) == nil) end - do CASE("should error when setting invalid pair") + do + CASE("should error when setting invalid pair") local world = jecs.World.new() local Eats = world:component() local Apples = world:component() @@ -657,7 +678,8 @@ TEST("world:query()", function() world:set(bob, pair(Eats, Apples), "bob eats apples") end - do CASE("should find target for ChildOf") + do + CASE("should find target for ChildOf") local world = jecs.World.new() local ChildOf = jecs.ChildOf @@ -680,8 +702,9 @@ TEST("world:query()", function() CHECK(count == 2) end - do CASE "despawning while iterating" - local world = jecs.World.new() + do + CASE("despawning while iterating") + local world = jecs.World.new() local A = world:component() local B = world:component() @@ -693,16 +716,18 @@ TEST("world:query()", function() local count = 0 for id in world:query(A) do - world:clear(id) + world:clear(id) count += 1 end CHECK(count == 2) end - do CASE "iterator invalidation" - do CASE "adding" + do + CASE("iterator invalidation") + do + CASE("adding") SKIP() - local world = jecs.World.new() + local world = jecs.World.new() local A = world:component() local B = world:component() @@ -722,8 +747,9 @@ TEST("world:query()", function() CHECK(count == 2) end - do CASE "spawning" - local world = jecs.World.new() + do + CASE("spawning") + local world = jecs.World.new() local A = world:component() local B = world:component() @@ -743,74 +769,78 @@ TEST("world:query()", function() end end - do CASE "should not find any entities" - local world = jecs.World.new() + do + CASE("should not find any entities") + local world = jecs.World.new() - local Hello = world:component() - local Bob = world:component() + local Hello = world:component() + local Bob = world:component() - local helloBob = world:entity() - world:add(helloBob, pair(Hello, Bob)) - world:add(helloBob, Bob) + local helloBob = world:entity() + world:add(helloBob, pair(Hello, Bob)) + world:add(helloBob, Bob) - local withoutCount = 0 - for _ in world:query(pair(Hello, Bob)):without(Bob) do - withoutCount += 1 - end + local withoutCount = 0 + for _ in world:query(pair(Hello, Bob)):without(Bob) do + withoutCount += 1 + end - CHECK(withoutCount == 0) - end + CHECK(withoutCount == 0) + end - do CASE "Empty Query" - do - local world = jecs.World.new() - local A = world:component() - local B = world:component() + do + CASE("Empty Query") + do + local world = jecs.World.new() + local A = world:component() + local B = world:component() - local e1 = world:entity() - world:add(e1, A) + local e1 = world:entity() + world:add(e1, A) - local query = world:query(B) - CHECK(query:without() == query) - CHECK(query:with() == query) - -- They always return the same EMPTY_LIST - CHECK(query:archetypes() == world:query(B):archetypes()) - end + local query = world:query(B) + CHECK(query:without() == query) + CHECK(query:with() == query) + -- They always return the same EMPTY_LIST + CHECK(query:archetypes() == world:query(B):archetypes()) + end - do - local world = jecs.World.new() - local A = world:component() - local B = world:component() + do + local world = jecs.World.new() + local A = world:component() + local B = world:component() - local e1 = world:entity() - world:add(e1, A) + local e1 = world:entity() + world:add(e1, A) - local count = 0 - for id in world:query(B) do - count += 1 - end + local count = 0 + for id in world:query(B) do + count += 1 + end - CHECK(count == 0) - end - end + CHECK(count == 0) + end + end - do CASE "without" - do - -- REGRESSION TEST - local world = jecs.World.new() - local _1, _2, _3 = world:component(), world:component(), world:component() + do + CASE("without") + do + -- REGRESSION TEST + local world = jecs.World.new() + local _1, _2, _3 = world:component(), world:component(), world:component() - local counter = 0 - for e, a, b in world:query(_1, _2):without(_3) do - counter += 1 - end - CHECK(counter == 0) - end - end + local counter = 0 + for e, a, b in world:query(_1, _2):without(_3) do + counter += 1 + end + CHECK(counter == 0) + end + end end) TEST("world:clear()", function() - do CASE("should remove its components") + do + CASE("should remove its components") local world = jecs.World.new() :: World local A = world:component() local B = world:component() @@ -828,7 +858,8 @@ TEST("world:clear()", function() CHECK(world:get(e, B) == nil) end - do CASE "should move last record" + do + CASE("should move last record") local world = world_new() local A = world:component() @@ -872,105 +903,112 @@ TEST("world:clear()", function() end) TEST("world:has()", function() - do CASE "should find Tag on entity" - local world = jecs.World.new() + do + CASE("should find Tag on entity") + local world = jecs.World.new() - local Tag = world:entity() + local Tag = world:entity() - local e = world:entity() - world:add(e, Tag) + local e = world:entity() + world:add(e, Tag) - CHECK(world:has(e, Tag)) - end + CHECK(world:has(e, Tag)) + end - do CASE "should return false when missing one tag" - local world = jecs.World.new() + do + CASE("should return false when missing one tag") + local world = jecs.World.new() - local A = world:entity() - local B = world:entity() - local C = world:entity() - local D = world:entity() + local A = world:entity() + local B = world:entity() + local C = world:entity() + local D = world:entity() - local e = world:entity() - world:add(e, A) - world:add(e, C) - world:add(e, D) + local e = world:entity() + world:add(e, A) + world:add(e, C) + world:add(e, D) - CHECK(world:has(e, A, B, C, D) == false) - end + CHECK(world:has(e, A, B, C, D) == false) + end end) TEST("world:component()", function() - do CASE "only components should have EcsComponent trait" - local world = jecs.World.new() :: World - local A = world:component() - local e = world:entity() - - CHECK(world:has(A, jecs.Component)) - CHECK(not world:has(e, jecs.Component)) - end - - do CASE "tag" - local world = jecs.World.new() :: World - local A = world:component() - local B = world:entity() - local C = world:entity() - local e = world:entity() - world:set(e, A, "test") - world:add(e, B, "test") - world:set(e, C, 11) - - CHECK(world:has(e, A)) - CHECK(world:get(e, A) == "test") - CHECK(world:get(e, B) == nil) - CHECK(world:get(e, C) == nil) + do + CASE("only components should have EcsComponent trait") + local world = jecs.World.new() :: World + local A = world:component() + local e = world:entity() + + CHECK(world:has(A, jecs.Component)) + CHECK(not world:has(e, jecs.Component)) + end + + do + CASE("tag") + local world = jecs.World.new() :: World + local A = world:component() + local B = world:entity() + local C = world:entity() + local e = world:entity() + world:set(e, A, "test") + world:add(e, B, "test") + world:set(e, C, 11) + + CHECK(world:has(e, A)) + CHECK(world:get(e, A) == "test") + CHECK(world:get(e, B) == nil) + CHECK(world:get(e, C) == nil) end end) TEST("world:delete", function() - do CASE "bug: Empty entity does not respect cleanup policy" - local world = world_new() - local parent = world:entity() - local tag = world:entity() - - local child = world:entity() - world:add(child, jecs.pair(jecs.ChildOf, parent)) - world:delete(parent) - - CHECK(not world:contains(parent)) - CHECK(not world:contains(child)) - - local entity = world:entity() - world:add(entity, tag) - world:delete(tag) - CHECK(world:contains(entity)) - CHECK(not world:contains(tag)) - CHECK(not world:has(entity, tag)) -- => true - end - do CASE("should allow deleting components") - local world = jecs.World.new() - - local Health = world:component() - local Poison = world:component() - - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) - local id1 = world:entity() - world:set(id1, Poison, 500) - world:set(id1, Health, 50) - - world:delete(id) - CHECK(not world:contains(id)) - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == nil) - - CHECK(world:get(id1, Poison) == 500) - CHECK(world:get(id1, Health) == 50) - end - - do CASE "delete entities using another Entity as component with Delete cleanup action" - local world = jecs.World.new() + do + CASE("bug: Empty entity does not respect cleanup policy") + local world = world_new() + local parent = world:entity() + local tag = world:entity() + + local child = world:entity() + world:add(child, jecs.pair(jecs.ChildOf, parent)) + world:delete(parent) + + CHECK(not world:contains(parent)) + CHECK(not world:contains(child)) + + local entity = world:entity() + world:add(entity, tag) + world:delete(tag) + CHECK(world:contains(entity)) + CHECK(not world:contains(tag)) + CHECK(not world:has(entity, tag)) -- => true + end + do + CASE("should allow deleting components") + local world = jecs.World.new() + + local Health = world:component() + local Poison = world:component() + + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) + local id1 = world:entity() + world:set(id1, Poison, 500) + world:set(id1, Health, 50) + + world:delete(id) + CHECK(not world:contains(id)) + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == nil) + + CHECK(world:get(id1, Poison) == 500) + CHECK(world:get(id1, Health) == 50) + end + + do + CASE("delete entities using another Entity as component with Delete cleanup action") + local world = jecs.World.new() local Health = world:entity() world:add(Health, pair(jecs.OnDelete, jecs.Delete)) @@ -998,8 +1036,9 @@ TEST("world:delete", function() CHECK(not world:has(id1, Health)) end - do CASE "delete children" - local world = jecs.World.new() + do + CASE("delete children") + local world = jecs.World.new() local Health = world:component() local Poison = world:component() @@ -1011,21 +1050,20 @@ TEST("world:delete", function() local children = {} for i = 1, 10 do - local child = world:entity() + local child = world:entity() world:set(child, Poison, 9999) world:set(child, Health, 100) world:add(child, pair(jecs.ChildOf, e)) table.insert(children, child) end - BENCH("delete children of entity", function() - world:delete(e) + world:delete(e) end) for i, child in children do - CHECK(not world:contains(child)) - CHECK(not world:has(child, pair(jecs.ChildOf, e))) + CHECK(not world:contains(child)) + CHECK(not world:has(child, pair(jecs.ChildOf, e))) CHECK(not world:has(child, Health)) end @@ -1033,7 +1071,7 @@ TEST("world:delete", function() local friends = {} for i = 1, 10 do - local friend = world:entity() + local friend = world:entity() world:set(friend, Poison, 9999) world:set(friend, Health, 100) world:add(friend, pair(FriendsWith, e)) @@ -1041,839 +1079,856 @@ TEST("world:delete", function() end BENCH("remove friends of entity", function() - world:delete(e) - end) + world:delete(e) + end) - for i, friend in friends do - CHECK(not world:has(friend, pair(FriendsWith, e))) - CHECK(world:has(friend, Health)) - end - end + for i, friend in friends do + CHECK(not world:has(friend, pair(FriendsWith, e))) + CHECK(world:has(friend, Health)) + end + end - do CASE "fast delete" - local world = jecs.World.new() + do + CASE("fast delete") + local world = jecs.World.new() - local entities = {} + local entities = {} local Health = world:component() local Poison = world:component() for i = 1, 100 do - local child = world:entity() + local child = world:entity() world:set(child, Poison, 9999) world:set(child, Health, 100) table.insert(entities, child) end BENCH("simple deletion of entity", function() - for i = 1, START(100) do + for i = 1, START(100) do local e = entities[i] world:delete(e) end end) for _, entity in entities do - CHECK(not world:contains(entity)) + CHECK(not world:contains(entity)) end end - do CASE "cycle" - local world = jecs.World.new() - local Likes = world:component() - world:add(Likes, pair(jecs.OnDeleteTarget, jecs.Delete)) - local bob = world:entity() - local alice = world:entity() + do + CASE("cycle") + local world = jecs.World.new() + local Likes = world:component() + world:add(Likes, pair(jecs.OnDeleteTarget, jecs.Delete)) + local bob = world:entity() + local alice = world:entity() - world:add(bob, pair(Likes, alice)) - world:add(alice, pair(Likes, bob)) + world:add(bob, pair(Likes, alice)) + world:add(alice, pair(Likes, bob)) - world:delete(bob) - CHECK(not world:contains(bob)) - CHECK(not world:contains(alice)) + world:delete(bob) + CHECK(not world:contains(bob)) + CHECK(not world:contains(alice)) end end) TEST("world:target", function() - do CASE "nth index" - local world = world_new() - local A = world:component() - local B = world:component() - local C = world:component() - local D = world:component() - local e = world:entity() - - world:add(e, pair(A, B)) - world:add(e, pair(A, C)) - world:add(e, pair(B, C)) - world:add(e, pair(B, D)) - world:add(e, pair(C, D)) - - CHECK(pair(A, B) < pair(A, C)) - - CHECK(world:target(e, A, 0) == B) - CHECK(world:target(e, A, 1) == C) - CHECK(world:target(e, B, 0) == C) - CHECK(world:target(e, B, 1) == D) - CHECK(world:target(e, C, 0) == D) - CHECK(world:target(e, C, 1) == nil) - end - - do CASE "infer index when unspecified" - local world = world_new() - local A = world:component() - local B = world:component() - local C = world:component() - local D = world:component() - local e = world:entity() - - world:add(e, pair(A, B)) - world:add(e, pair(A, C)) - world:add(e, pair(B, C)) - world:add(e, pair(B, D)) - world:add(e, pair(C, D)) - - CHECK(world:target(e, A) == world:target(e, A, 0)) - CHECK(world:target(e, B) == world:target(e, B, 0)) - CHECK(world:target(e, C) == world:target(e, C, 0)) - end - - do CASE "loop until no target" - local world = world_new() - - local ROOT = world:entity() - local e1 = world:entity() - local targets = {} - - for i = 1, 10 do - local target = world:entity() - targets[i] = target - world:add(e1, pair(ROOT, target)) - end - - local i = 0 - local target = world:target(e1, ROOT, 0) - while target do - i+=1 - CHECK(targets[i] == target) - target = world:target(e1, ROOT, i) - end - - CHECK(i == 10) - end + do + CASE("nth index") + local world = world_new() + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + local e = world:entity() + world:add(e, pair(A, B)) + world:add(e, pair(A, C)) + world:add(e, pair(B, C)) + world:add(e, pair(B, D)) + world:add(e, pair(C, D)) + + CHECK(pair(A, B) < pair(A, C)) + + CHECK(world:target(e, A, 0) == B) + CHECK(world:target(e, A, 1) == C) + CHECK(world:target(e, B, 0) == C) + CHECK(world:target(e, B, 1) == D) + CHECK(world:target(e, C, 0) == D) + CHECK(world:target(e, C, 1) == nil) + end + + do + CASE("infer index when unspecified") + local world = world_new() + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + local e = world:entity() + + world:add(e, pair(A, B)) + world:add(e, pair(A, C)) + world:add(e, pair(B, C)) + world:add(e, pair(B, D)) + world:add(e, pair(C, D)) + + CHECK(world:target(e, A) == world:target(e, A, 0)) + CHECK(world:target(e, B) == world:target(e, B, 0)) + CHECK(world:target(e, C) == world:target(e, C, 0)) + end + + do + CASE("loop until no target") + local world = world_new() + + local ROOT = world:entity() + local e1 = world:entity() + local targets = {} + + for i = 1, 10 do + local target = world:entity() + targets[i] = target + world:add(e1, pair(ROOT, target)) + end + + local i = 0 + local target = world:target(e1, ROOT, 0) + while target do + i += 1 + CHECK(targets[i] == target) + target = world:target(e1, ROOT, i) + end + + CHECK(i == 10) + end end) TEST("world:contains", function() - local world = jecs.World.new() + local world = jecs.World.new() local id = world:entity() CHECK(world:contains(id)) - do CASE "should not exist after delete" - world:delete(id) - CHECK(not world:contains(id)) - end + do + CASE("should not exist after delete") + world:delete(id) + CHECK(not world:contains(id)) + end end) -type Tracker = { track: (world: World, fn: (changes: { - added: () -> () -> (number, T), - removed: () -> () -> number, - changed: () -> () -> (number, T, T) - }) -> ()) -> () +type Tracker = { + track: ( + world: World, + fn: ( + changes: { + added: () -> () -> (number, T), + removed: () -> () -> number, + changed: () -> () -> (number, T, T), + } + ) -> () + ) -> (), } type Entity = number & { __nominal_type_dont_use: T } local function diff(a, b) - local size = 0 - for k, v in a do - if b[k] ~= v then - return true - end - size += 1 - end - for k, v in b do - size -= 1 - end - - if size ~= 0 then - return true - end - - return false + local size = 0 + for k, v in a do + if b[k] ~= v then + return true + end + size += 1 + end + for k, v in b do + size -= 1 + end + + if size ~= 0 then + return true + end + + return false end local function ChangeTracker(world, T: Entity): Tracker - local PreviousT = jecs.pair(jecs.Rest, T) - local add = {} - local added - local removed - local is_trivial - - local function changes_added() - added = true - local it = world:query(T):without(PreviousT):iter() - return function() - local id, data = it() - if not id then - return nil - end - - is_trivial = typeof(data) ~= "table" - - add[id] = data - - return id, data - end - end - - local function changes_changed() - local it = world:query(T, PreviousT):iter() - - return function() - local id, new, old = it() - while true do - if not id then - return nil - end - - if not is_trivial then - if diff(new, old) then - break - end - elseif new ~= old then - break - end - - id, new, old = it() - end - - add[id] = new - - return id, old, new - end - end - - local function changes_removed() - removed = true - - local it = world:query(PreviousT):without(T):iter() - return function() - local id = it() - if id then - world:remove(id, PreviousT) - end - return id - end - end - - local changes = { - added = changes_added, - changed = changes_changed, - removed = changes_removed, - } - - local function track(fn) - added = false - removed = false - - fn(changes) - - if not added then - for _ in changes_added() do - end - end - - if not removed then - for _ in changes_removed() do - end - end - - for e, data in add do - world:set(e, PreviousT, if is_trivial then data else table.clone(data)) - end - end - - local tracker = { track = track } - - return tracker + local PreviousT = jecs.pair(jecs.Rest, T) + local add = {} + local added + local removed + local is_trivial + + local function changes_added() + added = true + local it = world:query(T):without(PreviousT):iter() + return function() + local id, data = it() + if not id then + return nil + end + + is_trivial = typeof(data) ~= "table" + + add[id] = data + + return id, data + end + end + + local function changes_changed() + local it = world:query(T, PreviousT):iter() + + return function() + local id, new, old = it() + while true do + if not id then + return nil + end + + if not is_trivial then + if diff(new, old) then + break + end + elseif new ~= old then + break + end + + id, new, old = it() + end + + add[id] = new + + return id, old, new + end + end + + local function changes_removed() + removed = true + + local it = world:query(PreviousT):without(T):iter() + return function() + local id = it() + if id then + world:remove(id, PreviousT) + end + return id + end + end + + local changes = { + added = changes_added, + changed = changes_changed, + removed = changes_removed, + } + + local function track(fn) + added = false + removed = false + + fn(changes) + + if not added then + for _ in changes_added() do + end + end + + if not removed then + for _ in changes_removed() do + end + end + + for e, data in add do + world:set(e, PreviousT, if is_trivial then data else table.clone(data)) + end + end + + local tracker = { track = track } + + return tracker end TEST("changetracker:track()", function() - local world = jecs.World.new() - - do CASE "added" - local Test = world:component() :: Entity<{ foo: number }> - local TestTracker = ChangeTracker(world, Test) - - local e1 = world:entity() - local data = { foo = 11 } - world:set(e1, Test, data) - - TestTracker.track(function(changes) - local added = 0 - for e, test in changes.added() do - added+=1 - CHECK(test == data) - end - for e, old, new in changes.changed() do - CHECK(false) - end - for e in changes.removed() do - CHECK(false) - end - CHECK(added == 1) - end) - end - do CASE "changed" - local Test = world:component() :: Entity<{ foo: number }> - local TestTracker = ChangeTracker(world, Test) - - local data = { foo = 11 } - local e1 = world:entity() - world:set(e1, Test, data) - - TestTracker.track(function(changes) - end) - - data.foo += 1 - - TestTracker.track(function(changes) - for _ in changes.added() do - CHECK(false) - end - local changed = 0 - for e, old, new in changes.changed() do - CHECK(e == e1) - CHECK(new == data) - CHECK(old ~= new) - CHECK(diff(new, old)) - changed +=1 - end - CHECK(changed == 1) - end) - end - do CASE "removed" - local Test = world:component() :: Entity<{ foo: number }> - local TestTracker = ChangeTracker(world, Test) - - local data = { foo = 11 } - local e1 = world:entity() - world:set(e1, Test, data) - - TestTracker.track(function(changes) - end) - - world:remove(e1, Test) - - TestTracker.track(function(changes) - for _ in changes.added() do - CHECK(false) - end - for _ in changes.changed() do - CHECK(false) - end - local removed = 0 - for e in changes.removed() do - removed += 1 - CHECK(e == e1) - end - CHECK(removed == 1) - end) - end - - do CASE "multiple change trackers" - local A = world:component() - local B = world:component() - local trackerA = ChangeTracker(world, A) - local trackerB = ChangeTracker(world, B) - - local e1 = world:entity() - world:set(e1, A, "a1") - local e2 = world:entity() - world:set(e2, B, "b1") - - trackerA.track(function() end) - trackerB.track(function() end) - - world:set(e2, B, "b2") - trackerA.track(function(changes) - for _, old, new in changes.changed() do - end - end) - trackerB.track(function(changes) - for _, old, new in changes.changed() do - CHECK(new == "b2") - end - end) - - end + local world = jecs.World.new() + + do + CASE("added") + local Test = world:component() :: Entity<{ foo: number }> + local TestTracker = ChangeTracker(world, Test) + + local e1 = world:entity() + local data = { foo = 11 } + world:set(e1, Test, data) + + TestTracker.track(function(changes) + local added = 0 + for e, test in changes.added() do + added += 1 + CHECK(test == data) + end + for e, old, new in changes.changed() do + CHECK(false) + end + for e in changes.removed() do + CHECK(false) + end + CHECK(added == 1) + end) + end + do + CASE("changed") + local Test = world:component() :: Entity<{ foo: number }> + local TestTracker = ChangeTracker(world, Test) + + local data = { foo = 11 } + local e1 = world:entity() + world:set(e1, Test, data) + + TestTracker.track(function(changes) end) + + data.foo += 1 + + TestTracker.track(function(changes) + for _ in changes.added() do + CHECK(false) + end + local changed = 0 + for e, old, new in changes.changed() do + CHECK(e == e1) + CHECK(new == data) + CHECK(old ~= new) + CHECK(diff(new, old)) + changed += 1 + end + CHECK(changed == 1) + end) + end + do + CASE("removed") + local Test = world:component() :: Entity<{ foo: number }> + local TestTracker = ChangeTracker(world, Test) + + local data = { foo = 11 } + local e1 = world:entity() + world:set(e1, Test, data) + + TestTracker.track(function(changes) end) + + world:remove(e1, Test) + + TestTracker.track(function(changes) + for _ in changes.added() do + CHECK(false) + end + for _ in changes.changed() do + CHECK(false) + end + local removed = 0 + for e in changes.removed() do + removed += 1 + CHECK(e == e1) + end + CHECK(removed == 1) + end) + end + + do + CASE("multiple change trackers") + local A = world:component() + local B = world:component() + local trackerA = ChangeTracker(world, A) + local trackerB = ChangeTracker(world, B) + local e1 = world:entity() + world:set(e1, A, "a1") + local e2 = world:entity() + world:set(e2, B, "b1") + + trackerA.track(function() end) + trackerB.track(function() end) + + world:set(e2, B, "b2") + trackerA.track(function(changes) + for _, old, new in changes.changed() do + end + end) + trackerB.track(function(changes) + for _, old, new in changes.changed() do + CHECK(new == "b2") + end + end) + end end) local function create_cache(hook) - local columns = setmetatable({}, { - __index = function(self, component) - local column = {} - self[component] = column - return column - end - }) - - return function(world, component, fn) - local column = columns[component] - table.insert(column, fn) - world:set(component, hook, function(entity, value) - for _, callback in column do - callback(entity, value) - end - end) - end + local columns = setmetatable({}, { + __index = function(self, component) + local column = {} + self[component] = column + return column + end, + }) + + return function(world, component, fn) + local column = columns[component] + table.insert(column, fn) + world:set(component, hook, function(entity, value) + for _, callback in column do + callback(entity, value) + end + end) + end end local hooks = { - OnSet = create_cache(jecs.OnSet), - OnAdd = create_cache(jecs.OnAdd), - OnRemove = create_cache(jecs.OnRemove) + OnSet = create_cache(jecs.OnSet), + OnAdd = create_cache(jecs.OnAdd), + OnRemove = create_cache(jecs.OnRemove), } TEST("Hooks", function() - do CASE "OnAdd" - local world = jecs.World.new() - local Transform = world:component() - local e1 = world:entity() - world:set(Transform, jecs.OnAdd, function(entity) - CHECK(e1 == entity) - end) - world:add(e1, Transform) - end - - do CASE "OnSet" - local world = jecs.World.new() - local Number = world:component() - local e1 = world:entity() - - hooks.OnSet(world, Number, function(entity, data) - CHECK(e1 == entity) - CHECK(data == world:get(entity, Number)) - CHECK(data == 1) - end) - hooks.OnSet(world, Number, function(entity, data) - CHECK(e1 == entity) - CHECK(data == world:get(entity, Number)) - CHECK(data == 1) - end) - world:set(e1, Number, 1) - end - - do CASE "OnRemove" - do - -- basic - local world = jecs.World.new() - local A = world:component() - local e1 = world:entity() - world:add(e1, A) - world:set(A, jecs.OnRemove, function(entity) - CHECK(e1 == entity) - CHECK(not world:has(e1, A)) - end) - world:remove(e1, A) - CHECK(not world:has(e1, A)) - end - do - -- [BUG] https://github.com/Ukendio/jecs/issues/118 - local world = world_new() - local A = world:component() - local B = world:component() - local e = world:entity() - - world:set(A, jecs.OnRemove, function(entity) - world:set(entity, B, true) - CHECK(world:get(entity, A)) - CHECK(world:get(entity, B)) - end) - - world:set(e, A, true) - world:remove(e, A) - CHECK(not world:get(e, A)) - CHECK(not world:get(e, B)) - end - end - - do CASE "the filip incident" - local world = jecs.World.new() - - export type Iterator = () -> (Entity, T?, T?) - export type Destructor = () -> () - - -- Helpers - - type ValuesMap = { [Entity]: T? } - type ChangeSet = { [Entity]: true? } - type ChangeSets = { [ChangeSet]: true? } - type ChangeSetsCache = { - Added: ChangeSets, - Changed: ChangeSets, - Removed: ChangeSets, - } - - local cachedChangeSets = {} - local function getChangeSets(component): ChangeSetsCache - if cachedChangeSets[component] == nil then - local changeSetsAdded: ChangeSets = {} - local changeSetsChanged: ChangeSets = {} - local changeSetsRemoved: ChangeSets = {} - world:set(component, jecs.OnAdd, function(id) - for set in changeSetsAdded do - set[id] = true - end - end) - world:set(component, jecs.OnSet, function(id) - for set in changeSetsChanged do - set[id] = true - end - end) - world:set(component, jecs.OnRemove, function(id) - for set in changeSetsRemoved do - set[id] = true - end - end) - cachedChangeSets[component] = { - Added = changeSetsAdded, - Changed = changeSetsChanged, - Removed = changeSetsRemoved, - } - end - return cachedChangeSets[component] - end - - local function ChangeTracker(component): (Iterator, Destructor) - local values: ValuesMap = {} - local changeSet: ChangeSet = {} - - for id in world:query(component) do - changeSet[id] = true - end - - local changeSets = getChangeSets(component) - changeSets.Added[changeSet] = true - changeSets.Changed[changeSet] = true - changeSets.Removed[changeSet] = true - - local id: Entity? = nil - local iter: Iterator = function() - id = next(changeSet) - if id then - changeSet[id] = nil - local old: T? = values[id] - local new: T? = world:get(id, component) - if old ~= nil and new == nil then - -- Old value but no new value = removed - values[id] = nil - else - -- Old+new value or just new value = new becomes old - values[id] = new - end - return id, old, new - end - return nil :: any, nil, nil - end - - local destroy: Destructor = function() - changeSets.Added[changeSet] = nil - changeSets.Changed[changeSet] = nil - changeSets.Removed[changeSet] = nil - end - - return iter, destroy - end - - local Transform = world:component() - local iter, destroy = ChangeTracker(Transform) - - local e1 = world:entity() - world:set(e1, Transform, {1,1}) - local counter = 0 - for _ in iter do - counter += 1 - end - CHECK(counter == 1) - end + do + CASE("OnAdd") + local world = jecs.World.new() + local Transform = world:component() + local e1 = world:entity() + world:set(Transform, jecs.OnAdd, function(entity) + CHECK(e1 == entity) + end) + world:add(e1, Transform) + end + + do + CASE("OnSet") + local world = jecs.World.new() + local Number = world:component() + local e1 = world:entity() + + hooks.OnSet(world, Number, function(entity, data) + CHECK(e1 == entity) + CHECK(data == world:get(entity, Number)) + CHECK(data == 1) + end) + hooks.OnSet(world, Number, function(entity, data) + CHECK(e1 == entity) + CHECK(data == world:get(entity, Number)) + CHECK(data == 1) + end) + world:set(e1, Number, 1) + end + do + CASE("OnRemove") + do + -- basic + local world = jecs.World.new() + local A = world:component() + local e1 = world:entity() + world:add(e1, A) + world:set(A, jecs.OnRemove, function(entity) + CHECK(e1 == entity) + CHECK(not world:has(e1, A)) + end) + world:remove(e1, A) + CHECK(not world:has(e1, A)) + end + do + -- [BUG] https://github.com/Ukendio/jecs/issues/118 + local world = world_new() + local A = world:component() + local B = world:component() + local e = world:entity() + + world:set(A, jecs.OnRemove, function(entity) + world:set(entity, B, true) + CHECK(world:get(entity, A)) + CHECK(world:get(entity, B)) + end) + + world:set(e, A, true) + world:remove(e, A) + CHECK(not world:get(e, A)) + CHECK(not world:get(e, B)) + end + end + + do + CASE("the filip incident") + local world = jecs.World.new() + + export type Iterator = () -> (Entity, T?, T?) + export type Destructor = () -> () + + -- Helpers + + type ValuesMap = { [Entity]: T? } + type ChangeSet = { [Entity]: true? } + type ChangeSets = { [ChangeSet]: true? } + type ChangeSetsCache = { + Added: ChangeSets, + Changed: ChangeSets, + Removed: ChangeSets, + } + + local cachedChangeSets = {} + local function getChangeSets(component): ChangeSetsCache + if cachedChangeSets[component] == nil then + local changeSetsAdded: ChangeSets = {} + local changeSetsChanged: ChangeSets = {} + local changeSetsRemoved: ChangeSets = {} + world:set(component, jecs.OnAdd, function(id) + for set in changeSetsAdded do + set[id] = true + end + end) + world:set(component, jecs.OnSet, function(id) + for set in changeSetsChanged do + set[id] = true + end + end) + world:set(component, jecs.OnRemove, function(id) + for set in changeSetsRemoved do + set[id] = true + end + end) + cachedChangeSets[component] = { + Added = changeSetsAdded, + Changed = changeSetsChanged, + Removed = changeSetsRemoved, + } + end + return cachedChangeSets[component] + end + + local function ChangeTracker(component): (Iterator, Destructor) + local values: ValuesMap = {} + local changeSet: ChangeSet = {} + + for id in world:query(component) do + changeSet[id] = true + end + + local changeSets = getChangeSets(component) + changeSets.Added[changeSet] = true + changeSets.Changed[changeSet] = true + changeSets.Removed[changeSet] = true + + local id: Entity? = nil + local iter: Iterator = function() + id = next(changeSet) + if id then + changeSet[id] = nil + local old: T? = values[id] + local new: T? = world:get(id, component) + if old ~= nil and new == nil then + -- Old value but no new value = removed + values[id] = nil + else + -- Old+new value or just new value = new becomes old + values[id] = new + end + return id, old, new + end + return nil :: any, nil, nil + end + + local destroy: Destructor = function() + changeSets.Added[changeSet] = nil + changeSets.Changed[changeSet] = nil + changeSets.Removed[changeSet] = nil + end + + return iter, destroy + end + + local Transform = world:component() + local iter, destroy = ChangeTracker(Transform) + + local e1 = world:entity() + world:set(e1, Transform, { 1, 1 }) + local counter = 0 + for _ in iter do + counter += 1 + end + CHECK(counter == 1) + end end) TEST("scheduler", function() - type System = { - callback: (world: World) -> () - } - type Systems = { System } - - - type Events = { - RenderStepped: Systems, - Heartbeat: Systems - } - - local scheduler_new: (w: World) -> { - components: { - Disabled: Entity, - System: Entity, - Phase: Entity, - DependsOn: Entity - }, - - collect: { - under_event: (event: Entity) -> Systems, - all: () -> Events - }, - - systems: { - run: (events: Events) -> (), - new: (callback: (world: World) -> (), phase: Entity) -> Entity - }, - - phases: { - RenderStepped: Entity, - Heartbeat: Entity - }, - - phase: (after: Entity) -> Entity - } - - do - local world - local Disabled - local System - local DependsOn - local Phase - local Event - local RenderStepped - local Heartbeat - local Name - - local function scheduler_systems_run(events) - for _, system in events[RenderStepped] do - system.callback() - end - for _, system in events[Heartbeat] do - system.callback() - end - end - - local function scheduler_collect_systems_under_phase_recursive(systems, phase) - for _, system in world:query(System):with(pair(DependsOn, phase)) do - table.insert(systems, system) - end - for dependant in world:query(Phase):with(pair(DependsOn, phase)) do - scheduler_collect_systems_under_phase_recursive(systems, dependant) - end - end - - local function scheduler_collect_systems_under_event(event) - local systems = {} - scheduler_collect_systems_under_phase_recursive(systems, event) - return systems - end - - local function scheduler_collect_systems_all() - local systems = {} - for phase in world:query(Phase, Event) do - systems[phase] = scheduler_collect_systems_under_event(phase) - end - return systems - end - - local function scheduler_phase_new(after) - local phase = world:entity() - world:add(phase, Phase) - local dependency = pair(DependsOn, after) - world:add(phase, dependency) - return phase - end - - local function scheduler_systems_new(callback, phase) - local system = world:entity() - world:set(system, System, { callback = callback }) - world:add(system, pair(DependsOn, phase)) - return system - end - - function scheduler_new(w) - world = w - Disabled = world:component() - System = world:component() - Phase = world:component() - DependsOn = world:component() - Event = world:component() - - RenderStepped = world:component() - Heartbeat = world:component() - - world:add(RenderStepped, Phase) - world:add(RenderStepped, Event) - world:add(Heartbeat, Phase) - world:add(Heartbeat, Event) - - return { - phase = scheduler_phase_new, - - phases = { - RenderStepped = RenderStepped, - Heartbeat = Heartbeat, - }, - - world = world, - - components = { - DependsOn = DependsOn, - Disabled = Disabled, - Heartbeat = Heartbeat, - Phase = Phase, - RenderStepped = RenderStepped, - System = System, - }, - - collect = { - under_event = scheduler_collect_systems_under_event, - all = scheduler_collect_systems_all - }, - - systems = { - new = scheduler_systems_new, - run = scheduler_systems_run - } - } - end - end - - do CASE "event dependant phase" - - local world = jecs.World.new() - local scheduler = scheduler_new(world) - local components = scheduler.components - local phases = scheduler.phases - local Heartbeat = phases.Heartbeat - local DependsOn = components.DependsOn - - local Physics = scheduler.phase(Heartbeat) - CHECK(world:target(Physics, DependsOn, 0) == Heartbeat) - end - - do CASE "user-defined sub phases" - local world = jecs.World.new() - local scheduler = scheduler_new(world) - local components = scheduler.components - local phases = scheduler.phases - local DependsOn = components.DependsOn - - local A = scheduler.phase(phases.Heartbeat) - local B = scheduler.phase(A) - - CHECK(world:target(B, DependsOn, 0) == A) - end - - do CASE "phase order" - local world = jecs.World.new() - local scheduler = scheduler_new(world) - - local phases = scheduler.phases - local Physics = scheduler.phase(phases.Heartbeat) - local Collisions = scheduler.phase(Physics) + type System = { + callback: (world: World) -> (), + } + type Systems = { System } + + type Events = { + RenderStepped: Systems, + Heartbeat: Systems, + } + + local scheduler_new: ( + w: World + ) -> { + components: { + Disabled: Entity, + System: Entity, + Phase: Entity, + DependsOn: Entity, + }, + + collect: { + under_event: (event: Entity) -> Systems, + all: () -> Events, + }, + + systems: { + run: (events: Events) -> (), + new: (callback: (world: World) -> (), phase: Entity) -> Entity, + }, + + phases: { + RenderStepped: Entity, + Heartbeat: Entity, + }, + + phase: (after: Entity) -> Entity, + } + + do + local world + local Disabled + local System + local DependsOn + local Phase + local Event + local RenderStepped + local Heartbeat + local Name + + local function scheduler_systems_run(events) + for _, system in events[RenderStepped] do + system.callback() + end + for _, system in events[Heartbeat] do + system.callback() + end + end + + local function scheduler_collect_systems_under_phase_recursive(systems, phase) + for _, system in world:query(System):with(pair(DependsOn, phase)) do + table.insert(systems, system) + end + for dependant in world:query(Phase):with(pair(DependsOn, phase)) do + scheduler_collect_systems_under_phase_recursive(systems, dependant) + end + end + + local function scheduler_collect_systems_under_event(event) + local systems = {} + scheduler_collect_systems_under_phase_recursive(systems, event) + return systems + end + + local function scheduler_collect_systems_all() + local systems = {} + for phase in world:query(Phase, Event) do + systems[phase] = scheduler_collect_systems_under_event(phase) + end + return systems + end + + local function scheduler_phase_new(after) + local phase = world:entity() + world:add(phase, Phase) + local dependency = pair(DependsOn, after) + world:add(phase, dependency) + return phase + end + + local function scheduler_systems_new(callback, phase) + local system = world:entity() + world:set(system, System, { callback = callback }) + world:add(system, pair(DependsOn, phase)) + return system + end + + function scheduler_new(w) + world = w + Disabled = world:component() + System = world:component() + Phase = world:component() + DependsOn = world:component() + Event = world:component() + + RenderStepped = world:component() + Heartbeat = world:component() + + world:add(RenderStepped, Phase) + world:add(RenderStepped, Event) + world:add(Heartbeat, Phase) + world:add(Heartbeat, Event) + + return { + phase = scheduler_phase_new, + + phases = { + RenderStepped = RenderStepped, + Heartbeat = Heartbeat, + }, + + world = world, + + components = { + DependsOn = DependsOn, + Disabled = Disabled, + Heartbeat = Heartbeat, + Phase = Phase, + RenderStepped = RenderStepped, + System = System, + }, + + collect = { + under_event = scheduler_collect_systems_under_event, + all = scheduler_collect_systems_all, + }, + + systems = { + new = scheduler_systems_new, + run = scheduler_systems_run, + }, + } + end + end - local order = "BEGIN" + do + CASE("event dependant phase") - local function move() - order ..= "->move" - end + local world = jecs.World.new() + local scheduler = scheduler_new(world) + local components = scheduler.components + local phases = scheduler.phases + local Heartbeat = phases.Heartbeat + local DependsOn = components.DependsOn + + local Physics = scheduler.phase(Heartbeat) + CHECK(world:target(Physics, DependsOn, 0) == Heartbeat) + end + + do + CASE("user-defined sub phases") + local world = jecs.World.new() + local scheduler = scheduler_new(world) + local components = scheduler.components + local phases = scheduler.phases + local DependsOn = components.DependsOn + + local A = scheduler.phase(phases.Heartbeat) + local B = scheduler.phase(A) + + CHECK(world:target(B, DependsOn, 0) == A) + end + + do + CASE("phase order") + local world = jecs.World.new() + local scheduler = scheduler_new(world) + + local phases = scheduler.phases + local Physics = scheduler.phase(phases.Heartbeat) + local Collisions = scheduler.phase(Physics) + + local order = "BEGIN" + + local function move() + order ..= "->move" + end - local function hit() - order ..= "->hit" - end + local function hit() + order ..= "->hit" + end - local createSystem = scheduler.systems.new + local createSystem = scheduler.systems.new - createSystem(hit, Collisions) - createSystem(move, Physics) + createSystem(hit, Collisions) + createSystem(move, Physics) - local events = scheduler.collect.all() - scheduler.systems.run(events) + local events = scheduler.collect.all() + scheduler.systems.run(events) - order ..= "->END" + order ..= "->END" - CHECK(order == "BEGIN->move->hit->END") - end + CHECK(order == "BEGIN->move->hit->END") + end - do CASE "collect only systems under phase recursive" - local world = jecs.World.new() - local scheduler = scheduler_new(world) - local phases = scheduler.phases - local Heartbeat = phases.Heartbeat - local RenderStepped = phases.RenderStepped - local Render = scheduler.phase(RenderStepped) - local Physics = scheduler.phase(Heartbeat) - local Collisions = scheduler.phase(Physics) + do + CASE("collect only systems under phase recursive") + local world = jecs.World.new() + local scheduler = scheduler_new(world) + local phases = scheduler.phases + local Heartbeat = phases.Heartbeat + local RenderStepped = phases.RenderStepped + local Render = scheduler.phase(RenderStepped) + local Physics = scheduler.phase(Heartbeat) + local Collisions = scheduler.phase(Physics) - local function move() - end + local function move() end - local function hit() - end + local function hit() end - local function camera() - end + local function camera() end - local createSystem = scheduler.systems.new + local createSystem = scheduler.systems.new - createSystem(hit, Collisions) - createSystem(move, Physics) - createSystem(camera, Render) + createSystem(hit, Collisions) + createSystem(move, Physics) + createSystem(camera, Render) - local systems = scheduler.collect.under_event(Collisions) + local systems = scheduler.collect.under_event(Collisions) - CHECK(#systems == 1) - CHECK(systems[1].callback == hit) + CHECK(#systems == 1) + CHECK(systems[1].callback == hit) - systems = scheduler.collect.under_event(Physics) + systems = scheduler.collect.under_event(Physics) - CHECK(#systems == 2) + CHECK(#systems == 2) - systems = scheduler.collect.under_event(Heartbeat) + systems = scheduler.collect.under_event(Heartbeat) - CHECK(#systems == 2) + CHECK(#systems == 2) - systems = scheduler.collect.under_event(Render) + systems = scheduler.collect.under_event(Render) - CHECK(#systems == 1) - CHECK(systems[1].callback == camera) - end + CHECK(#systems == 1) + CHECK(systems[1].callback == camera) + end end) TEST("repro", function() - do CASE "" - local world = world_new() - local reproEntity = world:component() - local components = { Cooldown = world:component() } - world:set(reproEntity, components.Cooldown, 2) - - local function updateCooldowns(dt: number) - local toRemove = {} - - for id, cooldown in world:query(components.Cooldown):iter() do - cooldown -= dt - - if cooldown <= 0 then - table.insert(toRemove, id) - print('removing') - -- world:remove(id, components.Cooldown) - else - world:set(id, components.Cooldown, cooldown) - end - end - - for _, id in toRemove do - world:remove(id, components.Cooldown) - CHECK(not world:get(id, components.Cooldown)) - end - end - - updateCooldowns(1.5) - updateCooldowns(1.5) - end + do + CASE("") + local world = world_new() + local reproEntity = world:component() + local components = { Cooldown = world:component() } + world:set(reproEntity, components.Cooldown, 2) + + local function updateCooldowns(dt: number) + local toRemove = {} + + for id, cooldown in world:query(components.Cooldown):iter() do + cooldown -= dt + + if cooldown <= 0 then + table.insert(toRemove, id) + print("removing") + -- world:remove(id, components.Cooldown) + else + world:set(id, components.Cooldown, cooldown) + end + end + + for _, id in toRemove do + world:remove(id, components.Cooldown) + CHECK(not world:get(id, components.Cooldown)) + end + end + + updateCooldowns(1.5) + updateCooldowns(1.5) + end end) FINISH() diff --git a/testkit.luau b/testkit.luau index 2e89cd3d..96396b7e 100644 --- a/testkit.luau +++ b/testkit.luau @@ -41,7 +41,7 @@ local color = { end, orange = function(s: string): string - return if disable_ansi then s else `\27[38;5;208m{s}\27[0m` + return if disable_ansi then s else `\27[38;5;208m{s}\27[0m` end, } @@ -96,7 +96,7 @@ local function convert_units(unit: string, value: number): (number, string) return value * sign, prefix_colors[order](prefixes[order] .. unit) end -local WALL = color.gray "│" +local WALL = color.gray("│") -------------------------------------------------------------------------------- -- Testing @@ -118,7 +118,7 @@ type Case = { name: string, result: number, line: number?, - focus: boolean + focus: boolean, } local PASS, FAIL, NONE, ERROR, SKIPPED = 1, 2, 3, 4, 5 @@ -129,48 +129,51 @@ local test: Test? local tests: { Test } = {} local function output_test_result(test: Test) - if check_for_focused then local any_focused = test.focus for _, case in test.cases do any_focused = any_focused or case.focus end - if not any_focused then return end + if not any_focused then + return + end end print(color.white(test.name)) for _, case in test.cases do local status = ({ - [PASS] = color.green "PASS", - [FAIL] = color.red "FAIL", - [NONE] = color.orange "NONE", - [ERROR] = color.red "FAIL", - [SKIPPED] = color.yellow "SKIP" + [PASS] = color.green("PASS"), + [FAIL] = color.red("FAIL"), + [NONE] = color.orange("NONE"), + [ERROR] = color.red("FAIL"), + [SKIPPED] = color.yellow("SKIP"), })[case.result] local line = case.result == FAIL and color.red(`{case.line}:`) or "" - if check_for_focused and case.focus == false and test.focus == false then continue end + if check_for_focused and case.focus == false and test.focus == false then + continue + end print(`{status}{WALL} {line}{color.gray(case.name)}`) end if test.error then - print(color.gray "error: " .. color.red(test.error.message)) - print(color.gray "trace: " .. color.red(test.error.trace)) + print(color.gray("error: ") .. color.red(test.error.message)) + print(color.gray("trace: ") .. color.red(test.error.trace)) else print() end end local function CASE(name: string) - skip = false + skip = false assert(test, "no active test") local case = { name = name, result = NONE, - focus = false + focus = false, } test.case = case @@ -183,7 +186,7 @@ local function CHECK(value: T, stack: number?): T? local case = test.case if not case then - CASE "" + CASE("") case = test.case end @@ -192,7 +195,7 @@ local function CHECK(value: T, stack: number?): T? if case.result ~= FAIL then case.result = value and PASS or FAIL if skip then - case.result = SKIPPED + case.result = SKIPPED end case.line = debug.info(stack and stack + 1 or 2, "l") end @@ -208,7 +211,7 @@ local function TEST(name: string, fn: () -> ()) name = name, cases = {}, duration = 0, - focus = false + focus = false, } assert(test) @@ -221,7 +224,9 @@ local function TEST(name: string, fn: () -> ()) end) test.duration = os.clock() - start - if not test.case then CASE "" end + if not test.case then + CASE("") + end assert(test.case, "no active case") if not success then @@ -271,27 +276,14 @@ local function FINISH(): boolean output_test_result(test) end - print( - color.gray( - string.format( - `{passed_cases}/{total_cases} test cases passed in %.3f ms.`, - duration * 1e3 - ) - ) - ) + print(color.gray(string.format(`{passed_cases}/{total_cases} test cases passed in %.3f ms.`, duration * 1e3))) if check_for_focused then - print( - color.gray(`{passed_focus_cases}/{total_focus_cases} focused test cases passed`) - ) + print(color.gray(`{passed_focus_cases}/{total_focus_cases} focused test cases passed`)) end local fails = total_cases - passed_cases - print( - (fails > 0 and color.red or color.green)( - `{fails} {fails == 1 and "fail" or "fails"}` - ) - ) + print((fails > 0 and color.red or color.green)(`{fails} {fails == 1 and "fail" or "fails"}`)) check_for_focused = false return success, table.clear(tests) @@ -331,7 +323,7 @@ local function BENCH(name: string, fn: () -> ()) bench = {} assert(bench); - (collectgarbage :: any) "collect" + (collectgarbage :: any)("collect") local mem_start = gcinfo() local time_start = os.clock() @@ -345,7 +337,7 @@ local function BENCH(name: string, fn: () -> ()) local mem_stop = gcinfo() if not success then - print(`{WALL}{color.red "ERROR"}{WALL} {name}`) + print(`{WALL}{color.red("ERROR")}{WALL} {name}`) print(color.gray(err_msg :: string)) else time_start = bench.time_start or time_start @@ -353,14 +345,10 @@ local function BENCH(name: string, fn: () -> ()) local n = bench.iterations or 1 local d, d_unit = convert_units("s", (time_stop - time_start) / n) - local a, a_unit = - convert_units("B", math.round((mem_stop - mem_start) / n * 1e3)) + local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3)) local function round(x: number): string - return x > 0 - and x < 10 - and (x - math.floor(x)) > 0 - and string.format("%2.1f", x) + return x > 0 and x < 10 and (x - math.floor(x)) > 0 and string.format("%2.1f", x) or string.format("%3.f", x) end @@ -394,9 +382,9 @@ local function print2(v: unknown) if type(value) == "string" then local n = str.n - str[n + 1] = '"' + str[n + 1] = "\"" str[n + 2] = value - str[n + 3] = '"' + str[n + 3] = "\"" str.n = n + 3 elseif type(value) ~= "table" then local n = str.n @@ -468,25 +456,35 @@ end -------------------------------------------------------------------------------- local function shallow_eq(a: {}, b: {}): boolean - if #a ~= #b then return false end + if #a ~= #b then + return false + end for i, v in next, a do - if b[i] ~= v then return false end + if b[i] ~= v then + return false + end end for i, v in next, b do - if a[i] ~= v then return false end + if a[i] ~= v then + return false + end end return true end local function deep_eq(a: {}, b: {}): boolean - if #a ~= #b then return false end + if #a ~= #b then + return false + end for i, v in next, a do if type(b[i]) == "table" and type(v) == "table" then - if deep_eq(b[i], v) == false then return false end + if deep_eq(b[i], v) == false then + return false + end elseif b[i] ~= v then return false end @@ -494,7 +492,9 @@ local function deep_eq(a: {}, b: {}): boolean for i, v in next, b do if type(a[i]) == "table" and type(v) == "table" then - if deep_eq(a[i], v) == false then return false end + if deep_eq(a[i], v) == false then + return false + end elseif a[i] ~= v then return false end