Networking example
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled

This commit is contained in:
Ukendio 2025-04-28 23:40:03 +02:00
parent 3ae84301f2
commit 6a8d991185
35 changed files with 769 additions and 947 deletions

View file

@ -1,76 +1,55 @@
{
"name": "demo",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "src/ReplicatedStorage",
"ecs": {
"$path": "../jecs.luau"
},
"net": {
"$path": "net/client.luau"
},
"Packages": {
"$path": "Packages"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"$path": "src/ServerScriptService",
"net": {
"$path": "net/server.luau"
}
},
"Workspace": {
"$properties": {
"FilteringEnabled": true
},
"Baseplate": {
"$className": "Part",
"$properties": {
"Anchored": true,
"Color": [
0.38823,
0.37254,
0.38823
],
"Locked": true,
"Position": [
0,
-10,
0
],
"Size": [
512,
20,
512
]
}
}
},
"Lighting": {
"$properties": {
"Ambient": [
0,
0,
0
],
"Brightness": 2,
"GlobalShadows": true,
"Outlines": false,
"Technology": "Voxel"
}
},
"SoundService": {
"$properties": {
"RespectFilteringEnabled": true
}
},
"StarterPlayer": {
"StarterPlayerScripts": {
"$path": "src/StarterPlayer/StarterPlayerScripts"
}
}
}
}
{
"name": "demo",
"emitLegacyScripts": false,
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "src/ReplicatedStorage",
"ecs": {
"$path": "../jecs.luau"
},
"Packages": {
"$path": "Packages"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"$path": "src/ServerScriptService"
},
"Workspace": {
"$properties": {
"FilteringEnabled": true
},
"Baseplate": {
"$className": "Part",
"$properties": {
"Anchored": true,
"Color": [0.38823, 0.37254, 0.38823],
"Locked": true,
"Position": [0, -10, 0],
"Size": [512, 20, 512]
}
}
},
"Lighting": {
"$properties": {
"Ambient": [0, 0, 0],
"Brightness": 2,
"GlobalShadows": true,
"Outlines": false,
"Technology": "Voxel"
}
},
"SoundService": {
"$properties": {
"RespectFilteringEnabled": true
}
},
"StarterPlayer": {
"StarterPlayerScripts": {
"$path": "src/StarterPlayer/StarterPlayerScripts"
}
}
}
}

View file

@ -0,0 +1,28 @@
local function collect<T...>(
signal: {
Connect: (RBXScriptSignal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
): () -> (T...)
local enqueued = {}
local i = 0
local connection = (signal :: any):Connect(function(...)
table.insert(enqueued, { ... })
i += 1
end)
return function(): any
if i == 0 then
return
end
i -= 1
local args: any = table.remove(enqueued, 1)
return unpack(args)
end, connection
end
return collect

View file

@ -0,0 +1,36 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local types = require("./types")
local Networked = jecs.tag()
local NetworkedPair = jecs.tag()
local Renderable = jecs.component() :: jecs.Id<Instance>
jecs.meta(Renderable, Networked)
local Poison = jecs.component() :: jecs.Id<number>
jecs.meta(Poison, Networked)
local Health = jecs.component() :: jecs.Id<number>
jecs.meta(Health, Networked)
local Player = jecs.component() :: jecs.Id<Player>
jecs.meta(Player, Networked)
local components = {
Renderable = Renderable,
Player = Player,
Poison = Poison,
Health = Health,
Networked = Networked,
NetworkedPair = NetworkedPair,
}
for name, component in components do
jecs.meta(component, jecs.Name, name)
end
return components

View file

@ -1,4 +0,0 @@
_G.JECS_DEBUG = true
_G.JECS_HI_COMPONENT_ID = 32
require(game:GetService("ReplicatedStorage").ecs)
return

View file

@ -0,0 +1,13 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN
require(ReplicatedStorage.components)
local world = observers_add(jecs.world())
local systems = ReplicatedStorage.systems
SYSTEM(world, systems.receive_replication)
RUN(world)

View file

@ -0,0 +1,190 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
type Observer<T...> = {
callback: (jecs.Entity) -> (),
query: jecs.Query<T...>,
}
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
-- deleted: (PatchedWorld, () -> ()) -> () -> (),
observer: (PatchedWorld, Observer<any>) -> (),
monitor: (PatchedWorld, Observer<any>) -> (),
}
local function observers_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity)
end
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
end
local function monitors_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnAdd)
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnRemove)
end
end
for _, term in terms do
world:added(term, emplaced)
world:removed(term, removed)
end
end
local function observers_add(world: jecs.World): PatchedWorld
local signals = {
added = {},
emplaced = {},
removed = {},
deleted = {}
}
world = world :: jecs.World & {[string]: any}
world.added = function(_, component, fn)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_add(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnAdd, on_add)
idr.hooks.on_add = on_add :: any
idr_r.hooks.on_add = on_add :: any
end
table.insert(listeners, fn)
end
world.changed = function(_, component, fn)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_change(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnChange, on_change)
idr.hooks.on_change = on_change :: any
idr_r.hooks.on_change = on_change :: any
end
table.insert(listeners, fn)
end
world.removed = function(_, component, fn)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_remove(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnRemove, on_remove)
idr.hooks.on_remove = on_remove :: any
idr_r.hooks.on_remove = on_remove :: any
end
table.insert(listeners, fn)
end
world.signals = signals
world.observer = observers_new
world.monitor = monitors_new
-- local world_delete = world.delete
-- world.deleted = function(_, fn)
-- local listeners = signals.deleted
-- table.insert(listeners, fn)
-- end
-- world.delete = function(world, entity)
-- world_delete(world, entity)
-- for _, fn in signals.deleted do
-- fn(entity)
-- end
-- end
return world :: PatchedWorld
end
return observers_add

