Cleanup & refactor
Some checks failed
Continous Integration / Build (push) Successful in 11s
Continous Integration / Lint (push) Successful in 9s
Continous Integration / Styling (push) Failing after 3s
Continous Integration / Unit Testing (push) Failing after 30s

This commit is contained in:
marked 2025-05-07 00:37:24 +02:00
parent d3b6212463
commit 2a6907434a
50 changed files with 937 additions and 4110 deletions

View file

@ -0,0 +1,62 @@
--!strict
--!optimize 2
--[[
original author by @memorycode
MIT License
Copyright (c) 2024 Michael
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]]
--- What signals passed to `collect()` should be able to be coerced into
export type SignalLike<D, T...> = { connect: Connector<D, T...>, [any]: any } | { Connect: Connector<D, T...>, [any]: any }
type Connector<D, T...> = (self: SignalLike<D, T...>, (T...) -> ()) -> D
--- Collects all arguments fired through the given signal, and drains the collection on iteration.\
--- Expects signals to have a `Connect` or `connect` ***method***.
local function collect<D, T...>(event: SignalLike<D, T...>): (() -> (number, T...), D)
local storage = {}
local mt = {}
local iter = function()
local n = #storage
return function(): (number?, T...)
if n <= 0 then
mt.__iter = nil
return nil :: any
end
n -= 1
return n + 1, unpack(table.remove(storage, 1) :: any) :: any
end
end
local connect = event.Connect or event.connect
assert(connect ~= nil, "Signal is missing a Connect function - is it really a signal?")
local disconnect = connect(event, function(...)
table.insert(storage, { ... })
mt.__iter = iter :: any
end)
setmetatable(storage, mt)
return storage :: any, disconnect
end
return collect

View file

@ -0,0 +1,140 @@
--!strict
--!optimize 2
local jecs = require("../../jecs")
type Entity<T = unknown> = jecs.Entity<T>
type Id<T = unknown> = jecs.Id<T>
type World = jecs.World
export type Identity = {
--- Execute all commands and clear the buffer
flush: () -> (),
--- Peeks into the commands currently stored by the buffer
peek: () -> Commands,
--- Adds a component to the entity with no value
add: <T>(entity: Entity, component: Id<T>) -> (),
--- Assigns a value to a component on the given entity
set: <T>(entity: Entity, component: Id<T>, data: T) -> (),
--- Removes a component from the given entity
remove: <T>(entity: Entity, component: Id<T>) -> (),
--- Deletes an entity and all it's related components and relationships
delete: (entity: Entity) -> (),
}
export type Commands = {
add: { [Id]: { Entity } },
set: { [Id]: { [Entity]: unknown } },
remove: { [Id]: { Entity } },
delete: { Entity },
deletion_lookup: { [Entity]: true },
}
local function construct(world: World): Identity
local add_commands: { [Id]: { Entity } } = {}
local set_commands: { [Id]: { [Entity]: unknown } } = {}
local remove_commands: { [Id]: { Entity } } = {}
local delete_commands: { Entity } = {}
-- Double memory usage for deletions but preserve order while keeping O(1) performance for lookups
local deletion_lookup: { [Entity]: true } = {}
local function flush()
for _, entity in delete_commands do
world:delete(entity)
end
for component, entities in add_commands do
for _, entity in entities do
if deletion_lookup[entity] then
continue
end
world:add(entity, component)
end
end
table.clear(add_commands)
for component, entities in set_commands do
for entity, value in entities do
if deletion_lookup[entity] then
continue
end
world:set(entity, component, value)
end
end
table.clear(set_commands)
for component, entities in remove_commands do
for _, entity in entities do
if deletion_lookup[entity] then
continue
end
world:remove(entity, component)
end
end
table.clear(remove_commands)
table.clear(delete_commands)
table.clear(deletion_lookup)
end
local function peek()
return {
add = add_commands,
set = set_commands,
remove = remove_commands,
delete = delete_commands,
deletion_lookup = deletion_lookup,
}
end
local function add<T>(entity: Entity, component: Id<T>)
local cmds = add_commands[component]
if not cmds then
cmds = {}
add_commands[component] = cmds
end
table.insert(cmds, entity)
end
local function set<T>(entity: Entity, component: Id<T>, data: T)
local cmds = set_commands[component]
if not cmds then
cmds = {}
set_commands[component] = cmds
end
cmds[entity] = data
end
local function remove<T>(entity: Entity, component: Id<T>)
local cmds = remove_commands[component]
if not cmds then
cmds = {}
remove_commands[component] = cmds
end
table.insert(cmds, entity)
end
local function delete(entity: Entity)
table.insert(delete_commands, entity)
deletion_lookup[entity] = true
end
return {
flush = flush,
peek = peek,
add = add,
set = set,
remove = remove,
delete = delete,
}
end
return construct

