mirror of
https://github.com/Ukendio/jecs.git
synced 2026-02-04 15:15:21 +00:00
Cleanup repository
This commit is contained in:
parent
8ef122ccb4
commit
d86dff4bfe
45 changed files with 447 additions and 3504 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -1,2 +0,0 @@
|
|||
*.luau text eol=lf
|
||||
*.html linguist-vendored
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -71,3 +71,7 @@ profile.*
|
|||
*.patch
|
||||
|
||||
genhtml.perl
|
||||
|
||||
rokit.toml
|
||||
package-lock.json
|
||||
mirror.luau
|
||||
|
|
|
|||
8
.luaurc
8
.luaurc
|
|
@ -1,10 +1,8 @@
|
|||
{
|
||||
"aliases": {
|
||||
"jecs": "./jecs",
|
||||
"testkit": "./tools/testkit",
|
||||
"mirror": "./mirror",
|
||||
"tools": "./tools",
|
||||
"addons": "./addons"
|
||||
"jecs": "src/jecs",
|
||||
"modules": "modules",
|
||||
"mirror": "src/mirror",
|
||||
},
|
||||
"languageMode": "strict"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "all",
|
||||
"useTabs": true
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
syntax = "All"
|
||||
column_width = 120
|
||||
line_endings = "Unix"
|
||||
indent_type = "Tabs"
|
||||
indent_width = 4
|
||||
quote_style = "AutoPreferDouble"
|
||||
call_parentheses = "Always"
|
||||
space_after_function_names = "Never"
|
||||
collapse_simple_statement = "Never"
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
|
||||
--!optimize 2
|
||||
--!native
|
||||
|
||||
local testkit = require("@testkit")
|
||||
local BENCH, START = testkit.benchmark()
|
||||
local function TITLE(title: string)
|
||||
print()
|
||||
print(testkit.color.white(title))
|
||||
end
|
||||
|
||||
local jecs = require("@jecs")
|
||||
local mirror = require("@mirror")
|
||||
|
||||
do
|
||||
TITLE(testkit.color.white_underline("Jecs query"))
|
||||
local world = jecs.world() :: jecs.World
|
||||
|
||||
local A = world:component()
|
||||
|
||||
for i = 1, 100_000 do
|
||||
local e = world:entity()
|
||||
world:set(e, A, true)
|
||||
end
|
||||
|
||||
local archetypes = world:query(A):archetypes()
|
||||
|
||||
BENCH("", function()
|
||||
for _, archetype in archetypes do
|
||||
local column = archetype.columns[1]
|
||||
for row, entity in archetype.entities do
|
||||
local data = column[row]
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
end
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"Lib": {
|
||||
"$path": "jecs.luau"
|
||||
"$path": "../src/jecs.luau"
|
||||
},
|
||||
"benches": {
|
||||
"$path": "benches"
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local jecs = require("@jecs")
|
||||
local testkit = require("@testkit")
|
||||
local testkit = require("@modules/testkit")
|
||||
|
||||
local BENCH, START = testkit.benchmark()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
--!optimize 2
|
||||
--!native
|
||||
|
||||
local testkit = require("@testkit")
|
||||
local testkit = require("@modules/testkit")
|
||||
local BENCH, START = testkit.benchmark()
|
||||
local function TITLE(title: string)
|
||||
print()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "jecs",
|
||||
"tree": {
|
||||
"$path": "jecs.luau"
|
||||
"$path": "src/jecs.luau"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3415
mirror.luau
3415
mirror.luau
File diff suppressed because it is too large
Load diff
425
modules/ob.luau
Executable file
425
modules/ob.luau
Executable file
|
|
@ -0,0 +1,425 @@
|
|||
--!strict
|
||||
local jecs = require("@jecs")
|
||||
|
||||
type World = jecs.World
|
||||
|
||||
type Id<T=any> = jecs.Id<T>
|
||||
|
||||
|
||||
export type Observer = {
|
||||
disconnect: () -> (),
|
||||
}
|
||||
|
||||
export type Monitor = {
|
||||
disconnect: () -> (),
|
||||
added: ((jecs.Entity) -> ()) -> (),
|
||||
removed: ((jecs.Entity) -> ()) -> ()
|
||||
}
|
||||
|
||||
local function observers_new(
|
||||
query: jecs.Query<...any>,
|
||||
callback: (jecs.Entity) -> ()
|
||||
): Observer
|
||||
local cachedquery = query:cached()
|
||||
|
||||
local world = (cachedquery :: jecs.Query<any> & { world: World }).world
|
||||
callback = callback
|
||||
|
||||
local archetypes = cachedquery.archetypes_map
|
||||
local terms = query.filter_with :: { jecs.Id<any> }
|
||||
|
||||
local entity_index = world.entity_index
|
||||
|
||||
local function emplaced<a>(
|
||||
entity: jecs.Entity,
|
||||
id: jecs.Id<a>,
|
||||
value: a,
|
||||
oldarchetype: jecs.Archetype
|
||||
)
|
||||
local r = entity_index.sparse_array[jecs.ECS_ID(entity)]
|
||||
|
||||
local archetype = r.archetype
|
||||
|
||||
if archetypes[archetype.id] then
|
||||
callback(entity)
|
||||
end
|
||||
end
|
||||
|
||||
local cleanup = {}
|
||||
|
||||
for _, term in terms do
|
||||
if jecs.IS_PAIR(term) then
|
||||
local rel = jecs.ECS_PAIR_FIRST(term)
|
||||
local tgt = jecs.ECS_PAIR_SECOND(term)
|
||||
local wc = tgt == jecs.w
|
||||
|
||||
local function emplaced_w_pair(entity, id, value, oldarchetype: jecs.Archetype)
|
||||
if not wc and id ~= term then
|
||||
return
|
||||
end
|
||||
local r = jecs.record(world, entity)
|
||||
if archetypes[r.archetype.id] then
|
||||
callback(entity)
|
||||
end
|
||||
end
|
||||
|
||||
local onadded = world:added(rel, emplaced_w_pair)
|
||||
local onchanged = world:changed(rel, emplaced_w_pair)
|
||||
table.insert(cleanup, onadded)
|
||||
table.insert(cleanup, onchanged)
|
||||
else
|
||||
local onadded = world:added(term, emplaced)
|
||||
local onchanged = world:changed(term, emplaced)
|
||||
table.insert(cleanup, onadded)
|
||||
table.insert(cleanup, onchanged)
|
||||
end
|
||||
end
|
||||
|
||||
local without = query.filter_without
|
||||
if without then
|
||||
for _, term in without do
|
||||
if jecs.IS_PAIR(term) then
|
||||
local rel = jecs.ECS_PAIR_FIRST(term)
|
||||
local tgt = jecs.ECS_PAIR_SECOND(term)
|
||||
local wc = tgt == jecs.w
|
||||
local onremoved = world:removed(rel, function(entity, id, delete: boolean?)
|
||||
if not wc and id ~= term then
|
||||
return
|
||||
end
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if archetype then
|
||||
local dst = jecs.archetype_traverse_remove(world, id, archetype)
|
||||
if archetypes[dst.id] then
|
||||
callback(entity)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
table.insert(cleanup, onremoved)
|
||||
else
|
||||
local onremoved = world:removed(term, function(entity, id)
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if archetype then
|
||||
local dst = jecs.archetype_traverse_remove(world, id, archetype)
|
||||
if archetypes[dst.id] then
|
||||
callback(entity)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
table.insert(cleanup, onremoved)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function disconnect()
|
||||
for _, disconnect in cleanup do
|
||||
disconnect()
|
||||
end
|
||||
end
|
||||
|
||||
local observer = {
|
||||
disconnect = disconnect,
|
||||
}
|
||||
|
||||
return observer
|
||||
end
|
||||
|
||||
local function monitors_new(query: jecs.Query<...any>): Monitor
|
||||
local cachedquery = query:cached()
|
||||
|
||||
local world = (cachedquery :: jecs.Query<...any> & { world: World }).world :: jecs.World
|
||||
|
||||
local archetypes = cachedquery.archetypes_map
|
||||
local terms = cachedquery.filter_with :: { jecs.Id<any> }
|
||||
|
||||
local entity_index = world.entity_index :: any
|
||||
|
||||
local terms_lookup: { [jecs.Id<any>]: boolean } = {}
|
||||
for _, term in terms do
|
||||
terms_lookup[term] = true
|
||||
end
|
||||
|
||||
local callback_added: ((jecs.Entity) -> ())?
|
||||
local callback_removed: ((jecs.Entity) -> ())?
|
||||
|
||||
-- NOTE(marcus): Track the last (entity, old archetype) pair we processed to detect bulk operations.
|
||||
-- During bulk_insert from ROOT_ARCHETYPE, the entity is moved to the target archetype first,
|
||||
-- then all on_add callbacks fire sequentially with the same oldarchetype for the same entity.
|
||||
-- We track both entity and old archetype to distinguish between:
|
||||
-- 1. Same entity, same old archetype (bulk operation - skip)
|
||||
-- 2. Different entity, same old archetype (separate operation - don't skip)
|
||||
local last_old_archetype: jecs.Archetype? = nil
|
||||
local last_entity: jecs.Entity? = nil
|
||||
|
||||
local function emplaced<a>(
|
||||
entity: jecs.Entity,
|
||||
id: jecs.Id<a>,
|
||||
value: a,
|
||||
oldarchetype: jecs.Archetype
|
||||
)
|
||||
if callback_added == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local r = jecs.entity_index_try_get_fast(
|
||||
entity_index, entity :: any) :: jecs.Record
|
||||
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
|
||||
-- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before
|
||||
-- AND this component is in the query's terms. This detects bulk operations where
|
||||
-- the same entity transitions with multiple components, while allowing different
|
||||
-- entities to trigger even if they share the same old archetype.
|
||||
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
|
||||
return
|
||||
end
|
||||
|
||||
last_old_archetype = oldarchetype
|
||||
last_entity = entity
|
||||
callback_added(entity)
|
||||
else
|
||||
-- NOTE(marcus): Clear tracking when we see a different transition pattern
|
||||
last_old_archetype = nil
|
||||
last_entity = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Track which entity we've already processed for deletion to avoid duplicate callbacks
|
||||
-- during bulk deletion where multiple components are removed with delete=true
|
||||
local last_deleted_entity: jecs.Entity? = nil
|
||||
|
||||
local function removed(entity: jecs.Entity, component: jecs.Component, delete:boolean?)
|
||||
if callback_removed == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if delete then
|
||||
-- Deletion is a bulk removal - all components are removed with delete=true
|
||||
-- We should only trigger the callback once per entity, not once per component
|
||||
if last_deleted_entity == entity then
|
||||
return
|
||||
end
|
||||
|
||||
local r = jecs.record(world, entity)
|
||||
if r and r.archetype and archetypes[r.archetype.id] then
|
||||
-- Entity was in the monitor before deletion
|
||||
last_deleted_entity = entity
|
||||
-- Clear tracking when entity is deleted
|
||||
last_old_archetype = nil
|
||||
last_entity = nil
|
||||
callback_removed(entity)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local r = jecs.record(world, entity)
|
||||
local src = r.archetype
|
||||
|
||||
local dst = jecs.archetype_traverse_remove(world, component, src)
|
||||
|
||||
if not archetypes[dst.id] then
|
||||
-- Clear tracking when entity leaves the monitor to allow re-entry
|
||||
last_old_archetype = nil
|
||||
last_entity = nil
|
||||
last_deleted_entity = nil
|
||||
callback_removed(entity)
|
||||
end
|
||||
end
|
||||
|
||||
local cleanup = {}
|
||||
|
||||
for _, term in terms do
|
||||
if jecs.IS_PAIR(term) then
|
||||
local rel = jecs.ECS_PAIR_FIRST(term)
|
||||
local tgt = jecs.ECS_PAIR_SECOND(term)
|
||||
local wc = tgt == jecs.w
|
||||
|
||||
local onadded = world:added(rel, function(entity, id, _, oldarchetype: jecs.Archetype)
|
||||
if callback_added == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if not wc and id ~= term then
|
||||
return
|
||||
end
|
||||
|
||||
local r = jecs.entity_index_try_get_fast(
|
||||
entity_index, entity :: any) :: jecs.Record
|
||||
|
||||
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
|
||||
-- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before
|
||||
-- AND this component is in the query's terms.
|
||||
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
|
||||
return
|
||||
end
|
||||
|
||||
last_old_archetype = oldarchetype
|
||||
last_entity = entity
|
||||
callback_added(entity)
|
||||
else
|
||||
-- Clear tracking when we see a different transition pattern
|
||||
last_old_archetype = nil
|
||||
last_entity = nil
|
||||
end
|
||||
end)
|
||||
local onremoved = world:removed(rel, function(entity, id, deleted)
|
||||
if callback_removed == nil then
|
||||
return
|
||||
end
|
||||
if not wc and id ~= term then
|
||||
return
|
||||
end
|
||||
|
||||
local r = jecs.record(world, entity)
|
||||
if archetypes[r.archetype.id] then
|
||||
last_old_archetype = nil
|
||||
callback_removed(entity)
|
||||
end
|
||||
end)
|
||||
table.insert(cleanup, onadded)
|
||||
table.insert(cleanup, onremoved)
|
||||
else
|
||||
local onadded = world:added(term, emplaced)
|
||||
local onremoved = world:removed(term, removed)
|
||||
table.insert(cleanup, onadded)
|
||||
table.insert(cleanup, onremoved)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local without = query.filter_without
|
||||
if without then
|
||||
for _, term in without do
|
||||
if jecs.IS_PAIR(term) then
|
||||
local rel = jecs.ECS_PAIR_FIRST(term)
|
||||
local tgt = jecs.ECS_PAIR_SECOND(term)
|
||||
local wc = tgt == jecs.w
|
||||
local onadded = world:added(rel, function(entity, id, _, oldarchetype: jecs.Archetype)
|
||||
if callback_removed == nil then
|
||||
return
|
||||
end
|
||||
if not wc and id ~= term then
|
||||
return
|
||||
end
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
|
||||
-- NOTE(marcus): This check that it was presently in
|
||||
-- the query but distinctively leaves is important as
|
||||
-- sometimes it could be too eager to report that it
|
||||
-- removed a component even though the entity is not
|
||||
-- apart of the monitor
|
||||
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
|
||||
last_old_archetype = nil
|
||||
callback_removed(entity)
|
||||
end
|
||||
end)
|
||||
local onremoved = world:removed(rel, function(entity, id, delete)
|
||||
if delete then
|
||||
return
|
||||
end
|
||||
if callback_added == nil then
|
||||
return
|
||||
end
|
||||
if not wc and id ~= term then
|
||||
return
|
||||
end
|
||||
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
if last_old_archetype == archetype and terms_lookup[id] then
|
||||
return
|
||||
end
|
||||
|
||||
local dst = jecs.archetype_traverse_remove(world, id, archetype)
|
||||
|
||||
if archetypes[dst.id] then
|
||||
last_old_archetype = archetype
|
||||
callback_added(entity)
|
||||
end
|
||||
end)
|
||||
table.insert(cleanup, onadded)
|
||||
table.insert(cleanup, onremoved)
|
||||
else
|
||||
local onadded = world:added(term, function(entity, id, _, oldarchetype: jecs.Archetype)
|
||||
if callback_removed == nil then
|
||||
return
|
||||
end
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
|
||||
-- NOTE(marcus): Sometimes OnAdd listeners for excluded
|
||||
-- terms are too eager to report that it is leaving the
|
||||
-- monitor even though the entity is not apart of it
|
||||
-- already.
|
||||
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
|
||||
callback_removed(entity)
|
||||
end
|
||||
end)
|
||||
local onremoved = world:removed(term, function(entity, id, delete)
|
||||
if delete then
|
||||
return
|
||||
end
|
||||
if callback_added == nil then
|
||||
return
|
||||
end
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
local dst = jecs.archetype_traverse_remove(world, id, archetype)
|
||||
|
||||
-- NOTE(marcus): Inversely with the opposite operation, you
|
||||
-- only need to check if it is going to enter the query once
|
||||
-- because world:remove already stipulates that it is
|
||||
-- idempotent so that this hook won't be invoked if it is
|
||||
-- was already removed.
|
||||
if archetypes[dst.id] then
|
||||
callback_added(entity)
|
||||
end
|
||||
end)
|
||||
table.insert(cleanup, onadded)
|
||||
table.insert(cleanup, onremoved)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function disconnect()
|
||||
for _, disconnect in cleanup do
|
||||
disconnect()
|
||||
end
|
||||
end
|
||||
|
||||
local function monitor_added(callback)
|
||||
callback_added = callback
|
||||
end
|
||||
|
||||
local function monitor_removed(callback)
|
||||
callback_removed = callback
|
||||
end
|
||||
|
||||
local monitor = {
|
||||
disconnect = disconnect,
|
||||
added = monitor_added,
|
||||
removed = monitor_removed
|
||||
} :: Monitor
|
||||
|
||||
return monitor
|
||||
end
|
||||
|
||||
return {
|
||||
monitor = monitors_new,
|
||||
observer = observers_new,
|
||||
test = function(q: jecs.Query<...any>) end
|
||||
}
|
||||
|
|
@ -15,10 +15,9 @@
|
|||
],
|
||||
"homepage": "https://github.com/ukendio/jecs",
|
||||
"license": "MIT",
|
||||
"types": "jecs.d.ts",
|
||||
"types": "src/jecs.d.ts",
|
||||
"files": [
|
||||
"jecs.luau",
|
||||
"jecs.d.ts",
|
||||
"src",
|
||||
"LICENSE.md",
|
||||
"README.md"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
[tools]
|
||||
wally = "upliftgames/wally@0.3.2"
|
||||
rojo = "rojo-rbx/rojo@7.4.4"
|
||||
stylua = "johnnymorganz/stylua@2.0.1"
|
||||
Blink = "1Axen/Blink@0.14.1"
|
||||
wally-package-types = "JohnnyMorganz/wally-package-types@1.4.2"
|
||||
luau = "luau-lang/luau@0.701"
|
||||
|
|
|
|||
0
jecs.d.ts → src/jecs.d.ts
vendored
0
jecs.d.ts → src/jecs.d.ts
vendored
|
|
@ -1,11 +0,0 @@
|
|||
local function component()
|
||||
local id = 1
|
||||
local v
|
||||
local function instance()
|
||||
return id, v
|
||||
end
|
||||
return function(value)
|
||||
v = value
|
||||
return instance
|
||||
end
|
||||
end
|
||||
|
|
@ -136,7 +136,7 @@ local function pa(e)
|
|||
print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`)
|
||||
end
|
||||
|
||||
local tprint = require("@testkit").print
|
||||
local tprint = require("@modules/testkit").print
|
||||
local e1v0 = alloc()
|
||||
local e2v0 = alloc()
|
||||
local e3v0 = alloc()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
local jecs = require("@jecs")
|
||||
local testkit = require("@testkit")
|
||||
local testkit = require("@modules/testkit")
|
||||
local test = testkit.test()
|
||||
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
|
||||
local FOCUS = test.FOCUS
|
||||
local ob = require("@addons/ob")
|
||||
local ob = require("@modules/ob")
|
||||
|
||||
TEST("addons/ob::observer", function()
|
||||
TEST("modules/ob::observer", function()
|
||||
local world = jecs.world()
|
||||
do CASE [[should not invoke callbacks with a related but non-queried pair that
|
||||
while the entity still matches against the query]]
|
||||
|
|
@ -289,7 +289,7 @@ TEST("addons/ob::observer", function()
|
|||
end
|
||||
end)
|
||||
|
||||
TEST("addons/ob::monitor", function()
|
||||
TEST("modules/ob::monitor", function()
|
||||
local world = jecs.world()
|
||||
|
||||
do CASE [[should not invoke monitor.added callback multiple times in a bulk_move
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
local jecs = require("@jecs")
|
||||
|
||||
local testkit = require("@testkit")
|
||||
local testkit = require("@modules/testkit")
|
||||
local BENCH, START = testkit.benchmark()
|
||||
local __ = jecs.Wildcard
|
||||
local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
|
||||
|
|
@ -21,7 +21,7 @@ type World = jecs.World
|
|||
type Entity<T=nil> = jecs.Entity<T>
|
||||
type Id<T=unknown> = jecs.Id<T>
|
||||
|
||||
local entity_visualiser = require("@tools/entity_visualiser")
|
||||
local entity_visualiser = require("@modules/entity_visualiser")
|
||||
local dwi = entity_visualiser.stringify
|
||||
|
||||
TEST("Ensure archetype edges get cleaned", function()
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ realm = "shared"
|
|||
license = "MIT"
|
||||
include = [
|
||||
"default.project.json",
|
||||
"jecs.luau",
|
||||
"src",
|
||||
"wally.toml",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
]
|
||||
exclude = ["**"]
|
||||
exclude = ["src/mirror.luau"]
|
||||
|
|
|
|||
Loading…
Reference in a new issue