View file

@ -0,0 +1,50 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../ReplicatedStorage/types")
type Signal<T...> = {
Connect: (Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
type Remote<T...> = {
FireClient: (Remote<T...>, T...) -> (),
FireAllClients: (Remote<T...>, T...) -> (),
FireServer: (Remote<T...>) -> (),
OnServerEvent: {
Connect: (any, fn: (Player, T...) -> () ) -> ()
},
OnClientEvent: {
Connect: (any, fn: (T...) -> () ) -> ()
}
}
local function stream_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("RemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
local function datagram_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("UnreliableRemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
return {
input = datagram_ensure("input") :: Remote<string>,
replication = stream_ensure("replication") :: Remote<{
[string]: {
set: { types.Entity }?,
values: { any }?,
removed: { types.Entity }?
}
}>,
}

View file

@ -0,0 +1,136 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jabby = require(ReplicatedStorage.Packages.jabby)
local jecs = require(ReplicatedStorage.ecs)
jabby.set_check_function(function() return true end)
local scheduler = jabby.scheduler.create("jabby scheduler")
jabby.register({
applet = jabby.applets.scheduler,
name = "Scheduler",
configuration = {
scheduler = scheduler,
},
})
local ContextActionService = game:GetService("ContextActionService")
local function create_widget(_, state: Enum.UserInputState)
local client = jabby.obtain_client()
if state ~= Enum.UserInputState.Begin then return end
client.spawn_app(client.apps.home, nil)
end
local RunService = game:GetService("RunService")
local System = jecs.component() :: jecs.Id<{
fn: () -> (),
name: string,
}>
local DependsOn = jecs.component()
local Phase = jecs.tag()
local Event = jecs.component() :: jecs.Id<RBXScriptSignal>
local pair = jecs.pair
local types = require(ReplicatedStorage.types)
local function ECS_PHASE(world, after: types.Entity)
local phase = world:entity()
world:add(phase, Phase)
if after then
local dependency = pair(DependsOn, after)
world:add(phase, dependency)
end
return phase
end
local Heartbeat = jecs.tag()
jecs.meta(Heartbeat, Phase)
jecs.meta(Heartbeat, Event, RunService.Heartbeat)
local PreSimulation = jecs.tag()
jecs.meta(PreSimulation, Phase)
jecs.meta(PreSimulation, Event, RunService.PreSimulation)
local PreAnimation = jecs.tag()
jecs.meta(PreAnimation, Phase)
jecs.meta(PreAnimation, Event, RunService.PreAnimation)
local PreRender = jecs.tag()
jecs.meta(PreRender, Phase)
jecs.meta(PreRender, Event, RunService.PreRender)
local function ECS_SYSTEM(world: types.World, mod: ModuleScript, phase: types.Entity?)
local system = world:entity()
local p = phase or Heartbeat
local fn = require(mod) :: (...any) -> ()
world:set(system, System, {
fn = fn(world, 0) or fn,
name = mod.Name,
})
local depends_on = DependsOn :: jecs.Entity
world:add(system, pair(depends_on, p))
end
local function find_systems_w_phase(world: types.World, systems, phase: types.Entity)
local phase_name = world:get(phase, jecs.Name) :: string
for _, s in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, {
id = scheduler:register_system({
phase = phase_name,
name = s.name,
}),
fn = s.fn
})
end
for after in world:query(Phase, pair(DependsOn, phase)) do
find_systems_w_phase(world, systems, after)
end
return systems
end
local function ECS_RUN(world: types.World)
jabby.register({
applet = jabby.applets.world,
name = "MyWorld",
configuration = {
world = world,
},
})
if RunService:IsClient() then
ContextActionService:BindAction("Open Jabby Home", create_widget, false, Enum.KeyCode.F4)
end
for phase, event in world:query(Event, Phase) do
local systems = find_systems_w_phase(world, {}, phase)
event:Connect(function(...)
for _, system in systems do
scheduler:run(system.id, system.fn, world, ...)
end
end)
end
end
return {
PHASE = ECS_PHASE,
SYSTEM = ECS_SYSTEM,
RUN = ECS_RUN,
phases = {
Heartbeat = Heartbeat,
PreSimulation = PreSimulation,
PreAnimation = PreAnimation,
PreRender = PreRender
},
components = {
System = System,
DependsOn = DependsOn,
Phase = Phase,
Event = Event,
}
}

View file

@ -1,34 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local jabby = require(ReplicatedStorage.Packages.jabby)
local std = ReplicatedStorage.std
local scheduler = require(std.scheduler)
local world = require(std.world)
local function start(modules)
for _, module in modules do
require(module)
end
local events = scheduler.COLLECT()
scheduler.BEGIN(events)
jabby.set_check_function(function(player)
return true
end)
if RunService:IsClient() then
local player = game:GetService("Players").LocalPlayer
local playergui = player:WaitForChild("PlayerGui")
local client = jabby.obtain_client()
UserInputService.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.F4 then
local home = playergui:FindFirstChild("Home")
if home then
home:Destroy()
end
client.spawn_app(client.apps.home)
end
end)
end
end
return start

View file

@ -1,40 +0,0 @@
--!optimize 2
--!native
-- original author @centau
local FAILURE = -1
local RUNNING = 0
local SUCCESS = 1
local function SEQUENCE(nodes)
return function(...)
for _, node in nodes do
local status = node(...)
if status <= RUNNING then
return status
end
end
return SUCCESS
end
end
local function FALLBACK(nodes)
return function(...)
for _, node in nodes do
local status = node(...)
if status > FAILURE then
return status
end
end
return FAILURE
end
end
local bt = {
SEQUENCE = SEQUENCE,
FALLBACK = FALLBACK,
RUNNING = RUNNING,
}
return bt

View file

@ -1,67 +0,0 @@
--!nonstrict
--[[
local signal = Signal.new() :: Signal.Signal<string, string>
local events = collect(signal)
local function system(world)
for id, str1, str2 in events do
--
end
end
]]
--[[
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.
--]]
type Signal<T...> = { [any]: any }
local function collect<T...>(event: Signal<T...>)
local storage = {}
local mt = {}
local iter = function()
local n = #storage
return function()
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
end)
setmetatable(storage, mt)
return (storage :: any) :: () -> (number, T...), function()
disconnect()
end
end
return collect

View file

@ -1,30 +0,0 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local world = require(script.Parent.world)
type Entity<T = nil> = jecs.Entity<T>
local components: {
Character: Entity<Model>,
Mob: Entity,
Model: Entity<Model>,
Player: Entity,
Target: Entity,
Transform: Entity<{ new: CFrame, old: CFrame }>,
Velocity: Entity<number>,
Previous: Entity,
} =
{
Character = world:component(),
Mob = world:component(),
Model = world:component(),
Player = world:component(),
Target = world:component(),
Transform = world:component(),
Velocity = world:component(),
Previous = world:component(),
}
for name, component in components :: {[string]: jecs.Entity} do
world:set(component, jecs.Name, name)
end
return table.freeze(components)

View file

@ -1,19 +0,0 @@
local function interval(s)
local pin
local function throttle()
if not pin then
pin = os.clock()
end
local elapsed = os.clock() - pin > s
if elapsed then
pin = os.clock()
end
return elapsed
end
return throttle
end
return interval

View file

@ -1,14 +0,0 @@
local std = game:GetService("ReplicatedStorage").std
local Players = game:GetService("Players")
local scheduler = require(std.scheduler)
local PHASE = scheduler.PHASE
return {
PlayerAdded = PHASE({
event = Players.PlayerAdded
}),
PlayerRemoved = PHASE({
event = Players.PlayerRemoving
})
}

View file

@ -1,26 +0,0 @@
local world = require(script.Parent.world)
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local refs: {[any]: jecs.Entity} = {}
local function fini(key): () -> ()
return function()
refs[key] = nil
end
end
local function noop() end
local function ref(key): (jecs.Entity, () -> ())
if not key then
return world:entity(), noop
end
local e = refs[key]
if not e then
e = world:entity()
refs[key] = e
end
-- Cannot cache handles because they will get invalidated
return e, fini(key)
end
return ref

View file

@ -1,171 +0,0 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local jabby = require(ReplicatedStorage.Packages.jabby)
local jecs = require(ReplicatedStorage.ecs)
local pair = jecs.pair
local Name = jecs.Name
type World = jecs.World
type Entity<T = nil> = jecs.Entity<T>
type Id<T = unknown> = jecs.Id<T>
type System = {
callback: (world: World) -> (),
id: number,
}
type Systems = { System }
type Events = {
RenderStepped: Systems,
Heartbeat: Systems,
}
local world = require(script.Parent.world)
local Disabled = world:entity()
local System = world:component() :: Id<{ callback: (any) -> (), name: string}>
local DependsOn = world:entity()
local Event = world:component() :: Id<RBXScriptSignal>
local Phase = world:entity()
local PreRender = world:entity()
local Heartbeat = world:entity()
local PreAnimation = world:entity()
local PreSimulation = world:entity()
local sys: System
local dt: number
local jabby_scheduler = jabby.scheduler.create("Scheduler")
local a, b, c, d
local function run()
local id = sys.id
jabby_scheduler:run(id, sys.callback, a, b, c, d)
return nil
end
world:add(Heartbeat, Phase)
world:set(Heartbeat, Event, RunService.Heartbeat)
world:add(PreSimulation, Phase)
world:set(PreSimulation, Event, RunService.PreSimulation)
world:add(PreAnimation, Phase)
world:set(PreAnimation, Event, RunService.PreAnimation)
jabby.register({
applet = jabby.applets.world,
name = "MyWorld",
configuration = {
world = world,
},
})
jabby.register({
applet = jabby.applets.scheduler,
name = "Scheduler",
configuration = {
scheduler = jabby_scheduler,
},
})
if RunService:IsClient() then
world:add(PreRender, Phase)
world:set(PreRender, Event, (RunService :: RunService).PreRender)
end
local function begin(events: { [RBXScriptSignal]: Systems })
local connections = {}
for event, systems in events do
if not event then
continue
end
local event_name = tostring(event)
connections[event] = event:Connect(function(...)
debug.profilebegin(event_name)
for _, s in systems do
sys = s
a, b, c, d = ...
for _ in run do
break
end
end
debug.profileend()
end)
end
return connections
end
local function scheduler_collect_systems_under_phase_recursive(systems, phase: Entity)
local phase_name = world:get(phase, Name)
for _, s in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, {
id = jabby_scheduler:register_system({
name = s.name,
phase = phase_name,
} :: any),
callback = s.callback,
})
end
for after in world:query(Phase):with(pair(DependsOn, phase)):iter() do
scheduler_collect_systems_under_phase_recursive(systems, after)
end
end
local function scheduler_collect_systems_under_event(event)
local systems = {}
scheduler_collect_systems_under_phase_recursive(systems, event)
return systems
end
local function scheduler_collect_systems_all()
local events = {}
for phase, event in world:query(Event):with(Phase) do
events[event] = scheduler_collect_systems_under_event(phase)
end
return events
end
local function scheduler_phase_new(d: { after: Entity?, event: RBXScriptSignal? })
local phase = world:entity()
world:add(phase, Phase)
local after = d.after
if after then
local dependency = pair(DependsOn, after :: Entity)
world:add(phase, dependency)
end
local event = d.event
if event then
world:set(phase, Event, event)
end
return phase
end
local function scheduler_systems_new(callback: (any) -> (), phase: Entity?)
local system = world:entity()
world:set(system, System, { callback = callback, name = debug.info(callback, "n") })
local depends_on = DependsOn :: jecs.Entity
local p: Entity = phase or Heartbeat
world:add(system, pair(depends_on, p))
return system
end
return {
SYSTEM = scheduler_systems_new,
BEGIN = begin,
PHASE = scheduler_phase_new,
COLLECT = scheduler_collect_systems_all,
phases = {
Heartbeat = Heartbeat,
PreSimulation = PreSimulation,
PreAnimation = PreAnimation,
PreRender = PreRender
}
}

View file

@ -1,4 +0,0 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
-- I like the idea of only having the world be a singleton.
return jecs.World.new() :: jecs.World

View file

@ -0,0 +1,67 @@
local types = require("../types")
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local remotes = require("../remotes")
local collect = require("../collect")
local client_ids = {}
local function ecs_map_get(world: types.World, id: types.Entity)
local deserialised_id = 0
if not world:exists(id) or not world:contains(id) then
deserialised_id = world:entity(id)
client_ids[id] = deserialised_id
else
deserialised_id = client_ids[id]
end
return deserialised_id
end
local function ecs_make_alive_id(world: types.World, id: jecs.Id)
local rel = jecs.ECS_PAIR_FIRST(id)
local tgt = jecs.ECS_PAIR_SECOND(id)
ecs_map_get(world, rel)
ecs_map_get(world, tgt)
end
local snapshots = collect(remotes.replication.OnClientEvent)
return function(world: types.World)
return function()
for snapshot in snapshots do
for key, map in snapshot do
local id = (tonumber(key) :: any) :: jecs.Id
if jecs.IS_PAIR(id) then
ecs_make_alive_id(world, id)
end
local set = map.set
if set then
if jecs.is_tag(world, id) then
for _, entity in set do
entity = ecs_map_get(world, entity)
world:add(entity, id)
end
else
local values = map.values :: { any }
for i, entity in set do
entity = ecs_map_get(world, entity)
world:set(entity, id, values[i])
end
end
end
local removed = map.removed
if removed then
for i, e in removed do
if not world:contains(e) then
continue
end
world:remove(e, id)
end
end
end
end
end
end

