Cleanup & refactor
This commit is contained in:
parent
d3b6212463
commit
2a6907434a
50 changed files with 937 additions and 4110 deletions
62
lib/utilities/collect.luau
Normal file
62
lib/utilities/collect.luau
Normal 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
|
140
lib/utilities/command_buffer.luau
Normal file
140
lib/utilities/command_buffer.luau
Normal 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
85
lib/utilities/ref.luau
Normal 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
273
lib/utilities/tracker.luau
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue