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

@ -1,135 +0,0 @@
--!strict
--!optimize 2
local jecs = require("../jecs")
type entity<T = nil> = jecs.Entity<T>
type id<T = nil> = jecs.Id<T>
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>>`
local add_commands: { [jecs_world]: { [id]: { entity } } } = {}
--- `map<component_id, array<entity_id, component_value>>`
local set_commands: { [jecs_world]: { [id]: { [entity]: any } } } = {}
--- `map<component_id, array<entity_id>>`
local remove_commands: { [jecs_world]: { [id]: { entity } } } = {}
--- `array<entity_id>`
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 = {
--- Execute all buffered commands and clear the buffer
flush: () -> (),
--- Adds a component to the entity with no value
add: (entity: entity, component: id) -> (),
--- 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: (entity: entity, component: id) -> (),
--- Deletes an entity from the world
delete: (entity: entity) -> (),
}
local function flush()
for world, entities in delete_commands do
for _, entity in entities do
world:delete(entity)
end
end
for world, commands in add_commands do
for component, entities in commands do
for _, entity in entities do
if delete_commands[world][entity] then
continue
end
world:add(entity, component)
end
end
table.clear(add_commands[world])
end
for world, commands in set_commands do
for component, entities in commands do
for entity, value in entities do
if delete_commands[world][entity] then
continue
end
world:set(entity, component, value)
end
end
table.clear(set_commands[world])
end
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[world])
end
end
local function add(entity: entity, component: id)
local world = WORLD()
if not add_commands[world][component] then
add_commands[world][component] = {}
end
table.insert(add_commands[world][component], entity)
end
local function set<T>(entity: entity, component: id<T>, data: T)
local world = WORLD()
if not set_commands[world][component] then
set_commands[world][component] = {}
end
set_commands[world][component][entity] = data
end
local function remove(entity: entity, component: id)
local world = WORLD()
if not remove_commands[world][component] then
remove_commands[world][component] = {}
end
table.insert(remove_commands[world][component], entity)
end
local function delete(entity: entity)
local world = WORLD()
table.insert(delete_commands[world], entity)
end
local command_buffer: command_buffer = {
flush = flush,
add = add,
set = set,
remove = remove,
delete = delete,
}
return command_buffer

View file

@ -1,78 +0,0 @@
--!strict
--!optimize 2
local jecs = require("../jecs")
type entity<T = nil> = jecs.Entity<T>
type id<T = nil> = jecs.Id<T>
local world = require("./world").get
type interface = {
__index: interface,
new: (entity: entity) -> handle,
--- Checks if the entity has all of the given components
has: (self: handle, ...id) -> boolean,
--- Retrieves the value of up to 4 components. These values may be nil.
get: (<A>(self: handle, id<A>) -> A?)
& (<A, B>(self: handle, id<A>, id<B>) -> (A?, B?))
& (<A, B, C>(self: handle, id<A>, id<B>, id<C>) -> (A?, B?, C?))
& (<A, B, C, D>(self: handle, id<A>, id<B>, id<C>, id<D>) -> (A?, B?, C?, D?)),
--- Adds a component to the entity with no value
add: <T>(self: handle, id: id<T>) -> handle,
--- Assigns a value to a component on the given entity
set: <T>(self: handle, id: id<T>, data: T) -> handle,
--- Removes a component from the given entity
remove: (self: handle, id: id) -> handle,
--- Deletes the entity and all its related components and relationships. **Does not** refer to deleting the handle
delete: (self: handle) -> (),
--- Gets the entitys id
id: (self: handle) -> entity,
}
export type handle = typeof(setmetatable({} :: { entity: entity, world: jecs.World }, {} :: interface))
local handle = {} :: interface
handle.__index = handle
function handle.new(entity: entity)
local self = {
entity = entity,
world = world(),
}
return setmetatable(self, handle)
end
function handle:has(...: id): boolean
return self.world:has(self.entity, ...)
end
handle.get = function(self: handle, a: id, b: id?, c: id?, d: id?)
return self.world:get(self.entity, a, b :: any, c :: any, d :: any)
end :: any
function handle:add<T>(id: id<T>): handle
self.world:add(self.entity, id)
return self
end
function handle:set<T>(id: id<T>, value: T): handle
self.world:set(self.entity, id, value)
return self
end
function handle:remove(id: id): handle
self.world:remove(self.entity, id)
return self
end
function handle:delete()
self.world:delete(self.entity)
end
function handle:id(): entity
return self.entity
end
return handle.new

View file

@ -1,42 +1,22 @@
--!strict
--!optimize 2
local jecs = require("../jecs")
local collect = require("@self/utilities/collect")
export type SignalLike<T...> = collect.SignalLike<any, T...>
export type VerboseSignalLike<D, T...> = collect.SignalLike<D, T...>
local WORLD = require("./world")
local ref = require("@self/utilities/ref")
export type Ref = ref.Identity
local collect = require("./collect")
export type collect_signal_like<T...> = collect.signal_like<any, T...>
export type collect_verbose_signal_like<D, T...> = collect.signal_like<D, T...>
local tracker = require("@self/utilities/tracker")
export type Tracker = tracker.Identity
export type TrackerCommands = tracker.Commands
local command_buffer = require("./command_buffer")
export type command_buffer = command_buffer.command_buffer
local handle = require("./handle")
export type handle = handle.handle
local ref = require("./ref")
local replicator = require("./replicator")
export type replicator = replicator.replicator
export type changes = replicator.changes
local spawner = require("./spawner")
export type spawner<T...> = spawner.spawner<T...>
--- Set the world for all utilities.
--- Should be called once per context before any utility is used.
--- @param world jecs.World
local function initialize(world: jecs.World)
WORLD.set(world)
end
local command_buffer = require("@self/utilities/command_buffer")
export type CommandBuffer = command_buffer.Identity
return {
initialize = initialize,
collect = collect,
handle = handle,
replicator = replicator,
ref = ref,
tracker = tracker,
command_buffer = command_buffer,
spawner = spawner,
}

View file

@ -1,67 +0,0 @@
--!strict
--!optimize 2
local handle = require("./handle")
local jecs = require("../jecs")
local WORLD = require("./world").get
local refs: { [jecs.World]: { [any]: jecs.Entity<any> } } = {}
local function serve_clearer(key: any, world: jecs.World): () -> ()
return function()
refs[world][key] = nil
end
end
--- Gets an entity the given key references to.
--- If the key is nil, an entirely new entity is created and returned.
--- If the key doesn't reference an entity, a new entity is made for it to reference and returned.
--- @param key any
--- @return handle
local function ref(key: any): (handle.handle, () -> ()?)
local world = WORLD()
if not key then
return handle(world:entity())
end
if not refs[world] then
refs[world] = {}
end
local entity = refs[world][key]
if not entity then
entity = world:entity()
refs[world][key] = entity
end
return handle(entity), serve_clearer(key, world)
end
-- For the `__call`` metamethod
local function __call(_, key: any): (handle.handle, () -> ()?)
return ref(key)
end
local function search(key: any): (handle.handle?, () -> ()?)
local world = WORLD()
if not key then
return nil
end
local entity = refs[world][key]
if not entity then
return nil
end
return handle(entity), serve_clearer(key, world)
end
local metatable = {
__call = __call,
__index = {
search = search,
set_ref = ref,
},
}
local REF = setmetatable({}, metatable) :: typeof(ref) & typeof(metatable.__index)
return REF