View file

@ -1,48 +0,0 @@
local events = {}
local function trackers_invoke(event, component, entity, ...)
local trackers = events[event][component]
if not trackers then
return
end
for _, tracker in trackers do
tracker(entity, data)
end
end
local function trackers_init(event, component, fn)
local ob = events[event]
return {
connect = function(component, fn)
local trackers = ob[component]
if not trackers then
trackers = {}
ob[component] = trackers
end
table.insert(trackers, fn)
end,
invoke = function(component, ...)
trackers_invoke(event, component, ...)
end
}
return function(component, fn)
local trackers = ob[component]
if not trackers then
trackers = {}
ob[component] = trackers
end
table.insert(trackers, fn)
end
end
local trackers = {
emplace = trackers_init("emplace"),
add = trackers_init("added"),
remove = trackers_init("removed")
}
return trackers

View file

@ -0,0 +1,8 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local observers_add = require("../ReplicatedStorage/observers_add")
export type World = typeof(observers_add(jecs.world()))
export type Entity = jecs.Entity
export type Id<T> = jecs.Id<T>
return {}

View file

@ -1,4 +1,19 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local start = require(ReplicatedStorage.start)
local ServerScriptService = game:GetService("ServerScriptService")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
start(script.Parent:WaitForChild("systems"):GetChildren())
local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN
require(ReplicatedStorage.components)
local world = observers_add(jecs.world())
local systems = ServerScriptService.systems
SYSTEM(world, systems.replication)
SYSTEM(world, systems.players_added)
SYSTEM(world, systems.poison_hurts)
SYSTEM(world, systems.life_is_painful)
RUN(world, 0)

