Initial push
This commit is contained in:
commit
ee69c03334
31 changed files with 1578 additions and 0 deletions
73
lib/collect.luau
Normal file
73
lib/collect.luau
Normal 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
114
lib/command_buffer.luau
Normal 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
77
lib/handle.luau
Normal 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
26
lib/init.luau
Normal 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
27
lib/ref.luau
Normal 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
246
lib/replicator.luau
Normal 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
18
lib/world.luau
Normal 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,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue