Compare commits

..

1 commit

Author SHA1 Message Date
nonamie
39bfee2351
Merge e8575a0db6 into eaafd27280 2024-12-24 19:33:21 -03:00
19 changed files with 590 additions and 841 deletions

View file

@ -2,16 +2,18 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService") local UserInputService = game:GetService("UserInputService")
local jabby = require(ReplicatedStorage.Packages.jabby) local jabby = require(ReplicatedStorage.Packages.jabby)
local std = ReplicatedStorage.std local std = require(ReplicatedStorage.std)
local scheduler = require(std.scheduler) local Scheduler = std.Scheduler
local world = require(std.world) local world = std.world
local function start(modules) local function start(modules)
local scheduler = Scheduler.new(world, require(ReplicatedStorage.std.components))
for _, module in modules do for _, module in modules do
require(module) require(module)(scheduler)
end end
local events = scheduler.COLLECT() local events = scheduler.collect.all()
scheduler.BEGIN(events) scheduler.systems.begin(events)
jabby.set_check_function(function(player) jabby.set_check_function(function(player)
return true return true
end) end)

View file

@ -3,15 +3,15 @@
-- original author @centau -- original author @centau
local FAILURE = -1 local SUCCESS = 0
local RUNNING = 0 local FAILURE = 1
local SUCCESS = 1 local RUNNING = 2
local function SEQUENCE(nodes) local function SEQUENCE(nodes)
return function(...) return function(...)
for _, node in nodes do for _, node in nodes do
local status = node(...) local status = node(...)
if status <= RUNNING then if status == FAILURE or status == RUNNING then
return status return status
end end
end end
@ -23,7 +23,7 @@ local function FALLBACK(nodes)
return function(...) return function(...)
for _, node in nodes do for _, node in nodes do
local status = node(...) local status = node(...)
if status > FAILURE then if status == SUCCESS or status == RUNNING then
return status return status
end end
end end

View file

@ -8,7 +8,7 @@ local components: {
Model: Entity<Model>, Model: Entity<Model>,
Player: Entity, Player: Entity,
Target: Entity, Target: Entity,
Transform: Entity<{ new: CFrame, old: CFrame }>, Transform: Entity<CFrame>,
Velocity: Entity<number>, Velocity: Entity<number>,
Previous: Entity, Previous: Entity,
} = } =
@ -23,8 +23,4 @@ local components: {
Previous = world:component(), Previous = world:component(),
} }
for name, component in components :: {[string]: jecs.Entity} do
world:set(component, jecs.Name, name)
end
return table.freeze(components) return table.freeze(components)

View file

@ -0,0 +1,11 @@
local handle = require(script.Parent.handle)
local world = require(script.Parent.world)
local singleton = world:entity()
local function ctx()
-- Cannot cache handles because they will get invalidated
return handle(singleton)
end
return ctx

View file

@ -0,0 +1,56 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local world = require(script.Parent.world)
type Handle = {
has: (self: Handle, id: jecs.Entity) -> boolean,
get: <T>(self: Handle, id: jecs.Entity<T>) -> T?,
add: <T>(self: Handle, id: jecs.Entity<T>) -> Handle,
set: <T>(self: Handle, id: jecs.Entity<T>, value: T) -> Handle,
id: (self: Handle?) -> jecs.Entity,
}
local handle: (e: jecs.Entity) -> Handle
do
local e
local function has(_, id)
return world:has(e, id)
end
local function get(_, id)
return world:get(e, id)
end
local function set(self, id, value)
world:set(e, id, value)
return self
end
local function add(self, id)
world:add(e, id)
return self
end
local function clear(self)
world:clear(e)
return self
end
local function delete(self)
world:delete(e)
end
local function id()
return e
end
local entity = {
has = has,
get = get,
set = set,
add = add,
clear = clear,
id = id,
}
function handle(id)
e = id
return entity
end
end
return handle

View file

@ -0,0 +1,32 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local function create_cache(hook)
local columns = setmetatable({}, {
__index = function(self, component)
local column = {}
self[component] = column
return column
end,
})
return function(world, component, fn)
local column = columns[component]
table.insert(column, fn)
world:set(component, hook, function(entity, value)
for _, callback in column do
callback(entity, value)
end
end)
end
end
local hooks = {
OnSet = create_cache(jecs.OnSet),
OnAdd = create_cache(jecs.OnAdd),
OnRemove = create_cache(jecs.OnRemove),
}
return hooks

View file

@ -0,0 +1,25 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local world = require(script.world) :: jecs.World
export type World = jecs.World
local Scheduler = require(script.scheduler)
export type Scheduler = Scheduler.Scheduler
local std = {
ChangeTracker = require(script.changetracker),
Scheduler = Scheduler,
bt = require(script.bt),
collect = require(script.collect),
components = require(script.components),
ctx = require(script.ctx),
handle = require(script.handle),
interval = require(script.interval),
ref = require(script.ref),
world = world :: World,
pair = jecs.pair,
__ = jecs.w,
hooks = require(script.hooks),
}
return std

View file

@ -1,14 +0,0 @@
local std = game:GetService("ReplicatedStorage").std
local Players = game:GetService("Players")
local scheduler = require(std.scheduler)
local PHASE = scheduler.PHASE
return {
PlayerAdded = PHASE({
event = Players.PlayerAdded
}),
PlayerRemoved = PHASE({
event = Players.PlayerRemoving
})
}

View file