View file

@ -0,0 +1,12 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
local types = require(ReplicatedStorage.types)
return function(world: types.World, dt: number)
for e in world:query(ct.Player):without(ct.Health) do
world:set(e, ct.Health, 100)
end
for e in world:query(ct.Player, ct.Health):without(ct.Poison) do
world:set(e, ct.Poison, 10)
end
end

View file

@ -1,88 +0,0 @@
--!optimize 2
--!native
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(game:GetService("ServerScriptService").net)
local jecs = require(ReplicatedStorage.ecs)
local __ = jecs.Wildcard
local std = ReplicatedStorage.std
local ref = require(std.ref)
local interval = require(std.interval)
local world = require(std.world)
local cts = require(std.components)
local Mob = cts.Mob
local Transform = cts.Transform
local Velocity = cts.Velocity
local Player = cts.Player
local Character = cts.Character
local characters = world
:query(Character)
:with(Player)
:cached()
local moving_mobs = world
:query(Transform, Velocity)
:with(Mob)
:cached()
local function mobsMove(dt: number)
local targets = {}
for _, character in characters do
table.insert(targets, (character.PrimaryPart :: Part).Position)
end
for mob, transform, v in moving_mobs do
local cf = transform.new
local p = cf.Position
local target
local closest
for _, pos in targets do
local distance = (p - pos).Magnitude
if not target or distance < closest then
target = pos
closest = distance
end
end
if not target then
continue
end
local moving = CFrame.new(p + (target - p).Unit * dt * v)
transform.new = moving
blink.UpdateTransform.FireAll(mob, moving)
end
end
local throttle = interval(5)
local function spawnMobs()
if throttle() then
local p = Vector3.new(0, 5, 0)
local cf = CFrame.new(p)
local v = 5
local e = world:entity()
world:set(e, Velocity, v)
world:set(e, Transform, { new = cf })
world:add(e, Mob)
blink.SpawnMob.FireAll(e, cf, v)
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(spawnMobs)
scheduler.SYSTEM(mobsMove)
return 0