View file

@ -1,247 +0,0 @@
--!strict
--!optimize 2
local jecs = require("../jecs")
type entity<T = nil> = jecs.Entity<T>
type i53 = number
local ref = require("./ref")
local WORLD = require("./world").get
--- A replicator keeps track of all entities with the passed components and their values -
--- whenever a component is changed (add, change, remove) and the replicator listens to it, it's also changed within the contained raw data.\
--- The developer can then calculate the difference on the server and send it to the client every time,
--- on which the difference is then applied to the world.\
--- Albeit it's called a replicator, it doesn't replicate the data by itself.
--- This allows the developer to use any networking libary to replicate the changes.
--- ```luau
--- -- server
--- local replicator = jecs_utils.create_replicator(component_a, component_b, ...)
---
--- local function system()
--- local difference = replicator.calculate_difference()
--- -- There might not be any difference
--- if not difference then
--- return
--- end
--- data_replication_event.send_to_all(difference)
--- end
--- ```
--- ```luau
--- -- client
--- local replicator = jecs_utils.replicator(component_a, component_b, ...)
---
--- local function system()
--- for _, difference in data_replication_event.poll() do
--- replicator.apply_difference(difference)
--- end
--- end
--- ```
export type replicator = {
--- Gets the full data representing the entire world.
--- Useful for initial replication to every player.
--- ```luau
--- local replicator = jecs_utils.replicator(component_a, component_b, ...)
---
--- Players.PlayerAdded:Connect(function(player)
--- data_replication_event.send_to(player, replicator.get_full_data())
--- end)
--- ```
--- @return changes
get_full_data: () -> changes,
--- Calculates the difference between last sent data and currently stored data.
--- ```luau
--- local replicator = jecs_utils.create_replicator(component_a, component_b, ...)
---
--- local function system()
--- local difference = replicator.calculate_difference()
--- -- There might not be any difference
--- if not difference then
--- return
--- end
--- data_replication_event.send_to_all(difference)
--- end
--- ```
--- @return changes? -- There might not be any difference
calculate_difference: () -> changes?,
--- Applies the difference to the current data.
--- ```luau
--- local replicator = jecs_utils.replicator(component_a, component_b, ...)
---
--- local function system()
--- for _, difference in data_replication_event.poll() do
--- replicator.apply_difference(difference)
--- end
--- end
--- ```
--- @param difference changes
apply_difference: (difference: changes) -> (),
}
--- `map<component_id, array<entity_id>>`
type changes_added = { [i53]: { i53 } }
--- `map<component_id, array<entity_id, component_value>>`
type changes_set = { [i53]: { [i53]: unknown } }
--- `map<component_id, array<entity_id>>`
type changes_removed = { [i53]: { i53 } }
export type changes = {
added: changes_added,
set: changes_set,
removed: changes_removed,
}
--- A replicator keeps track of all entities with the passed components and their values -
--- whenever a component is changed (add, change, remove) and the replicator listens to it, it's also changed within the contained raw data.\
--- The developer can then calculate the difference on the server and send it to the client every time,
--- on which the difference is then applied to the world.\
--- Albeit it's called a replicator, it doesn't replicate the data by itself.
--- This allows the developer to use any networking libary to replicate the changes.
--- ```luau
--- -- server
--- local replicator = jecs_utils.create_replicator(component_a, component_b, ...)
---
--- local function system()
--- local difference = replicator.calculate_difference()
--- -- There might not be any difference
--- if not difference then
--- return
--- end
--- data_replication_event.send_to_all(difference)
--- end
--- ```
--- ```luau
--- -- client
--- local replicator = jecs_utils.replicator(component_a, component_b, ...)
---
--- local function system()
--- for _, difference in data_replication_event.poll() do
--- replicator.apply_difference(difference)
--- end
--- end
--- ```
--- @param ... entity
--- @return replicator
local function replicator(...: entity): replicator
local world = WORLD()
local components = { ... }
-- don't index a changes table start
local raw_added: changes_added = {}
local raw_set: changes_set = {}
local raw_removed: changes_removed = {}
local changes_added: changes_added = {}
local changes_set: changes_set = {}
local changes_removed: changes_removed = {}
-- don't index a changes table end
for _, component in components do
world:set(component, jecs.OnAdd, function(entity)
if not raw_added[component] then
raw_added[component] = {}
end
if not changes_added[component] then
changes_added[component] = {}
end
table.insert(raw_added[component], entity)
table.insert(changes_added[component], entity)
end)
world:set(component, jecs.OnSet, function(entity, value)
if not raw_set[component] then
raw_set[component] = {}
end
if not changes_set[component] then
changes_set[component] = {}
end
raw_set[component][entity] = value
changes_set[component][entity] = value
end)
world:set(component, jecs.OnRemove, function(entity)
if not raw_removed[component] then
raw_removed[component] = {}
end
if not changes_removed[component] then
changes_removed[component] = {}
end
table.insert(raw_removed[component], entity)
table.insert(changes_removed[component], entity)
end)
end
local function get_full_data(): changes
return {
added = raw_added,
set = raw_set,
removed = raw_removed,
}
end
local function calculate_difference(): changes?
local difference_added = changes_added
local difference_set = changes_set
local difference_removed = changes_removed
changes_added = {}
changes_set = {}
changes_removed = {}
local added_not_empty = next(difference_added) ~= nil
local set_not_empty = next(difference_set) ~= nil
local removed_not_empty = next(difference_removed) ~= nil
if not added_not_empty and not set_not_empty and not removed_not_empty then
return nil
end
return {
added = difference_added,
set = difference_set,
removed = difference_removed,
}
end
local function apply_difference(difference: changes)
for component, entities in difference.added do
for _, entity_id in entities do
local entity = ref(`replicated-{entity_id}`)
local exists = entity:has(component)
if exists then
continue
end
entity:add(component)
end
end
for component, entities in difference.set do
for entity_id, value in entities do
local entity = ref(`replicated-{entity_id}`)
local existing_value = entity:get(component)
if existing_value == value then
continue
end
entity:set(component, value)
end
end
for component, entities in difference.removed do
for _, entity_id in entities do
local entity = ref(`replicated-{entity_id}`)
local exists = entity:has(component)
if exists then
continue
end
entity:remove(component)
end
end
end
return {
get_full_data = get_full_data,
calculate_difference = calculate_difference,
apply_difference = apply_difference,
}
end
return replicator

