From 8e06781be654de835abb8678952d53f61e19bb02 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 30 Nov 2025 08:59:04 +0100 Subject: [PATCH] Replace demo with advanced examples --- demo/.gitignore | 6 - demo/README.md | 15 - demo/default.project.json | 55 --- demo/src/ReplicatedStorage/components.luau | 65 --- demo/src/ReplicatedStorage/main.client.luau | 10 - demo/src/ReplicatedStorage/schedule.luau | 91 ---- .../systems/entities_delete.luau | 13 - demo/src/ReplicatedStorage/types.luau | 14 - demo/src/ServerScriptService/main.server.luau | 21 - .../systems/health_regen.luau | 15 - .../systems/life_is_painful.luau | 19 - .../systems/lifetimes_expire.luau | 12 - .../systems/players_added.luau | 24 - .../systems/poison_hurts.luau | 18 - demo/wally.toml | 8 - .../networking/networking_recv.luau | 243 +++++----- .../networking/networking_send.luau | 446 +++++++++--------- .../networking}/remotes.luau | 12 +- examples/networking/types.luau | 19 + .../collect.luau | 64 ++- modules/remotes.luau | 39 ++ 21 files changed, 438 insertions(+), 771 deletions(-) delete mode 100755 demo/.gitignore delete mode 100755 demo/README.md delete mode 100755 demo/default.project.json delete mode 100755 demo/src/ReplicatedStorage/components.luau delete mode 100755 demo/src/ReplicatedStorage/main.client.luau delete mode 100755 demo/src/ReplicatedStorage/schedule.luau delete mode 100755 demo/src/ReplicatedStorage/systems/entities_delete.luau delete mode 100755 demo/src/ReplicatedStorage/types.luau delete mode 100755 demo/src/ServerScriptService/main.server.luau delete mode 100755 demo/src/ServerScriptService/systems/health_regen.luau delete mode 100755 demo/src/ServerScriptService/systems/life_is_painful.luau delete mode 100755 demo/src/ServerScriptService/systems/lifetimes_expire.luau delete mode 100755 demo/src/ServerScriptService/systems/players_added.luau delete mode 100755 demo/src/ServerScriptService/systems/poison_hurts.luau delete mode 100755 demo/wally.toml rename demo/src/ReplicatedStorage/systems/receive_replication.luau => examples/networking/networking_recv.luau (84%) rename demo/src/ServerScriptService/systems/replication.luau => examples/networking/networking_send.luau (87%) rename {demo/src/ReplicatedStorage => examples/networking}/remotes.luau (74%) create mode 100755 examples/networking/types.luau rename {demo/src/ReplicatedStorage => modules}/collect.luau (95%) create mode 100755 modules/remotes.luau diff --git a/demo/.gitignore b/demo/.gitignore deleted file mode 100755 index cf9d94d..0000000 --- a/demo/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Project place file -/example.rbxlx - -# Roblox Studio lock files -/*.rbxlx.lock -/*.rbxl.lock \ No newline at end of file diff --git a/demo/README.md b/demo/README.md deleted file mode 100755 index 241cc25..0000000 --- a/demo/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Demo - -## Build with Rojo -To build the place, run the following commands from the root of the repository: - -```bash -cd demo -rojo build -o "demo.rbxl" -``` - -Next, open `demo.rbxl` in Roblox Studio and start the Rojo server: - -```bash -rojo serve -``` diff --git a/demo/default.project.json b/demo/default.project.json deleted file mode 100755 index e95cf06..0000000 --- a/demo/default.project.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "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" - } - } - } -} diff --git a/demo/src/ReplicatedStorage/components.luau b/demo/src/ReplicatedStorage/components.luau deleted file mode 100755 index b4f0ccb..0000000 --- a/demo/src/ReplicatedStorage/components.luau +++ /dev/null @@ -1,65 +0,0 @@ ---!strict -local RunService = game:GetService("RunService") -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local jecs = require(ReplicatedStorage.ecs) -local types = require("./types") - -local Networked = jecs.tag() -local NetworkedPair = jecs.tag() - -local InstanceMapping = jecs.component() :: jecs.Id -jecs.meta(InstanceMapping, jecs.OnAdd, function(component) - jecs.meta(component, jecs.OnAdd, function(entity, _, instance) - if RunService:IsServer() then - instance:SetAttribute("entity_server") - else - instance:SetAttribute("entity_client") - end - end) -end) - -local function networked_id(ct) - jecs.meta(ct, Networked) - return ct -end -local function networked_pair(ct) - jecs.meta(ct, NetworkedPair) - return ct -end -local function instance_mapping_id(ct) - jecs.meta(ct, InstanceMapping) - return ct -end - -local Renderable = jecs.component() :: types.Id -local Poison = jecs.component() :: types.Id -local Health = jecs.component() :: types.Id -local Player = jecs.component() :: types.Id -local Debuff = jecs.tag() :: types.Entity -local Lifetime = jecs.component() :: types.Id<{ - duration: number, - created: number -}> -local Destroy = jecs.tag() - -local components = { - Renderable = networked_id(instance_mapping_id(Renderable)), - Player = networked_id(Player), - Poison = networked_id(Poison), - Health = networked_id(Health), - Lifetime = networked_id(Lifetime), - Debuff = networked_id(Debuff), - Destroy = networked_id(Destroy), - - -- We have to define that some builtin IDs can also be networked - ChildOf = networked_pair(jecs.ChildOf), - - Networked = Networked, - NetworkedPair = NetworkedPair, -} - -for name, component in components :: {[string]: types.Id } do - jecs.meta(component, jecs.Name, name) -end - -return components diff --git a/demo/src/ReplicatedStorage/main.client.luau b/demo/src/ReplicatedStorage/main.client.luau deleted file mode 100755 index 699c2a1..0000000 --- a/demo/src/ReplicatedStorage/main.client.luau +++ /dev/null @@ -1,10 +0,0 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local jecs = require(ReplicatedStorage.ecs) -local schedule = require(ReplicatedStorage.schedule) - -local heartbeat = schedule(world, - systems.entities_delete, - systems.replication -) - -game:GetService("RunService").Heartbeat:Connect(heartbeat) diff --git a/demo/src/ReplicatedStorage/schedule.luau b/demo/src/ReplicatedStorage/schedule.luau deleted file mode 100755 index fc41894..0000000 --- a/demo/src/ReplicatedStorage/schedule.luau +++ /dev/null @@ -1,91 +0,0 @@ ---!strict -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local jabby = require(ReplicatedStorage.Packages.jabby) -local ct = require(ReplicatedStorage.components) -local jecs = require(ReplicatedStorage.ecs) - -jabby.set_check_function(function(player) return true end) - -local scheduler = jabby.scheduler.create() - -jabby.register({ - applet = jabby.applets.scheduler, - name = "Scheduler", - configuration = { - scheduler = scheduler, - }, -}::any) - -local ContextActionService = game:GetService("ContextActionService") - -local function create_widget(_, state: Enum.UserInputState): Enum.ContextActionResult - local client = jabby.obtain_client() - if state ~= Enum.UserInputState.Begin then - return Enum.ContextActionResult.Pass - end - client.spawn_app(client.apps.home::any, nil) - return Enum.ContextActionResult.Sink -end - -local RunService = game:GetService("RunService") - -local function schedule(world, ...) - local function get_entity_from_part(part: BasePart): (jecs.Entity?, PVInstance?) - for id, model in world:query(ct.Renderable) do - if not part:IsDescendantOf(model) then continue end - return id, model - end - return nil, nil - end - - jabby.register({ - applet = jabby.applets.world, - name = "World", - configuration = { - world = world, - get_entity_from_part = get_entity_from_part, - }, - }::any) - - local systems = { ... } - - local function systems_load(mod: ModuleScript, ...) - local fn = require(mod) :: (...any) -> () - local system = fn(...) or fn - - local system_id = scheduler:register_system({ - name = mod.Name, - module = mod, - }) - - return { - system = system, - id = system_id - } - end - - for i, mod in systems do - systems[i] = systems_load(mod, world, 0) - end - - if RunService:IsClient() then - ContextActionService:BindAction( - "Open Jabby Home", - create_widget, - false, - Enum.KeyCode.F4 - ) - end - - return function(dt: number, input: InputObject?) - for i, config in systems do - -- config.system(world, dt, input) - local system = config.system - local id = config.id - scheduler:run(id, system, - world, dt, input) - end - end -end - -return schedule diff --git a/demo/src/ReplicatedStorage/systems/entities_delete.luau b/demo/src/ReplicatedStorage/systems/entities_delete.luau deleted file mode 100755 index 1045364..0000000 --- a/demo/src/ReplicatedStorage/systems/entities_delete.luau +++ /dev/null @@ -1,13 +0,0 @@ - ---!strict -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local types = require(ReplicatedStorage.types) -local ct = require(ReplicatedStorage.components) - -local function entities_delete(world: types.World, dt: number) - for e in world:each(ct.Destroy) do - world:delete(e) - end -end - -return entities_delete diff --git a/demo/src/ReplicatedStorage/types.luau b/demo/src/ReplicatedStorage/types.luau deleted file mode 100755 index 538534c..0000000 --- a/demo/src/ReplicatedStorage/types.luau +++ /dev/null @@ -1,14 +0,0 @@ -local jecs = require(game:GetService("ReplicatedStorage").ecs) - -export type World = typeof(jecs.world()) -export type Entity = jecs.Entity -export type Id = jecs.Id -export type Snapshot = { - [string]: { - set: { jecs.Entity }?, - values: { any }?, - removed: { jecs.Entity }? - } -} - -return {} diff --git a/demo/src/ServerScriptService/main.server.luau b/demo/src/ServerScriptService/main.server.luau deleted file mode 100755 index a1cf0a2..0000000 --- a/demo/src/ServerScriptService/main.server.luau +++ /dev/null @@ -1,21 +0,0 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") -local jecs = require(ReplicatedStorage.ecs) -local schedule = require(ReplicatedStorage.schedule) - -require(ReplicatedStorage.components) -local world = jecs.world() - -local systems = ServerScriptService.systems - -local heartbeat = schedule(world, - systems.players_added, - systems.poison_hurts, - systems.health_regen, - systems.lifetimes_expire, - systems.life_is_painful, - systems.entities_delete, - systems.replication -) - -game:GetService("RunService").Heartbeat:Connect(heartbeat) diff --git a/demo/src/ServerScriptService/systems/health_regen.luau b/demo/src/ServerScriptService/systems/health_regen.luau deleted file mode 100755 index 605b08d..0000000 --- a/demo/src/ServerScriptService/systems/health_regen.luau +++ /dev/null @@ -1,15 +0,0 @@ -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, health in world:query(ct.Health) do - if math.random() < 1 / 60 / 30 then - world:set(e, ct.Health, 100) - end - end -end diff --git a/demo/src/ServerScriptService/systems/life_is_painful.luau b/demo/src/ServerScriptService/systems/life_is_painful.luau deleted file mode 100755 index 023883b..0000000 --- a/demo/src/ServerScriptService/systems/life_is_painful.luau +++ /dev/null @@ -1,19 +0,0 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ct = require(ReplicatedStorage.components) -local types = require(ReplicatedStorage.types) -local jecs = require(ReplicatedStorage.ecs) - -return function(world: types.World, dt: number) - if math.random() < (1 / 60 / 7) then - for e in world:each(ct.Health) do - local poison = world:entity() - world:add(poison, ct.Debuff) - world:add(poison, jecs.pair(jecs.ChildOf, e)) - world:set(poison, ct.Poison, 10) - world:set(poison, ct.Lifetime, { - duration = 3, - created = os.clock() - }) - end - end -end diff --git a/demo/src/ServerScriptService/systems/lifetimes_expire.luau b/demo/src/ServerScriptService/systems/lifetimes_expire.luau deleted file mode 100755 index 7f295af..0000000 --- a/demo/src/ServerScriptService/systems/lifetimes_expire.luau +++ /dev/null @@ -1,12 +0,0 @@ - -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ct = require(ReplicatedStorage.components) -local types = require(ReplicatedStorage.types) - -return function(world: types.World, dt: number) - for e, lifetime in world:query(ct.Lifetime) do - if os.clock() > lifetime.created + lifetime.duration then - world:add(e, ct.Destroy) - end - end -end diff --git a/demo/src/ServerScriptService/systems/players_added.luau b/demo/src/ServerScriptService/systems/players_added.luau deleted file mode 100755 index 597f5ec..0000000 --- a/demo/src/ServerScriptService/systems/players_added.luau +++ /dev/null @@ -1,24 +0,0 @@ -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 not character then - continue - end - if not character.Parent then - continue - end - - world:set(entity, ct.Renderable, character) - end -end diff --git a/demo/src/ServerScriptService/systems/poison_hurts.luau b/demo/src/ServerScriptService/systems/poison_hurts.luau deleted file mode 100755 index 6991d39..0000000 --- a/demo/src/ServerScriptService/systems/poison_hurts.luau +++ /dev/null @@ -1,18 +0,0 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ct = require(ReplicatedStorage.components) -local types = require(ReplicatedStorage.types) -local jecs = require(ReplicatedStorage.ecs) - -return function(world: types.World, dt: number) - for e, poison_tick in world:query(ct.Poison, jecs.pair(jecs.ChildOf, jecs.w)) do - local tgt = world:target(e, jecs.ChildOf) - local health = world:get(tgt, ct.Health) - if not health then - continue - end - - if math.random() < 1 / 60 / 1 and health > 1 then - world:set(tgt, ct.Health, health - 1) - end - end -end diff --git a/demo/wally.toml b/demo/wally.toml deleted file mode 100755 index a663a77..0000000 --- a/demo/wally.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "marcus/demo" -version = "0.1.0" -registry = "https://github.com/UpliftGames/wally-index" -realm = "shared" - -[dependencies] -jabby = "alicesaidhi/jabby@0.2.2" diff --git a/demo/src/ReplicatedStorage/systems/receive_replication.luau b/examples/networking/networking_recv.luau similarity index 84% rename from demo/src/ReplicatedStorage/systems/receive_replication.luau rename to examples/networking/networking_recv.luau index 5a38bc1..63de53f 100755 --- a/demo/src/ReplicatedStorage/systems/receive_replication.luau +++ b/examples/networking/networking_recv.luau @@ -1,121 +1,122 @@ -local types = require("../types") -local jecs = require(game:GetService("ReplicatedStorage").ecs) -local remotes = require("../remotes") -local collect = require("../collect") -local components = require("../components") - - -local client_ids: {[jecs.Entity]: jecs.Entity } = {} - -local function ecs_ensure_entity(world: jecs.World, id: jecs.Entity) - local e = 0 - - local ser_id = id - local deser_id = client_ids[ser_id] - if deser_id then - if deser_id == 0 then - local new_id = world:entity() - client_ids[ser_id] = new_id - deser_id = new_id - end - else - if not world:exists(ser_id) - or (world:contains(ser_id) and not world:get(ser_id, jecs.Name)) - then - deser_id = world:entity() - else - if world:contains(ser_id) and world:get(ser_id, jecs.Name) then - deser_id = ser_id - else - deser_id = world:entity() - end - end - client_ids[ser_id] = deser_id - end - - e = deser_id - - return e -end - --- local rel_render = `e{jecs.ECS_ID(rel)}v{jecs.ECS_GENERATION(rel)}` --- local tgt_render = `e{jecs.ECS_ID(tgt)}v{jecs.ECS_GENERATION(tgt)}` --- local function ecs_deser_pairs_str(world, token) --- local tokens = string.split(token, ",") --- local rel = tonumber(tokens[1]) :: jecs.Entity --- local tgt = tonumber(tokens[2]) :: jecs.Entity - --- rel = ecs_ensure_entity(world, rel) --- tgt = ecs_ensure_entity(world, tgt) - --- return jecs.pair(rel, tgt) --- end - -local function ecs_deser_pairs(world, rel, tgt) - rel = ecs_ensure_entity(world, rel) - tgt = ecs_ensure_entity(world, tgt) - - return jecs.pair(rel, tgt) -end - -local snapshots = collect(remotes.replication.OnClientEvent) - -return function(world: jecs.World) - for entity in world:each(components.Destroy) do - client_ids[entity] = nil - end - for snapshot in snapshots do - for ser_id, map in snapshot do - local id = (tonumber(ser_id) :: any) :: jecs.Entity - if jecs.IS_PAIR(id) and map.pair then - id = ecs_deser_pairs(world, map.relation, map.target) - elseif id then - id = ecs_ensure_entity(world, id) - end - -- if not id then - -- id = ecs_deser_pairs_str(world, ser_id) - -- else - -- id = ecs_ensure_entity(world, id) - -- end - local members = world:get(id, components.NetworkedMembers) - - local set = map.set - if set then - if jecs.is_tag(world, id) then - for _, entity in set do - entity = ecs_ensure_entity(world, entity) - world:add(entity, id) - end - else - local values = map.values :: { any } - for i, entity in set do - entity = ecs_ensure_entity(world, entity) - local value = values[i] - if members then - for _, member in members do - local data = value[member] :: {jecs.Entity} | jecs.Entity -- targets - if typeof(data) == "table" then - for pos, tgt in data :: { jecs.Entity } do - data[pos] = ecs_ensure_entity(world, tgt) - end - else - value[member] = ecs_ensure_entity(world, data :: any) - end - end - end - world:set(entity, id, value) - end - end - end - - local removed = map.removed - - if removed then - for _, entity in removed do - entity = ecs_ensure_entity(world, entity) - world:remove(entity, id) - end - end - end - end -end +--!strict +local remotes = require("./remotes") +local jecs = require("@jecs") +local collect = require("@modules/collect") + +-- this is just an example, you need an actually populated map +local components: { [string]: jecs.Id } = {} + +local client_ids: {[jecs.Entity]: jecs.Entity } = {} + +local function ecs_ensure_entity(world: jecs.World, id: jecs.Entity) + local e = 0 + + local ser_id = id + local deser_id = client_ids[ser_id] + if deser_id then + if deser_id == 0::any then + local new_id = world:entity() + client_ids[ser_id] = new_id + deser_id = new_id + end + else + if not world:exists(ser_id) + or (world:contains(ser_id) and not world:get(ser_id, jecs.Name)) + then + deser_id = world:entity() + else + if world:contains(ser_id) and world:get(ser_id, jecs.Name) then + deser_id = ser_id + else + deser_id = world:entity() + end + end + client_ids[ser_id] = deser_id + end + + e = deser_id + + return e +end + +-- local rel_render = `e{jecs.ECS_ID(rel)}v{jecs.ECS_GENERATION(rel)}` +-- local tgt_render = `e{jecs.ECS_ID(tgt)}v{jecs.ECS_GENERATION(tgt)}` +-- local function ecs_deser_pairs_str(world, token) +-- local tokens = string.split(token, ",") +-- local rel = tonumber(tokens[1]) :: jecs.Entity +-- local tgt = tonumber(tokens[2]) :: jecs.Entity + +-- rel = ecs_ensure_entity(world, rel) +-- tgt = ecs_ensure_entity(world, tgt) + +-- return jecs.pair(rel, tgt) +-- end + +local function ecs_deser_pairs(world, rel: jecs.Entity, tgt: jecs.Entity) + rel = ecs_ensure_entity(world, rel) + tgt = ecs_ensure_entity(world, tgt) + + return jecs.pair(rel, tgt) +end + +local snapshots = collect(remotes.replication.OnClientEvent) + +return function(world: jecs.World) + for entity in world:each(components.Destroy) do + client_ids[entity] = nil + end + for snapshot in snapshots do + for ser_id, map in snapshot do + local id = (tonumber(ser_id) :: any) :: jecs.Entity + if jecs.IS_PAIR(id) and map.pair == true then + id = ecs_deser_pairs(world, map.relation, map.target) + elseif id then + id = ecs_ensure_entity(world, id) + end + -- if not id then + -- id = ecs_deser_pairs_str(world, ser_id) + -- else + -- id = ecs_ensure_entity(world, id) + -- end + local members = world:get(id, components.NetworkedMembers) + + local set = map.set + if set then + if jecs.is_tag(world, id) then + for _, entity in set do + entity = ecs_ensure_entity(world, entity) + world:add(entity, id) + end + else + local values = map.values :: { any } + for i, entity in set do + entity = ecs_ensure_entity(world, entity) + local value = values[i] + if members then + for _, member in members do + local data = value[member] :: {jecs.Entity} | jecs.Entity -- targets + if typeof(data) == "table" then + for pos, tgt in data :: { jecs.Entity } do + data[pos] = ecs_ensure_entity(world, tgt) + end + else + value[member] = ecs_ensure_entity(world, data :: any) + end + end + end + world:set(entity, id, value) + end + end + end + + local removed = map.removed + + if removed then + for _, entity in removed do + entity = ecs_ensure_entity(world, entity) + world:remove(entity, id) + end + end + end + end +end diff --git a/demo/src/ServerScriptService/systems/replication.luau b/examples/networking/networking_send.luau similarity index 87% rename from demo/src/ServerScriptService/systems/replication.luau rename to examples/networking/networking_send.luau index 24bf995..eb47502 100755 --- a/demo/src/ServerScriptService/systems/replication.luau +++ b/examples/networking/networking_send.luau @@ -1,222 +1,224 @@ ---!strict -local Players = game:GetService("Players") -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ct = require(ReplicatedStorage.components) -local components = ct :: { [string]: jecs.Entity } -local remotes = require(ReplicatedStorage.remotes) -local jecs = require(ReplicatedStorage.ecs) -local collect = require(ReplicatedStorage.collect) -local ty = require(ReplicatedStorage.types) - -return function(world: jecs.World) - local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }} - local networked_components = {} - local networked_pairs = {} - - for component in world:each(ct.Networked) do - local name = world:get(component, jecs.Name) - assert(name) - if components[name] == nil then - error("Invalid component:"..name) - end - - storages[component] = {} - - table.insert(networked_components, component) - end - - for relation in world:each(ct.NetworkedPair) do - local name = world:get(relation, jecs.Name) - assert(name) - if not components[name] then - error("Invalid component") - end - table.insert(networked_pairs, relation) - end - - for _, component in networked_components do - local name = world:get(component, jecs.Name) - if not name or not components[name] then - -- error("Invalid component") - error(`Networked Component (%id{component}%name{name})`) - end - local is_tag = jecs.is_tag(world, component) - local storage = storages[component] - 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 networked_pairs do - world:added(relation, function(entity: jecs.Entity, id: jecs.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: jecs.Id, id: jecs.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) - - 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 - - -- local requested_snapshots = collect(remotes.request_snapshot.OnServerEvent) - local players_added = collect(Players.PlayerAdded) - - return function(_, dt: number) - local snapshot_lazy: ty.snapshot - local set_ids_lazy: { jecs.Entity } - - -- In the future maybe it should be requested by the player instead when they - -- are ready to receive the replication. Otherwise streaming could be complicated - -- with intances references being nil. - for player in players_added do - if not snapshot_lazy then - snapshot_lazy, set_ids_lazy = {}, {} - - for component, storage in storages do - local set_values = {} - local set_n = 0 - - local q = world:query(component) - local is_tag = jecs.is_tag(world, component) - for _, archetype in q:archetypes() do - local entities = archetype.entities - local entities_len = #entities - table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy) - if not is_tag then - local column = archetype.columns_map[component] - table.move(column, 1, entities_len, set_n + 1, set_values) - end - - set_n += entities_len - end - - local set = table.move(set_ids_lazy, 1, set_n, 1, {}) - - local map = { - set = if set_n > 0 then set else nil, - values = if set_n > 0 then set_values else nil, - } - - if jecs.IS_PAIR(component) then - map.relation = jecs.pair_first(world, component) - map.target = jecs.pair_second(world, component) - map.pair = true - end - snapshot_lazy[tostring(component)] = map - end - end - - remotes.replication:FireClient(player, snapshot_lazy) - end - - -- accumulator += dt - - -- Purposely sending less diffs of the world because doing it at 60hz - -- gets expensive. But this requires interpolated elements in the scene. - -- if accumulator > 1/60 then - -- accumulator = 0 - - local snapshot = {} :: ty.snapshot - - local set_ids = {} - local removed_ids = {} - - for component, storage in storages do - local set_values = {} :: { any } - 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 - local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity } - local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity } - - -- local ser_id: string = nil :: any - - -- if jecs.IS_PAIR(component) then - -- ser_id = `{jecs.pair_first(world, component)},{jecs.pair_second(world, component)}` - -- else - -- ser_id = tostring(component) - -- end - - local map = { - set = if set_n > 0 then set else nil, - values = if set_n > 0 then set_values else nil, - removed = if removed_n > 0 then removed else nil, - } - - if jecs.IS_PAIR(component) then - map.relation = jecs.pair_first(world, component) - map.target = jecs.pair_second(world, component) - map.pair = true - end - - snapshot[tostring(component)] = map - end - end - if next(snapshot) ~= nil then - remotes.replication:FireAllClients(snapshot) - -- print(snapshot) - end - end -end +--!strict +local Players = require("@game/Players") +local remotes = require("./remotes") +local jecs = require("@jecs") +local collect = require("@modules/collect") +local ty = require("./types") + +-- this is just an example, you need an actually populated map +local components: { [string]: jecs.Id } = {} +local ct = components + + +return function(world: jecs.World) + local storages = {} :: { [jecs.Id]: {[jecs.Id]: any }} + local networked_components = {} + local networked_pairs = {} + + for component in world:each(ct.Networked) do + local name = world:get(component, jecs.Name) + assert(name) + if components[name] == nil then + error("Invalid component:"..name) + end + + storages[component] = {} + + table.insert(networked_components, component) + end + + for relation in world:each(ct.NetworkedPair) do + local name = world:get(relation, jecs.Name) + assert(name) + if not components[name] then + error("Invalid component") + end + table.insert(networked_pairs, relation) + end + + for _, component in networked_components do + local name = world:get(component, jecs.Name) + if not name or not components[name] then + -- error("Invalid component") + error(`Networked Component (%id{component}%name{name})`) + end + local is_tag = jecs.is_tag(world, component) + local storage = storages[component] + 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 networked_pairs do + world:added(relation, function(entity: jecs.Entity, id: jecs.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: jecs.Id, id: jecs.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) + + 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 + + -- local requested_snapshots = collect(remotes.request_snapshot.OnServerEvent) + local players_added = collect(Players.PlayerAdded) + + return function(_, dt: number) + local snapshot_lazy: ty.snapshot + local set_ids_lazy: { jecs.Id } + + -- In the future maybe it should be requested by the player instead when they + -- are ready to receive the replication. Otherwise streaming could be complicated + -- with intances references being nil. + for player in players_added do + if not snapshot_lazy then + snapshot_lazy, set_ids_lazy = {}::any, {} + + for component, storage in storages do + local set_values = {} + local set_n = 0 + + local q = world:query(component) + local is_tag = jecs.is_tag(world, component) + for _, archetype in q:archetypes() do + local entities = archetype.entities + local entities_len = #entities + table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy) + if not is_tag then + local column = archetype.columns_map[component] + table.move(column, 1, entities_len, set_n + 1, set_values) + end + + set_n += entities_len + end + + local set = table.move(set_ids_lazy, 1, set_n, 1, {}::any) + + local map = { + set = if set_n > 0 then set else nil, + values = if set_n > 0 then set_values else nil, + } + + if jecs.IS_PAIR(component) then + map.relation = jecs.pair_first(world, component) + map.target = jecs.pair_second(world, component) + map.pair = true + end + snapshot_lazy[tostring(component)] = map + end + end + + remotes.replication:FireClient(player, snapshot_lazy) + end + + -- accumulator += dt + + -- Purposely sending less diffs of the world because doing it at 60hz + -- gets expensive. But this requires interpolated elements in the scene. + -- if accumulator > 1/60 then + -- accumulator = 0 + + local snapshot = {} :: ty.snapshot + + local set_ids = {} + local removed_ids = {} + + for component, storage in storages do + local set_values = {} :: { any } + 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 + local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity } + local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity } + + -- local ser_id: string = nil :: any + + -- if jecs.IS_PAIR(component) then + -- ser_id = `{jecs.pair_first(world, component)},{jecs.pair_second(world, component)}` + -- else + -- ser_id = tostring(component) + -- end + + local map = { + set = if set_n > 0 then set else nil, + values = if set_n > 0 then set_values else nil, + removed = if removed_n > 0 then removed else nil, + } + + if jecs.IS_PAIR(component) then + map.relation = jecs.pair_first(world, component) + map.target = jecs.pair_second(world, component) + map.pair = true + end + + snapshot[tostring(component)] = map + end + end + if next(snapshot::any) ~= nil then + remotes.replication:FireAllClients(snapshot) + -- print(snapshot) + end + end +end diff --git a/demo/src/ReplicatedStorage/remotes.luau b/examples/networking/remotes.luau similarity index 74% rename from demo/src/ReplicatedStorage/remotes.luau rename to examples/networking/remotes.luau index 1c2077f..674ee03 100755 --- a/demo/src/ReplicatedStorage/remotes.luau +++ b/examples/networking/remotes.luau @@ -1,5 +1,5 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local types = require("../ReplicatedStorage/types") +local ReplicatedStorage = require("@game/ReplicatedStorage") +local types = require("./types") type Remote = { FireClient: (Remote, Player, T...) -> (), @@ -31,12 +31,6 @@ end return { input = datagram_ensure("input") :: Remote, - replication = stream_ensure("replication") :: Remote<{ - [string]: { - set: { types.Entity }?, - values: { any }?, - removed: { types.Entity }? - } - }>, + replication = stream_ensure("replication") :: Remote, } diff --git a/examples/networking/types.luau b/examples/networking/types.luau new file mode 100755 index 0000000..d461764 --- /dev/null +++ b/examples/networking/types.luau @@ -0,0 +1,19 @@ +local jecs = require("@jecs") + +export type snapshot = { + [string]: { + set: { jecs.Entity }?, + values: { any }?, + removed: { jecs.Entity }?, + pair: true, + relation: jecs.Entity, + target: jecs.Entity + } | { + set: { jecs.Entity }?, + values: { any }?, + removed: { jecs.Entity }?, + pair: false, + } +} + +return {} diff --git a/demo/src/ReplicatedStorage/collect.luau b/modules/collect.luau similarity index 95% rename from demo/src/ReplicatedStorage/collect.luau rename to modules/collect.luau index 43bfd61..16bdffc 100755 --- a/demo/src/ReplicatedStorage/collect.luau +++ b/modules/collect.luau @@ -1,33 +1,31 @@ - ---!strict -local function collect(signal) - local enqueued = {} - - local i = 0 - - local connection = signal: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 - -type Signal = { - Connect: (self: Signal, callback: (T...) -> ()) -> RBXScriptConnection, - ConnectParallel: (self: Signal, callback: (T...) -> ()) -> RBXScriptConnection, - Once: (self: Signal, callback: (T...) -> ()) -> RBXScriptConnection, - Wait: (self: Signal) -> (T...) -} - -return collect :: (Signal) -> (() -> (T...), RBXScriptConnection) +local function collect(signal) + local enqueued = {} + + local i = 0 + + local connection = signal: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 + +type Signal = { + Connect: (self: Signal, callback: (T...) -> ()) -> RBXScriptConnection, + ConnectParallel: (self: Signal, callback: (T...) -> ()) -> RBXScriptConnection, + Once: (self: Signal, callback: (T...) -> ()) -> RBXScriptConnection, + Wait: (self: Signal) -> (T...) +} + +return collect :: (Signal) -> (() -> (T...), RBXScriptConnection) diff --git a/modules/remotes.luau b/modules/remotes.luau new file mode 100755 index 0000000..87fd2d1 --- /dev/null +++ b/modules/remotes.luau @@ -0,0 +1,39 @@ +-- A simple way to safely type remote events without hassle + +local ReplicatedStorage = require("@game/ReplicatedStorage") +local jecs = require("@jecs") +local ty = require("./") + +type Remote = { + FireClient: (Remote, Player, T...) -> (), + FireAllClients: (Remote, T...) -> (), + FireServer: (Remote, T...) -> (), + OnServerEvent: RBXScriptSignal<(Player, T...)>, + OnClientEvent: RBXScriptSignal +} + +local function stream_ensure(name) + local remote = ReplicatedStorage:FindFirstChild(name) + if not remote then + remote = Instance.new("RemoteEvent") + remote.Name = name + remote.Parent = ReplicatedStorage + end + return remote +end + +local function datagram_ensure(name) + local remote = ReplicatedStorage:FindFirstChild(name) + if not remote then + remote = Instance.new("UnreliableRemoteEvent") + remote.Name = name + remote.Parent = ReplicatedStorage + end + return remote +end + + +return { + input = datagram_ensure("input") :: Remote, + replication = stream_ensure("replication") :: Remote +}