@ -1,18 +1,16 @@
local handle = require(script.Parent.handle)
local world = require(script.Parent.world) local world = require(script.Parent.world)
local jecs = require(game:GetService("ReplicatedStorage").ecs) local refs = {}
local refs: {[any]: jecs.Entity} = {}
local function fini(key): () -> () local function fini(key)
return function() return function()
refs[key] = nil refs[key] = nil
end end
end end
local function noop() end local function ref(key): (handle.Handle, (() -> ())?)
local function ref(key): (jecs.Entity, () -> ())
if not key then if not key then
return world:entity(), noop return handle(world:entity())
end end
local e = refs[key] local e = refs[key]
if not e then if not e then
@ -20,7 +18,7 @@ local function ref(key): (jecs.Entity, () -> ())
refs[key] = e refs[key] = e
end end
-- Cannot cache handles because they will get invalidated -- Cannot cache handles because they will get invalidated
return e, fini(key) return handle(e), fini(key)
end end
return ref return ref

View file

@ -0,0 +1,31 @@
local reserved = 0
local function reserve()
reserved += 1
return reserved
end
-- If you don't like passing around a world singleton
-- and you need to register component IDs, just register them.
-- I dont use this because I like adding component traits
--[[
local components = {
Model = registry.reserve(),
Transform = registry.reserve(),
}
local world = registry.register(jecs.World.new())
local e = world:entity()
world:set(e, components.Transform, CFrame)
]]
local function register(world)
for _ = 1, reserved do
world:component()
end
return world
end
return {
reserve = reserve,
register = register,
}

View file

@ -1,7 +1,6 @@
--!native --!native
--!optimize 2 --!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local jabby = require(ReplicatedStorage.Packages.jabby) local jabby = require(ReplicatedStorage.Packages.jabby)
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local pair = jecs.pair local pair = jecs.pair
@ -9,7 +8,6 @@ local Name = jecs.Name
type World = jecs.World type World = jecs.World
type Entity<T = nil> = jecs.Entity<T> type Entity<T = nil> = jecs.Entity<T>
type Id<T = unknown> = jecs.Id<T>
type System = { type System = {
callback: (world: World) -> (), callback: (world: World) -> (),
@ -23,92 +21,115 @@ type Events = {
Heartbeat: Systems, Heartbeat: Systems,
} }
local world = require(script.Parent.world) export type Scheduler = {
local Disabled = world:entity() components: {
local System = world:component() :: Id<{ callback: (any) -> (), name: string}> Disabled: Entity,
local DependsOn = world:entity() System: Entity<System>,
local Event = world:component() :: Id<RBXScriptSignal> Phase: Entity,
local Phase = world:entity() DependsOn: Entity,
},
local PreRender = world:entity() collect: {
local Heartbeat = world:entity() under_event: (event: Entity) -> Systems,
local PreAnimation = world:entity() all: () -> Events,
local PreSimulation = world:entity() },
systems: {
begin: (events: Events) -> { [Entity]: thread },
new: (callback: (dt: number) -> (), phase: Entity) -> Entity,
},
phases: {
RenderStepped: Entity,
Heartbeat: Entity,
},
phase: (after: Entity) -> Entity,
debugging: boolean,
}
local scheduler_new: (w: World, components: { [string]: Entity }) -> Scheduler
do
local world: World
local Disabled: Entity
local System: Entity<System>
local DependsOn: Entity
local Phase: Entity
local Event: Entity<RBXScriptSignal>
local scheduler
local RenderStepped
local Heartbeat
local PreAnimation
local PreSimulation
local sys: System local sys: System
local dt: number local dt
local jabby_scheduler = jabby.scheduler.create("Scheduler")
local a, b, c, d
local function run() local function run()
local id = sys.id local id = sys.id
jabby_scheduler:run(id, sys.callback, a, b, c, d) scheduler:run(id, sys.callback, dt)
return nil
end end
world:add(Heartbeat, Phase) local function panic(str)
world:set(Heartbeat, Event, RunService.Heartbeat) -- We don't want to interrupt the loop when we error
task.spawn(error, str)
world:add(PreSimulation, Phase)
world:set(PreSimulation, Event, RunService.PreSimulation)
world:add(PreAnimation, Phase)
world:set(PreAnimation, Event, RunService.PreAnimation)
table.insert(jabby.public, {
class_name = "World",
name = "MyWorld",
world = world,
debug = Name,
entities = {},
})
jabby.public.updated = true
table.insert(jabby.public, jabby_scheduler)
if RunService:IsClient() then
world:add(PreRender, Phase)
world:set(PreRender, Event, (RunService :: RunService).PreRender)
end end
local function begin(events: { Systems })
local function begin(events: { [RBXScriptSignal]: Systems })
local connections = {} local connections = {}
for event, systems in events do for event, systems in events do
if not event then if not event then
continue continue
end end
local event_name = tostring(event) local event_name = tostring(event)
connections[event] = event:Connect(function(...) connections[event] = event:Connect(function(delta)
debug.profilebegin(event_name) debug.profilebegin(event_name)
for _, s in systems do for _, s in systems do
sys = s sys = s
a, b, c, d = ... dt = delta
local didNotYield, why = xpcall(function()
for _ in run do for _ in run do
break break
end end
end, debug.traceback)
if didNotYield then
continue
end
if string.find(why, "thread is not yieldable") then
panic(
"Not allowed to yield in the systems."
.. "\n"
.. "System: "
.. debug.info(s.callback, "n")
.. " has been ejected"
)
continue
end
panic(why)
end end
debug.profileend() debug.profileend()
end) end)
end end
return connections return threads
end end
local function scheduler_collect_systems_under_phase_recursive(systems, phase: Entity) local function scheduler_collect_systems_under_phase_recursive(systems, phase)
local phase_name = world:get(phase, Name) local phase_name = world:get(phase, Name)
for _, s in world:query(System):with(pair(DependsOn, phase)) do for _, s in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, { table.insert(systems, {
id = jabby_scheduler:register_system({ id = scheduler:register_system({
name = s.name, name = s.name,
phase = phase_name, phase = phase_name,
} :: any), }),
callback = s.callback, callback = s.callback,
}) })
end end
for after in world:query(Phase):with(pair(DependsOn, phase)):iter() do for after in world:query(Phase):with(pair(DependsOn, phase)) do
scheduler_collect_systems_under_phase_recursive(systems, after) scheduler_collect_systems_under_phase_recursive(systems, after)
end end
end end
@ -127,41 +148,99 @@ local function scheduler_collect_systems_all()
return events return events
end end
local function scheduler_phase_new(d: { after: Entity?, event: RBXScriptSignal? }) local function scheduler_phase_new(after)
local phase = world:entity() local phase = world:entity()
world:add(phase, Phase) world:add(phase, Phase)
local after = d.after local dependency = pair(DependsOn, after)
if after then
local dependency = pair(DependsOn, after :: Entity)
world:add(phase, dependency) world:add(phase, dependency)
end
local event = d.event
if event then
world:set(phase, Event, event)
end
return phase return phase
end end
local function scheduler_systems_new(callback: (any) -> (), phase: Entity?) local function scheduler_systems_new(callback, phase)
local system = world:entity() local system = world:entity()
world:set(system, System, { callback = callback, name = debug.info(callback, "n") }) local name = debug.info(callback, "n")
local depends_on = DependsOn :: jecs.Entity world:set(system, System, { callback = callback, name = name })
local p: Entity = phase or Heartbeat world:add(system, pair(DependsOn, phase))
world:add(system, pair(depends_on, p))
return system return system
end end
function scheduler_new(w: World, components: { [string]: Entity })
world = w
Disabled = world:component()
System = world:component()
Phase = world:component()
DependsOn = world:component()
Event = world:component()
RenderStepped = world:component()
Heartbeat = world:component()
PreSimulation = world:component()
PreAnimation = world:component()
local RunService = game:GetService("RunService")
if RunService:IsClient() then
world:add(RenderStepped, Phase)
world:set(RenderStepped, Event, RunService.RenderStepped)
end
world:add(Heartbeat, Phase)
world:set(Heartbeat, Event, RunService.Heartbeat)
world:add(PreSimulation, Phase)
world:set(PreSimulation, Event, RunService.PreSimulation)
world:add(PreAnimation, Phase)
world:set(PreAnimation, Event, RunService.PreAnimation)
for name, component in components do
world:set(component, Name, name)
end
table.insert(jabby.public, {
class_name = "World",
name = "MyWorld",
world = world,
debug = Name,
entities = {},
})
jabby.public.updated = true
scheduler = jabby.scheduler.create("scheduler")
table.insert(jabby.public, scheduler)
return { return {
SYSTEM = scheduler_systems_new, phase = scheduler_phase_new,
BEGIN = begin,
PHASE = scheduler_phase_new,
COLLECT = scheduler_collect_systems_all,
phases = { phases = {
Heartbeat = Heartbeat, RenderStepped = RenderStepped,
PreSimulation = PreSimulation, PreSimulation = PreSimulation,
Heartbeat = Heartbeat,
PreAnimation = PreAnimation, PreAnimation = PreAnimation,
PreRender = PreRender },
world = world,
components = {
DependsOn = DependsOn,
Disabled = Disabled,
Phase = Phase,
System = System,
},
collect = {
under_event = scheduler_collect_systems_under_event,
all = scheduler_collect_systems_all,
},
systems = {
new = scheduler_systems_new,
begin = begin,
},
} }
end
end
return {
new = scheduler_new,
} }