85
lib/utilities/ref.luau Normal file
View file

@ -0,0 +1,85 @@
--!strict
--!optimize 2
local jecs = require("../../jecs")
type Entity<T = unknown> = jecs.Entity<T>
export type Identity = typeof(setmetatable(
{},
{} :: {
__call: <T>(any, key: unknown) -> (Entity<T>, Cleaner),
__index: {
reference: <T>(key: unknown) -> (Entity<T>, Cleaner),
find: <T>(key: unknown) -> (Entity<T>?, Cleaner?),
},
}
))
type Cleaner = () -> ()
local ref_cache: { [jecs.World]: Identity } = {}
local function construct(world: jecs.World, skip_cache: boolean?): Identity
if not skip_cache then
local hit = ref_cache[world]
if hit then
return hit
end
end
local lookup: { [unknown]: Entity } = {}
local cleaner_cache: { [unknown]: Cleaner } = {}
local function serve_cleaner(key: unknown): () -> ()
local hit = cleaner_cache[key]
if hit then
return hit
end
local function cleaner()
lookup[key] = nil
cleaner_cache[key] = nil
end
cleaner_cache[key] = cleaner
return cleaner
end
local function ref<T>(key: unknown): (Entity<T>, Cleaner)
local entity = lookup[key]
if not entity then
entity = world:entity()
lookup[key] = entity
end
return entity, serve_cleaner(key)
end
local function find<T>(key: unknown): (Entity<T>?, Cleaner?)
local entity = lookup[key]
if not entity then
return nil, nil
end
return entity, serve_cleaner(key)
end
local function call<T>(_, key: unknown): (Entity<T>, Cleaner)
return ref(key)
end
local self = setmetatable({}, {
__call = call,
__index = {
reference = ref,
find = find,
},
})
if not skip_cache then
ref_cache[world] = self
end
return self
end
return construct

273
lib/utilities/tracker.luau Normal file
View file

