fix: Refs and cmd buffers, add unit tests, v0.1.0

- Refs and command buffers used to share data with every single world, this is now mitigated by using a separate data set for every world

- Working unit tests have been added

- jecs-utils has now been released at mark-marks/jecs-utils@v0.1.0!
This commit is contained in:
Mark Marks 2024-09-22 00:50:30 +02:00
parent 9a0aa37667
commit 5a043b2912
4 changed files with 92 additions and 52 deletions

View file

@ -4,16 +4,27 @@ local jecs = require("./jecs")
type entity<T = nil> = jecs.Entity<T> type entity<T = nil> = jecs.Entity<T>
type id<T = nil> = jecs.Id<T> type id<T = nil> = jecs.Id<T>
local WORLD = require("./world").get local _world = require("./world")
local WORLD = _world.get
-- luau-lsp literally dies if you use the actual world type
type jecs_world = any
--- `map<component_id, array<entity_id>>` --- `map<component_id, array<entity_id>>`
local add_commands: { [id]: { entity } } = {} local add_commands: { [jecs_world]: { [id]: { entity } } } = {}
--- `map<component_id, array<entity_id, component_value>>` --- `map<component_id, array<entity_id, component_value>>`
local set_commands: { [id]: { [entity]: any } } = {} local set_commands: { [jecs_world]: { [id]: { [entity]: any } } } = {}
--- `map<component_id, array<entity_id>>` --- `map<component_id, array<entity_id>>`
local remove_commands: { [id]: { entity } } = {} local remove_commands: { [jecs_world]: { [id]: { entity } } } = {}
--- `array<entity_id>` --- `array<entity_id>`
local delete_commands: { entity } = {} local delete_commands: { [jecs_world]: { entity } } = {}
_world.on_set(function(world)
add_commands[world] = {}
set_commands[world] = {}
remove_commands[world] = {}
delete_commands[world] = {}
end)
export type command_buffer = { export type command_buffer = {
--- Execute all buffered commands and clear the buffer --- Execute all buffered commands and clear the buffer
@ -30,74 +41,86 @@ export type command_buffer = {
} }
local function flush() local function flush()
local world = WORLD() for world, entities in delete_commands do
for _, entity in delete_commands do
world:delete(entity)
end
for component, entities in add_commands do
for _, entity in entities do for _, entity in entities do
if delete_commands[entity] then world:delete(entity)
continue
end
world:add(entity, component)
end end
end end
table.clear(add_commands)
for component, entities in set_commands do for world, commands in add_commands do
for entity, value in entities do for component, entities in commands do
if delete_commands[entity] then for _, entity in entities do
continue if delete_commands[world][entity] then
continue
end
world:add(entity, component)
end end
world:set(entity, component, value)
end end
table.clear(add_commands[world])
end end
table.clear(set_commands)
for component, entities in remove_commands do for world, commands in set_commands do
for _, entity in entities do for component, entities in commands do
if delete_commands[entity] then for entity, value in entities do
continue if delete_commands[world][entity] then
continue
end
world:set(entity, component, value)
end end
world:remove(entity, component)
end end
table.clear(set_commands[world])
end end
table.clear(remove_commands)
table.clear(delete_commands) for world, commands in remove_commands do
for component, entities in commands do
for _, entity in entities do
if delete_commands[world][entity] then
continue
end
world:remove(entity, component)
end
end
table.clear(remove_commands[world])
end
for world in delete_commands do
table.clear(delete_commands)
end
end end
local function add(entity: entity, component: id) local function add(entity: entity, component: id)
if not add_commands[component] then local world = WORLD()
add_commands[component] = {} if not add_commands[world][component] then
add_commands[world][component] = {}
end end
table.insert(add_commands[component], entity) table.insert(add_commands[world][component], entity)
end end
local function set<T>(entity: entity, component: id<T>, data: T) local function set<T>(entity: entity, component: id<T>, data: T)
if not set_commands[component] then local world = WORLD()
set_commands[component] = {} if not set_commands[world][component] then
set_commands[world][component] = {}
end end
set_commands[component][entity] = data set_commands[world][component][entity] = data
end end
local function remove(entity: entity, component: id) local function remove(entity: entity, component: id)
if not remove_commands[component] then local world = WORLD()
remove_commands[component] = {} if not remove_commands[world][component] then
remove_commands[world][component] = {}
end end
table.insert(remove_commands[component], entity) table.insert(remove_commands[world][component], entity)
end end
local function delete(entity: entity) local function delete(entity: entity)
table.insert(delete_commands, entity) local world = WORLD()
table.insert(delete_commands[world], entity)
end end
local command_buffer: command_buffer = { local command_buffer: command_buffer = {

View file

@ -30,7 +30,7 @@ type interface = {
id: (self: handle) -> entity, id: (self: handle) -> entity,
} }
export type handle = typeof(setmetatable({} :: { entity: entity }, {} :: interface)) export type handle = typeof(setmetatable({} :: { entity: entity, world: jecs.World }, {} :: interface))
local handle = {} :: interface local handle = {} :: interface
handle.__index = handle handle.__index = handle
@ -38,26 +38,27 @@ handle.__index = handle
function handle.new(entity: entity) function handle.new(entity: entity)
local self = { local self = {
entity = entity, entity = entity,
world = world(),
} }
return setmetatable(self, handle) return setmetatable(self, handle)
end end
function handle:has(...: id): boolean function handle:has(...: id): boolean
return world():has(self.entity, ...) return self.world:has(self.entity, ...)
end end
handle.get = function(self: handle, a: id, b: id?, c: id?, d: id?) handle.get = function(self: handle, a: id, b: id?, c: id?, d: id?)
return world():get(self.entity, a, b :: any, c :: any, d :: any) return self.world:get(self.entity, a, b :: any, c :: any, d :: any)
end :: any end :: any
function handle:add<T>(id: id<T>): handle function handle:add<T>(id: id<T>): handle
world():add(self.entity, id) self.world:add(self.entity, id)
return self return self
end end
function handle:set<T>(id: id<T>, value: T): handle function handle:set<T>(id: id<T>, value: T): handle
world():set(self.entity, id, value) self.world:set(self.entity, id, value)
return self return self
end end
@ -67,7 +68,7 @@ function handle:remove(id: id): handle
end end
function handle:delete() function handle:delete()
world():delete(self.entity) self.world:delete(self.entity)
end end
function handle:id(): entity function handle:id(): entity

View file

@ -1,9 +1,10 @@
--!strict --!strict
--!optimize 2 --!optimize 2
local handle = require("./handle") local handle = require("./handle")
local jecs = require("./jecs")
local WORLD = require("./world").get local WORLD = require("./world").get
local refs = {} local refs: { [jecs.World]: { [any]: jecs.Entity } } = {}
--- Gets an entity the given key references to. --- Gets an entity the given key references to.
--- If the key is nil, an entirely new entity is created and returned. --- If the key is nil, an entirely new entity is created and returned.
@ -16,10 +17,14 @@ local function ref(key: any): handle.handle
return handle(world:entity()) return handle(world:entity())
end end
local entity = refs[key] if not refs[world] then
refs[world] = {}
end
local entity = refs[world][key]
if not entity then if not entity then
entity = world:entity() entity = world:entity()
refs[key] = entity refs[world][key] = entity
end end
return handle(entity) return handle(entity)

View file

@ -4,15 +4,26 @@ local jecs = require("./jecs")
local WORLD: jecs.World local WORLD: jecs.World
local listeners: { (jecs.World) -> () } = {}
local function get(): jecs.World local function get(): jecs.World
return WORLD return WORLD
end end
local function set(world: jecs.World) local function set(world: jecs.World)
WORLD = world WORLD = world
for _, fn in listeners do
fn(world)
end
end
local function on_set(fn: (jecs.World) -> ())
table.insert(listeners, fn)
end end
return { return {
get = get, get = get,
set = set, set = set,
on_set = on_set,
} }