View file

@ -1,49 +0,0 @@
--!strict
local spawner_type = require("./spawner_type")
local WORLD = require("./world").get
local handle = require("./handle")
export type spawner<T...> = spawner_type.spawner<T...>
--- Creates an entity spawner.
--- ```luau
--- local spawner = jecs_utils.spawner(components.part, components.velocity, components.position)
--- for _ = 1, 1000 do
--- spawner.spawn(part_template:Clone(), Vector3.zero, Vector3.zero)
--- end
--- ```
--- @param ... T... -- Components to use.
--- @return spawner<T...>
local function spawner(...)
local components = { ... }
local world = WORLD()
local function spawn(...)
local passed = { ... }
local entity = world:entity()
for idx, component in components do
world:set(entity, component, passed[idx])
end
return entity
end
local function spawn_with_handle(...)
local passed = { ... }
local entity = handle(world:entity())
for idx, component in components do
entity:set(component, passed[idx])
end
return entity
end
return {
spawn = spawn,
spawn_with_handle = spawn_with_handle,
}
end
return (spawner :: any) :: spawner_type.create_spawner

View file

@ -1,391 +0,0 @@
--!strict
local jecs = require("../jecs")
type entity<T = nil> = jecs.Entity<T>
type id<T = nil> = jecs.Id<T>
local handle = require("./handle")
export type spawner<T...> = {
--- Creates an entity with the given components.
--- @param ... T...
--- @return entity
spawn: (T...) -> entity,
--- Creates an entity with the given components and returns a handle to it.
--- @param ... T...
--- @return handle
spawn_with_handle: (T...) -> handle.handle,
}
-- Very beautiful type incoming!
-- Sadly this has to be done, components are of different types than their values (`entity<T>` vs `T`)
export type create_spawner =
(<A>(id<A>) -> spawner<A>)
& (<A, B>(id<A>, id<B>) -> spawner<A, B>)
& (<A, B, C>(id<A>, id<B>, id<C>) -> spawner<A, B, C>)
& (<A, B, C, D>(id<A>, id<B>, id<C>, id<D>) -> spawner<A, B, C, D>)
& (<A, B, C, D, E>(id<A>, id<B>, id<C>, id<D>, id<E>) -> spawner<A, B, C, D, E>)
& (<A, B, C, D, E, F>(id<A>, id<B>, id<C>, id<D>, id<E>, id<F>) -> spawner<A, B, C, D, E, F>)
& (<A, B, C, D, E, F, G>(id<A>, id<B>, id<C>, id<D>, id<E>, id<F>, id<G>) -> spawner<A, B, C, D, E, F, G>)
& (<A, B, C, D, E, F, G, H>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>
) -> spawner<A, B, C, D, E, F, G, H>)
& (<A, B, C, D, E, F, G, H, I>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>
) -> spawner<A, B, C, D, E, F, G, H, I>)
& (<A, B, C, D, E, F, G, H, I, J>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>
) -> spawner<A, B, C, D, E, F, G, H, I, J>)
& (<A, B, C, D, E, F, G, H, I, J, K>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K>)
& (<A, B, C, D, E, F, G, H, I, J, K, L>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>,
id<W>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>,
id<W>,
id<X>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>,
id<W>,
id<X>,
id<Y>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>,
id<W>,
id<X>,
id<Y>,
id<Z>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z>)
return {}