@ -0,0 +1,273 @@
--!strict
--!optimize 2
local command_buffer = require("./command_buffer")
local jecs = require("../../jecs")
type Entity<T = unknown> = jecs.Entity<T>
type Id<T = unknown> = jecs.Id<T>
type i53 = number
local OnAdd = jecs.OnAdd
local OnSet = jecs.OnSet
local OnRemove = jecs.OnRemove
local construct_ref = require("./ref")
-- The external type differs for better DX
export type Commands = {
added: { [i53]: { i53 } },
set: { [i53]: { [i53]: unknown } },
removed: { [i53]: { i53 } },
}
type Added = { [Id]: { Entity } }
type Set = { [Id]: { [Entity]: unknown } }
type Removed = { [Id]: { Entity } }
type Lookup = { [Id]: { [Entity]: number } }
type InternalCommands = {
added: Added,
set: Set,
removed: Removed,
}
--- Tracks operations on entities for the provided world.
export type Identity = {
--- Gets the current state.
--- A state is a representation of the minimum of commands necessary to produce the current world from a clean slate.
state: () -> Commands,
--- Gets the currently tracked snapshot.
--- A snapshot is a representation of the minimum of commands necessary to produce the current world back from when the last snapshot was taken.
snapshot: () -> Commands?,
--- Applies a set of commands to the tracked world, optionally doing it through a command buffer.
apply: (snapshot: Commands, buf: command_buffer.Identity?) -> (),
}
local function get_non_nilable<T, K>(container: {} & T, index: K): index<T, K>
local data = container[index]
if not data then
data = {}
container[index] = data
end
return data
end
local function insert_unique<T, V>(container: T, value: V, lookup: { [V]: number }?)
if lookup then
if lookup[value] then
return
end
local idx = #lookup + 1
lookup[value] = idx;
(container :: any)[idx] = value
return
end
if table.find(container, value) then
return
end
table.insert(container, value)
end
local function construct(world: jecs.World, ...: Entity<any>): Identity
local components = { ... }
local ref = construct_ref(world, true)
local state_added: Added = {}
local state_added_lookup: Lookup = {}
local state_set: Set = {}
local state_removed: Removed = {}
local state_removed_lookup: Lookup = {}
local snapshot_added: Added = {}
local snapshot_set: Set = {}
local snapshot_removed: Removed = {}
for _, component in components do
world:set(component, OnAdd, function(entity: Entity)
local snapshot = get_non_nilable(snapshot_added, component)
insert_unique(snapshot, entity)
local state = get_non_nilable(state_added, component)
local lookup = get_non_nilable(state_added_lookup, component)
insert_unique(state, entity, lookup)
-- Clean up previous operations
local set_state = state_set[component]
if set_state and set_state[entity] then
set_state[entity] = nil
end
local removed_lookup = state_removed_lookup[component]
if removed_lookup then
local idx = removed_lookup[entity]
if idx then
removed_lookup[entity] = nil
local removed_state = state_removed[component]
if removed_state then
-- Shifting around the array could be expensive, prefer `tbl[idx] = nil`
removed_state[idx] = nil
end
end
end
end)
world:set(component, OnSet, function(entity, value)
local snapshot = get_non_nilable(snapshot_set, component)
snapshot[entity] = value
local state = get_non_nilable(state_set, component)
state[entity] = value
-- Clean up previous operations
local added_lookup = state_added_lookup[component]
if added_lookup then
local idx = added_lookup[entity]
if idx then
added_lookup[entity] = nil
local added_state = state_added[component]
if added_state then
-- Shifting around the array could get expensive, prefer `array[idx] = nil`
added_state[idx] = nil
end
end
end
local removed_lookup = state_removed_lookup[component]
if removed_lookup then
local idx = removed_lookup[entity]
if idx then
removed_lookup[entity] = nil
local removed_state = state_removed[component]
if removed_state then
-- Shifting around the array could get expensive, prefer `array[idx] = nil`
removed_state[idx] = nil
end
end
end
end)
world:set(component, OnRemove, function(entity: Entity)
local snapshot = get_non_nilable(snapshot_removed, component)
insert_unique(snapshot, entity)
local state = get_non_nilable(state_removed, component)
local lookup = get_non_nilable(state_removed_lookup, component)
-- Clean up previous operations
local added_lookup = state_added_lookup[component]
if added_lookup then
local idx = added_lookup[entity]
if idx then
added_lookup[entity] = nil
local added_state = state_added[component]
if added_state then
-- Shifting around the array could get expensive, prefer `array[idx] = nil`
added_state[idx] = nil
end
end
end
local set_state = state_set[component]
if set_state and set_state[entity] then
set_state[entity] = nil
end
insert_unique(state, entity, lookup)
end)
end
-- We cast anything exposing `Commands` as `any` to improve the types for the end user
local function get_state(): InternalCommands
return {
added = state_added,
set = state_set,
removed = state_removed,
}
end
local function get_snapshot(): InternalCommands?
local diff_added = snapshot_added
local diff_set = snapshot_set
local diff_removed = snapshot_removed
snapshot_added = {}
snapshot_set = {}
snapshot_removed = {}
if next(diff_added) == nil and next(diff_set) == nil and next(diff_removed) == nil then
return nil
end
return {
added = diff_added,
set = diff_set,
removed = diff_removed,
}
end
local function apply_snapshot(snapshot: InternalCommands, buf: command_buffer.Identity?)
local add
local set
local remove
do
if buf then
add = buf.add
set = buf.set
remove = buf.remove
else
function add<T>(entity: Entity, component: Id<T>)
world:add(entity, component)
end
function set<T>(entity: Entity, component: Id<T>, data: T)
world:set(entity, component, data)
end
function remove<T>(entity: Entity, component: Id<T>)
world:remove(entity, component)
end
end
end
for component, entities in snapshot.added do
for _, id in entities do
local entity = ref(`foreign-{id}`)
if world:has(entity, component) then
continue
end
add(entity, component)
end
end
for component, entities in snapshot.set do
for id, data in entities do
local entity = ref(`foreign-{id}`)
if world:get(entity, component) == data then
continue
end
set(entity, component, data)
end
end
for component, entities in snapshot.removed do
for _, id in entities do
local entity = ref(`foreign-{id}`)
if world:has(entity, component) then
continue
end
remove(entity, component)
end
end
end
-- Public types differ for better DX
return {
state = get_state,
snapshot = get_snapshot,
apply = apply_snapshot,
} :: any
end
return construct