View file

@ -6,12 +6,13 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(game:GetService("ServerScriptService").net) local blink = require(game:GetService("ServerScriptService").net)
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local __ = jecs.Wildcard local __ = jecs.Wildcard
local std = ReplicatedStorage.std
local ref = require(std.ref)
local interval = require(std.interval)
local world = require(std.world) local std = require(ReplicatedStorage.std)
local cts = require(std.components) local ref = std.ref
local interval = std.interval
local world: std.World = std.world
local cts = std.components
local Mob = cts.Mob local Mob = cts.Mob
local Transform = cts.Transform local Transform = cts.Transform
@ -19,26 +20,13 @@ local Velocity = cts.Velocity
local Player = cts.Player local Player = cts.Player
local Character = cts.Character local Character = cts.Character
local characters = world
:query(Character)
:with(Player)
:cached()
local moving_mobs = world
:query(Transform, Velocity)
:with(Mob)
:cached()
local function mobsMove(dt: number) local function mobsMove(dt: number)
local targets = {} local targets = {}
for _, character in world:query(Character):with(Player):iter() do
for _, character in characters do
table.insert(targets, (character.PrimaryPart :: Part).Position) table.insert(targets, (character.PrimaryPart :: Part).Position)
end end
for mob, transform, v in moving_mobs do for mob, transform, v in world:query(Transform, Velocity):with(Mob):iter() do
local cf = transform.new local cf = transform.new
local p = cf.Position local p = cf.Position
@ -71,18 +59,15 @@ local function spawnMobs()
local cf = CFrame.new(p) local cf = CFrame.new(p)
local v = 5 local v = 5
local e = world:entity() local id = ref():set(Velocity, v):set(Transform, { new = cf }):add(Mob).id()
world:set(e, Velocity, v)
world:set(e, Transform, { new = cf })
world:add(e, Mob)
blink.SpawnMob.FireAll(e, cf, v) blink.SpawnMob.FireAll(id, cf, v)
end end
end end
local scheduler = require(std.scheduler) return function(scheduler: std.Scheduler)
local phases = scheduler.phases
scheduler.SYSTEM(spawnMobs) local system_new = scheduler.systems.new
scheduler.SYSTEM(mobsMove) system_new(mobsMove, phases.Heartbeat)
system_new(spawnMobs, phases.Heartbeat)
return 0 end

