mirror of
https://github.com/Ukendio/jecs.git
synced 2026-03-18 00:44:32 +00:00
Compare commits
22 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19823453aa | ||
|
|
7170dbf6a1 | ||
|
|
4d76e28425 | ||
|
|
99c2b1b56e | ||
|
|
8ea8f1f235 | ||
|
|
a0f6a9a632 | ||
|
|
96dfde0d2e | ||
|
|
043bff1ff8 | ||
|
|
74254717f1 | ||
|
|
0a8c827573 | ||
|
|
683c7f28aa | ||
|
|
e2bfe80bb8 | ||
|
|
360423b634 | ||
|
|
73019547bd | ||
|
|
9acec0e954 | ||
|
|
f9764634e6 | ||
|
|
4b10b622bf | ||
|
|
29c93e5b0c | ||
|
|
6552a5d2d1 | ||
|
|
5f76674723 | ||
|
|
622c7c9638 | ||
|
|
4236bd02fd |
16 changed files with 625 additions and 483 deletions
17
.github/workflows/publish-npm.yml
vendored
17
.github/workflows/publish-npm.yml
vendored
|
|
@ -1,17 +0,0 @@
|
|||
name: publish-npm
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
- uses: JS-DevTools/npm-publish@v3
|
||||
with:
|
||||
token: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
132
.github/workflows/release.yaml
vendored
132
.github/workflows/release.yaml
vendored
|
|
@ -1,71 +1,95 @@
|
|||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*", "workflow_dispatch"]
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
- name: Install Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: wally install
|
||||
- name: Install Dependencies
|
||||
run: wally install
|
||||
|
||||
- name: Build
|
||||
run: rojo build --output build.rbxm default.project.json
|
||||
- name: Build
|
||||
run: rojo build --output build.rbxm default.project.json
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: build.rbxm
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: build.rbxm
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
release:
|
||||
name: Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Jecs Build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
- name: Download Build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
||||
- name: Rename Build
|
||||
run: mv build/build.rbxm jecs.rbxm
|
||||
- name: Rename Build
|
||||
run: mv build/build.rbxm jecs.rbxm
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Jecs ${{ github.ref_name }}
|
||||
files: |
|
||||
jecs.rbxm
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Jecs ${{ github.ref_name }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
files: jecs.rbxm
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
needs: [release]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
publish-wally:
|
||||
name: Publish to Wally
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
- name: Install Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Wally Login
|
||||
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
|
||||
- name: Wally Login
|
||||
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
run: wally publish
|
||||
- name: Publish
|
||||
run: wally publish
|
||||
|
||||
publish-npm:
|
||||
name: Publish to NPM
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install
|
||||
run: npm install
|
||||
|
||||
- name: Publish
|
||||
run: npm publish
|
||||
|
|
|
|||
|
|
@ -47,8 +47,7 @@ end
|
|||
-- local tgt = tonumber(tokens[2]) :: jecs.Entity
|
||||
|
||||
-- rel = ecs_ensure_entity(world, rel)
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
-- tgt = ecs_ensure_entity(world, tgt)
|
||||
|
||||
-- return jecs.pair(rel, tgt)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ about the code base is lost, ability to work on it at the same level of quality
|
|||
is lost. Over time code quality will decline as code size grows.
|
||||
|
||||
- Tacit knowledge is very hard to recover by looking at a maze of code,
|
||||
and it takes
|
||||
and it takes a long time to do so.
|
||||
|
||||
- You will often hear that "every semantic distinction deserves its own
|
||||
component or tag". Sometimes this is correct. A well chosen component boundary
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
|
||||
local jecs = require("@jecs")
|
||||
local jabby = require("@modules/jabby")
|
||||
local jabby = require("@modules/Jabby/module")
|
||||
|
||||
local world = jecs.world()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ type World = jecs.World
|
|||
|
||||
type Id<T=any> = jecs.Id<any>
|
||||
|
||||
local function duplicate(query: jecs.Query<...any>): jecs.CachedQuery<...any>
|
||||
local world = (query :: jecs.Query<any> & { world: World }).world
|
||||
local function duplicate(query: jecs.Query<...any>): jecs.Cached_Query<...any>
|
||||
local world = (query :: { world: World }).world
|
||||
local dup = world:query()
|
||||
dup.filter_with = table.clone(query.filter_with)
|
||||
if query.filter_without then
|
||||
if query.filter_without then
|
||||
dup.filter_without = query.filter_without
|
||||
end
|
||||
return dup:cached()
|
||||
|
|
@ -152,11 +152,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
|
||||
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) -> ())?
|
||||
|
||||
|
|
@ -176,7 +171,7 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
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
|
||||
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
|
||||
if last_old_archetype == oldarchetype and last_entity == entity then
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -193,13 +188,12 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
if callback_removed == nil then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
local r = jecs.record(world, entity)
|
||||
if not r then return end
|
||||
|
||||
|
||||
local src = r.archetype
|
||||
if not src then return end
|
||||
|
||||
|
||||
if not archetypes[src.id] then return end
|
||||
|
||||
if last_entity == entity and last_old_archetype == src then
|
||||
|
|
@ -232,7 +226,7 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
entity_index, entity :: any) :: jecs.Record
|
||||
|
||||
if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then
|
||||
if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then
|
||||
if last_old_archetype == oldarchetype and last_entity == entity then
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -253,10 +247,17 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
end
|
||||
|
||||
local r = jecs.record(world, entity)
|
||||
if archetypes[r.archetype.id] then
|
||||
last_old_archetype = nil
|
||||
callback_removed(entity)
|
||||
local src = r.archetype
|
||||
|
||||
if last_entity == entity and last_old_archetype == src then
|
||||
return
|
||||
end
|
||||
if not archetypes[src.id] then
|
||||
return
|
||||
end
|
||||
last_entity = entity
|
||||
last_old_archetype = src
|
||||
callback_removed(entity)
|
||||
end)
|
||||
table.insert(cleanup, onadded)
|
||||
table.insert(cleanup, onremoved)
|
||||
|
|
@ -285,16 +286,13 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
end
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
|
||||
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)
|
||||
local onremoved = world:removed(rel, function(entity: jecs.Entity, id: jecs.Id, delete)
|
||||
if delete then
|
||||
return
|
||||
end
|
||||
|
|
@ -307,10 +305,7 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
|
||||
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
|
||||
if last_old_archetype == archetype then
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -330,9 +325,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
end
|
||||
local r = jecs.record(world, entity)
|
||||
local archetype = r.archetype
|
||||
if not archetype then
|
||||
return
|
||||
end
|
||||
|
||||
if archetypes[oldarchetype.id] and not archetypes[archetype.id] then
|
||||
callback_removed(entity)
|
||||
|
|
@ -347,9 +339,6 @@ local function monitors_new(query: jecs.Query<...any>): Monitor
|
|||
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)
|
||||
|
||||
if archetypes[dst.id] then
|
||||
|
|
|
|||
0
modules/perfgraph.py → modules/PerfGraph/perfgraph.py
Executable file → Normal file
0
modules/perfgraph.py → modules/PerfGraph/perfgraph.py
Executable file → Normal file
0
modules/svg.py → modules/PerfGraph/svg.py
Executable file → Normal file
0
modules/svg.py → modules/PerfGraph/svg.py
Executable file → Normal file
47
modules/deserialize.luau
Normal file
47
modules/deserialize.luau
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
local jecs = require("@jecs")
|
||||
|
||||
local function ecs_ensure_entity(world: jecs.World, id: jecs.Entity<any>, ctx: {[jecs.Entity<any>]: jecs.Entity<any>})
|
||||
local e = 0
|
||||
|
||||
local ser_id = id
|
||||
|
||||
local deser_id = ctx[ser_id]
|
||||
if not deser_id then
|
||||
if not world:exists(ser_id)
|
||||
or (world:contains(ser_id) and not world:get(ser_id, jecs.Name))
|
||||
then
|
||||
deser_id = world:entity(id)
|
||||
else
|
||||
if world:contains(ser_id) then
|
||||
if world:has(ser_id, jecs.Name) then
|
||||
deser_id = ser_id
|
||||
else
|
||||
deser_id = world:entity(ser_id)
|
||||
end
|
||||
else
|
||||
if world:exists(ser_id) then
|
||||
deser_id = world:entity()
|
||||
else
|
||||
deser_id = world:entity(ser_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
ctx[ser_id] = deser_id
|
||||
end
|
||||
|
||||
e = deser_id
|
||||
|
||||
return e
|
||||
end
|
||||
|
||||
local function ecs_deser_pairs(world: jecs.World, rel, tgt, ctx)
|
||||
rel = ecs_ensure_entity(world, rel, ctx)
|
||||
tgt = ecs_ensure_entity(world, tgt, ctx)
|
||||
|
||||
return jecs.pair(rel, tgt)
|
||||
end
|
||||
|
||||
return {
|
||||
ecs_ensure_entity = ecs_ensure_entity,
|
||||
ecs_deser_pairs = ecs_deser_pairs,
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
-- A simple way to safely type remote events without hassle
|
||||
|
||||
local ReplicatedStorage = require("@game/ReplicatedStorage")
|
||||
local jecs = require("@jecs")
|
||||
local ty = require("./")
|
||||
|
||||
type Remote<T...> = {
|
||||
FireClient: (Remote<T...>, Player, T...) -> (),
|
||||
FireAllClients: (Remote<T...>, T...) -> (),
|
||||
FireServer: (Remote<T...>, T...) -> (),
|
||||
OnServerEvent: RBXScriptSignal<(Player, T...)>,
|
||||
OnClientEvent: RBXScriptSignal<T...>
|
||||
}
|
||||
|
||||
local function stream_ensure(name)
|
||||
local remote = ReplicatedStorage:FindFirstChild(name)
|
||||
if not remote then
|
||||
remote = Instance.new("RemoteEvent")
|
||||
remote.Name = name
|
||||
remote.Parent = ReplicatedStorage
|
||||
end
|
||||
return remote
|
||||
end
|
||||
|
||||
local function datagram_ensure(name)
|
||||
local remote = ReplicatedStorage:FindFirstChild(name)
|
||||
if not remote then
|
||||
remote = Instance.new("UnreliableRemoteEvent")
|
||||
remote.Name = name
|
||||
remote.Parent = ReplicatedStorage
|
||||
end
|
||||
return remote
|
||||
end
|
||||
|
||||
|
||||
return {
|
||||
input = datagram_ensure("input") :: Remote<string>,
|
||||
replication = stream_ensure("replication") :: Remote<snapshot>
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "@rbxts/jecs",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0",
|
||||
"description": "Stupidly fast Entity Component System",
|
||||
"main": "src/jecs.luau",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ukendio/jecs.git"
|
||||
"url": "https://github.com/Ukendio/jecs"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Ukendio",
|
||||
|
|
|
|||
407
src/jecs.luau
407
src/jecs.luau
|
|
@ -90,7 +90,7 @@ export type Cached_Query<T...> = typeof(setmetatable(
|
|||
archetypes: (Cached_Query<T...>, override: boolean?) -> { Archetype },
|
||||
has: (Cached_Query<T...>, Entity) -> boolean,
|
||||
fini: (Cached_Query<T...>) -> (),
|
||||
|
||||
|
||||
ids: { Id<any> },
|
||||
filter_with: { Id<any> }?,
|
||||
filter_without: { Id<any> }?,
|
||||
|
|
@ -156,6 +156,7 @@ type componentrecord = {
|
|||
counts: { [i53]: number },
|
||||
flags: number,
|
||||
size: number,
|
||||
cache: { number },
|
||||
|
||||
on_add: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
|
||||
on_change: ((entity: i53, id: i53, value: any, oldarchetype: archetype) -> ())?,
|
||||
|
|
@ -533,7 +534,11 @@ end
|
|||
local function entity_index_get_alive(entity_index: entityindex, entity: i53): i53?
|
||||
local r = entity_index_try_get_any(entity_index, entity :: number)
|
||||
if r then
|
||||
return entity_index.dense_array[r.dense]
|
||||
local dense = r.dense
|
||||
if dense > entity_index.alive_count then
|
||||
return nil
|
||||
end
|
||||
return entity_index.dense_array[dense]
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
|
@ -563,17 +568,21 @@ end
|
|||
|
||||
local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range"
|
||||
|
||||
local function ENTITY_INDEX_NEW_ID(entity_index: entityindex): i53
|
||||
local function ENTITY_INDEX_NEW_ID(world: world): i53
|
||||
local entity_index = world.entity_index
|
||||
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
|
||||
local dense_array = entity_index.dense_array
|
||||
local alive_count = entity_index.alive_count
|
||||
local sparse_array = entity_index.sparse_array
|
||||
local max_id = entity_index.max_id
|
||||
local next_count = alive_count + 1
|
||||
|
||||
if alive_count < max_id then
|
||||
alive_count += 1
|
||||
entity_index.alive_count = alive_count
|
||||
local id = dense_array[alive_count]
|
||||
return id
|
||||
local id = dense_array[next_count]
|
||||
if id then
|
||||
entity_index.alive_count = next_count
|
||||
return id
|
||||
end
|
||||
end
|
||||
|
||||
local id = max_id + 1
|
||||
|
|
@ -581,10 +590,9 @@ local function ENTITY_INDEX_NEW_ID(entity_index: entityindex): i53
|
|||
ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY)
|
||||
|
||||
entity_index.max_id = id
|
||||
alive_count += 1
|
||||
entity_index.alive_count = alive_count
|
||||
dense_array[alive_count] = id
|
||||
sparse_array[id] = { dense = alive_count } :: record
|
||||
entity_index.alive_count = next_count
|
||||
dense_array[next_count] = id
|
||||
sparse_array[id] = { dense = next_count, row = 0, archetype = ROOT_ARCHETYPE }
|
||||
|
||||
return id
|
||||
end
|
||||
|
|
@ -911,6 +919,7 @@ local function id_record_create(
|
|||
records = {},
|
||||
counts = {},
|
||||
flags = flags,
|
||||
cache = {},
|
||||
|
||||
on_add = on_add,
|
||||
on_change = on_change,
|
||||
|
|
@ -948,6 +957,7 @@ local function archetype_append_to_records(
|
|||
idr_records[archetype_id] = index
|
||||
idr_counts[archetype_id] = 1
|
||||
columns_map[id] = column
|
||||
table.insert(idr.cache, archetype_id)
|
||||
else
|
||||
local max_count = idr_counts[archetype_id] + 1
|
||||
idr_counts[archetype_id] = max_count
|
||||
|
|
@ -1039,8 +1049,10 @@ local function world_range(world: world, range_begin: number, range_end: number?
|
|||
for i = max_id + 1, range_begin do
|
||||
dense_array[i] = i
|
||||
sparse_array[i] = {
|
||||
dense = 0
|
||||
} :: record
|
||||
dense = 0,
|
||||
row = 0,
|
||||
archetype = world.ROOT_ARCHETYPE
|
||||
}
|
||||
end
|
||||
entity_index.max_id = range_begin
|
||||
entity_index.alive_count = range_begin
|
||||
|
|
@ -1080,10 +1092,11 @@ local function find_archetype_without(
|
|||
): archetype
|
||||
local id_types = node.types
|
||||
local at = table.find(id_types, id)
|
||||
|
||||
if at == nil then
|
||||
return node
|
||||
end
|
||||
local dst = table.clone(id_types)
|
||||
table.remove(dst, at)
|
||||
|
||||
return archetype_ensure(world, dst)
|
||||
end
|
||||
|
||||
|
|
@ -1206,6 +1219,13 @@ local function archetype_destroy(world: world, archetype: archetype)
|
|||
if archetype == world.ROOT_ARCHETYPE then
|
||||
return
|
||||
end
|
||||
-- RAII / idempotent: already destroyed or still has entities → no-op
|
||||
if world.archetypes[archetype.id] ~= archetype then
|
||||
return
|
||||
end
|
||||
if #archetype.entities > 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local component_index = world.component_index
|
||||
local archetype_edges = world.archetype_edges
|
||||
|
|
@ -1751,10 +1771,10 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
|
|||
col6_u = col6
|
||||
col7_u = col7
|
||||
end
|
||||
|
||||
|
||||
local row = i_u
|
||||
i_u -= 1
|
||||
|
||||
|
||||
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row]
|
||||
end
|
||||
else
|
||||
|
|
@ -1773,13 +1793,13 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
|
|||
local col7 = col7_u
|
||||
local ids = ids_u
|
||||
local columns_map = columns_map_u
|
||||
|
||||
|
||||
while e == nil do
|
||||
last_archetype_u += 1
|
||||
local compatible_archetypes = compatible_archetypes_u
|
||||
local archetype = compatible_archetypes[last_archetype_u]
|
||||
archetype_u = archetype
|
||||
|
||||
|
||||
if not archetype then
|
||||
return nil
|
||||
end
|
||||
|
|
@ -1809,18 +1829,18 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
|
|||
col6_u = col6
|
||||
col7_u = col7
|
||||
end
|
||||
|
||||
|
||||
local row = i_u
|
||||
i_u -= 1
|
||||
|
||||
|
||||
for i = 9, ids_len do
|
||||
output[i - 8] = columns_map[ids[i]::any][row]
|
||||
end
|
||||
|
||||
|
||||
return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
query.next = world_query_iter_next
|
||||
return world_query_iter_next
|
||||
end
|
||||
|
|
@ -2431,18 +2451,18 @@ local function query_cached(query: QueryInner)
|
|||
|
||||
return archetypes_map[entityarchetype.id] ~= nil
|
||||
end
|
||||
|
||||
|
||||
local function cached_query_fini()
|
||||
local create_pos = table.find(query_cache_on_create, observer_for_create)
|
||||
if create_pos then
|
||||
if create_pos then
|
||||
table.remove(query_cache_on_create, create_pos)
|
||||
end
|
||||
|
||||
|
||||
local delete_pos = table.find(query_cache_on_delete, observer_for_delete)
|
||||
if delete_pos then
|
||||
if delete_pos then
|
||||
table.remove(query_cache_on_delete, delete_pos)
|
||||
end
|
||||
|
||||
|
||||
compatible_archetypes_u = nil
|
||||
-- NOTE(marcus): Maybe we have to be even more aggressive with cleaning
|
||||
-- things up to ensure it the memory is free`d. But since most of it are
|
||||
|
|
@ -2557,7 +2577,7 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
|
|||
end
|
||||
local from = r.archetype
|
||||
local component_index = world.component_index
|
||||
if not from then
|
||||
if from == world.ROOT_ARCHETYPE then
|
||||
local dst_types = table.clone(ids)
|
||||
table.sort(dst_types)
|
||||
|
||||
|
|
@ -2645,6 +2665,8 @@ local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values:
|
|||
end
|
||||
end
|
||||
|
||||
local ON_REMOVE_STRUCTURAL_WARN = "jecs: on_remove must not perform structural changes (world:add/world:remove); this will be removed as a lint in future versions and can cause silent failures"
|
||||
|
||||
local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 })
|
||||
local entity_index = world.entity_index
|
||||
local r = entity_index_try_get(entity_index, entity)
|
||||
|
|
@ -2653,13 +2675,12 @@ local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 })
|
|||
end
|
||||
local from = r.archetype
|
||||
local component_index = world.component_index
|
||||
if not from then
|
||||
return
|
||||
end
|
||||
|
||||
local remove: { [i53]: boolean } = {}
|
||||
|
||||
local columns_map = from.columns_map
|
||||
|
||||
local dst_types = table.clone(from.types) :: { i53 }
|
||||
|
||||
for i, id in ids do
|
||||
if not columns_map[id] then
|
||||
|
|
@ -2672,22 +2693,15 @@ local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 })
|
|||
local on_remove = idr.on_remove
|
||||
if on_remove then
|
||||
on_remove(entity, id)
|
||||
if from ~= r.archetype then
|
||||
error(ON_REMOVE_STRUCTURAL_WARN)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local to = r.archetype
|
||||
if from ~= to then
|
||||
from = to
|
||||
end
|
||||
|
||||
local dst_types = table.clone(from.types) :: { i53 }
|
||||
|
||||
for id in remove do
|
||||
local at = table.find(dst_types, id)
|
||||
table.remove(dst_types, at)
|
||||
end
|
||||
|
||||
to = archetype_ensure(world, dst_types)
|
||||
local to = archetype_ensure(world, dst_types)
|
||||
|
||||
if from ~= to then
|
||||
entity_move(entity_index, entity, r, to)
|
||||
|
|
@ -2698,12 +2712,12 @@ local function world_new(DEBUG: boolean?)
|
|||
local eindex_dense_array = {} :: { i53 }
|
||||
local eindex_sparse_array = {} :: { record }
|
||||
|
||||
local entity_index = {
|
||||
local entity_index: entityindex = {
|
||||
dense_array = eindex_dense_array,
|
||||
sparse_array = eindex_sparse_array,
|
||||
alive_count = 0,
|
||||
max_id = 0,
|
||||
} :: entityindex
|
||||
}
|
||||
|
||||
-- NOTE(marcus): with the way the component index is accessed, we want to
|
||||
-- ensure that components range has fast access.
|
||||
|
|
@ -2743,31 +2757,6 @@ local function world_new(DEBUG: boolean?)
|
|||
signals = signals,
|
||||
} :: world
|
||||
|
||||
|
||||
local function entity_index_new_id(entity_index: entityindex): i53
|
||||
local alive_count = entity_index.alive_count
|
||||
local max_id = entity_index.max_id
|
||||
|
||||
if alive_count < max_id then
|
||||
alive_count += 1
|
||||
entity_index.alive_count = alive_count
|
||||
local id = eindex_dense_array[alive_count]
|
||||
return id
|
||||
end
|
||||
|
||||
local id = max_id + 1
|
||||
local range_end = entity_index.range_end
|
||||
ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY)
|
||||
|
||||
entity_index.max_id = id
|
||||
alive_count += 1
|
||||
entity_index.alive_count = alive_count
|
||||
eindex_dense_array[alive_count] = id
|
||||
eindex_sparse_array[id] = { dense = alive_count } :: record
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
local ROOT_ARCHETYPE = archetype_create(world, {}, "")
|
||||
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
|
||||
|
|
@ -2897,9 +2886,7 @@ local function world_new(DEBUG: boolean?)
|
|||
return
|
||||
end
|
||||
|
||||
local from: archetype = record.archetype
|
||||
local ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
local src = from or ROOT_ARCHETYPE
|
||||
local src = record.archetype
|
||||
local column = src.columns_map[id]
|
||||
if column then
|
||||
local idr = component_index[id]
|
||||
|
|
@ -2929,9 +2916,9 @@ local function world_new(DEBUG: boolean?)
|
|||
local id_types = src.types
|
||||
if on_remove then
|
||||
on_remove(entity, id_types[cr])
|
||||
src = record.archetype
|
||||
id_types = src.types
|
||||
cr = idr.records[src.id]
|
||||
if src ~= record.archetype then
|
||||
error(ON_REMOVE_STRUCTURAL_WARN)
|
||||
end
|
||||
end
|
||||
|
||||
to = exclusive_traverse_add(src, cr, id)
|
||||
|
|
@ -2954,11 +2941,8 @@ local function world_new(DEBUG: boolean?)
|
|||
if cr then
|
||||
local id_types = src.types
|
||||
on_remove(entity, id_types[cr])
|
||||
local arche = record.archetype
|
||||
if src ~= arche then
|
||||
id_types = arche.types
|
||||
cr = idr.records[arche.id]
|
||||
to = exclusive_traverse_add(arche, cr, id)
|
||||
if src ~= record.archetype then
|
||||
error(ON_REMOVE_STRUCTURAL_WARN)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2977,7 +2961,9 @@ local function world_new(DEBUG: boolean?)
|
|||
idr = component_index[id]
|
||||
end
|
||||
|
||||
if from then
|
||||
local ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
local src_is_root_archetype = src == ROOT_ARCHETYPE
|
||||
if not src_is_root_archetype then
|
||||
-- If there was a previous archetype, then the entity needs to move the archetype
|
||||
inner_entity_move(entity, record, to)
|
||||
else
|
||||
|
|
@ -3004,9 +2990,7 @@ local function world_new(DEBUG: boolean?)
|
|||
return
|
||||
end
|
||||
|
||||
local from = record.archetype
|
||||
local ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
local src = from or ROOT_ARCHETYPE
|
||||
local src = record.archetype
|
||||
if src.columns_map[id] then
|
||||
return
|
||||
end
|
||||
|
|
@ -3028,10 +3012,9 @@ local function world_new(DEBUG: boolean?)
|
|||
local id_types = src.types
|
||||
if on_remove then
|
||||
on_remove(entity, id_types[cr])
|
||||
|
||||
src = record.archetype
|
||||
id_types = src.types
|
||||
cr = idr.records[src.id]
|
||||
if src ~= record.archetype then
|
||||
error(ON_REMOVE_STRUCTURAL_WARN)
|
||||
end
|
||||
end
|
||||
|
||||
to = exclusive_traverse_add(src, cr, id)
|
||||
|
|
@ -3077,7 +3060,9 @@ local function world_new(DEBUG: boolean?)
|
|||
idr = component_index[id]
|
||||
end
|
||||
|
||||
if from then
|
||||
local ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
local src_is_root_archetype = src == ROOT_ARCHETYPE
|
||||
if not src_is_root_archetype then
|
||||
inner_entity_move(entity, record, to)
|
||||
else
|
||||
if #to.types > 0 then
|
||||
|
|
@ -3100,9 +3085,6 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
|
||||
local archetype = record.archetype
|
||||
if not archetype then
|
||||
return nil
|
||||
end
|
||||
|
||||
local columns_map = archetype.columns_map
|
||||
local row = record.row
|
||||
|
|
@ -3160,7 +3142,8 @@ local function world_new(DEBUG: boolean?)
|
|||
table.insert(listeners, fn)
|
||||
return function()
|
||||
local n = #listeners
|
||||
local i = table.find(listeners, fn)
|
||||
local i = table.find(listeners, fn::Listener<any>)
|
||||
assert(i, "Listener not found, maybe you tried to disconnect it twice")
|
||||
listeners[i] = listeners[n]
|
||||
listeners[n] = nil
|
||||
end
|
||||
|
|
@ -3205,7 +3188,8 @@ local function world_new(DEBUG: boolean?)
|
|||
table.insert(listeners, fn)
|
||||
return function()
|
||||
local n = #listeners
|
||||
local i = table.find(listeners, fn)
|
||||
local i = table.find(listeners, fn::Listener<any>)
|
||||
assert(i, "Listener not found, maybe you tried to disconnect it twice")
|
||||
listeners[i] = listeners[n]
|
||||
listeners[n] = nil
|
||||
end
|
||||
|
|
@ -3250,6 +3234,7 @@ local function world_new(DEBUG: boolean?)
|
|||
return function()
|
||||
local n = #listeners
|
||||
local i = table.find(listeners, fn::Listener<any>)
|
||||
assert(i, "Listener not found, maybe you tried to disconnect it twice")
|
||||
listeners[i] = listeners[n]
|
||||
listeners[n] = nil
|
||||
end
|
||||
|
|
@ -3322,10 +3307,13 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
|
||||
local function world_entity(world: world, entity: i53?): i53
|
||||
local sparse_array = eindex_sparse_array
|
||||
local dense_array = eindex_dense_array
|
||||
|
||||
if entity then
|
||||
local index = ECS_ID(entity)
|
||||
local alive_count = entity_index.alive_count
|
||||
local r = eindex_sparse_array[index]
|
||||
local r = sparse_array[index]
|
||||
if r then
|
||||
local dense = r.dense
|
||||
|
||||
|
|
@ -3335,17 +3323,17 @@ local function world_new(DEBUG: boolean?)
|
|||
alive_count += 1
|
||||
entity_index.alive_count = alive_count
|
||||
r.dense = alive_count
|
||||
eindex_dense_array[alive_count] = entity
|
||||
dense_array[alive_count] = entity
|
||||
return entity
|
||||
end
|
||||
|
||||
-- If dense > 0, check if there's an existing entity at that position
|
||||
local existing_entity = eindex_dense_array[dense]
|
||||
local existing_entity = dense_array[dense]
|
||||
if existing_entity and existing_entity ~= entity then
|
||||
alive_count += 1
|
||||
entity_index.alive_count = alive_count
|
||||
r.dense = alive_count
|
||||
eindex_dense_array[alive_count] = entity
|
||||
dense_array[alive_count] = entity
|
||||
return entity
|
||||
end
|
||||
|
||||
|
|
@ -3354,30 +3342,41 @@ local function world_new(DEBUG: boolean?)
|
|||
local max_id = entity_index.max_id
|
||||
|
||||
if index > max_id then
|
||||
-- Pre-populate all intermediate IDs to keep sparse_array as an array
|
||||
for i = max_id + 1, index - 1 do
|
||||
if not eindex_sparse_array[i] then
|
||||
-- NOTE(marcus): We have to do this check to see if
|
||||
-- they exist first because world:range() may have
|
||||
-- pre-populated some slots already.
|
||||
end
|
||||
|
||||
eindex_sparse_array[i] = { dense = 0 } :: record
|
||||
sparse_array[i] = { dense = 0, row = 0, archetype = ROOT_ARCHETYPE }
|
||||
end
|
||||
entity_index.max_id = index
|
||||
end
|
||||
|
||||
alive_count += 1
|
||||
entity_index.alive_count = alive_count
|
||||
eindex_dense_array[alive_count] = entity
|
||||
dense_array[alive_count] = entity
|
||||
|
||||
r = { dense = alive_count } :: record
|
||||
eindex_sparse_array[index] = r
|
||||
r = { dense = alive_count, row = 0, archetype = ROOT_ARCHETYPE }
|
||||
sparse_array[index] = r
|
||||
|
||||
return entity
|
||||
end
|
||||
end
|
||||
return entity_index_new_id(entity_index)
|
||||
|
||||
local alive_count = entity_index.alive_count
|
||||
local max_id = entity_index.max_id
|
||||
local next_count = alive_count + 1
|
||||
if alive_count < max_id then
|
||||
entity = dense_array[next_count]
|
||||
if entity then
|
||||
entity_index.alive_count = next_count
|
||||
return entity
|
||||
end
|
||||
end
|
||||
local id = max_id + 1
|
||||
local range_end = entity_index.range_end
|
||||
ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY)
|
||||
entity_index.max_id = id
|
||||
entity_index.alive_count = next_count
|
||||
dense_array[next_count] = id
|
||||
sparse_array[id] = { dense = next_count, row = 0, archetype = ROOT_ARCHETYPE }
|
||||
return id
|
||||
end
|
||||
|
||||
local function world_remove(world: world, entity: i53, id: i53)
|
||||
|
|
@ -3387,10 +3386,6 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
local from = record.archetype
|
||||
|
||||
if not from then
|
||||
return
|
||||
end
|
||||
|
||||
if from.columns_map[id] then
|
||||
local idr = world.component_index[id]
|
||||
local on_remove = idr.on_remove
|
||||
|
|
@ -3468,7 +3463,6 @@ local function world_new(DEBUG: boolean?)
|
|||
for i = n, 1, -1 do
|
||||
world_delete(world, entities[i])
|
||||
end
|
||||
|
||||
archetype_destroy(world, idr_archetype)
|
||||
end
|
||||
else
|
||||
|
|
@ -3485,13 +3479,10 @@ local function world_new(DEBUG: boolean?)
|
|||
local r = eindex_sparse_array[ECS_ID(e :: number)]
|
||||
local from = r.archetype
|
||||
if from ~= idr_archetype then
|
||||
-- unfortunately the on_remove hook allows a window where `e` can have changed archetype
|
||||
-- this is hypothetically not that expensive of an operation anyways
|
||||
to = archetype_traverse_remove(world, entity, from)
|
||||
end
|
||||
inner_entity_move(e, r, to)
|
||||
end
|
||||
|
||||
archetype_destroy(world, idr_archetype)
|
||||
end
|
||||
else
|
||||
|
|
@ -3504,18 +3495,21 @@ local function world_new(DEBUG: boolean?)
|
|||
local e = entities[i]
|
||||
entity_move(entity_index, e, eindex_sparse_array[ECS_ID(e :: number)], to)
|
||||
end
|
||||
|
||||
archetype_destroy(world, idr_archetype)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if idr_t then
|
||||
local archetype_ids = idr_t.records
|
||||
local to_remove = {}:: { [i53]: componentrecord}
|
||||
|
||||
for archetype_id in archetype_ids do
|
||||
if idr_t then
|
||||
local to_remove = {} :: { [i53]: componentrecord }
|
||||
local cache = idr_t.cache
|
||||
for i = #cache, 1, -1 do
|
||||
local archetype_id = cache[i]
|
||||
local idr_t_archetype = archetypes[archetype_id]
|
||||
if not idr_t_archetype then
|
||||
continue
|
||||
end
|
||||
local idr_t_types = idr_t_archetype.types
|
||||
local entities = idr_t_archetype.entities
|
||||
local deleted_any = false
|
||||
|
|
@ -3525,20 +3519,14 @@ local function world_new(DEBUG: boolean?)
|
|||
if not ECS_IS_PAIR(id) then
|
||||
continue
|
||||
end
|
||||
local object = entity_index_get_alive(
|
||||
entity_index, ECS_PAIR_SECOND(id))
|
||||
local object = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id))
|
||||
if object ~= entity then
|
||||
continue
|
||||
end
|
||||
local id_record = component_index[id]
|
||||
local flags = id_record.flags
|
||||
local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE)
|
||||
if flags_delete_mask then
|
||||
for i = #entities, 1, -1 do
|
||||
local child = entities[i]
|
||||
world_delete(world, child)
|
||||
end
|
||||
deleted_any = true
|
||||
local has_delete = bit32.btest(id_record.flags, ECS_ID_DELETE)
|
||||
if has_delete then
|
||||
deleted_any = true
|
||||
break
|
||||
else
|
||||
to_remove[id] = id_record
|
||||
|
|
@ -3546,60 +3534,50 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
end
|
||||
|
||||
if deleted_any then
|
||||
continue
|
||||
end
|
||||
if deleted_any then
|
||||
for row = #entities, 1, -1 do
|
||||
world_delete(world, entities[row])
|
||||
end
|
||||
archetype_destroy(world, idr_t_archetype)
|
||||
continue
|
||||
end
|
||||
if remove_count == 1 then
|
||||
local id, id_record = next(to_remove)
|
||||
local to = archetype_traverse_remove(world, id::i53, idr_t_archetype)
|
||||
local on_remove = id_record.on_remove
|
||||
for row = #entities, 1, -1 do
|
||||
local child = entities[row]
|
||||
local r = entity_index_try_get_unsafe(child)::record
|
||||
local dst = to
|
||||
if on_remove then
|
||||
on_remove(child, id :: i53)
|
||||
end
|
||||
inner_entity_move(child, r, dst)
|
||||
end
|
||||
archetype_destroy(world, idr_t_archetype)
|
||||
else
|
||||
local dst_types = table.clone(idr_t_archetype.types)
|
||||
for id in to_remove do
|
||||
table.remove(dst_types, table.find(dst_types, id))
|
||||
end
|
||||
|
||||
local dst = archetype_ensure(world, dst_types)
|
||||
|
||||
for row = #entities, 1, -1 do
|
||||
local child = entities[row]
|
||||
local r = entity_index_try_get_unsafe(child) :: record
|
||||
for id, component_record in to_remove do
|
||||
local on_remove = component_record.on_remove
|
||||
if on_remove then
|
||||
on_remove(child, id)
|
||||
end
|
||||
end
|
||||
inner_entity_move(child, r, dst)
|
||||
end
|
||||
archetype_destroy(world, idr_t_archetype)
|
||||
end
|
||||
|
||||
if remove_count == 1 then
|
||||
local id, id_record = next(to_remove)
|
||||
local to_u = archetype_traverse_remove(world, id :: i53, idr_t_archetype)
|
||||
local on_remove = id_record.on_remove
|
||||
for i = #entities, 1, -1 do
|
||||
local child = entities[i]
|
||||
local r = entity_index_try_get_unsafe(child) :: record
|
||||
local to = to_u
|
||||
if on_remove then
|
||||
on_remove(child, id :: i53)
|
||||
local src = r.archetype
|
||||
if src ~= idr_t_archetype then
|
||||
to = archetype_traverse_remove(world, id::i53, src)
|
||||
end
|
||||
end
|
||||
|
||||
inner_entity_move(child, r, to)
|
||||
end
|
||||
elseif remove_count > 1 then
|
||||
local dst_types = table.clone(idr_t_types)
|
||||
for id, component_record in to_remove do
|
||||
table.remove(dst_types, table.find(dst_types, id))
|
||||
end
|
||||
|
||||
local to_u = archetype_ensure(world, dst_types)
|
||||
for i = #entities, 1, -1 do
|
||||
local child = entities[i]
|
||||
local r = entity_index_try_get_unsafe(child) :: record
|
||||
|
||||
local to = to_u
|
||||
for id, component_record in to_remove do
|
||||
local on_remove = component_record.on_remove
|
||||
if on_remove then
|
||||
-- NOTE(marcus): We could be smarter with this and
|
||||
-- assume hooks are deterministic and that they will
|
||||
-- move to the same archetype. However users often are not reasonable people.
|
||||
on_remove(child, id)
|
||||
local src = r.archetype
|
||||
if src ~= idr_t_archetype then
|
||||
to = archetype_traverse_remove(world, id, src)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
inner_entity_move(child, r, to)
|
||||
end
|
||||
end
|
||||
|
||||
table.clear(to_remove)
|
||||
archetype_destroy(world, idr_t_archetype)
|
||||
table.clear(to_remove)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -3654,14 +3632,11 @@ local function world_new(DEBUG: boolean?)
|
|||
local r = entity_index_try_get_unsafe(e) :: record
|
||||
inner_entity_move(e, r, node)
|
||||
end
|
||||
|
||||
archetype_destroy(world, idr_r_archetype)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
local dense = record.dense
|
||||
local i_swap = entity_index.alive_count
|
||||
entity_index.alive_count = i_swap - 1
|
||||
|
|
@ -3669,8 +3644,8 @@ local function world_new(DEBUG: boolean?)
|
|||
local e_swap = eindex_dense_array[i_swap]
|
||||
local r_swap = entity_index_try_get_any(e_swap) :: record
|
||||
r_swap.dense = dense
|
||||
record.archetype = nil :: any
|
||||
record.row = nil :: any
|
||||
record.archetype = ROOT_ARCHETYPE
|
||||
record.row = 0
|
||||
record.dense = i_swap
|
||||
|
||||
eindex_dense_array[dense] = e_swap
|
||||
|
|
@ -3692,12 +3667,16 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
end
|
||||
archetype_delete(world, record.archetype, record.row)
|
||||
record.archetype = nil :: any
|
||||
record.row = nil :: any
|
||||
record.archetype = world.ROOT_ARCHETYPE
|
||||
record.row = 0
|
||||
end
|
||||
|
||||
local function world_exists(world: world, entity: i53): boolean
|
||||
return entity_index_try_get_any(entity) ~= nil
|
||||
local r = entity_index_try_get_any(entity)
|
||||
if not r or r.dense == 0 then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function world_contains(world: world, entity: i53): boolean
|
||||
|
|
@ -3738,7 +3717,7 @@ local function world_new(DEBUG: boolean?)
|
|||
|
||||
return max_component_id
|
||||
end
|
||||
|
||||
|
||||
world.entity = world_entity
|
||||
world.query = world_query :: any
|
||||
world.remove = world_remove
|
||||
|
|
@ -3788,9 +3767,9 @@ local function world_new(DEBUG: boolean?)
|
|||
]], 2)
|
||||
end
|
||||
end
|
||||
|
||||
local function DEBUG_ID_IS_INVALID(id: number)
|
||||
if ECS_IS_PAIR(id) then
|
||||
|
||||
local function DEBUG_ID_IS_INVALID(id: number)
|
||||
if ECS_IS_PAIR(id) then
|
||||
if ECS_ID_IS_WILDCARD(id) then
|
||||
error([[
|
||||
You tried to pass in a wildcard pair. This is strictly
|
||||
|
|
@ -3801,7 +3780,7 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
local first = ecs_pair_first(world, id)
|
||||
local second = ecs_pair_second(world, id)
|
||||
|
||||
|
||||
assert(world:contains(first), `The first element of the pair is invalid because it is not alive in the entity index. You might be holding onto an outdated handle or may have forward declared ids via jecs.component() and jecs.tag(). In the latter case, ensure that their calls precede jecs.world() or otherwise they will not register correctly`)
|
||||
assert(world:contains(second), `The second element of the pair is invalid because it is not alive in the entity index. You might be holding onto an outdated handle or may have forward declared ids via jecs.component() and jecs.tag(). In the latter case, ensure that their calls precede jecs.world() or otherwise they will not register correctly`)
|
||||
else
|
||||
|
|
@ -3848,7 +3827,7 @@ local function world_new(DEBUG: boolean?)
|
|||
end
|
||||
|
||||
for i = 1, EcsRest do
|
||||
entity_index_new_id(entity_index)
|
||||
ENTITY_INDEX_NEW_ID(world)
|
||||
end
|
||||
|
||||
for i = 1, max_component_id do
|
||||
|
|
@ -3884,7 +3863,7 @@ local function world_new(DEBUG: boolean?)
|
|||
world_add(world, EcsOnDeleteTarget, EcsExclusive)
|
||||
|
||||
for i = EcsRest + 1, ecs_max_tag_id do
|
||||
entity_index_new_id(entity_index)
|
||||
ENTITY_INDEX_NEW_ID(world)
|
||||
end
|
||||
|
||||
for i, bundle in ecs_metadata do
|
||||
|
|
@ -3902,7 +3881,7 @@ end
|
|||
|
||||
local function ecs_is_tag(world: world, entity: i53): boolean
|
||||
if ECS_IS_PAIR(entity) then
|
||||
return ecs_is_tag(world, ecs_pair_first(world, entity)) or ecs_is_tag(world, ecs_pair_second(world, entity))
|
||||
return ecs_is_tag(world, ecs_pair_first(world, entity)) and ecs_is_tag(world, ecs_pair_second(world, entity))
|
||||
end
|
||||
local idr = world.component_index[entity]
|
||||
if idr then
|
||||
|
|
@ -3915,7 +3894,7 @@ local function ecs_entity_record(world: world, entity: i53)
|
|||
return entity_index_try_get(world.entity_index, entity)
|
||||
end
|
||||
|
||||
local function entity_index_ensure(entity_index: entityindex, e: i53)
|
||||
local function entity_index_ensure(entity_index: entityindex, e: i53)
|
||||
local eindex_sparse_array = entity_index.sparse_array
|
||||
local eindex_dense_array = entity_index.dense_array
|
||||
local index = ECS_ID(e)
|
||||
|
|
@ -3971,33 +3950,33 @@ local function entity_index_ensure(entity_index: entityindex, e: i53)
|
|||
end
|
||||
|
||||
local function new(world: world)
|
||||
local e = ENTITY_INDEX_NEW_ID(world.entity_index)
|
||||
local e = ENTITY_INDEX_NEW_ID(world)
|
||||
return e
|
||||
end
|
||||
|
||||
local function new_low_id(world: world)
|
||||
local function new_low_id(world: world)
|
||||
local entity_index = world.entity_index
|
||||
|
||||
|
||||
local e = 0
|
||||
if world.max_component_id < HI_COMPONENT_ID then
|
||||
while true do
|
||||
if world.max_component_id < HI_COMPONENT_ID then
|
||||
while true do
|
||||
world.max_component_id += 1
|
||||
e = world.max_component_id
|
||||
if not (entity_index_try_get_any(entity_index, e) ~= nil and e <= HI_COMPONENT_ID) then
|
||||
if not (entity_index_try_get_any(entity_index, e) ~= nil and e <= HI_COMPONENT_ID) then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if e == 0 or e >= HI_COMPONENT_ID then
|
||||
e = ENTITY_INDEX_NEW_ID(entity_index)
|
||||
if e == 0 or e >= HI_COMPONENT_ID then
|
||||
e = ENTITY_INDEX_NEW_ID(world)
|
||||
else
|
||||
entity_index_ensure(entity_index, e)
|
||||
end
|
||||
return e
|
||||
end
|
||||
|
||||
local function new_w_id(world: world, id: i53)
|
||||
local e = ENTITY_INDEX_NEW_ID(world.entity_index)
|
||||
local function new_w_id(world: world, id: i53)
|
||||
local e = ENTITY_INDEX_NEW_ID(world)
|
||||
world.add(world, e, id)
|
||||
return e
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ local mirror = require(ReplicatedStorage.mirror:Clone())
|
|||
return {
|
||||
ParameterGenerator = function()
|
||||
local ecs = jecs.world()
|
||||
ecs:range(1000, 20000)
|
||||
local mcs = mirror.World.new()
|
||||
return ecs, mcs
|
||||
end,
|
||||
|
|
@ -19,14 +18,14 @@ return {
|
|||
Mirror = function(_, ecs, mcs)
|
||||
for i = 1, 100 do
|
||||
|
||||
mcs:entity()
|
||||
local _e = mcs:entity()
|
||||
end
|
||||
end,
|
||||
|
||||
Jecs = function(_, ecs, mcs)
|
||||
for i = 1, 100 do
|
||||
|
||||
ecs:entity()
|
||||
local _e = ecs:entity()
|
||||
end
|
||||
end,
|
||||
},
|
||||
|
|
|
|||
52
test/ob.luau
52
test/ob.luau
|
|
@ -309,19 +309,19 @@ TEST("modules/ob::monitor", function()
|
|||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
||||
|
||||
local count = 0
|
||||
ob.monitor(world:query(A, C)).removed(function()
|
||||
ob.monitor(world:query(A, C)).removed(function()
|
||||
count += 1
|
||||
end)
|
||||
|
||||
|
||||
local e = world:entity()
|
||||
jecs.bulk_insert(world, e, {A, B}, {0,0})
|
||||
CHECK(count==0)
|
||||
world:remove(e, A)
|
||||
CHECK(count==0)
|
||||
end
|
||||
|
||||
|
||||
do CASE [[should not invoke monitor.added callback multiple times in a bulk_move
|
||||
]]
|
||||
local A = world:component()
|
||||
|
|
@ -838,27 +838,47 @@ TEST("modules/ob::monitor", function()
|
|||
do CASE "monitor with wildcard pair should handle bulk_insert"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
local e3 = world:entity()
|
||||
local C = world:component()
|
||||
local Relation = world:component()
|
||||
local entity1 = world:entity()
|
||||
local entity = world:entity()
|
||||
|
||||
local monitor = ob.monitor(world:query(A, jecs.pair(B, jecs.w)))
|
||||
local monitor = ob.monitor(world:query(A, B, C, jecs.pair(Relation, jecs.w)))
|
||||
local c = 0
|
||||
monitor.added(function()
|
||||
c += 1
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, A)
|
||||
CHECK(c == 0)
|
||||
world:add(e, jecs.pair(B, e1))
|
||||
CHECK(c == 1)
|
||||
world:add(e, jecs.pair(B, e2))
|
||||
CHECK(c == 1)
|
||||
world:add(e, jecs.pair(B, e3))
|
||||
jecs.bulk_insert(world, entity, { A, B, C, jecs.pair(Relation, entity1) }, { 1, 2, 3, 4 })
|
||||
CHECK(c == 1)
|
||||
end
|
||||
|
||||
do CASE "monitor with wildcard pair: bulk_remove reports removed exactly once"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local Relation = world:component()
|
||||
local entity1 = world:entity()
|
||||
local entity = world:entity()
|
||||
|
||||
local monitor = ob.monitor(world:query(A, B, C, jecs.pair(Relation, jecs.w)))
|
||||
local added_count = 0
|
||||
local removed_count = 0
|
||||
monitor.added(function()
|
||||
added_count += 1
|
||||
end)
|
||||
monitor.removed(function()
|
||||
removed_count += 1
|
||||
end)
|
||||
|
||||
jecs.bulk_insert(world, entity, { A, B, C, jecs.pair(Relation, entity1) }, { 1, 2, 3, 4 })
|
||||
CHECK(added_count == 1)
|
||||
CHECK(removed_count == 0)
|
||||
|
||||
jecs.bulk_remove(world, entity, { A, B, C, jecs.pair(Relation, entity1) })
|
||||
CHECK(removed_count == 1)
|
||||
end
|
||||
|
||||
do CASE "monitor with multiple pairs should handle separate operations correctly"
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
|
|
|
|||
345
test/tests.luau
345
test/tests.luau
|
|
@ -24,6 +24,174 @@ type Id<T=unknown> = jecs.Id<T>
|
|||
local entity_visualiser = require("@modules/entity_visualiser")
|
||||
local dwi = entity_visualiser.stringify
|
||||
|
||||
-- FOCUS()
|
||||
TEST("Stale to_remove", function()
|
||||
local world = jecs.world()
|
||||
|
||||
local a = world:component()
|
||||
local b = world:component()
|
||||
local c = world:component()
|
||||
local d = world:component()
|
||||
local marker = world:component()
|
||||
|
||||
local target = world:entity()
|
||||
|
||||
local first = world:entity()
|
||||
world:add(first, jecs.pair(a, target))
|
||||
world:add(first, jecs.pair(b, target))
|
||||
world:add(first, jecs.pair(jecs.ChildOf, target))
|
||||
|
||||
local second = world:entity()
|
||||
world:add(second, jecs.pair(c, target))
|
||||
world:add(second, jecs.pair(d, target))
|
||||
world:add(second, marker)
|
||||
|
||||
print("-------")
|
||||
world:delete(target)
|
||||
print("-------")
|
||||
|
||||
CHECK(world:contains(second))
|
||||
CHECK(world:has(second, marker))
|
||||
end)
|
||||
|
||||
-- FOCUS()
|
||||
-- Exercises idr_t multi-remove path: entity has multiple pairs with same target (no cascade); delete target → both pairs removed, on_remove each called.
|
||||
TEST("Target delete: multi-remove path (removing_count > 1)", function()
|
||||
local world = jecs.world()
|
||||
local rel1 = world:entity()
|
||||
local rel2 = world:entity()
|
||||
local tag = world:component()
|
||||
local target = world:entity()
|
||||
local e = world:entity()
|
||||
world:add(e, jecs.pair(rel1, target))
|
||||
world:add(e, jecs.pair(rel2, target))
|
||||
world:add(e, tag)
|
||||
local removed_ids = {}
|
||||
world:removed(rel1, function(_e, id) table.insert(removed_ids, id) end)
|
||||
world:removed(rel2, function(_e, id) table.insert(removed_ids, id) end)
|
||||
world:delete(target)
|
||||
CHECK(world:contains(e))
|
||||
CHECK(world:has(e, tag))
|
||||
CHECK(not world:has(e, jecs.pair(rel1, target)))
|
||||
CHECK(not world:has(e, jecs.pair(rel2, target)))
|
||||
CHECK(#removed_ids == 2)
|
||||
end)
|
||||
|
||||
-- FOCUS()
|
||||
TEST("repro 2", function()
|
||||
local sessionDeletedCount = 0
|
||||
local slotDeletedCount = 0
|
||||
|
||||
for i = 1, 100 do
|
||||
local world = jecs.world(true);
|
||||
|
||||
-- randomness
|
||||
for _ = 1, i % 40 do
|
||||
world:entity()
|
||||
end
|
||||
|
||||
local ofMatch = world:component()
|
||||
local ofTeam = world:component()
|
||||
local ofRound = world:component()
|
||||
local ownedBy = world:component()
|
||||
local nextTeam = world:component()
|
||||
local activeRound = world:component()
|
||||
local team = world:component()
|
||||
local session = world:component()
|
||||
local round = world:component()
|
||||
|
||||
world:add(ofTeam, jecs.Exclusive)
|
||||
world:add(ownedBy, jecs.Exclusive)
|
||||
world:add(nextTeam, jecs.Exclusive)
|
||||
|
||||
world:add(activeRound, jecs.Exclusive)
|
||||
|
||||
local slotEntity = world:entity()
|
||||
local matchEntity = world:entity()
|
||||
world:add(matchEntity, jecs.pair(jecs.ChildOf, slotEntity))
|
||||
|
||||
local teams = {}
|
||||
for t = 1, 2 do
|
||||
local teamEntity = world:entity()
|
||||
world:add(teamEntity, team)
|
||||
world:add(teamEntity, jecs.pair(jecs.ChildOf, matchEntity))
|
||||
world:add(teamEntity, jecs.pair(ofMatch, matchEntity))
|
||||
table.insert(teams, teamEntity)
|
||||
end
|
||||
world:add(matchEntity, jecs.pair(nextTeam, teams[1]))
|
||||
|
||||
local roundEntity = world:entity()
|
||||
world:add(roundEntity, round)
|
||||
world:add(roundEntity, jecs.pair(jecs.ChildOf, matchEntity))
|
||||
|
||||
-- doing something as simple as adding this pair causes error rate to change. When this isn't here, sessionDeletedCount drops from 100% to 80%.
|
||||
world:add(matchEntity, jecs.pair(activeRound, roundEntity))
|
||||
|
||||
local sessions = {}
|
||||
for j = 1, #teams do
|
||||
-- random number of players on team
|
||||
for _ = 1, 1 + (i % 5) do
|
||||
local player = world:entity()
|
||||
|
||||
local sessionEntity = world:entity()
|
||||
world:add(sessionEntity, session)
|
||||
world:add(sessionEntity, jecs.pair(ofMatch, matchEntity))
|
||||
world:add(sessionEntity, jecs.pair(ofTeam, teams[j]))
|
||||
|
||||
-- not adding this next pair makes sessionDeletedCount to drop to 0%??
|
||||
world:add(sessionEntity, jecs.pair(ownedBy, player))
|
||||
world:add(sessionEntity, jecs.pair(ofRound, roundEntity))
|
||||
table.insert(sessions, sessionEntity)
|
||||
end
|
||||
end
|
||||
|
||||
world:delete(matchEntity)
|
||||
|
||||
-- session should stay alive after match deletion
|
||||
for _, entity in sessions do
|
||||
if not world:contains(entity) then
|
||||
sessionDeletedCount += 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not world:contains(slotEntity) then
|
||||
slotDeletedCount += 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
CHECK(sessionDeletedCount == 0)
|
||||
CHECK(slotDeletedCount == 0)
|
||||
|
||||
if (sessionDeletedCount + slotDeletedCount > 0)then
|
||||
print(`repro 2 incorrect session deletion count: {sessionDeletedCount}`)
|
||||
|
||||
-- this has never been above 0, but it's the issue i'm having
|
||||
print(`repro 2 incorrect slot deletion count: {slotDeletedCount}`)
|
||||
end
|
||||
end)
|
||||
TEST("migrating to real records", function()
|
||||
local world = jecs.world(true)
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
print(jecs.record(world, e1).row)
|
||||
world:add(e1, jecs.pair(jecs.ChildOf, e2))
|
||||
world:set(e1, jecs.Name, "hello")
|
||||
CHECK(jecs.record(world, e1).row~=0)
|
||||
CHECK(world:get(e1, jecs.Name)=="hello")
|
||||
CHECK(world:has(e1, jecs.pair(jecs.ChildOf, jecs.Wildcard)))
|
||||
end)
|
||||
|
||||
TEST("e2 is nil", function()
|
||||
local world = jecs.world(true)
|
||||
local e1 = world:entity(1000)
|
||||
local e2 = world:entity()
|
||||
|
||||
CHECK(e1 and world:contains(e1))
|
||||
CHECK(e2 and world:contains(e2))
|
||||
end)
|
||||
-- FOCUS()
|
||||
TEST("reproduce idr_t nil archetype bug", function()
|
||||
local world = jecs.world(true)
|
||||
|
||||
|
|
@ -538,44 +706,6 @@ TEST("world:add()", function()
|
|||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
end
|
||||
|
||||
do CASE "exclusive relations invoke on_remove hooks that should allow side effects"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
|
||||
world:add(A, jecs.Exclusive)
|
||||
local call_count = 0
|
||||
world:set(A, jecs.OnRemove, function(e, id)
|
||||
call_count += 1
|
||||
if call_count == 1 then
|
||||
world:add(e, C)
|
||||
else
|
||||
world:add(e, D)
|
||||
end
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, pair(A, B))
|
||||
world:add(e, pair(A, C))
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, C))
|
||||
|
||||
|
||||
-- We have to ensure that it actually invokes hooks everytime it
|
||||
-- traverses the archetype
|
||||
e = world:entity()
|
||||
world:add(e, pair(A, B))
|
||||
world:add(e, pair(A, C))
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, D))
|
||||
end
|
||||
|
||||
do CASE "idempotent"
|
||||
local world = jecs.world()
|
||||
local d = dwi(world)
|
||||
|
|
@ -598,8 +728,8 @@ TEST("world:add()", function()
|
|||
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)
|
||||
CHECK(d.tbl(e) == world.ROOT_ARCHETYPE)
|
||||
CHECK(d.row(e) == 0)
|
||||
|
||||
local archetypes = #world.archetypes
|
||||
-- This should create a new archetype
|
||||
|
|
@ -653,6 +783,71 @@ TEST("world:children()", function()
|
|||
jecs.ECS_META_RESET()
|
||||
end)
|
||||
|
||||
-- Arauser repro: many parents, only some have routes+checkpoints; delete all parents.
|
||||
-- With the bug: 1 checkpoint can remain and/or world:target returns non-alive parent.
|
||||
-- Scale and structure match arauser so the test FAILS when the bug is present.
|
||||
TEST("ChildOf cascade: world_target must not return non-alive parent", function()
|
||||
local w = jecs.world(true)
|
||||
local ParentTag = w:component()
|
||||
local Anything = w:component()
|
||||
local RouteTag = w:entity()
|
||||
local CheckpointTag = w:entity()
|
||||
local RouteOf = w:entity()
|
||||
|
||||
-- Like arauser: many parents (tiles) with two components, only a subset get routes + checkpoints
|
||||
local nParents = 2000
|
||||
local nWithChildren = 10
|
||||
local parents = {}
|
||||
for i = 1, nParents do
|
||||
local p = w:entity()
|
||||
w:set(p, ParentTag, {
|
||||
row = math.floor((i - 1) / 50) + 1,
|
||||
col = ((i - 1) % 50) + 1,
|
||||
})
|
||||
w:add(p, Anything)
|
||||
parents[i] = p
|
||||
end
|
||||
local used = {}
|
||||
local picked = 0
|
||||
while picked < nWithChildren do
|
||||
local idx = math.random(1, nParents)
|
||||
if not used[idx] then
|
||||
used[idx] = true
|
||||
picked += 1
|
||||
local p = parents[idx]
|
||||
local route = w:entity()
|
||||
w:add(route, RouteTag)
|
||||
w:add(route, pair(ChildOf, p))
|
||||
local checkpoint = w:entity()
|
||||
w:add(checkpoint, CheckpointTag)
|
||||
w:add(checkpoint, pair(ChildOf, p))
|
||||
w:add(checkpoint, pair(RouteOf, route))
|
||||
end
|
||||
end
|
||||
|
||||
-- Delete all parents (collect first to avoid iterator invalidation)
|
||||
local toDelete = {}
|
||||
for e in w:query(ParentTag):iter() do
|
||||
toDelete[#toDelete + 1] = e
|
||||
end
|
||||
for _, e in ipairs(toDelete) do
|
||||
w:delete(e)
|
||||
end
|
||||
|
||||
-- These must hold; with the bug one of them fails (checkpoint remains or parent not alive)
|
||||
local count = 0
|
||||
for checkpoint in w:query(CheckpointTag):iter() do
|
||||
count += 1
|
||||
CHECK(w:contains(checkpoint))
|
||||
local parent = w:target(checkpoint, ChildOf)
|
||||
if parent ~= nil then
|
||||
CHECK(w:contains(parent))
|
||||
end
|
||||
end
|
||||
CHECK(count == 0)
|
||||
jecs.ECS_META_RESET()
|
||||
end)
|
||||
|
||||
-- TEST("world:purge()", function()
|
||||
-- do CASE "should remove all instances of specified component"
|
||||
-- local world = jecs.world()
|
||||
|
|
@ -968,24 +1163,6 @@ TEST("world:delete()", function()
|
|||
-- CHECK(B_OnRemove_called)
|
||||
end
|
||||
|
||||
do CASE "idr_t//remove//on_remove//changed_archetype@3123..3126"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
world:set(A, jecs.OnRemove, function(entity, id)
|
||||
world:set(entity, B, true)
|
||||
end)
|
||||
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
world:add(e2, pair(A, e2))
|
||||
world:set(e2, pair(A, e1), true)
|
||||
|
||||
world:delete(e1)
|
||||
|
||||
CHECK(not world:has(e2, pair(A, e1)))
|
||||
end
|
||||
|
||||
do CASE "pair(OnDelete, Delete)"
|
||||
local world = jecs.world()
|
||||
local ct = world:component()
|
||||
|
|
@ -1528,6 +1705,7 @@ TEST("world:added", function()
|
|||
end
|
||||
end)
|
||||
|
||||
-- FOCUS()
|
||||
TEST("world:range()", function()
|
||||
|
||||
do CASE "spawn entity under min range"
|
||||
|
|
@ -2146,17 +2324,17 @@ TEST("world:query()", function()
|
|||
CHECK(not world:has(e1, B))
|
||||
end
|
||||
end
|
||||
|
||||
do CASE "query:archetypes(override) should create new archetypes list"
|
||||
|
||||
do CASE "query:archetypes(override) should create new archetypes list"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
|
||||
|
||||
local q = world:query(A, B)
|
||||
local e = world:entity()
|
||||
world:set(e, A, false)
|
||||
world:set(e, B, true)
|
||||
|
||||
|
||||
CHECK(q:archetypes() == q:archetypes())
|
||||
CHECK(q:archetypes() ~= q:archetypes(true))
|
||||
end
|
||||
|
|
@ -2835,43 +3013,6 @@ TEST("world:set()", function()
|
|||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
end
|
||||
|
||||
do CASE "exclusive relations invoke on_remove hooks that should allow side effects"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
|
||||
world:add(A, jecs.Exclusive)
|
||||
local call_count = 0
|
||||
world:set(A, jecs.OnRemove, function(e, id)
|
||||
call_count += 1
|
||||
if call_count == 1 then
|
||||
world:set(e, C, true)
|
||||
else
|
||||
world:set(e, D, true)
|
||||
end
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:set(e, pair(A, B), true)
|
||||
world:set(e, pair(A, C), true)
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, C))
|
||||
|
||||
|
||||
-- We have to ensure that it actually invokes hooks everytime it
|
||||
-- traverses the archetype
|
||||
e = world:entity()
|
||||
world:set(e, pair(A, B), true)
|
||||
world:set(e, pair(A, C), true)
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, D))
|
||||
end
|
||||
do CASE "archetype move"
|
||||
local world = jecs.world()
|
||||
|
||||
|
|
@ -2882,8 +3023,8 @@ TEST("world:set()", function()
|
|||
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)
|
||||
CHECK(d.tbl(e) == world.ROOT_ARCHETYPE)
|
||||
CHECK(d.row(e) == 0)
|
||||
|
||||
local archetypes = #world.archetypes
|
||||
-- This should create a new archetype since it is the first
|
||||
|
|
@ -3219,7 +3360,7 @@ TEST("Hooks", function()
|
|||
local B = world:component()
|
||||
local e = world:entity()
|
||||
|
||||
world:set(A, jecs.OnRemove, function(entity)
|
||||
world:set(A, jecs.OnRemove, function(entity: jecs.Entity)
|
||||
world:set(entity, B, true)
|
||||
CHECK(world:get(entity, A))
|
||||
CHECK(world:get(entity, B))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ukendio/jecs"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
license = "MIT"
|
||||
|
|
|
|||
Loading…
Reference in a new issue