Initial push

This commit is contained in:
Mark Marks 2024-09-21 19:12:15 +02:00
commit ee69c03334
31 changed files with 1578 additions and 0 deletions

73
lib/collect.luau Normal file
View file

@ -0,0 +1,73 @@
--!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 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
--- 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)
local storage = {}
local mt = {}
local iter = function()
local n = #storage
return function(): (number?, T...)
if n <= 0 then
mt.__iter = nil
return nil
end
n -= 1
return n + 1, unpack(table.remove(storage, 1) :: any)
end
end
local disconnect = event:Connect(function(...)
table.insert(storage, { ... })
mt.__iter = iter :: any
end)
setmetatable(storage, mt)
return storage :: any, disconnect
end
return collect

114
lib/command_buffer.luau Normal file
View file

@ -0,0 +1,114 @@
--!strict
--!optimize 2
local jecs = require("@pkg/jecs")
type entity<T = nil> = jecs.Entity<T>
type id<T = nil> = jecs.Id<T>
local world = require("./world").get()
--- `map<component_id, array<entity_id>>`
local add_commands: { [id]: { entity } } = {}
--- `map<component_id, array<entity_id, component_value>>`
local set_commands: { [id]: { [entity]: any } } = {}
--- `map<component_id, array<entity_id>>`
local remove_commands: { [id]: { entity } } = {}
--- `array<entity_id>`
local delete_commands: { entity } = {}
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()
local adds = add_commands
local sets = set_commands
local removes = remove_commands
local deletes = delete_commands
table.clear(add_commands)
table.clear(set_commands)
table.clear(remove_commands)
table.clear(delete_commands)
for _, id in deletes do
world:delete(id)
end
for component, ids in adds do
for _, id in ids do
if deletes[id] then
continue
end
world:add(id, component)
end
end
for component, ids in sets do
for id, value in ids do
if deletes[id] then
continue
end
world:set(id, component, value)
end
end
for component, ids in removes do
for _, id in ids do
if deletes[id] then
continue
end
world:remove(id, component)
end
end
end
local function add(entity: entity, component: id)
if not add_commands[component] then
add_commands[component] = {}
end
table.insert(add_commands[component], entity)
end
local function set<T>(entity: entity, component: id<T>, data: T)
if not set_commands[component] then
set_commands[component] = {}
end
set_commands[component][entity] = data
end
local function remove(entity: entity, component: id)
if not remove_commands[component] then
remove_commands[component] = {}
end
table.insert(remove_commands[component], entity)
end
local function delete(entity: entity)
table.insert(delete_commands, entity)
end
local command_buffer: command_buffer = {
flush = flush,
add = add,
set = set,
remove = remove,
delete = delete,
}
return command_buffer

77
lib/handle.luau Normal file
View file

@ -0,0 +1,77 @@
--!strict
--!optimize 2
local jecs = require("@pkg/jecs")
export type entity<T = nil> = jecs.Entity<T>
export type id<T = nil> = entity<T> | jecs.Pair
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 }, {} :: interface))
local handle = {} :: interface
handle.__index = handle
function handle.new(entity: entity)
local self = {
entity = entity,
}
return setmetatable(self, handle)
end
function handle:has(...: id): boolean
return world:has(self.entity, ...)
end
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)
end :: any
function handle:add<T>(id: id<T>): handle
world:add(self.entity, id)
return self
end
function handle:set<T>(id: id<T>, value: T): handle
world:set(self.entity, id, value)
return self
end
function handle:remove(id: id): handle
world:remove(self.entity, id)
return self
end
function handle:delete()
world:delete(self.entity)
end
function handle:id(): entity
return self.entity
end
return handle

26
lib/init.luau Normal file
View file

@ -0,0 +1,26 @@
--!strict
--!optimize 2
local WORLD = require("./world")
local collect = require("./collect")
local command_buffer = require("./command_buffer")
local handle = require("./handle")
local jecs = require("@pkg/jecs")
local ref = require("./ref")
local replicator = require("./replicator")
--- 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
return {
initialize = initialize,
collect = collect,
handle = handle,
replicator = replicator,
ref = ref,
command_buffer = command_buffer,
}

27
lib/ref.luau Normal file
View file

@ -0,0 +1,27 @@
--!strict
--!optimize 2
local handle = require("./handle")
local world = require("./world").get()
local refs = {}
--- 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
if not key then
return handle.new(world:entity())
end
local entity = refs[key]
if not entity then
entity = world:entity()
refs[key] = entity
end
return handle.new(entity)
end
return ref

246
lib/replicator.luau Normal file
View file

@ -0,0 +1,246 @@
--!strict
--!optimize 2
local jecs = require("@pkg/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 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

18
lib/world.luau Normal file
View file

@ -0,0 +1,18 @@
--!strict
--!optimize 2
local jecs = require("@pkg/jecs")
local WORLD: jecs.World
local function get(): jecs.World
return WORLD
end
local function set(world: jecs.World)
WORLD = world
end
return {
get = get,
set = set,
}