View file

@ -1,40 +0,0 @@
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = ReplicatedStorage.std
local ref = require(std.ref)
local collect = require(std.collect)
local cts = require(std.components)
local world = require(std.world)
local Player = cts.Player
local Character = cts.Character
local conn = {}
local function playersAdded(player: Player)
local e = ref(player.UserId)
world:set(e, Player, player)
local characterAdd = player.CharacterAdded
conn[e] = characterAdd:Connect(function(rig)
while rig.Parent ~= workspace do
task.wait()
end
world:set(e, Character, rig)
end)
end
local function playersRemoved(player: Player)
local e = ref(player.UserId)
world:clear(e)
local connection = conn[e]
connection:Disconnect()
conn[e] = nil
end
local scheduler = require(std.scheduler)
local phases = require(std.phases)
scheduler.SYSTEM(playersAdded, phases.PlayerAdded)
scheduler.SYSTEM(playersRemoved, phases.PlayerRemoved)
return 0

View file

@ -0,0 +1,20 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local collect = require(ReplicatedStorage.collect)
local types = require(ReplicatedStorage.types)
local ct = require(ReplicatedStorage.components)
local Players = game:GetService("Players")
local player_added = collect(Players.PlayerAdded)
return function(world: types.World, dt: number)
for player in player_added do
local entity = world:entity()
world:set(entity, ct.Player, player)
end
for entity, player in world:query(ct.Player):without(ct.Renderable) do
local character = player.Character
if character and character.Parent ~= nil then
world:set(entity, ct.Renderable, character)
end
end
end

View file

@ -0,0 +1,12 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
return function(world, dt)
for e, poison, health in world:query(ct.Poison, ct.Health) do
local health_after_tick = health - poison * dt * 0.05
if health_after_tick < 0 then
world:remove(e, ct.Health)
continue
end
world:set(e, ct.Health, health_after_tick)
end
end

View file