View file

@ -1,40 +1,43 @@
local Players = game:GetService("Players") local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = ReplicatedStorage.std local std = require(ReplicatedStorage.std)
local ref = require(std.ref) local ref = std.ref
local collect = require(std.collect) local collect = std.collect
local cts = require(std.components) local cts = std.components
local world = require(std.world)
local Player = cts.Player local Player = cts.Player
local Character = cts.Character local Character = cts.Character
local playersAdded = collect(Players.PlayerAdded)
local playersRemoved = collect(Players.PlayerRemoving)
local world: std.World = std.world
local conn = {} local conn = {}
local function playersAdded(player: Player) local function players()
local e = ref(player.UserId) for _, player in playersAdded do
world:set(e, Player, player) world:set(std.world:entity(), cts.Transform)
local e = ref(player.UserId):set(Player, player)
local characterAdd = player.CharacterAdded local characterAdd = player.CharacterAdded
conn[e] = characterAdd:Connect(function(rig) conn[e.id()] = characterAdd:Connect(function(rig)
while rig.Parent ~= workspace do while rig.Parent ~= workspace do
task.wait() task.wait()
end end
world:set(e, Character, rig) e:set(Character, rig)
end) end)
end end
local function playersRemoved(player: Player) for _, player in playersRemoved do
local e = ref(player.UserId) local id = ref(player.UserId):clear().id()
world:clear(e) conn[id]:Disconnect()
local connection = conn[e] conn[id] = nil
connection:Disconnect() end
conn[e] = nil
end end
local scheduler = require(std.scheduler) return function(scheduler: std.Scheduler)
local phases = require(std.phases) local phases = scheduler.phases
scheduler.SYSTEM(playersAdded, phases.PlayerAdded) local system_new = scheduler.systems.new
scheduler.SYSTEM(playersRemoved, phases.PlayerRemoved) system_new(players, phases.Heartbeat)
end
return 0

View file

@ -1,22 +1,20 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(ReplicatedStorage.net) local blink = require(ReplicatedStorage.net)
local std = ReplicatedStorage.std local std = require(ReplicatedStorage.std)
local world = require(std.world) local world = std.world
local ref = require(std.ref) local ref = std.ref
local cts = require(std.components) local cts = std.components
local Model = cts.Model local Model = cts.Model
local Transform = cts.Transform local Transform = cts.Transform
local moving_models = world:query(Transform, Model):cached()
local function move(dt: number) local function move(dt: number)
for _, transform, model in moving_models do for _, transform, model in world:query(Transform, Model):iter() do
local cf = transform.new local cf = transform.new
if cf ~= transform.old then if cf ~= transform.old then
local part = model.PrimaryPart :: BasePart local origo = model.PrimaryPart.CFrame
local origo = part.CFrame model.PrimaryPart.CFrame = origo:Lerp(cf, 1)
part.CFrame = origo:Lerp(cf, 1)
transform.old = cf transform.old = cf
end end
end end
@ -24,8 +22,8 @@ end
local function syncTransforms() local function syncTransforms()
for _, id, cf in blink.UpdateTransform.Iter() do for _, id, cf in blink.UpdateTransform.Iter() do
local e = ref("server-" .. tostring(id)) local e = ref("server-" .. id)
local transform = world:get(e, Transform) local transform = e:get(cts.Transform)
if not transform then if not transform then
continue continue
end end
@ -33,9 +31,9 @@ local function syncTransforms()
end end
end end
local scheduler = require(std.scheduler) return function(scheduler: std.Scheduler)
local phases = scheduler.phases
scheduler.SYSTEM(move) local system_new = scheduler.systems.new
scheduler.SYSTEM(syncTransforms) system_new(move, phases.Heartbeat)
system_new(syncTransforms, phases.RenderStepped)
return 0 end

View file

@ -1,9 +1,9 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(ReplicatedStorage.net) local blink = require(ReplicatedStorage.net)
local std = ReplicatedStorage.std local std = require(ReplicatedStorage.std)
local ref = require(std.ref) local ref = std.ref
local world = require(std.world) local world = std.world
local cts = require(std.components) local cts = std.components
local function syncMobs() local function syncMobs()
for _, id, cf, vel in blink.SpawnMob.Iter() do for _, id, cf, vel in blink.SpawnMob.Iter() do
@ -16,16 +16,16 @@ local function syncMobs()
part.Parent = model part.Parent = model
model.Parent = workspace model.Parent = workspace
local e = ref("server-" .. tostring(id)) ref("server-" .. id)
world:set(e, cts.Transform, { new = cf, old = cf }) :set(cts.Transform, { new = cf, old = cf })
world:set(e, cts.Velocity, vel) :set(cts.Velocity, vel)
world:set(e, cts.Model, model) :set(cts.Model, model)
world:add(e, cts.Mob) :add(cts.Mob)
end end
end end
local scheduler = require(std.scheduler) return function(scheduler: std.Scheduler)
scheduler.SYSTEM(syncMobs) local phases = scheduler.phases
local system_new = scheduler.systems.new
return 0 system_new(syncMobs, phases.RenderStepped)
end

View file

@ -1,44 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = ReplicatedStorage.std
local world = require(std.world)
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local function flip()
return math.random() >= 0.15
end
for i = 1, 2^8 do
local e = world:entity()
if flip() then
world:set(e, A, true)
end
if flip() then
world:set(e, B, true)
end
if flip() then
world:set(e, C, true)
end
if flip() then
world:set(e, D, true)
end
end
local function uncached()
for _ in world:query(A, B, C, D) do
end
end
local q = world:query(A, B, C, D):cached()
local function cached()
for _ in q do
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(uncached)
scheduler.SYSTEM(cached)
return 0

548
jecs.luau
View file

@ -89,9 +89,7 @@ local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
local EcsDelete = HI_COMPONENT_ID + 9 local EcsDelete = HI_COMPONENT_ID + 9
local EcsRemove = HI_COMPONENT_ID + 10 local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11 local EcsName = HI_COMPONENT_ID + 11
local EcsArchetypeCreate = HI_COMPONENT_ID + 12 local EcsRest = HI_COMPONENT_ID + 12
local EcsArchetypeDelete = HI_COMPONENT_ID + 13
local EcsRest = HI_COMPONENT_ID + 14
local ECS_PAIR_FLAG = 0x8 local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10 local ECS_ID_FLAGS_MASK = 0x10
@ -170,7 +168,7 @@ local function _STRIP_GENERATION(e: i53): i24
end end
local function ECS_PAIR(pred: i53, obj: i53): i53 local function ECS_PAIR(pred: i53, obj: i53): i53
return ECS_COMBINE(ECS_ENTITY_T_LO(pred), ECS_ENTITY_T_LO(obj)) + FLAGS_ADD(--[[isPair]] true) :: i53 return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + FLAGS_ADD(--[[isPair]] true) :: i53
end end
local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record? local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record?
@ -244,42 +242,12 @@ end
-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits -- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits
local function ecs_pair_first(world, e) local function ecs_pair_first(world, e)
return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_HI(e))
end end
-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits -- ECS_PAIR_SECOND gets the relationship / pred / LOW bits
local function ecs_pair_second(world, e) local function ecs_pair_second(world, e)
return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_HI(e)) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e))
end
local function query_match(query, archetype: Archetype)
local records = archetype.records
local with = query.filter_with
for _, id in with do
if not records[id] then
return false
end
end
local without = query.filter_without
if without then
for _, id in without do
if records[id] then
return false
end
end
end
return true
end
local function find_observers(world: World, event, component): { Observer }?
local cache = world.observerable[event]
if not cache then
return nil
end
return cache[component] :: any
end end
local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24)
@ -581,20 +549,6 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?)
local columns = (table.create(length) :: any) :: { Column } local columns = (table.create(length) :: any) :: { Column }
local records: { ArchetypeRecord } = {} local records: { ArchetypeRecord } = {}
local archetype: Archetype = {
columns = columns,
entities = {},
id = archetype_id,
records = records,
type = ty,
types = id_types,
add = {},
remove = {},
refs = {} :: GraphEdge,
}
for i, componentId in id_types do for i, componentId in id_types do
local idr = id_record_ensure(world, componentId) local idr = id_record_ensure(world, componentId)
archetype_append_to_records(idr, archetype_id, records, componentId, i) archetype_append_to_records(idr, archetype_id, records, componentId, i)
@ -618,17 +572,18 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?)
end end
end end
for _, id in id_types do local archetype: Archetype = {
local observer_list = find_observers(world, EcsArchetypeCreate, id) columns = columns,
if not observer_list then entities = {},
continue id = archetype_id,
end records = records,
for _, observer in observer_list do type = ty,
if query_match(observer.query, archetype) then types = id_types,
observer.callback(archetype)
end add = {},
end remove = {},
end refs = {} :: GraphEdge,
}
world.archetypeIndex[ty] = archetype world.archetypeIndex[ty] = archetype
world.archetypes[archetype_id] = archetype world.archetypes[archetype_id] = archetype
@ -671,13 +626,13 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number
end end
local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype
local id_types = node.types local types = node.types
-- Component IDs are added incrementally, so inserting and sorting -- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion -- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array. -- point in the types array.
local dst = table.clone(node.types) :: { i53 } local dst = table.clone(node.types) :: { i53 }
local at = find_insert(id_types, id) local at = find_insert(types, id)
if at == -1 then if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it -- If it finds a duplicate, it just means it is the same archetype so it can return it
-- directly instead of needing to hash types for a lookup to the archetype. -- directly instead of needing to hash types for a lookup to the archetype.
@ -689,13 +644,13 @@ local function find_archetype_with(world: World, node: Archetype, id: i53): Arch
end end
local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype
local id_types = node.types local types = node.types
local at = table.find(id_types, id) local at = table.find(types, id)
if at == nil then if at == nil then
return node return node
end end
local dst = table.clone(id_types) local dst = table.clone(types)
table.remove(dst, at) table.remove(dst, at)
return archetype_ensure(world, dst) return archetype_ensure(world, dst)
@ -1051,18 +1006,6 @@ local function archetype_destroy(world: World, archetype: Archetype)
world.archetypeIndex[archetype.type] = nil :: any world.archetypeIndex[archetype.type] = nil :: any
local records = archetype.records local records = archetype.records
for id in records do
local observer_list = find_observers(world, EcsArchetypeDelete, id)
if not observer_list then
continue
end
for _, observer in observer_list do
if query_match(observer.query, archetype) then
observer.callback(archetype)
end
end
end
for id in records do for id in records do
local idr = component_index[id] local idr = component_index[id]
idr.cache[archetype_id] = nil :: any idr.cache[archetype_id] = nil :: any
@ -1121,30 +1064,23 @@ do
local idr = component_index[delete] local idr = component_index[delete]
if idr then if idr then
local children = {}
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]
for i, child in idr_archetype.entities do
table.insert(children, child)
end
end
local flags = idr.flags local flags = idr.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for archetype_id in idr.cache do for _, child in children do
local idr_archetype = archetypes[archetype_id] -- Cascade deletion to children
world_delete(world, child)
local entities = idr_archetype.entities
local n = #entities
for i = n, 1, -1 do
world_delete(world, entities[i])
end
end end
else else
for archetype_id in idr.cache do for _, child in children do
local idr_archetype = archetypes[archetype_id] world_remove(world, child, delete)
local entities = idr_archetype.entities
local n = #entities
for i = n, 1, -1 do
world_remove(world, entities[i], delete)
end
end
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]
archetype_destroy(world, idr_archetype)
end end
end end
end end
@ -1231,7 +1167,7 @@ local EMPTY_QUERY = {
setmetatable(EMPTY_QUERY, EMPTY_QUERY) setmetatable(EMPTY_QUERY, EMPTY_QUERY)
local function query_iter_init(query: QueryInner): () -> (number, ...any) local function query_iter_init(query): () -> (number, ...any)
local world_query_iter_next local world_query_iter_next
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query.compatible_archetypes
@ -1310,7 +1246,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
records = archetype.records local records = archetype.records
a = columns[records[A].column] a = columns[records[A].column]
end end
@ -1333,7 +1269,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
records = archetype.records local records = archetype.records
a = columns[records[A].column] a = columns[records[A].column]
b = columns[records[B].column] b = columns[records[B].column]
end end
@ -1357,7 +1293,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
records = archetype.records local records = archetype.records
a = columns[records[A].column] a = columns[records[A].column]
b = columns[records[B].column] b = columns[records[B].column]
c = columns[records[C].column] c = columns[records[C].column]
@ -1382,7 +1318,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
records = archetype.records local records = archetype.records
a = columns[records[A].column] a = columns[records[A].column]
b = columns[records[B].column] b = columns[records[B].column]
c = columns[records[C].column] c = columns[records[C].column]
@ -1409,7 +1345,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
records = archetype.records local records = archetype.records
if not F then if not F then
a = columns[records[A].column] a = columns[records[A].column]
@ -1478,64 +1414,65 @@ local function query_iter(query): () -> (number, ...any)
return query_next return query_next
end end
local function query_without(query: QueryInner, ...: i53) local function query_without(query: { compatible_archetypes: { Archetype } }, ...)
local without = { ... }
query.filter_without = without
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query.compatible_archetypes
local N = select("#", ...)
for i = #compatible_archetypes, 1, -1 do for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i] local archetype = compatible_archetypes[i]
local records = archetype.records local records = archetype.records
local matches = true local shouldRemove = false
for _, id in without do for j = 1, N do
local id = select(j, ...)
if records[id] then if records[id] then
matches = false shouldRemove = true
break break
end end
end end
if matches then if shouldRemove then
continue
end
local last = #compatible_archetypes local last = #compatible_archetypes
if last ~= i then if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last] compatible_archetypes[i] = compatible_archetypes[last]
end end
compatible_archetypes[last] = nil :: any compatible_archetypes[last] = nil :: any
end end
end
if #compatible_archetypes == 0 then
return EMPTY_QUERY
end
return query :: any return query :: any
end end
local function query_with(query: QueryInner, ...: i53) local function query_with(query: { compatible_archetypes: { Archetype } }, ...)
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query.compatible_archetypes
local with = { ... } local N = select("#", ...)
query.filter_with = with
for i = #compatible_archetypes, 1, -1 do for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i] local archetype = compatible_archetypes[i]
local records = archetype.records local records = archetype.records
local matches = true local shouldRemove = false
for _, id in with do for j = 1, N do
local id = select(j, ...)
if not records[id] then if not records[id] then
matches = false shouldRemove = true
break break
end end
end end
if matches then if shouldRemove then
continue
end
local last = #compatible_archetypes local last = #compatible_archetypes
if last ~= i then if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last] compatible_archetypes[i] = compatible_archetypes[last]
end end
compatible_archetypes[last] = nil :: any compatible_archetypes[last] = nil :: any
end end
end
if #compatible_archetypes == 0 then
return EMPTY_QUERY
end
return query :: any return query :: any
end end
@ -1546,311 +1483,6 @@ local function query_archetypes(query)
return query.compatible_archetypes return query.compatible_archetypes
end end
local function query_cached(query: QueryInner)
local archetypes = query.compatible_archetypes
local world = query.world :: World
-- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively
-- because the event will be emitted for all components of that Archetype.
local first = query.ids[1]
local observerable = world.observerable
local on_create_action = observerable[EcsArchetypeCreate]
if not on_create_action then
on_create_action = {}
observerable[EcsArchetypeCreate] = on_create_action
end
local query_cache_on_create = on_create_action[first]
if not query_cache_on_create then
query_cache_on_create = {}
on_create_action[first] = query_cache_on_create
end
local on_delete_action = observerable[EcsArchetypeDelete]
if not on_delete_action then
on_delete_action = {}
observerable[EcsArchetypeDelete] = on_delete_action
end
local query_cache_on_delete = on_delete_action[first]
if not query_cache_on_delete then
query_cache_on_delete = {}
on_delete_action[first] = query_cache_on_delete
end
local function on_create_callback(archetype)
table.insert(archetypes, archetype)
end
local function on_delete_callback(archetype)
local i = table.find(archetypes, archetype) :: number
local n = #archetypes
archetypes[i] = archetypes[n]
archetypes[n] = nil
end
local with = query.filter_with
local ids = query.ids
if with then
table.move(ids, 1, #ids, #with, with)
else
query.filter_with = ids
end
local observer_for_create = { query = query, callback = on_create_callback }
local observer_for_delete = { query = query, callback = on_delete_callback }
table.insert(query_cache_on_create, observer_for_create)
table.insert(query_cache_on_delete, observer_for_delete)
local compatible_archetypes = query.compatible_archetypes
local lastArchetype = 1
local A, B, C, D, E, F, G, H, I = unpack(ids)
local a: Column, b: Column, c: Column, d: Column
local e: Column, f: Column, g: Column, h: Column
local world_query_iter_next
local columns: { Column }
local entities: { i53 }
local i: number
local archetype: Archetype
local records: { ArchetypeRecord }
if not B then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
records = archetype.records
a = columns[records[A].column]
end
local row = i
i -= 1
return entityId, a[row]
end
elseif not C then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
a = columns[records[A].column]
b = columns[records[B].column]
end
local row = i
i -= 1
return entityId, a[row], b[row]
end
elseif not D then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
end
local row = i
i -= 1
return entityId, a[row], b[row], c[row]
end
elseif not E then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
end
local row = i
i -= 1
return entityId, a[row], b[row], c[row], d[row]
end
else
local queryOutput = {}
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
if not F then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
elseif not G then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
elseif not H then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
elseif not I then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
h = columns[records[H].column]
end
end
local row = i
i -= 1
if not F then
return entityId, a[row], b[row], c[row], d[row], e[row]
elseif not G then
return entityId, a[row], b[row], c[row], d[row], e[row], f[row]
elseif not H then
return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row]
elseif not I then
return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row]
end
local records = archetype.records
for j, id in ids do
queryOutput[j] = columns[records[id].column][row]
end
return entityId, unpack(queryOutput)
end
end
local function cached_query_iter()
lastArchetype = 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return NOOP
end
entities = archetype.entities
i = #entities
records = archetype.records
columns = archetype.columns
if not B then
a = columns[records[A].column]
elseif not C then
a = columns[records[A].column]
b = columns[records[B].column]
elseif not D then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
elseif not E then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
elseif not F then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
elseif not G then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
elseif not H then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
elseif not I then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
h = columns[records[H].column]
end
return world_query_iter_next
end
local cached_query = query :: any
cached_query.archetypes = query_archetypes
cached_query.__iter = cached_query_iter
cached_query.iter = cached_query_iter
setmetatable(cached_query, cached_query)
return cached_query
end
local Query = {} local Query = {}
Query.__index = Query Query.__index = Query
Query.__iter = query_iter Query.__iter = query_iter
@ -1858,7 +1490,6 @@ Query.iter = query_iter_init
Query.without = query_without Query.without = query_without
Query.with = query_with Query.with = query_with
Query.archetypes = query_archetypes Query.archetypes = query_archetypes
Query.cached = query_cached
local function world_query(world: World, ...) local function world_query(world: World, ...)
local compatible_archetypes = {} local compatible_archetypes = {}
@ -1871,16 +1502,10 @@ local function world_query(world: World, ...)
local idr: IdRecord? local idr: IdRecord?
local componentIndex = world.componentIndex local componentIndex = world.componentIndex
local q = setmetatable({
ids = ids,
compatible_archetypes = compatible_archetypes,
world = world,
}, Query)
for _, id in ids do for _, id in ids do
local map = componentIndex[id] local map = componentIndex[id]
if not map then if not map then
return q return EMPTY_QUERY
end end
if idr == nil or map.size < idr.size then if idr == nil or map.size < idr.size then
@ -1889,7 +1514,7 @@ local function world_query(world: World, ...)
end end
if not idr then if not idr then
return q return EMPTY_QUERY
end end
for archetype_id in idr.cache do for archetype_id in idr.cache do
@ -1917,6 +1542,15 @@ local function world_query(world: World, ...)
compatible_archetypes[length] = compatibleArchetype compatible_archetypes[length] = compatibleArchetype
end end
if length == 0 then
return EMPTY_QUERY
end
local q = setmetatable({
compatible_archetypes = compatible_archetypes,
ids = ids,
}, Query) :: any
return q return q
end end
@ -2102,7 +1736,6 @@ function World.new()
nextComponentId = 0 :: number, nextComponentId = 0 :: number,
nextEntityId = 0 :: number, nextEntityId = 0 :: number,
ROOT_ARCHETYPE = (nil :: any) :: Archetype, ROOT_ARCHETYPE = (nil :: any) :: Archetype,
observerable = {},
}, World) :: any }, World) :: any
self.ROOT_ARCHETYPE = archetype_create(self, {}, "") self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
@ -2148,7 +1781,7 @@ type function ecs_entity_t(entity)
return entity:components()[2]:readproperty(types.singleton("__T")) return entity:components()[2]:readproperty(types.singleton("__T"))
end end
type function Pair(first, second) export type function Pair(first, second)
local thing = first:components()[2] local thing = first:components()[2]
if thing:readproperty(types.singleton("__T")):is("nil") then if thing:readproperty(types.singleton("__T")):is("nil") then
@ -2164,38 +1797,15 @@ export type Entity<T = unknown> = number & { __T: T }
type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...) type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
export type Query<T...> = typeof(setmetatable({}, { type Query<T...> = typeof(setmetatable({}, {
__iter = (nil :: any) :: Iter<T...>, __iter = (nil :: any) :: Iter<T...>,
})) & { })) & {
iter: Iter<T...>, iter: Iter<T...>,
with: (self: Query<T...>, ...Id) -> Query<T...>, with: (self: Query<T...>, ...i53) -> Query<T...>,
without: (self: Query<T...>, ...Id) -> Query<T...>, without: (self: Query<T...>, ...i53) -> Query<T...>,
archetypes: (self: Query<T...>) -> { Archetype }, archetypes: (self: Query<T...>) -> { Archetype },
cached: (self: Query<T...>) -> Query<T...>,
} }
type QueryInner = {
compatible_archetypes: { Archetype },
filter_with: { i53 }?,
filter_without: { i53 }?,
ids: { i53 },
world: {}, -- Downcasted to be serializable by the analyzer
next: () -> Item<any>
}
type Observer = {
callback: (archetype: Archetype) -> (),
query: QueryInner,
}
type function ecs_partial_t(ty)
local output = types.newtable()
for k, v in ty:properties() do
output:setproperty(k, types.unionof(v.write, types.singleton(nil)))
end
return output
end
export type World = { export type World = {
archetypeIndex: { [string]: Archetype }, archetypeIndex: { [string]: Archetype },
archetypes: Archetypes, archetypes: Archetypes,
@ -2206,8 +1816,6 @@ export type World = {
nextComponentId: number, nextComponentId: number,
nextEntityId: number, nextEntityId: number,
nextArchetypeId: number, nextArchetypeId: number,
observerable: { [i53]: { [i53]: { { query: Query<i53> } } } },
} & { } & {
--- Creates a new entity --- Creates a new entity
entity: (self: World) -> Entity, entity: (self: World) -> Entity,

View file

@ -59,14 +59,9 @@ local function debug_world_inspect(world: World)
records = records, records = records,
row = row, row = row,
tuple = tuple, tuple = tuple,
columns = columns
} }
end end
local function name(world, e)
return world:get(e, jecs.Name)
end
TEST("archetype", function() TEST("archetype", function()
local archetype_append_to_records = jecs.archetype_append_to_records local archetype_append_to_records = jecs.archetype_append_to_records
local id_record_ensure = jecs.id_record_ensure local id_record_ensure = jecs.id_record_ensure
@ -363,39 +358,6 @@ TEST("world:add()", function()
end) end)
TEST("world:query()", function() TEST("world:query()", function()
do CASE "cached"
local world = world_new()
local Foo = world:component()
local Bar = world:component()
local Baz = world:component()
local e = world:entity()
local q = world:query(Foo, Bar):without(Baz):cached()
world:set(e, Foo, true)
world:set(e, Bar, false)
local i = 0
for _, e in q:iter() do
i=1
end
CHECK(i == 1)
for _, e in q:iter() do
i=2
end
CHECK(i == 2)
for _, e in q do
i=3
end
CHECK(i == 3)
for _, e in q do
i=4
end
CHECK(i == 4)
CHECK(#q:archetypes() == 1)
CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")]))
world:delete(Foo)
CHECK(#q:archetypes() == 0)
end
do CASE("multiple iter") do CASE("multiple iter")
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component()
@ -851,6 +813,40 @@ TEST("world:query()", function()
CHECK(withoutCount == 0) CHECK(withoutCount == 0)
end end
do
CASE("Empty Query")
do
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local e1 = world:entity()
world:add(e1, A)
local query = world:query(B)
CHECK(query:without() == query)
CHECK(query:with() == query)
-- They always return the same EMPTY_LIST
CHECK(query:archetypes() == world:query(B):archetypes())
end
do
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local e1 = world:entity()
world:add(e1, A)
local count = 0
for id in world:query(B) do
count += 1
end
CHECK(count == 0)
end
end
do do
CASE("without") CASE("without")
do do
@ -1209,38 +1205,25 @@ TEST("world:delete", function()
end) end)
TEST("world:target", function() TEST("world:target", function()
do CASE("nth index") do
CASE("nth index")
local world = world_new() local world = world_new()
local A = world:component() local A = world:component()
world:set(A, jecs.Name, "A")
local B = world:component() local B = world:component()
world:set(B, jecs.Name, "B")
local C = world:component() local C = world:component()
world:set(C, jecs.Name, "C")
local D = world:component() local D = world:component()
world:set(D, jecs.Name, "D")
local E = world:component()
world:set(E, jecs.Name, "E")
local e = world:entity() local e = world:entity()
world:add(e, pair(A, B)) world:add(e, pair(A, B))
world:add(e, pair(A, C)) world:add(e, pair(A, C))
world:add(e, pair(A, D))
world:add(e, pair(A, E))
world:add(e, pair(B, C)) world:add(e, pair(B, C))
world:add(e, pair(B, D)) world:add(e, pair(B, D))
world:add(e, pair(C, D)) world:add(e, pair(C, D))
CHECK(pair(A, B) < pair(A, C)) CHECK(pair(A, B) < pair(A, C))
CHECK(pair(A, E) < pair(B, C))
local records = debug_world_inspect(world).records(e)
CHECK(jecs.pair_first(world, pair(B, C)) == B)
CHECK(records[pair(B, C)].column > records[pair(A, E)].column)
CHECK(world:target(e, A, 0) == B) CHECK(world:target(e, A, 0) == B)
CHECK(world:target(e, A, 1) == C) CHECK(world:target(e, A, 1) == C)
CHECK(world:target(e, A, 2) == D)
CHECK(world:target(e, A, 3) == E)
CHECK(world:target(e, B, 0) == C) CHECK(world:target(e, B, 0) == C)
CHECK(world:target(e, B, 1) == D) CHECK(world:target(e, B, 1) == D)
CHECK(world:target(e, C, 0) == D) CHECK(world:target(e, C, 0) == D)