[](LICENSE-APACHE)
diff --git a/jecs_darkmode.svg b/jecs_darkmode.svg
new file mode 100644
index 0000000..f64b173
--- /dev/null
+++ b/jecs_darkmode.svg
@@ -0,0 +1,6 @@
+
diff --git a/jecs_lightmode.svg b/jecs_lightmode.svg
new file mode 100644
index 0000000..dbcd08c
--- /dev/null
+++ b/jecs_lightmode.svg
@@ -0,0 +1,6 @@
+
diff --git a/logo.png b/logo_old.png
similarity index 100%
rename from logo.png
rename to logo_old.png
From 887c892c2ebb4406e5e6f7f7d3be4e018b031046 Mon Sep 17 00:00:00 2001
From: Marcus
Date: Tue, 7 May 2024 21:30:36 +0200
Subject: [PATCH 2/7] Move root archetype (#33)
---
lib/init.lua | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lib/init.lua b/lib/init.lua
index 8c0b5bd..ce0951e 100644
--- a/lib/init.lua
+++ b/lib/init.lua
@@ -190,6 +190,7 @@ function World.new()
nextEntityId = 0;
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
}, World)
+ self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
return self
end
@@ -272,6 +273,7 @@ local function ensureEdge(archetype: Archetype, componentId: i53)
end
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
+ from = from or world.ROOT_ARCHETYPE
if not from then
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
From bf5908a8f5da83641be19bff397bdf54e852b1a4 Mon Sep 17 00:00:00 2001
From: Marcus
Date: Tue, 7 May 2024 21:32:56 +0200
Subject: [PATCH 3/7] Adds symmetic and idempotent function add (#26)
* Adds symmetic function add
* Should be componentId not entityId
---
lib/init.lua | 29 ++++++++++++++++++-----------
1 file changed, 18 insertions(+), 11 deletions(-)
diff --git a/lib/init.lua b/lib/init.lua
index ce0951e..250a56f 100644
--- a/lib/init.lua
+++ b/lib/init.lua
@@ -274,15 +274,6 @@ end
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
from = from or world.ROOT_ARCHETYPE
- if not from then
- -- If there was no source archetype then it should return the ROOT_ARCHETYPE
- local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
- if not ROOT_ARCHETYPE then
- ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
- world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
- end
- from = ROOT_ARCHETYPE
- end
local edge = ensureEdge(from, componentId)
local add = edge.add
@@ -307,7 +298,23 @@ local function ensureRecord(entityIndex, entityId: i53): Record
return record :: Record
end
-function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
+
+function World.add(world: World, entityId: i53, componentId: i53)
+ local record = ensureRecord(world.entityIndex, entityId)
+ local from = record.archetype
+ local to = archetypeTraverseAdd(world, componentId, from)
+ if from then
+ moveEntity(world.entityIndex, entityId, record, to)
+ else
+ if #to.types > 0 then
+ newEntity(entityId, record, to)
+ onNotifyAdd(world, to, from, record.row, { componentId })
+ end
+ end
+end
+
+-- Symmetric like `World.add` but idempotent
+function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
local record = ensureRecord(world.entityIndex, entityId)
local from = record.archetype
local to = archetypeTraverseAdd(world, componentId, from)
@@ -331,7 +338,7 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
onNotifyAdd(world, to, from, record.row, {componentId})
end
end
-
+
local archetypeRecord = to.records[componentId]
to.columns[archetypeRecord][record.row] = data
end
From e8b78f7b50033d02c33fc755c7ec584da12c3acb Mon Sep 17 00:00:00 2001
From: Marcus
Date: Tue, 7 May 2024 21:33:42 +0200
Subject: [PATCH 4/7] Add world.delete (#22)
---
lib/init.lua | 50 +++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 43 insertions(+), 7 deletions(-)
diff --git a/lib/init.lua b/lib/init.lua
index 250a56f..defc1b3 100644
--- a/lib/init.lua
+++ b/lib/init.lua
@@ -596,15 +596,51 @@ function World.entity(world: World)
return nextEntityId + REST
end
-function World.delete(world: World, entityId: i53)
+-- should reuse this logic in World.set instead of swap removing in transition archetype
+local function destructColumns(columns, count, row)
+ if row == count then
+ for _, column in columns do
+ column[count] = nil
+ end
+ else
+ for _, column in columns do
+ column[row] = column[count]
+ column[count] = nil
+ end
+ end
+end
+
+local function archetypeDelete(entityIndex, archetype: Archetype, row: i24, destruct: boolean)
+ local entities = archetype.entities
+ local last = #entities
+
+ local entityToMove = entities[last]
+ --local entityToDelete = entities[row]
+ entities[row] = entityToMove
+ entities[last] = nil
+
+ if row ~= last then
+ local recordToMove = entityIndex[entityToMove]
+ if recordToMove then
+ recordToMove.row = row
+ end
+ end
+
+ local columns = archetype.columns
+
+ if not destruct then
+ return
+ end
+
+ destructColumns(columns, last, row)
+end
+
+function World.delete(world: World, entityId: i53)
local entityIndex = world.entityIndex
local record = entityIndex[entityId]
- moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
- -- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from
- -- the entities array and delete the record. We know there won't be the hole since
- -- we are always removing the last row.
- --world.ROOT_ARCHETYPE.entities[record.row] = nil
- --entityIndex[entityId] = nil
+ local archetype = record.archetype
+ archetypeDelete(entityIndex, archetype, record.row, true)
+ entityIndex[entityId] = nil
end
function World.observer(world: World, ...)
From 91d3fcabc3d63d54ed3362da659d262fa9806286 Mon Sep 17 00:00:00 2001
From: Marcus
Date: Wed, 8 May 2024 00:57:22 +0200
Subject: [PATCH 5/7] Add case for when component is not found in archetype
(#25)
* Add case for when component is not found in archetype
* Check only destination archetype first
* Omit onNotifyAdd
---
.gitignore | 3 ---
lib/init.lua | 51 ++++++++++++++++++++++++++++++-------------------
tests/test1.lua | 21 ++++++++++++++++++--
3 files changed, 50 insertions(+), 25 deletions(-)
diff --git a/.gitignore b/.gitignore
index 9143a00..a43fa5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,3 @@ WallyPatches
roblox.toml
sourcemap.json
drafts/*.lua
-
-*.code-workspace
-roblox.yml
diff --git a/lib/init.lua b/lib/init.lua
index defc1b3..a57ae9e 100644
--- a/lib/init.lua
+++ b/lib/init.lua
@@ -287,23 +287,31 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet
return add
end
-local function ensureRecord(entityIndex, entityId: i53): Record
+local function ensureRecord(world, entityId: i53): Record
+ local entityIndex = world.entityIndex
local record = entityIndex[entityId]
- if not record then
- record = {}
- entityIndex[entityId] = record
+ if record then
+ return record
end
- return record :: Record
+ local ROOT = world.ROOT_ARCHETYPE
+ local row = #ROOT.entities + 1
+ ROOT.entities[row] = entityId
+ record = {
+ archetype = ROOT,
+ row = row
+ }
+ entityIndex[entityId] = record
+ return record
end
function World.add(world: World, entityId: i53, componentId: i53)
- local record = ensureRecord(world.entityIndex, entityId)
+ local record = ensureRecord(world, entityId)
local from = record.archetype
local to = archetypeTraverseAdd(world, componentId, from)
- if from then
+ if from and not (from == world.ROOT_ARCHETYPE) then
moveEntity(world.entityIndex, entityId, record, to)
else
if #to.types > 0 then
@@ -315,19 +323,20 @@ end
-- Symmetric like `World.add` but idempotent
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
- local record = ensureRecord(world.entityIndex, entityId)
+ local record = ensureRecord(world, entityId)
local from = record.archetype
- local to = archetypeTraverseAdd(world, componentId, from)
- if from == to then
+ local archetypeRecord = from.records[componentId]
+ if archetypeRecord then
-- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly.
- local archetypeRecord = to.records[componentId]
from.columns[archetypeRecord][record.row] = data
-- Should fire an OnSet event here.
return
end
+ local to = archetypeTraverseAdd(world, componentId, from)
+
if from then
-- If there was a previous archetype, then the entity needs to move the archetype
moveEntity(world.entityIndex, entityId, record, to)
@@ -335,22 +344,25 @@ 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
- local archetypeRecord = to.records[componentId]
+ archetypeRecord = to.records[componentId]
to.columns[archetypeRecord][record.row] = data
end
-local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
- local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
+local function archetypeTraverseRemove(world: World, componentId: i53, from: Archetype): Archetype
local edge = ensureEdge(from, componentId)
local remove = edge.remove
if not remove then
local to = table.clone(from.types)
- table.remove(to, table.find(to, componentId))
+ local at = table.find(to, componentId)
+ if not at then
+ return from
+ end
+ table.remove(to, at)
remove = ensureArchetype(world, to, from)
edge.remove = remove :: never
end
@@ -359,13 +371,12 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype
end
function World.remove(world: World, entityId: i53, componentId: i53)
- local entityIndex = world.entityIndex
- local record = ensureRecord(entityIndex, entityId)
+ local record = ensureRecord(world, entityId)
local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
- moveEntity(entityIndex, entityId, record, destinationArchetype)
+ moveEntity(world.entityIndex, entityId, record, destinationArchetype)
end
end
@@ -732,4 +743,4 @@ return table.freeze({
ON_ADD = ON_ADD;
ON_REMOVE = ON_REMOVE;
ON_SET = ON_SET;
-})
+})
\ No newline at end of file
diff --git a/tests/test1.lua b/tests/test1.lua
index 7ff3b5a..3fe86da 100644
--- a/tests/test1.lua
+++ b/tests/test1.lua
@@ -35,14 +35,17 @@ TEST("world:query", function()
local world = jecs.World.new()
local A = world:component()
local B = world:component()
+ local C = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
- world:set(id, A, true)
+ -- specifically put them in disorder to track regression
+ -- https://github.com/Ukendio/jecs/pull/15
world:set(id, B, true)
- if i > 5 then world:remove(id, B, true) end
+ world:set(id, A, true)
+ if i > 5 then world:remove(id, B) end
entities[i] = id
end
@@ -110,6 +113,20 @@ TEST("world:query", function()
CHECK(world:get(id, Health) == nil)
end
+ do CASE "show allow remove that doesn't exist on entity"
+ local world = jecs.World.new()
+
+ local Health = world:entity()
+ local Poison = world:component()
+
+ local id = world:entity()
+ world:set(id, Health, 50)
+ world:remove(id, Poison)
+
+ CHECK(world:get(id, Poison) == nil)
+ CHECK(world:get(id, Health) == 50)
+ end
+
do CASE "Should allow iterating the whole world"
local world = jecs.World.new()
From 1de41447b62caae571bd1b34a254ea9518b69311 Mon Sep 17 00:00:00 2001
From: Marcus
Date: Wed, 8 May 2024 01:04:11 +0200
Subject: [PATCH 6/7] Remove observer for now (#34)
* Add case for when component is not found in archetype
* Check only destination archetype first
* Omit onNotifyAdd
* Remove observers
---
lib/init.lua | 53 -----------------------------------------------
lib/init.spec.lua | 16 --------------
2 files changed, 69 deletions(-)
diff --git a/lib/init.lua b/lib/init.lua
index a57ae9e..29396a5 100644
--- a/lib/init.lua
+++ b/lib/init.lua
@@ -654,59 +654,6 @@ function World.delete(world: World, entityId: i53)
entityIndex[entityId] = nil
end
-function World.observer(world: World, ...)
- local componentIds = {...}
- local idsCount = #componentIds
- local hooks = world.hooks
-
- return {
- event = function(event)
- local hook = hooks[event]
- hooks[event] = nil
-
- local last, change
- return function()
- last, change = next(hook, last)
- if not last then
- return
- end
-
- local matched = false
- local ids = change.ids
-
- while not matched do
- local skip = false
- for _, id in ids do
- if not table.find(componentIds, id) then
- skip = true
- break
- end
- end
-
- if skip then
- last, change = next(hook, last)
- ids = change.ids
- continue
- end
-
- matched = true
- end
-
- local queryOutput = table.create(idsCount)
- local row = change.offset
- local archetype = change.archetype
- local columns = archetype.columns
- local archetypeRecords = archetype.records
- for index, id in componentIds do
- queryOutput[index] = columns[archetypeRecords[id]][row]
- end
-
- return archetype.entities[row], unpack(queryOutput, 1, idsCount)
- end
- end;
- }
-end
-
function World.__iter(world: World): () -> (number?, unknown?)
local entityIndex = world.entityIndex
local last
diff --git a/lib/init.spec.lua b/lib/init.spec.lua
index 98f485b..553c9a4 100644
--- a/lib/init.spec.lua
+++ b/lib/init.spec.lua
@@ -176,22 +176,6 @@ return function()
expect(added).to.equal(0)
end)
- it("track changes", function()
- local Position = world:entity()
-
- local moving = world:entity()
- world:set(moving, Position, Vector3.new(1, 2, 3))
-
- local count = 0
-
- for e, position in world:observer(Position).event(jecs.ON_ADD) do
- count += 1
- expect(e).to.equal(moving)
- expect(position).to.equal(Vector3.new(1, 2, 3))
- end
- expect(count).to.equal(1)
- end)
-
it("should query all matching entities", function()
local world = jecs.World.new()
From 87d49e513422a47d5022932c1c8143a4dbd0504a Mon Sep 17 00:00:00 2001
From: Ukendio
Date: Thu, 9 May 2024 02:20:54 +0200
Subject: [PATCH 7/7] Reorganize file
---
lib/a.lua | 5 +++
lib/init.lua | 94 ++++++++++++++++++++++++++--------------------------
2 files changed, 52 insertions(+), 47 deletions(-)
create mode 100644 lib/a.lua
diff --git a/lib/a.lua b/lib/a.lua
new file mode 100644
index 0000000..68b844c
--- /dev/null
+++ b/lib/a.lua
@@ -0,0 +1,5 @@
+local test = {
+ ez = "godo"
+}
+
+ test.ez = "good"
\ No newline at end of file
diff --git a/lib/init.lua b/lib/init.lua
index 29396a5..93e5021 100644
--- a/lib/init.lua
+++ b/lib/init.lua
@@ -143,14 +143,14 @@ local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archet
end
end
-local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
+local function archetypeOf(world: any, 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)
for index in types do
columns[index] = {}
@@ -174,51 +174,6 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet
return archetype
end
-local World = {}
-World.__index = World
-function World.new()
- local self = setmetatable({
- archetypeIndex = {};
- archetypes = {};
- componentIndex = {};
- entityIndex = {};
- hooks = {
- [ON_ADD] = {};
- };
- nextArchetypeId = 0;
- nextComponentId = 0;
- nextEntityId = 0;
- ROOT_ARCHETYPE = (nil :: any) :: Archetype;
- }, World)
- self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
- return self
-end
-
-local function emit(world, eventDescription)
- local event = eventDescription.event
-
- table.insert(world.hooks[event], {
- archetype = eventDescription.archetype;
- ids = eventDescription.ids;
- offset = eventDescription.offset;
- otherArchetype = eventDescription.otherArchetype;
- })
-end
-
-local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
- if #added > 0 then
- emit(world, {
- archetype = archetype;
- event = ON_ADD;
- ids = added;
- offset = row;
- otherArchetype = otherArchetype;
- })
- end
-end
-
-export type World = typeof(World.new())
-
local function ensureArchetype(world: World, types, prev)
if #types < 1 then
return world.ROOT_ARCHETYPE
@@ -306,6 +261,51 @@ local function ensureRecord(world, entityId: i53): Record
return record
end
+local World = {}
+World.__index = World
+function World.new()
+ local self = setmetatable({
+ archetypeIndex = {};
+ archetypes = {};
+ componentIndex = {};
+ entityIndex = {};
+ hooks = {
+ [ON_ADD] = {};
+ };
+ nextArchetypeId = 0;
+ nextComponentId = 0;
+ nextEntityId = 0;
+ ROOT_ARCHETYPE = (nil :: any) :: Archetype;
+ }, World)
+ self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
+ return self
+end
+
+local function emit(world, eventDescription)
+ local event = eventDescription.event
+
+ table.insert(world.hooks[event], {
+ archetype = eventDescription.archetype;
+ ids = eventDescription.ids;
+ offset = eventDescription.offset;
+ otherArchetype = eventDescription.otherArchetype;
+ })
+end
+
+local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
+ if #added > 0 then
+ emit(world, {
+ archetype = archetype;
+ event = ON_ADD;
+ ids = added;
+ offset = row;
+ otherArchetype = otherArchetype;
+ })
+ end
+end
+
+export type World = typeof(World.new())
+
function World.add(world: World, entityId: i53, componentId: i53)
local record = ensureRecord(world, entityId)