@ -0,0 +1,122 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components")
local jecs = require(ReplicatedStorage.ecs)
local remotes = require("../../ReplicatedStorage/remotes")
return function(world: types.World)
local storages = {}
for component in world:query(ct.Networked) do
local is_tag = jecs.is_tag(world, component)
local storage = {} :: { [types.Entity]: any }
storages[component] = storage
if is_tag then
world:added(component, function(entity)
storage[entity] = true
end)
else
world:added(component, function(entity, _, value)
storage[entity] = value
end)
world:changed(component, function(entity, _, value)
storage[entity] = value
end)
end
world:removed(component, function(entity)
storage[entity] = "jecs.Remove"
end)
end
for relation in world:query(ct.NetworkedPair) do
world:added(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
if is_tag then
storage[entity] = true
else
storage[entity] = value
end
end)
world:changed(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
if is_tag then
return
end
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = value
end :: <T>(types.Entity, types.Id<T>, T) -> ())
world:removed(relation, function(entity, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = "jecs.Remove"
end)
end
return function()
local snapshot = {} :: {
[string]: {
set: { types.Entity }?,
values: { any }?,
removed: { types.Entity }?
}
}
local set_ids = {} :: { types.Entity }
local removed_ids = {} :: { types.Entity }
for component, storage in storages do
local set_values = {}
local set_n = 0
local removed_n = 0
for e, v in storage do
if v ~= "jecs.Remove" then
set_n += 1
set_ids[set_n] = e
set_values[set_n] = v or true
elseif world:contains(e) then
removed_n += 1
removed_ids[removed_n] = e
end
end
table.clear(storage)
local dirty = false
if set_n > 0 or removed_n > 0 then
dirty = true
end
if dirty then
snapshot[tostring(component)] = {
set = if set_n > 0 then table.move(set_ids, 1, set_n, 1, {}) else nil,
values = if set_n > 0 then set_values else nil,
removed = if removed_n > 0 then table.move(removed_ids, 1, removed_n, 1, {} :: { types.Entity }) else nil
} :: any
end
end
if next(snapshot) ~= nil then
remotes.replication:FireAllClients(snapshot)
end
end
end

View file

@ -1,4 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local start = require(ReplicatedStorage.start)
start(script.Parent:WaitForChild("systems"):GetChildren())

View file

@ -1,67 +0,0 @@
--!optimize 2
--!native
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local __ = jecs.Wildcard
local std = ReplicatedStorage.std
local world = require(std.world)
local Position = world:component() :: jecs.Entity<vector>
local Previous = jecs.Rest
local pre = jecs.pair(Position, Previous)
local added = world
:query(Position)
:without(pre)
:cached()
local changed = world
:query(Position, pre)
:cached()
local removed = world
:query(pre)
:without(Position)
:cached()
local children = {}
for i = 1, 10 do
local e = world:entity()
world:set(e, Position, vector.create(i, i, i))
table.insert(children, e)
end
local function flip()
return math.random() > 0.5
end
local function system()
for i, child in children do
world:set(child, Position, vector.create(i,i,i))
end
for e, p in added:iter() do
world:set(e, pre, p)
end
for i, child in children do
if flip() then
world:set(child, Position, vector.create(i + 1, i + 1, i + 1))
end
end
for e, new, old in changed:iter() do
if new ~= old then
world:set(e, pre, new)
end
end
for i, child in children do
world:remove(child, Position)
end
for e in removed:iter() do
world:remove(e, pre)
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(system)
return 0

View file

@ -1,90 +0,0 @@
--!optimize 2
--!native
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local __ = jecs.Wildcard
local std = ReplicatedStorage.std
local world = require(std.world)
local Position = world:component() :: jecs.Entity<vector>
local Previous = jecs.Rest
local pre = jecs.pair(Position, Previous)
local added = world
:query(Position)
:without(pre)
:cached()
local changed = world
:query(Position, pre)
:cached()
local removed = world
:query(pre)
:without(Position)
:cached()
local children = {}
for i = 1, 10 do
local e = world:entity()
world:set(e, Position, vector.create(i, i, i))
table.insert(children, e)
end
local function flip()
return math.random() > 0.5
end
local entity_index = world.entity_index
local function copy(archetypes, id)
for _, archetype in archetypes do
local to = jecs.archetype_traverse_add(world, pre, archetype)
local columns = to.columns
local records = to.records
local old = columns[records[pre].column]
local new = columns[records[id].column]
if to ~= archetype then
for _, entity in archetype.entities do
local r = jecs.entity_index_try_get_fast(entity_index, entity)
jecs.entity_move(entity_index, entity, r, to)
end
end
table.move(new, 1, #new, 1, old)
end
end
local function system2()
for i, child in children do
world:set(child, Position, vector.create(i,i,i))
end
for e, p in added:iter() do
end
copy(added:archetypes(), Position)
for i, child in children do
if flip() then
world:set(child, Position, vector.create(i + 1, i + 1, i + 1))
end
end
for e, new, old in changed:iter() do
if new ~= old then
end
end
copy(changed:archetypes(), Position)
for i, child in children do
world:remove(child, Position)
end
for e in removed:iter() do
world:remove(e, pre)
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(system2)
return 0

View file

@ -1,46 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(ReplicatedStorage.net)
local std = ReplicatedStorage.std
local world = require(std.world)
local ref = require(std.ref)
local cts = require(std.components)
local Model = cts.Model
local Transform = cts.Transform
local moved_models = world:query(Model, Transform):cached()
local updated_models = {}
local i = 0
local function processed(n)
i += 1
if i > n then
i = 0
return true
end
return false
end
local function move(dt: number)
for entity, model in moved_models do
if updated_models[entity] then
updated_models[entity] = nil
model.PrimaryPart.CFrame = transform
end
end
end
local function syncTransforms()
for _, id, cf in blink.UpdateTransform.Iter() do
local e = ref("server-" .. tostring(id))
world:set(e, Transform, cf)
moved_models[e] = true
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(move)
scheduler.SYSTEM(syncTransforms)
return 0

View file

@ -1,31 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(ReplicatedStorage.net)
local std = ReplicatedStorage.std
local ref = require(std.ref)
local world = require(std.world)
local cts = require(std.components)
local function syncMobs()
for _, id, cf, vel in blink.SpawnMob.Iter() do
local part = Instance.new("Part")
part.Size = Vector3.one * 5
part.BrickColor = BrickColor.Red()
part.Anchored = true
local model = Instance.new("Model")
model.PrimaryPart = part
part.Parent = model
model.Parent = workspace
local e = ref("server-" .. tostring(id))
world:set(e, cts.Transform, { new = cf, old = cf })
world:set(e, cts.Velocity, vel)
world:set(e, cts.Model, model)
world:add(e, cts.Mob)
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(syncMobs)
return 0

View file

@ -1,44 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = ReplicatedStorage.std
local world = require(std.world)
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local function flip()
return math.random() >= 0.15
end
for i = 1, 2^8 do
local e = world:entity()
if flip() then
world:set(e, A, true)
end
if flip() then
world:set(e, B, true)
end
if flip() then
world:set(e, C, true)
end
if flip() then
world:set(e, D, true)
end
end
local function uncached()
for _ in world:query(A, B, C, D) do
end
end
local q = world:query(A, B, C, D):cached()
local function cached()
for _ in q do
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(uncached)
scheduler.SYSTEM(cached)
return 0

View file

@ -2672,8 +2672,8 @@ return {
ECS_META_RESET = ECS_META_RESET,
IS_PAIR = (ECS_IS_PAIR :: any) :: <P, O>(pair: Pair<P, O>) -> boolean,
ECS_PAIR_FIRST = ECS_PAIR_FIRST,
ECS_PAIR_SECOND = ECS_PAIR_SECOND,
ECS_PAIR_FIRST = ECS_PAIR_FIRST :: <P, O>(pair: Pair<P, O>) -> Id<P>,
ECS_PAIR_SECOND = ECS_PAIR_SECOND :: <P, O>(pair: Pair<P, O>) -> Id<O>,
pair_first = (ecs_pair_first :: any) :: <P, O>(world: World, pair: Pair<P, O>) -> Id<P>,
pair_second = (ecs_pair_second :: any) :: <P, O>(world: World, pair: Pair<P, O>) -> Id<O>,
entity_index_get_alive = entity_index_get_alive,

View file

@ -3,3 +3,4 @@ wally = "upliftgames/wally@0.3.2"
rojo = "rojo-rbx/rojo@7.4.4"
stylua = "johnnymorganz/stylua@2.0.1"
Blink = "1Axen/Blink@0.14.1"
wally-package-types = "JohnnyMorganz/wally-package-types@1.4.2"