View file

@ -1,6 +1,5 @@
--!strict
--!optimize 2
--[[
original author by @memorycode
@ -28,24 +27,12 @@ SOFTWARE.
--]]
--- What signals passed to `collect()` should be able to be coerced into
export type signal_like<D, T...> = { connect: confn<D, T...>, [any]: any } | { Connect: confn<D, T...>, [any]: any }
type confn<D, T...> = (self: signal_like<D, T...>, (T...) -> ()) -> D
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` ***method***.
--- ```luau
--- local sig = collect(some_signal)
---
--- -- Imagine this as an ECS scheduler loop
--- while task.wait() do
--- for index, arg1 in sig do -- arg1, arg2, etc
--- print(arg1)
--- end
--- end
--- ```
--- @param event signal<T...>
--- @return () -> (number, T...), D -- iterator and disconnector
local function collect<D, T...>(event: signal_like<D, T...>): (() -> (number, T...), D)
--- 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()
@ -53,11 +40,11 @@ local function collect<D, T...>(event: signal_like<D, T...>): (() -> (number, T.
return function(): (number?, T...)
if n <= 0 then
mt.__iter = nil
return nil
return nil :: any
end
n -= 1
return n + 1, unpack(table.remove(storage, 1) :: any)
return n + 1, unpack(table.remove(storage, 1) :: any) :: any
end
end

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

View file

@ -1,29 +0,0 @@
--!strict
--!optimize 2
local jecs = require("../jecs")
local WORLD: jecs.World
local listeners: { (jecs.World) -> () } = {}
local function get(): jecs.World
return WORLD
end
local function set(world: jecs.World)
WORLD = world
for _, fn in listeners do
fn(world)
end
end
local function on_set(fn: (jecs.World) -> ())
table.insert(listeners, fn)
end
return {
get = get,
set = set,
on_set = on_set,
}