├── demo ├── .styluaignore ├── .gitignore ├── selene.toml ├── .luaurc ├── src │ ├── client │ │ ├── systems │ │ │ ├── apply_colors.luau │ │ │ ├── replecs_client.luau │ │ │ └── creating_cubes.luau │ │ ├── replicator.luau │ │ ├── systems_order.luau │ │ └── init.client.luau │ ├── server │ │ ├── replicator.luau │ │ ├── systems_order.luau │ │ ├── systems │ │ │ └── replecs_server.luau │ │ ├── init.server.luau │ │ └── controllers │ │ │ └── spawn_cubes.luau │ └── shared │ │ ├── world.luau │ │ ├── replicator.luau │ │ ├── utils │ │ ├── import_paths.luau │ │ ├── interval.luau │ │ ├── ecs │ │ │ ├── count_query.luau │ │ │ ├── add_names.luau │ │ │ ├── add_shared.luau │ │ │ ├── query_first.luau │ │ │ ├── ref.luau │ │ │ └── scheduler.luau │ │ └── collect.luau │ │ ├── custom_ids │ │ └── player.luau │ │ ├── systems │ │ ├── lifetimes.luau │ │ ├── timers.luau │ │ ├── wooble.luau │ │ └── transforms.luau │ │ ├── start.luau │ │ ├── players.luau │ │ ├── components │ │ └── init.luau │ │ └── stories │ │ └── repro.story.luau ├── .stylua.toml ├── selene_definitions.yml ├── .vscode │ └── settings.json ├── rokit.toml ├── config.zap ├── default.project.json ├── pesde.toml ├── patches │ └── wally#alicesaidhi+jabby-0.3.1+roblox.patch └── pesde.lock ├── tests ├── test.luau ├── .luaurc ├── check.luau ├── utils.luau ├── observers.luau └── bench │ └── replecs.bench.luau ├── .gitattributes ├── docs ├── .styluaignore ├── src │ ├── content │ │ └── docs │ │ │ ├── reference │ │ │ ├── types.luau │ │ │ ├── masking-controller.mdx │ │ │ ├── custom-ids.mdx │ │ │ ├── replecs.mdx │ │ │ ├── server-replicator.mdx │ │ │ └── client-replicator.mdx │ │ │ ├── started │ │ │ ├── examples │ │ │ │ ├── client-replicator.luau │ │ │ │ ├── server-replicator.luau │ │ │ │ ├── creating-replicators.luau │ │ │ │ ├── apply-updates.luau │ │ │ │ ├── apply-all.luau │ │ │ │ ├── collect-updates.luau │ │ │ │ └── collect-all.luau │ │ │ ├── installation.md │ │ │ ├── starting-replication.mdx │ │ │ ├── sending-updates.mdx │ │ │ ├── setting-up.mdx │ │ │ └── summary.mdx │ │ │ ├── guides │ │ │ ├── server-client-translation.mdx │ │ │ ├── component-serialization.mdx │ │ │ ├── player-filtering.mdx │ │ │ ├── networking-entity.mdx │ │ │ ├── networking-hooks.mdx │ │ │ └── entity-remapping.mdx │ │ │ └── index.mdx │ ├── assets │ │ └── houston.webp │ ├── content.config.ts │ └── styles │ │ ├── custom.css │ │ └── theme.css ├── .vscode │ ├── extensions.json │ └── launch.json ├── tsconfig.json ├── .gitignore ├── package.json ├── public │ └── favicon.svg ├── astro.config.mjs └── README.md ├── src ├── ver.luau ├── customid.luau ├── types.luau ├── masking │ └── mask_generator.luau ├── init.luau ├── common.luau └── index.d.ts ├── testez-companion.toml ├── zune.toml ├── .lune ├── process.luau ├── darklua.luau └── ts-process.luau ├── .stylua.toml ├── .styluaignore ├── default.project.json ├── selene_definitions.yml ├── selene.toml ├── .luaurc ├── .gitignore ├── serve-ts.project.json ├── wally.lock ├── wally.toml ├── pesde.lock ├── serve.project.json ├── pesde.toml ├── .vscode ├── launch.json └── settings.json ├── rokit.toml ├── README.md ├── .darklua.json ├── jecs.luau ├── .github └── workflows │ ├── deploy-docs.yml │ ├── unit-tests.yml │ ├── build.yml │ ├── ci.yml │ └── cd.yml ├── tsconfig.json ├── LICENSE └── package.json /demo/.styluaignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test.luau: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.patch binary -------------------------------------------------------------------------------- /docs/.styluaignore: -------------------------------------------------------------------------------- 1 | docs/src -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | **/zap.luau -------------------------------------------------------------------------------- /docs/src/content/docs/reference/types.luau: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/selene.toml: -------------------------------------------------------------------------------- 1 | std = "selene_definitions" 2 | -------------------------------------------------------------------------------- /src/ver.luau: -------------------------------------------------------------------------------- 1 | -- Automatically generated 2 | return "" 3 | -------------------------------------------------------------------------------- /testez-companion.toml: -------------------------------------------------------------------------------- 1 | roots = ["ReplicatedStorage/tests"] 2 | -------------------------------------------------------------------------------- /zune.toml: -------------------------------------------------------------------------------- 1 | [workspace.scripts] 2 | test = "tests/replecs.spec.luau" 3 | -------------------------------------------------------------------------------- /demo/.luaurc: -------------------------------------------------------------------------------- 1 | { 2 | "languageMode": "strict", 3 | "aliases": { } 4 | } 5 | -------------------------------------------------------------------------------- /.lune/process.luau: -------------------------------------------------------------------------------- 1 | local darklua = require "./darklua" 2 | 3 | darklua.run() 4 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | call_parentheses = "None" 2 | collapse_simple_statement = "Never" 3 | 4 | syntax = "Luau" 5 | -------------------------------------------------------------------------------- /.styluaignore: -------------------------------------------------------------------------------- 1 | roblox_packages/ 2 | luau_packages/ 3 | lune_packages/ 4 | serve/ 5 | Packages/ 6 | **/patches/ -------------------------------------------------------------------------------- /demo/src/client/systems/apply_colors.luau: -------------------------------------------------------------------------------- 1 | function APPLY_COLORS() end 2 | 3 | return APPLY_COLORS 4 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replecs", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/.stylua.toml: -------------------------------------------------------------------------------- 1 | call_parentheses = "None" 2 | collapse_simple_statement = "Never" 3 | 4 | syntax = "Luau" 5 | -------------------------------------------------------------------------------- /docs/src/assets/houston.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PepeElToro41/replecs/HEAD/docs/src/assets/houston.webp -------------------------------------------------------------------------------- /docs/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /selene_definitions.yml: -------------------------------------------------------------------------------- 1 | base: roblox 2 | name: selene_definitions 3 | globals: 4 | require: 5 | args: 6 | - type: string 7 | -------------------------------------------------------------------------------- /demo/selene_definitions.yml: -------------------------------------------------------------------------------- 1 | base: roblox 2 | name: selene_definitions 3 | globals: 4 | require: 5 | args: 6 | - type: string 7 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/examples/client-replicator.luau: -------------------------------------------------------------------------------- 1 | local replicator = require("@shared/replicator") 2 | 3 | return replicator.client 4 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/examples/server-replicator.luau: -------------------------------------------------------------------------------- 1 | local replicator = require("@shared/replicator") 2 | 3 | return replicator.server 4 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/masking-controller.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Masking Controller 3 | description: Masking Controller API Reference 4 | sidebar: 5 | order: 4 6 | --- 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/examples/creating-replicators.luau: -------------------------------------------------------------------------------- 1 | local replecs = require("@pkg/replecs") 2 | local world = require("world") 3 | 4 | return replecs.create(world) 5 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "selene_definitions" 2 | exclude = ["*.spec.luau"] 3 | 4 | [lints] 5 | undefined_variable = "allow" 6 | shadowing = "allow" 7 | if_same_then_else = "allow" 8 | -------------------------------------------------------------------------------- /tests/.luaurc: -------------------------------------------------------------------------------- 1 | { 2 | "aliases": { 3 | "replecs": "../src/", 4 | "pkg": "../luau_packages/", 5 | "testkit": "./testkit" 6 | }, 7 | "lintErrors": false 8 | } 9 | -------------------------------------------------------------------------------- /demo/src/client/replicator.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local replicator = require(ReplicatedStorage.code.replicator) 3 | 4 | return replicator.client 5 | -------------------------------------------------------------------------------- /demo/src/server/replicator.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local replicator = require(ReplicatedStorage.code.replicator) 3 | 4 | return replicator.server 5 | -------------------------------------------------------------------------------- /demo/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "luau-lsp.completion.autocompleteEnd": true, 4 | "[luau]": { 5 | "editor.defaultFormatter": "JohnnyMorganz.stylua" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/shared/world.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local jecs = require(ReplicatedStorage.packages.jecs) 3 | require(ReplicatedStorage.packages.replecs) 4 | 5 | return jecs.world() 6 | -------------------------------------------------------------------------------- /demo/src/shared/replicator.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local world = require(ReplicatedStorage.code.world) 3 | local replecs = require(ReplicatedStorage.packages.replecs) 4 | 5 | return replecs.create(world :: any) 6 | -------------------------------------------------------------------------------- /.luaurc: -------------------------------------------------------------------------------- 1 | { 2 | "languageMode": "strict", 3 | "aliases": { 4 | "lune": "~/.lune/.typedefs/0.10.4/" 5 | }, 6 | "lintErrors": true, 7 | "typeErrors": true, 8 | "lint": { 9 | "*": true, 10 | "LocalShadow": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsLoader } from '@astrojs/starlight/loaders'; 3 | import { docsSchema } from '@astrojs/starlight/schema'; 4 | 5 | export const collections = { 6 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | sourcemap.json 2 | 3 | **/*.rbx[lm]* 4 | roblox.yml 5 | roblox.toml 6 | 7 | **/serve 8 | **/lune_packages 9 | **/luau_packages 10 | **/roblox_packages 11 | Packages/ 12 | .pesde 13 | 14 | **/sourcemap.json 15 | **/darklua-sourcemap.json 16 | 17 | **/node_modules 18 | include 19 | out 20 | 21 | .zed 22 | dist -------------------------------------------------------------------------------- /demo/rokit.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Rokit, a toolchain manager for Roblox projects. 2 | # For more information, see https://github.com/rojo-rbx/rokit 3 | 4 | # New tools can be added by running `rokit add ` in a terminal. 5 | 6 | [tools] 7 | zap = "red-blox/zap@0.6.26" 8 | lune = "lune-org/lune@0.8.9" 9 | -------------------------------------------------------------------------------- /serve-ts.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replecs-dev", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "$className": "ReplicatedStorage", 7 | "replecs": { 8 | "$path": "out/replecs" 9 | }, 10 | "jecs": { 11 | "$path": "out/jecs.luau" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /tests/check.luau: -------------------------------------------------------------------------------- 1 | local replecs = require "@replecs" 2 | local jecs = require "@pkg/jecs" 3 | 4 | local member = newproxy() 5 | 6 | local world = jecs.world() 7 | local replicator = replecs.create_server(world) 8 | 9 | replicator.masking:register_client(member) 10 | 11 | replicator:mark_player_ready(member) 12 | replicator:init() 13 | -------------------------------------------------------------------------------- /wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "pepeeltoro41/replecs" 7 | version = "0.0.1" 8 | dependencies = [["jecs", "ukendio/jecs@0.8.3"]] 9 | 10 | [[package]] 11 | name = "ukendio/jecs" 12 | version = "0.8.3" 13 | dependencies = [] 14 | -------------------------------------------------------------------------------- /demo/src/shared/utils/import_paths.luau: -------------------------------------------------------------------------------- 1 | function import_paths(container: Instance, deep: boolean?) 2 | local children = container:GetChildren() 3 | for _, child in children do 4 | if child.ClassName == "ModuleScript" then 5 | (require)(child) 6 | end 7 | if deep then 8 | import_paths(child, deep) 9 | end 10 | end 11 | end 12 | 13 | return import_paths 14 | -------------------------------------------------------------------------------- /demo/src/shared/utils/interval.luau: -------------------------------------------------------------------------------- 1 | local function interval(s) 2 | local pin: number = nil 3 | 4 | local function throttle() 5 | if not pin then 6 | pin = os.clock() 7 | end 8 | 9 | local elapsed = os.clock() - pin > s 10 | if elapsed then 11 | pin = os.clock() 12 | end 13 | 14 | return elapsed 15 | end 16 | return throttle 17 | end 18 | 19 | return interval 20 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | sidebar: 4 | order: 0 5 | --- 6 | 7 | ### Installing Replecs 8 | 9 | Replecs is available on: 10 | 11 | - [Wally](https://wally.run/package/pepeeltoro41/replecs?version=0.0.2) 12 | - [NPM](https://www.npmjs.com/package/@rbxts/replecs) 13 | 14 | Replecs requires Jecs `v0.9.0-rc.9` or above to work properly. 15 | -------------------------------------------------------------------------------- /demo/src/shared/utils/ecs/count_query.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local jecs = require(ReplicatedStorage.packages.jecs) 3 | 4 | function count_query(query: jecs.Query) 5 | local count = 0 6 | for _, archetype in query:archetypes() do 7 | count += #archetype.entities 8 | end 9 | return count 10 | end 11 | 12 | return count_query 13 | -------------------------------------------------------------------------------- /demo/src/shared/utils/ecs/add_names.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local Jecs = require(ReplicatedStorage.packages.jecs) 3 | local world = require(ReplicatedStorage.code.world) 4 | 5 | function add_names(entities: T): T 6 | for name, entity in pairs(entities :: any) do 7 | world:set(entity, Jecs.Name, name) 8 | end 9 | return entities 10 | end 11 | 12 | return add_names 13 | -------------------------------------------------------------------------------- /demo/src/shared/utils/ecs/add_shared.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local world = require(ReplicatedStorage.code.world) 3 | local replecs = require(ReplicatedStorage.packages.replecs) 4 | 5 | function add_shared(entities: T): T 6 | for name, entity in pairs(entities :: any) do 7 | world:add(entity, replecs.shared) 8 | end 9 | return entities 10 | end 11 | 12 | return add_shared 13 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pepeeltoro41/replecs" 3 | version = "0.2.3" 4 | description = "Networking library for Jecs" 5 | registry = "https://github.com/UpliftGames/wally-index" 6 | realm = "shared" 7 | include = [ 8 | "default.project.json", 9 | "src", 10 | "src/**", 11 | "wally.toml", 12 | "README.md", 13 | "LICENSE", 14 | ] 15 | exclude = ["**"] 16 | 17 | [dependencies] 18 | jecs = "ukendio/jecs@0.9.0" 19 | -------------------------------------------------------------------------------- /pesde.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by pesde. 2 | # It is not intended for manual editing. 3 | format = 2 4 | name = "pepeeltoro41/replecs" 5 | version = "0.0.1" 6 | target = "luau" 7 | 8 | [graph."marked/jecs@0.9.0 luau"] 9 | direct = ["jecs", { name = "marked/jecs", version = "^0.9.0", index = "default" }, "standard"] 10 | 11 | [graph."marked/jecs@0.9.0 luau".pkg_ref] 12 | ref_ty = "pesde" 13 | index_url = "https://github.com/pesde-pkg/index" 14 | -------------------------------------------------------------------------------- /serve.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replecs", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "$className": "ReplicatedStorage", 7 | "luau_packages": { 8 | "$className": "Folder", 9 | "replecs": { 10 | "$path": "src/" 11 | }, 12 | "jecs": { 13 | "$path": "jecs.luau" 14 | } 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replecs-docs", 3 | "private": true, 4 | "type": "module", 5 | "version": "0.0.1", 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/starlight": "^0.35.2", 15 | "astro": "^5.6.1", 16 | "astro-expressive-code": "^0.41.3", 17 | "sharp": "^0.34.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pesde.toml: -------------------------------------------------------------------------------- 1 | name = "pepeeltoro41/replecs" 2 | authors = ["PepeElToro41"] 3 | description = "Networking library for Jecs" 4 | includes = ["pesde.toml", "README.md", "LICENSE.md", "src"] 5 | license = "MIT" 6 | repository = "https://github.com/PepeElToro41/replecs" 7 | version = "0.0.1" 8 | 9 | [indices] 10 | default = "https://github.com/pesde-pkg/index" 11 | 12 | [target] 13 | environment = "luau" 14 | lib = "src/init.luau" 15 | 16 | [dependencies] 17 | jecs = { name = "marked/jecs", version = "^0.9.0" } 18 | -------------------------------------------------------------------------------- /demo/src/shared/custom_ids/player.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local c = require(ReplicatedStorage.code.components) 3 | local replicator = require(ReplicatedStorage.code.replicator) 4 | local ref = require(ReplicatedStorage.code.utils.ecs.ref) 5 | local replecs = require(ReplicatedStorage.packages.replecs) 6 | 7 | local handler = replecs.create_custom_id("player", function(ctx) 8 | return ref(ctx.component(c.player)) 9 | end) 10 | 11 | replicator:register_custom_id(handler) 12 | return handler 13 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/examples/apply-updates.luau: -------------------------------------------------------------------------------- 1 | local replicator = require("@client/replicator") 2 | local collect = require("@utils/collect") 3 | local system = require("@utils/scheduler/system") 4 | 5 | -- collect converts events into iterators 6 | local updates = collect(remotes_client.send_updates) 7 | 8 | -- system running every frame 9 | function replecs_client() 10 | for buf, variants in updates do 11 | replicator:apply_updates(buf, variants) 12 | end 13 | end 14 | 15 | system(replecs_client) 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "zune-luau", 9 | "request": "launch", 10 | "name": "Launch", 11 | "program": "\"${workspaceFolder}\"/tests/replecs.spec.luau", 12 | "args": [], 13 | "stopOnEntry": false 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /demo/src/server/systems_order.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local ServerScriptService = game:GetService "ServerScriptService" 3 | local lifetimes = require(ReplicatedStorage.code.systems.lifetimes) 4 | local transforms = require(ReplicatedStorage.code.systems.transforms) 5 | local replecs_server = require(ServerScriptService.code.systems.replecs_server) 6 | 7 | local ordered: { string | ((...any) -> ()) } = { 8 | "__other__", 9 | transforms, 10 | replecs_server, 11 | lifetimes, 12 | } 13 | 14 | return ordered 15 | -------------------------------------------------------------------------------- /demo/src/client/systems_order.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local lifetimes = require(ReplicatedStorage.code.systems.lifetimes) 3 | local replecs_client = require(script.Parent.systems.replecs_client) 4 | local transforms = require(ReplicatedStorage.code.systems.transforms) 5 | local apply_colors = require(script.Parent.systems.apply_colors) 6 | 7 | local ordered: { string | ((...any) -> ()) } = { 8 | "__other__", 9 | transforms, 10 | apply_colors, 11 | replecs_client, 12 | lifetimes, 13 | } 14 | 15 | return ordered 16 | -------------------------------------------------------------------------------- /rokit.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Rokit, a toolchain manager for Roblox projects. 2 | # For more information, see https://github.com/rojo-rbx/rokit 3 | 4 | # New tools can be added by running `rokit add ` in a terminal. 5 | 6 | [tools] 7 | selene = "kampfkarren/selene@0.29.0" 8 | rojo = "rojo-rbx/rojo@7.5.1" 9 | wally = "UpliftGames/wally@0.3.2" 10 | stylua = "JohnnyMorganz/stylua@2.1.0" 11 | darklua = "seaofvoices/darklua@0.17.2" 12 | zune = "Scythe-Technology/zune@0.5.1" 13 | lune = "lune-org/lune@0.10.4" 14 | luau-lsp = "JohnnyMorganz/luau-lsp@1.57.1" 15 | -------------------------------------------------------------------------------- /demo/config.zap: -------------------------------------------------------------------------------- 1 | opt server_output = "src/server/zap.luau" 2 | opt client_output = "src/shared/zap.luau" 3 | 4 | funct WaitForServer = { 5 | call: Async, 6 | rets: ( 7 | buffer?, 8 | unknown, 9 | ), 10 | } 11 | 12 | event OnUnreliableUpdates = { 13 | from: Server, 14 | type: OrderedUnreliable, 15 | call: SingleAsync, 16 | data: ( 17 | buf: buffer, 18 | variants: unknown, 19 | ), 20 | } 21 | 22 | event OnReliableUpdates = { 23 | from: Server, 24 | type: Reliable, 25 | call: SingleAsync, 26 | data: ( 27 | buf: buffer, 28 | variants: unknown, 29 | ), 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/examples/apply-all.luau: -------------------------------------------------------------------------------- 1 | local replicator = require("@client/replicator") 2 | local collect = require("@utils/collect") 3 | local system = require("@utils/scheduler/system") 4 | 5 | local updates = collect(remotes_client.send_updates) 6 | local unreliables = collect(remotes_client.send_unreliables) 7 | 8 | function replecs_client() 9 | for buf, variants in updates do 10 | replicator:apply_updates(buf, variants) 11 | end 12 | for buf, variants in unreliables do 13 | replicator:apply_unreliable(buf, variants) 14 | end 15 | end 16 | 17 | system(replecs_client) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Replecs ⚡ 2 | 3 | Replecs is a fast, fine controlled JECS replication library 4 | 5 | Replecs is a feature rich, fast, flexible, powerful, and runtime/library agnostic replication library for ECS. 6 | 7 | - Per-entity replication 8 | - Heavily optimized granular player filtering for entities and components 9 | - Entity ID remapping 10 | - Optional serdes for values 11 | - Luau runtime agnostic 12 | - Buffer-based replication 13 | - Bandwidth efficient 14 | - Supports tags and relationships 15 | - Unreliable replication, with automatic 1kb buffer size limit 16 | 17 | Demo place: https://www.roblox.com/games/132070456195381/ 18 | -------------------------------------------------------------------------------- /.darklua.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "rule": "convert_require", 5 | "current": { 6 | "name": "luau", 7 | "use_luau_configuration": true 8 | }, 9 | "target": { 10 | "name": "roblox", 11 | "rojo_sourcemap": "darklua-sourcemap.json", 12 | "indexing_style": "wait_for_child" 13 | } 14 | }, 15 | "compute_expression", 16 | "remove_unused_if_branch", 17 | "remove_unused_while", 18 | "filter_after_early_return", 19 | "remove_nil_declaration", 20 | "remove_empty_do" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /demo/src/shared/systems/lifetimes.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local c = require(ReplicatedStorage.code.components) 3 | local scheduler = require(ReplicatedStorage.code.utils.ecs.scheduler) 4 | local world = require(ReplicatedStorage.code.world) 5 | 6 | local q_lifetimes = world:query(c.unix, c.lifetime):cached() 7 | 8 | function LIFETIMES() 9 | local now = os.clock() 10 | 11 | for entity, unix, lifetime in q_lifetimes:iter() do 12 | if now - unix > lifetime then 13 | world:delete(entity) 14 | end 15 | end 16 | end 17 | 18 | scheduler.System(LIFETIMES) 19 | return LIFETIMES 20 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/examples/collect-updates.luau: -------------------------------------------------------------------------------- 1 | local replicator = require("@server/replicator") 2 | local interval = require("@utils/interval") 3 | local system = require("@utils/scheduler/system") 4 | 5 | -- interval throttles systems to a specific interval 6 | local updates_interval = interval(1 / 20) 7 | 8 | function replecs_server() 9 | -- will send updates 20 times a second 10 | if updates_interval() then 11 | for player, buf, variants in replicator:collect_updates() do 12 | remotes_server.send_updates:fire(player, buf, variants) 13 | end 14 | end 15 | end 16 | 17 | system(replecs_server) 18 | -------------------------------------------------------------------------------- /docs/src/styles/custom.css: -------------------------------------------------------------------------------- 1 | .card { 2 | border-radius: 0.7rem; 3 | } 4 | 5 | :root { 6 | --sl-content-width: 57rem; 7 | -- code { 8 | color: var(--sl-color-accent-high); 9 | } 10 | } 11 | 12 | .inner { 13 | padding: 1.2rem; 14 | } 15 | 16 | .padded { 17 | padding: 1rem; 18 | margin-left: 1rem; 19 | border-left: solid; 20 | border-left-color: var(--sl-color-gray-4); 21 | border-left-width: 1px; 22 | } 23 | 24 | .starlight-aside code { 25 | color: var(--sl-color-asides-text-accent); 26 | } 27 | 28 | .sub-heading { 29 | color: var(--sl-color-gray-3); 30 | font-size: var(--sl-text-sm); 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/shared/utils/ecs/query_first.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local Jecs = require(ReplicatedStorage.packages.jecs) 3 | 4 | function query_first( 5 | query: Jecs.Query, 6 | predicator: ((entity: Jecs.Entity, T...) -> boolean)? 7 | ): (Jecs.Entity, T...) 8 | local iterator = query:iter() 9 | if predicator then 10 | while true do 11 | local values = { iterator() } 12 | if not values[1] then 13 | break 14 | end 15 | if predicator(unpack(values)) then 16 | return unpack(values) :: any 17 | end 18 | end 19 | return nil :: any 20 | else 21 | return iterator() 22 | end 23 | end 24 | 25 | return query_first 26 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/src/shared/systems/timers.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local c = require(ReplicatedStorage.code.components) 3 | local scheduler = require(ReplicatedStorage.code.utils.ecs.scheduler) 4 | local world = require(ReplicatedStorage.code.world) 5 | 6 | local q_timers = world:query(c.timer):cached() 7 | 8 | function TIMERS() 9 | local now = os.clock() 10 | 11 | for entity, timer in q_timers:iter() do 12 | local elapsed = now - timer.start 13 | 14 | if elapsed < timer.wait then 15 | continue 16 | end 17 | timer.callback(entity) 18 | if timer.repeats then 19 | timer.start = now 20 | else 21 | world:delete(entity) 22 | end 23 | end 24 | end 25 | 26 | scheduler.System(TIMERS) 27 | return TIMERS 28 | -------------------------------------------------------------------------------- /jecs.luau: -------------------------------------------------------------------------------- 1 | local module = require "./luau_packages/jecs" 2 | export type Archetype = module.Archetype 3 | export type QueryInner = module.QueryInner 4 | export type Entity = module.Entity 5 | export type Id = module.Id 6 | export type Pair = module.Pair 7 | export type Item = module.Item 8 | export type Iter = module.Iter 9 | export type Query = module.Query 10 | export type Observer = module.Observer 11 | export type World = module.World 12 | export type Record = module.Record 13 | export type ComponentRecord = module.ComponentRecord 14 | export type ComponentIndex = module.ComponentIndex 15 | export type Archetypes = module.Archetypes 16 | export type EntityIndex = module.EntityIndex 17 | 18 | return module 19 | -------------------------------------------------------------------------------- /demo/src/client/systems/replecs_client.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | 3 | local collect = require(ReplicatedStorage.code.utils.collect) 4 | local replicator = require(script.Parent.Parent.replicator) 5 | local scheduler = require(ReplicatedStorage.code.utils.ecs.scheduler) 6 | local zap = require(ReplicatedStorage.code.zap) 7 | 8 | local reliable_updates = collect(zap.OnReliableUpdates.SetCallback) 9 | local unreliable_updates = collect(zap.OnUnreliableUpdates.SetCallback) 10 | 11 | function REPLECS_CLIENT() 12 | for _, buf, variants in reliable_updates do 13 | replicator:apply_updates(buf, variants) 14 | end 15 | for _, buf, variants in unreliable_updates do 16 | replicator:apply_unreliable(buf, variants) 17 | end 18 | end 19 | 20 | scheduler.System(REPLECS_CLIENT) 21 | return REPLECS_CLIENT 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout your repository using git 18 | uses: actions/checkout@v4 19 | - name: Install, build, and upload your site 20 | uses: withastro/action@v3 21 | with: 22 | path: ./docs 23 | 24 | deploy: 25 | environment: 26 | name: github-pages 27 | url: ${{ steps.deployment.outputs.page_url }} 28 | needs: build 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Deploy to GitHub Pages 32 | id: deployment 33 | uses: actions/deploy-pages@v4 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // required 4 | "allowSyntheticDefaultImports": true, 5 | "downlevelIteration": true, 6 | "jsx": "react", 7 | "jsxFactory": "React.createElement", 8 | "jsxFragmentFactory": "React.Fragment", 9 | "module": "commonjs", 10 | "moduleResolution": "Node", 11 | "noLib": true, 12 | "resolveJsonModule": true, 13 | "experimentalDecorators": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "moduleDetection": "force", 16 | "strict": true, 17 | "target": "ESNext", 18 | "typeRoots": ["node_modules/@rbxts"], 19 | 20 | // configurable 21 | "rootDir": "src", 22 | "outDir": "out", 23 | "incremental": true, 24 | "tsBuildInfoFile": "out/tsconfig.tsbuildinfo", 25 | "declaration": true 26 | }, 27 | "include": ["src/**/*"] 28 | } 29 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/examples/collect-all.luau: -------------------------------------------------------------------------------- 1 | local replicator = require("@server/replicator") 2 | local interval = require("@utils/interval") 3 | local system = require("@utils/scheduler/system") 4 | 5 | local updates_interval = interval(1 / 20) 6 | local unreliables_interval = interval(1 / 30) 7 | 8 | function replecs_server() 9 | -- will send updates 20 times a second 10 | if updates_interval() then 11 | for player, buf, variants in replicator:collect_updates() do 12 | remotes_server.send_updates:fire(player, buf, variants) 13 | end 14 | end 15 | 16 | -- will send unreliables 30 times a second 17 | if unreliables_interval() then 18 | for player, buf, variants in replicator:collect_unreliable() do 19 | remotes_server.send_unreliables:fire(player, buf, variants) 20 | end 21 | end 22 | end 23 | 24 | system(replecs_server) 25 | -------------------------------------------------------------------------------- /demo/default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replecs-demo", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ServerScriptService": { 6 | "$className": "ServerScriptService", 7 | "code": { 8 | "$path": "src/server" 9 | } 10 | }, 11 | "ReplicatedStorage": { 12 | "$className": "ReplicatedStorage", 13 | "packages": { 14 | "$path": "roblox_packages", 15 | "replecs": { 16 | "$path": "../serve/src" 17 | } 18 | }, 19 | "code": { 20 | "$path": "src/shared" 21 | } 22 | }, 23 | "StarterPlayer": { 24 | "$className": "StarterPlayer", 25 | "StarterPlayerScripts": { 26 | "$className": "StarterPlayerScripts", 27 | "code": { 28 | "$path": "src/client" 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /demo/src/client/systems/creating_cubes.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local c = require(ReplicatedStorage.code.components) 3 | local scheduler = require(ReplicatedStorage.code.utils.ecs.scheduler) 4 | local world = require(ReplicatedStorage.code.world) 5 | 6 | local q_no_cube = world:query(c.cube, c.transform, c.size, c.color):without(c.basepart):cached() 7 | 8 | function CREATING_CUBES() 9 | for entity, cube, transform, size, color in q_no_cube:iter() do 10 | local new_cube = Instance.new "Part" 11 | new_cube.Size = size 12 | new_cube.Anchored = true 13 | new_cube.CanCollide = false 14 | new_cube.CFrame = transform 15 | new_cube.Color = color 16 | 17 | new_cube.Parent = workspace 18 | world:set(entity, c.basepart, new_cube) 19 | world:set(entity, c.cleanup, function() 20 | new_cube:Destroy() 21 | end) 22 | end 23 | end 24 | 25 | scheduler.System(CREATING_CUBES) 26 | return CREATING_CUBES 27 | -------------------------------------------------------------------------------- /docs/src/styles/theme.css: -------------------------------------------------------------------------------- 1 | /* Dark mode colors. */ 2 | :root { 3 | --sl-color-accent-low: #131e4f; 4 | --sl-color-accent: #3447ff; 5 | --sl-color-accent-high: #b3c7ff; 6 | --sl-color-white: #ffffff; 7 | --sl-color-gray-1: #eef0f7; 8 | --sl-color-gray-2: #bdc2ce; 9 | --sl-color-gray-3: #838ba1; 10 | --sl-color-gray-4: #50576c; 11 | --sl-color-gray-5: #31374b; 12 | --sl-color-gray-6: #151824; 13 | --sl-color-black: #10131b; 14 | } 15 | /* Light mode colors. */ 16 | :root[data-theme="light"] { 17 | --sl-color-accent-low: #c7d6ff; 18 | --sl-color-accent: #364bff; 19 | --sl-color-accent-high: #182775; 20 | --sl-color-white: #151820; 21 | --sl-color-gray-1: #202638; 22 | --sl-color-gray-2: #31374b; 23 | --sl-color-gray-3: #50576c; 24 | --sl-color-gray-4: #838ba1; 25 | --sl-color-gray-5: #bdc2ce; 26 | --sl-color-gray-6: #eaedf7; 27 | --sl-color-gray-7: #f4f6fb; 28 | --sl-color-black: #ffffff; 29 | } 30 | -------------------------------------------------------------------------------- /demo/src/shared/utils/collect.luau: -------------------------------------------------------------------------------- 1 | --!nonstrict 2 | 3 | type Signal = { [any]: any } 4 | type Cleanup = () -> () 5 | 6 | local function collect(event: (listener: (T...) -> ()) -> Cleanup | Signal) 7 | local storage = {} 8 | local mt = {} 9 | local iter = function() 10 | local n = #storage 11 | return function() 12 | if n <= 0 then 13 | mt.__iter = nil 14 | return nil 15 | end 16 | 17 | n -= 1 18 | return n + 1, unpack(table.remove(storage, 1) :: any) 19 | end 20 | end 21 | 22 | local disconnect 23 | if type(event) == "function" then 24 | disconnect = event(function(...) 25 | table.insert(storage, { ... }) 26 | mt.__iter = iter 27 | end) 28 | else 29 | disconnect = event:Connect(function(...) 30 | table.insert(storage, { ... }) 31 | mt.__iter = iter 32 | end) 33 | end 34 | 35 | setmetatable(storage, mt) 36 | return (storage :: any) :: () -> (number, T...), function() 37 | disconnect() 38 | end 39 | end 40 | 41 | return collect 42 | -------------------------------------------------------------------------------- /demo/pesde.toml: -------------------------------------------------------------------------------- 1 | name = "pepeeltoro41/replecs_demo" 2 | private = true 3 | authors = ["PepeElToro41"] 4 | version = "0.0.1" 5 | 6 | [indices] 7 | default = "https://github.com/pesde-pkg/index" 8 | 9 | [wally_indices] 10 | default = "https://github.com/UpliftGames/wally-index" 11 | 12 | [scripts] 13 | roblox_sync_config_generator = ".pesde/scripts/roblox_sync_config_generator.luau" 14 | sourcemap_generator = ".pesde/scripts/sourcemap_generator.luau" 15 | 16 | [target] 17 | build_files = ["src"] 18 | environment = "roblox" 19 | lib = "src/init.luau" 20 | 21 | [dev_dependencies] 22 | scripts = { name = "pesde/scripts_rojo", version = "^0.2.0", target = "lune" } 23 | 24 | [dependencies] 25 | jecs = { wally = "ukendio/jecs", version = "^0.9.0" } 26 | jabby = { wally = "alicesaidhi/jabby", version = "^0.3.1" } 27 | [overrides] 28 | "jabby>jecs" = { wally = "ukendio/jecs", version = "^0.9.0" } 29 | 30 | [patches] 31 | "wally#alicesaidhi/jabby" = { "0.3.1 roblox" = "patches/wally#alicesaidhi+jabby-0.3.1+roblox.patch" } 32 | -------------------------------------------------------------------------------- /demo/src/server/systems/replecs_server.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local ServerScriptService = game:GetService "ServerScriptService" 3 | local replicator = require(ServerScriptService.code.replicator) 4 | local zap = require(ServerScriptService.code.zap) 5 | local interval = require(ReplicatedStorage.code.utils.interval) 6 | local scheduler = require(ReplicatedStorage.code.utils.ecs.scheduler) 7 | 8 | local updates_interval = interval(1 / 20) 9 | local unreliable_interval = interval(1 / 30) 10 | 11 | function REPLECS_SERVER() 12 | if updates_interval() then 13 | for player, buf, variants in replicator:collect_updates() do 14 | zap.OnReliableUpdates.Fire(player, buf, variants) 15 | end 16 | end 17 | if unreliable_interval() then 18 | for player, buf, variants in replicator:collect_unreliable() do 19 | zap.OnUnreliableUpdates.Fire(player, buf, variants) 20 | end 21 | end 22 | end 23 | 24 | scheduler.System(REPLECS_SERVER) 25 | return REPLECS_SERVER 26 | -------------------------------------------------------------------------------- /demo/src/shared/systems/wooble.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local c = require(ReplicatedStorage.code.components) 3 | local scheduler = require(ReplicatedStorage.code.utils.ecs.scheduler) 4 | local world = require(ReplicatedStorage.code.world) 5 | local jecs = require(ReplicatedStorage.packages.jecs) 6 | 7 | local started_at = world:component() :: jecs.Entity 8 | 9 | local q_non_started = world:query(c.transform):without(started_at):cached() 10 | local q_wooble = world:query(c.unix, started_at):with(c.wooble):cached() 11 | 12 | function WOOBLE() 13 | local now = os.clock() 14 | 15 | for entity, transform in q_non_started:iter() do 16 | world:set(entity, started_at, transform) 17 | end 18 | for entity, unix: number, started: CFrame in q_wooble:iter() do 19 | local elapsed = now - unix 20 | world:set(entity, c.transform, started + Vector3.new(0, math.sin(elapsed) * 10, 0)) 21 | end 22 | end 23 | 24 | scheduler.System(WOOBLE) 25 | return WOOBLE 26 | -------------------------------------------------------------------------------- /demo/src/shared/systems/transforms.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local Workspace = game:GetService "Workspace" 3 | local c = require(ReplicatedStorage.code.components) 4 | local scheduler = require(ReplicatedStorage.code.utils.ecs.scheduler) 5 | local world = require(ReplicatedStorage.code.world) 6 | 7 | local q_transforms = world:query(c.basepart, c.transform):cached() 8 | 9 | function TRANSFORMS() 10 | for _, archetype in q_transforms:archetypes() do 11 | local basepart_index = world.component_index[c.basepart] 12 | local transform_index = world.component_index[c.transform] 13 | local basepart_record = basepart_index.records[archetype.id] 14 | local transform_record = transform_index.records[archetype.id] 15 | 16 | local baseparts = archetype.columns[basepart_record] 17 | local transforms = archetype.columns[transform_record] 18 | 19 | Workspace:BulkMoveTo(baseparts, transforms) 20 | end 21 | end 22 | 23 | scheduler.System(TRANSFORMS) 24 | return TRANSFORMS 25 | -------------------------------------------------------------------------------- /demo/src/server/init.server.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | require(ReplicatedStorage.packages.replecs) 3 | 4 | local players = require(ReplicatedStorage.code.players) 5 | local systems_order = require(script.systems_order) 6 | local start = require(ReplicatedStorage.code.start) 7 | local import_paths = require(ReplicatedStorage.code.utils.import_paths) 8 | local zap = require(script.zap) 9 | 10 | local replicator = require(script.replicator) 11 | replicator:init() 12 | 13 | import_paths(script.systems) 14 | import_paths(ReplicatedStorage.code.systems) 15 | 16 | start(systems_order) 17 | 18 | import_paths(script.controllers) 19 | 20 | players.start_current_players() 21 | players.listen_for_players() 22 | 23 | zap.WaitForServer.SetCallback(function(player) 24 | if replicator:is_player_ready(player) then 25 | return nil, nil 26 | end 27 | replicator:mark_player_ready(player) 28 | local buf, variants = replicator:get_full(player) 29 | 30 | return buf, variants 31 | end) 32 | 33 | game:BindToClose(function() 34 | task.wait(2) 35 | end) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 replecs authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Testing 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | unit-testing: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Install Rokit 14 | uses: CompeyDev/setup-rokit@v0.1.2 15 | with: 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | - name: Setup Pesde 19 | uses: lumin-org/setup-pesde@v0.4.1 20 | with: 21 | version: v0.7.0-rc.7+registry.0.2.3-rc.3 22 | 23 | - name: Install Packages 24 | run: pesde install 25 | 26 | - name: Run Unit Tests 27 | id: run_tests 28 | run: | 29 | output=$(lune run tests/replecs.spec.luau) 30 | echo "$output" 31 | if [[ "$output" == *"0 fails"* ]]; then 32 | echo "Unit Tests Passed" 33 | else 34 | echo "Error: One or More Unit Tests Failed." 35 | exit 1 36 | fi 37 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | version: 7 | required: false 8 | type: string 9 | default: "" 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout Repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Install Rokit 20 | uses: CompeyDev/setup-rokit@v0.1.2 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: Setup Pesde 25 | uses: lumin-org/setup-pesde@v0.4.1 26 | with: 27 | version: v0.7.0-rc.7+registry.0.2.3-rc.3 28 | 29 | - name: Install Packages 30 | run: pesde install 31 | 32 | - name: Darklua Process 33 | run: lune run .lune/process.luau ${{ inputs.version }} 34 | 35 | - name: Build 36 | run: rojo build serve/default.project.json -o replecs.rbxm 37 | 38 | - name: Upload Artifact 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: build 42 | path: replecs.rbxm 43 | -------------------------------------------------------------------------------- /src/customid.luau: -------------------------------------------------------------------------------- 1 | local common = require "./common" 2 | 3 | type Entity = common.Entity 4 | type Component = common.Component 5 | type World = common.World 6 | 7 | export type HandleContext = { 8 | entity_id: number, 9 | component: (component: Component) -> T, 10 | target: (relation: Component, index: number?) -> Entity?, 11 | pair_value: (relation: Component, target: Entity) -> T?, 12 | has_pair: (relation: Component, target: Entity) -> boolean, 13 | 14 | entity: (server_entity: number) -> Entity, 15 | has: (tag: Entity) -> boolean, 16 | } 17 | 18 | export type CustomId = { 19 | identifier: string, 20 | handle_callback: ((ctx: HandleContext) -> Entity?)?, 21 | 22 | handle: (self: CustomId, handler: (ctx: HandleContext) -> Entity?) -> (), 23 | } 24 | 25 | local custom_id = {} 26 | custom_id.__index = custom_id 27 | 28 | function custom_id.handle(custom: CustomId, callback: (ctx: HandleContext) -> Entity?) 29 | custom.handle_callback = callback 30 | end 31 | 32 | function create(identifier: string, callback: ((ctx: HandleContext) -> Entity?)?): CustomId 33 | local handler = {} :: CustomId 34 | handler.identifier = identifier 35 | handler.handle_callback = callback 36 | 37 | return setmetatable(handler, custom_id) :: any 38 | end 39 | 40 | return { create = create } 41 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/server-client-translation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server/Client Translation 3 | sidebar: 4 | order: 6 5 | --- 6 | 7 | Replecs provides ways to translate entities and components when you want to send them through the network. 8 | 9 | 10 | ## Translating Entities 11 | 12 | Replecs has a lookup map that saves the client's equivalent of server entities. You can access this map from the client replicator. 13 | 14 | - `client_replicator:get_server_entity(client_entity)` to translate a client entity to its equivalent in the server. 15 | - `client_replicator:get_client_entity(server_entity)` to translate a server entity to its equivalent in the client. 16 | 17 | ## Translating Components 18 | 19 | Replecs has a special way to translate components in a way that the client and server can agree on. 20 | This system relies on the `replecs.shared` tag. 21 | 22 | You can use this same system to serialize components that get sent through the network. 23 | 24 | - `replicator:encode_component(component)` to encode a component. This returns a number between 1 to 255. 25 | - `replicator:decode_component(number)` to decode a component. This takes the number from `encode_component`. 26 | 27 | The methods for translating components are available in both the client and server replicators. 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | uses: ./.github/workflows/build.yml 15 | 16 | test: 17 | uses: ./.github/workflows/unit-tests.yml 18 | 19 | lint: 20 | name: Lint 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout Repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Install Rokit 27 | uses: CompeyDev/setup-rokit@v0.1.2 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Lint 32 | run: | 33 | selene src/ 34 | 35 | style: 36 | name: Style 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout Repository 40 | uses: actions/checkout@v4 41 | 42 | - name: Install Rokit 43 | uses: CompeyDev/setup-rokit@v0.1.2 44 | with: 45 | token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | - name: Check Code Style 48 | run: | 49 | stylua --check --respect-ignores src/ 50 | -------------------------------------------------------------------------------- /demo/src/client/init.client.luau: -------------------------------------------------------------------------------- 1 | local Players = game:GetService "Players" 2 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 3 | local zap = require(ReplicatedStorage.code.zap) 4 | 5 | local c = require(ReplicatedStorage.code.components) 6 | 7 | local players = require(ReplicatedStorage.code.players) 8 | local systems_order = require(script.systems_order) 9 | local start = require(ReplicatedStorage.code.start) 10 | local ref = require(ReplicatedStorage.code.utils.ecs.ref) 11 | local import_paths = require(ReplicatedStorage.code.utils.import_paths) 12 | local world = require(ReplicatedStorage.code.world) 13 | 14 | import_paths(ReplicatedStorage.code.components) 15 | 16 | local replicator = require(script.replicator) 17 | replicator:init() 18 | 19 | local buf, variants = zap.WaitForServer.Call() 20 | 21 | import_paths(script.systems) 22 | import_paths(ReplicatedStorage.code.systems) 23 | import_paths(script.controllers) 24 | 25 | replicator:handle_global(function(id) 26 | return ref(`global-{id}`) :: any 27 | end) 28 | 29 | start(systems_order) 30 | 31 | players.start_current_players() 32 | players.listen_for_players() 33 | 34 | local player = ref(Players.LocalPlayer) 35 | world:add(player, c.client) 36 | 37 | if buf then 38 | replicator:apply_full(buf, variants :: any) 39 | end 40 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from 'astro/config'; 3 | import starlight from '@astrojs/starlight'; 4 | import astroExpressiveCode from 'astro-expressive-code'; 5 | 6 | // https://astro.build/config 7 | export default defineConfig({ 8 | site: 'https://pepeeltoro41.github.io', 9 | base: '/replecs', 10 | integrations: [ 11 | astroExpressiveCode({ 12 | themes: ['aurora-x'], 13 | }), 14 | starlight({ 15 | title: 'Replecs', 16 | social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/PepeElToro41/replecs' }], 17 | sidebar: [ 18 | { 19 | label: 'Get Started', 20 | autogenerate: { directory: 'started' }, 21 | }, 22 | { 23 | label: 'Guides', 24 | autogenerate: { directory: 'guides' }, 25 | }, 26 | { 27 | label: 'API Reference', 28 | autogenerate: { directory: 'reference' }, 29 | }, 30 | ], 31 | customCss: [ 32 | // Path to your Tailwind base styles: 33 | './src/styles/theme.css', 34 | './src/styles/custom.css', 35 | ], 36 | }), 37 | ], 38 | }); -------------------------------------------------------------------------------- /.lune/darklua.luau: -------------------------------------------------------------------------------- 1 | local fs = require "@lune/fs" 2 | local process = require "@lune/process" 3 | 4 | local OUTPUT = "serve" 5 | local DARKLUA_CONFIG = ".darklua.json" 6 | 7 | local function process_darklua() 8 | if fs.isDir(OUTPUT) then 9 | fs.removeDir(OUTPUT) 10 | end 11 | 12 | fs.writeDir(OUTPUT) 13 | 14 | fs.copy("serve.project.json", OUTPUT .. "/serve.project.json") 15 | fs.copy("default.project.json", OUTPUT .. "/default.project.json") 16 | fs.copy("src", OUTPUT .. "/src") 17 | fs.copy("luau_packages", OUTPUT .. "/luau_packages") 18 | fs.copy("jecs.luau", OUTPUT .. "/jecs.luau") 19 | fs.copy("wally.toml", OUTPUT .. "/wally.toml") 20 | fs.copy("LICENSE", OUTPUT .. "/LICENSE") 21 | fs.copy("README.md", OUTPUT .. "/README.md") 22 | 23 | process.exec("rojo", { 24 | "sourcemap", 25 | OUTPUT .. "/serve.project.json", 26 | "--output", 27 | "darklua-sourcemap.json", 28 | }, { stdio = "forward" }) 29 | 30 | process.exec("darklua", { 31 | "process", 32 | "src", 33 | `{OUTPUT}/src`, 34 | `--config`, 35 | DARKLUA_CONFIG, 36 | }, { stdio = "forward" }) 37 | 38 | if process.args[1] and type(process.args[1]) == "string" then 39 | fs.writeFile(OUTPUT .. "/src/ver.luau", `return "{process.args[1]}"`) 40 | end 41 | end 42 | 43 | return { 44 | run = process_darklua, 45 | OUTPUT = OUTPUT, 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/replecs", 3 | "version": "0.2.3", 4 | "description": "Replication library for Jecs", 5 | "main": "out/init.luau", 6 | "types": "out/index.d.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./out/index.d.ts", 10 | "default": "./out/init.luau" 11 | } 12 | }, 13 | "directories": { 14 | "doc": "docs", 15 | "test": "tests" 16 | }, 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "files": [ 21 | "out", 22 | "!**/*.spec.lua", 23 | "!**/*.spec.d.ts" 24 | ], 25 | "scripts": { 26 | "serve": "rojo sourcemap serve.project.json -o sourcemap.json --watch && rojo serve serve.project.json" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/PepeElToro41/replecs.git" 31 | }, 32 | "author": "PepeElToro41", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/PepeElToro41/replecs/issues" 36 | }, 37 | "homepage": "https://github.com/PepeElToro41/replecs#readme", 38 | "devDependencies": { 39 | "@rbxts/compiler-types": "^2.3.0-types.1", 40 | "@rbxts/types": "^1.0.768", 41 | "roblox-ts": "3.0.0", 42 | "typescript": "=5.5.3", 43 | "prettier": "^3.2.5" 44 | }, 45 | "peerDependencies": { 46 | "@rbxts/jecs": "^0.9.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Replecs 3 | description: Agnostic Replication Library for ECS. 4 | template: splash 5 | hero: 6 | tagline: Agnostic Replication Library for ECS 7 | image: 8 | file: ../../assets/houston.webp 9 | actions: 10 | - text: Get Started 11 | link: /replecs/started/installation/ 12 | icon: right-arrow 13 | - text: API Reference 14 | link: /replecs/reference/todo/ 15 | variant: minimal 16 | icon: right-arrow 17 | --- 18 | 19 | import { Card, CardGrid } from '@astrojs/starlight/components'; 20 | 21 | 22 | 23 | Per-entity replication with granular component selection and player filtering 24 | 25 | 26 | Able to track and serialize up to 50K entities **with player filters** at 60 frames per second. 27 | 28 | 29 | Bandwidth efficient buffer based replication with optional
per-component serialization. 30 |
31 | 32 | Supports any kind of replication: components, tags, pairs, pair values and more. 33 | 34 | 35 | Able to send components unreliably with automatic packet splitting for 1KB packet limit. 36 | 37 | 38 | Written to easily support any ECS library
and run in non-roblox environments. 39 |
40 |
41 | -------------------------------------------------------------------------------- /demo/src/shared/start.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local RunService = game:GetService "RunService" 3 | local UserInputService = game:GetService "UserInputService" 4 | local scheduler = require(ReplicatedStorage.code.utils.ecs.scheduler) 5 | local world = require(ReplicatedStorage.code.world) 6 | local jabby = require(ReplicatedStorage.packages.jabby) 7 | 8 | function start_jabby() 9 | local prefix = RunService:IsServer() and "Server" or "Client" 10 | jabby.register { 11 | applet = jabby.applets.world, 12 | name = `World - {prefix}`, 13 | configuration = { 14 | world = world, 15 | }, 16 | } 17 | jabby.register { 18 | applet = jabby.applets.scheduler, 19 | name = `Scheduler - {prefix}`, 20 | configuration = { 21 | scheduler = scheduler.Scheduler :: any, 22 | }, 23 | } 24 | 25 | jabby.set_check_function(function() 26 | return true 27 | end) 28 | 29 | if RunService:IsClient() then 30 | local mounted = false 31 | local client = jabby.obtain_client() 32 | 33 | UserInputService.InputBegan:Connect(function(input) 34 | if input.KeyCode ~= Enum.KeyCode.F4 then 35 | return 36 | end 37 | 38 | if mounted then 39 | client.unmount_all() 40 | mounted = false 41 | else 42 | client.spawn_app(client.apps.home :: any) 43 | mounted = true 44 | end 45 | end) 46 | end 47 | end 48 | 49 | function start(systems_order: any) 50 | start_jabby() 51 | local events = scheduler.CollectSystems() 52 | scheduler.Initialize(events, systems_order) 53 | end 54 | 55 | return start 56 | -------------------------------------------------------------------------------- /demo/src/shared/utils/ecs/ref.luau: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 3 | local Jecs = require(ReplicatedStorage.packages.jecs) 4 | local world = require(ReplicatedStorage.code.world) 5 | local refs: { [any]: any } = {} 6 | 7 | --- Gets an entity the given key references to. 8 | --- If the key is nil, an entirely new entity is created and returned. 9 | --- If the key doesn't reference an entity, a new entity is made for it to reference and returned. 10 | --- @param key any 11 | local function ref(key: any, initer: ((entity: Jecs.Entity) -> ())?): Jecs.Entity 12 | if not key then 13 | return world:entity() 14 | end 15 | 16 | local entity = refs[key] 17 | if not entity then 18 | entity = world:entity() 19 | 20 | if initer then 21 | initer(entity) 22 | end 23 | 24 | refs[key] = entity 25 | end 26 | 27 | return entity 28 | end 29 | 30 | -- For the `__call`` metamethod 31 | local function __call(_, key: any, initer: (entity: Jecs.Entity) -> ()) 32 | return ref(key, initer) 33 | end 34 | 35 | local function search(key: any) 36 | if not key then 37 | return nil 38 | end 39 | local entity = refs[key] 40 | 41 | if not entity then 42 | return nil 43 | end 44 | 45 | return entity 46 | end 47 | 48 | function set(key: any, entity: Jecs.Entity) 49 | refs[key] = entity 50 | end 51 | 52 | function unlist(key: any) 53 | if not key then 54 | return 55 | end 56 | refs[key] = nil 57 | end 58 | 59 | local metatable = { 60 | __call = __call, 61 | } 62 | 63 | local lib = { 64 | set = set, 65 | search = search, 66 | unlist = unlist, 67 | } 68 | 69 | local REF = (setmetatable :: any)(lib, metatable) :: typeof(ref) & typeof(setmetatable(lib, metatable)) 70 | 71 | return REF 72 | -------------------------------------------------------------------------------- /demo/src/server/controllers/spawn_cubes.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local ServerScriptService = game:GetService "ServerScriptService" 3 | local replicator = require(ServerScriptService.code.replicator) 4 | local c = require(ReplicatedStorage.code.components) 5 | local count_query = require(ReplicatedStorage.code.utils.ecs.count_query) 6 | local world = require(ReplicatedStorage.code.world) 7 | 8 | local MAX_CUBES = 100 9 | 10 | function create_cube() 11 | local new_cube = world:entity() 12 | 13 | local random_pos = Vector3.new(math.random(-200, 200), math.random(20, 200), math.random(-200, 200)) 14 | local cube_size = math.random(5, 10) 15 | 16 | world:set(new_cube, c.transform, CFrame.new(random_pos)) 17 | world:set(new_cube, c.color, BrickColor.random().Color) 18 | world:set(new_cube, c.size, Vector3.new(cube_size, cube_size, cube_size)) 19 | world:add(new_cube, c.cube) 20 | world:add(new_cube, c.wooble) 21 | world:set(new_cube, c.unix, os.clock()) 22 | 23 | task.spawn(function() 24 | task.wait(1) 25 | world:set(new_cube, c.unix, os.clock()) 26 | end) 27 | 28 | replicator:set_networked(new_cube) 29 | replicator:set_unreliable(new_cube, c.transform) 30 | replicator:set_reliable(new_cube, c.color) 31 | replicator:set_reliable(new_cube, c.cube) 32 | replicator:set_reliable(new_cube, c.size) 33 | 34 | return new_cube 35 | end 36 | 37 | local q_cubes = world:query(c.cube):cached() 38 | 39 | task.spawn(function() 40 | for i = 1, MAX_CUBES do 41 | task.wait(0.2) 42 | create_cube() 43 | end 44 | 45 | while true do 46 | task.wait(0.2) 47 | local count = count_query(q_cubes) 48 | if count >= MAX_CUBES then 49 | continue 50 | end 51 | create_cube() 52 | end 53 | end) 54 | 55 | return nil 56 | -------------------------------------------------------------------------------- /src/types.luau: -------------------------------------------------------------------------------- 1 | local customid = require "./customid" 2 | local common = require "./common" 3 | 4 | export type FunctionFilter = (player: Player) -> boolean 5 | export type AnyFilter = PlayerFilter | Player 6 | 7 | type Entity = common.Entity 8 | type World = common.World 9 | type Component = common.Component 10 | 11 | export type PlayerFilter = { 12 | [Player]: boolean, 13 | } 14 | 15 | export type SharedInfo = { 16 | lookup: { [string]: T }, 17 | keys: { string }, 18 | indexes: { [number]: T }, 19 | members: { [T]: number }, 20 | } 21 | export type HandshakeInfo = { 22 | components: { [string]: boolean }, 23 | custom_ids: { [string]: boolean }, 24 | serdes: { [string]: { 25 | includes_variants: boolean?, 26 | bytespan: number?, 27 | } }, 28 | } 29 | 30 | export type Serdes = { 31 | includes_variants: boolean?, 32 | bytespan: number?, 33 | serialize: (value: any) -> (buffer, { any }?), 34 | deserialize: (buffer, { any }?) -> any, 35 | } 36 | 37 | export type Components = { 38 | shared: Entity, 39 | Shared: Entity, 40 | networked: Component, 41 | Networked: Component, 42 | reliable: Component, 43 | Reliable: Component, 44 | unreliable: Component, 45 | Unreliable: Component, 46 | pair: Component, 47 | Pair: Component, 48 | 49 | serdes: Component, 50 | Serdes: Component, 51 | custom: Entity, 52 | Custom: Entity, 53 | custom_handler: Component<(val: any) -> Entity?>, 54 | CustomHandler: Component<(val: any) -> Entity?>, 55 | global: Component, 56 | Global: Component, 57 | __alive_tracking__: Entity, 58 | } 59 | 60 | export type Shared = { 61 | components: SharedInfo, 62 | custom_ids: SharedInfo, 63 | 64 | serdes: { [Entity]: Serdes }, 65 | } 66 | 67 | return nil 68 | -------------------------------------------------------------------------------- /.lune/ts-process.luau: -------------------------------------------------------------------------------- 1 | local fs = require "@lune/fs" 2 | local process = require "@lune/process" 3 | 4 | local OUTPUT = "dist" 5 | local DARKLUA_CONFIG = ".darklua.json" 6 | 7 | local barrel = [[ 8 | import "./jecs" 9 | 10 | import replecs from "./replecs" 11 | 12 | export = replecs; 13 | ]] 14 | 15 | local function process_darklua() 16 | if fs.isDir(OUTPUT) then 17 | fs.removeDir(OUTPUT) 18 | end 19 | 20 | fs.writeDir(OUTPUT) 21 | 22 | -- Write project files with $path pointing to "out" for rbxtsc 23 | fs.writeFile(OUTPUT .. "/default.project.json", fs.readFile("default.project.json"):gsub('"src"', '"out"')) 24 | 25 | fs.copy("./serve-ts.project.json", OUTPUT .. "/serve.project.json") 26 | fs.copy("package.json", OUTPUT .. "/package.json") 27 | fs.copy("src", OUTPUT .. "/src/replecs") 28 | fs.copy("tsconfig.json", OUTPUT .. "/tsconfig.json") 29 | fs.copy(".darklua.json", OUTPUT .. "/.darklua.json") 30 | fs.copy("LICENSE", OUTPUT .. "/LICENSE") 31 | fs.copy("README.md", OUTPUT .. "/README.md") 32 | 33 | fs.writeFile(OUTPUT .. "/src/jecs.ts", 'export * from "@rbxts/jecs"\n') 34 | fs.writeFile(OUTPUT .. "/src/index.ts", barrel) 35 | 36 | process.exec("npm", { "install" }, { shell = true, stdio = "forward", cwd = OUTPUT }) 37 | process.exec("npx", { "rbxtsc", "--verbose" }, { shell = true, stdio = "forward", cwd = OUTPUT }) 38 | 39 | process.exec("rojo", { 40 | "sourcemap", 41 | OUTPUT .. "/serve.project.json", 42 | "--output", 43 | OUTPUT .. "/darklua-sourcemap.json", 44 | }, { stdio = "forward" }) 45 | 46 | process.exec("darklua", { 47 | "process", 48 | `out/replecs`, 49 | `out/replecs`, 50 | `--config`, 51 | DARKLUA_CONFIG, 52 | }, { stdio = "forward", cwd = OUTPUT }) 53 | 54 | if process.args[1] and type(process.args[1]) == "string" then 55 | fs.writeFile(OUTPUT .. "/src/ver.luau", `return "{process.args[1]}"`) 56 | end 57 | end 58 | 59 | process_darklua() 60 | -------------------------------------------------------------------------------- /demo/src/shared/players.luau: -------------------------------------------------------------------------------- 1 | local Players = game:GetService "Players" 2 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 3 | local RunService = game:GetService "RunService" 4 | local c = require(script.Parent.components) 5 | local replicator = require(script.Parent.replicator) 6 | local ref = require(ReplicatedStorage.code.utils.ecs.ref) 7 | local replecs = require(ReplicatedStorage.packages.replecs) 8 | local world = require(script.Parent.world) 9 | 10 | local players = {} 11 | 12 | function setup(player: Player) 13 | if RunService:IsServer() then 14 | -- shuffling the entityid to make sure custom ids are working 15 | world:entity() 16 | world:entity() 17 | world:entity() 18 | world:entity() 19 | end 20 | 21 | local entity = ref(player) 22 | world:set(entity, c.player, player) 23 | 24 | if RunService:IsServer() then 25 | world:add(entity, replecs.networked) 26 | replicator.server:set_custom(entity, c.player) 27 | replicator.server:set_reliable(entity, c.player) 28 | replicator.server:set_reliable(entity, c.unix) 29 | world:set(entity, c.unix, 0) 30 | 31 | world:set(entity, c.timer, { 32 | start = os.clock(), 33 | wait = 1, 34 | repeats = true, 35 | callback = function() 36 | world:set(entity, c.unix, world:get(entity, c.unix) :: number + 1) 37 | end, 38 | }) 39 | end 40 | 41 | for _, callback in world:query(c.setup_player):iter() do 42 | callback(player, entity) 43 | end 44 | end 45 | 46 | function players.cleanup(player: Player) 47 | local entity = ref.search(player) 48 | if entity then 49 | world:delete(entity) 50 | end 51 | end 52 | 53 | function players.start_current_players() 54 | for _, player in Players:GetPlayers() do 55 | setup(player) 56 | end 57 | end 58 | 59 | function players.listen_for_players() 60 | Players.PlayerAdded:Connect(setup) 61 | Players.PlayerRemoving:Connect(players.cleanup) 62 | end 63 | 64 | return players 65 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Starlight Starter Kit: Basics 2 | 3 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 4 | 5 | ``` 6 | npm create astro@latest -- --template starlight 7 | ``` 8 | 9 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 10 | 11 | ## 🚀 Project Structure 12 | 13 | Inside of your Astro + Starlight project, you'll see the following folders and files: 14 | 15 | ``` 16 | . 17 | ├── public/ 18 | ├── src/ 19 | │ ├── assets/ 20 | │ ├── content/ 21 | │ │ └── docs/ 22 | │ └── content.config.ts 23 | ├── astro.config.mjs 24 | ├── package.json 25 | └── tsconfig.json 26 | ``` 27 | 28 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. 29 | 30 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 31 | 32 | Static assets, like favicons, can be placed in the `public/` directory. 33 | 34 | ## 🧞 Commands 35 | 36 | All commands are run from the root of the project, from a terminal: 37 | 38 | | Command | Action | 39 | | :------------------------ | :----------------------------------------------- | 40 | | `npm install` | Installs dependencies | 41 | | `npm run dev` | Starts local dev server at `localhost:4321` | 42 | | `npm run build` | Build your production site to `./dist/` | 43 | | `npm run preview` | Preview your build locally, before deploying | 44 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 45 | | `npm run astro -- --help` | Get help using the Astro CLI | 46 | 47 | ## 👀 Want to learn more? 48 | 49 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). 50 | -------------------------------------------------------------------------------- /demo/patches/wally#alicesaidhi+jabby-0.3.1+roblox.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/client/spawn_app.luau b/src/client/spawn_app.luau 2 | index 192dcdf..55f0661 100644 3 | --- a/src/client/spawn_app.luau 4 | +++ b/src/client/spawn_app.luau 5 | @@ -1,36 +1,36 @@ 6 | -local Players = game:GetService("Players") 7 | - 8 | -local vide = require(script.Parent.Parent.Parent.vide) 9 | -local types = require(script.Parent.Parent.modules.types) 10 | - 11 | -local destroy_fn = {} 12 | - 13 | -local function unmount_all() 14 | - for destroy in destroy_fn do 15 | - destroy() 16 | - end 17 | -end 18 | - 19 | -local function spawn_app(app: types.Application, props: T): () -> () 20 | - return vide.root(function(destroy) 21 | - 22 | - local destroy = function() 23 | - destroy_fn[destroy] = nil 24 | - destroy() 25 | - end 26 | - 27 | - local application = app.mount(props, destroy) 28 | - application.Parent = Players.LocalPlayer.PlayerGui 29 | - 30 | - vide.cleanup(application) 31 | - 32 | - destroy_fn[destroy] = true 33 | - 34 | - return destroy 35 | - end) 36 | -end 37 | - 38 | -return { 39 | - unmount_all = unmount_all, 40 | - spawn_app = spawn_app 41 | -} 42 | \ No newline at end of file 43 | +local Players = game:GetService("Players") 44 | + 45 | +local vide = require(script.Parent.Parent.Parent.vide) 46 | +local types = require(script.Parent.Parent.modules.types) 47 | + 48 | +local destroy_fn = {} 49 | + 50 | +local function unmount_all() 51 | + for _, destroy in destroy_fn do 52 | + destroy() 53 | + end 54 | +end 55 | + 56 | +local function spawn_app(app: types.Application, props: T): () -> () 57 | + local _, unmount = vide.root(function(destroy) 58 | + local cleanup = function() 59 | + destroy_fn[destroy] = nil 60 | + destroy() 61 | + end 62 | + 63 | + local application = app.mount(props, cleanup) 64 | + application.Parent = Players.LocalPlayer.PlayerGui 65 | + 66 | + vide.cleanup(application) 67 | + 68 | + destroy_fn[destroy] = cleanup 69 | + 70 | + return cleanup 71 | + end) 72 | + return unmount 73 | +end 74 | + 75 | +return { 76 | + unmount_all = unmount_all, 77 | + spawn_app = spawn_app, 78 | +} 79 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/starting-replication.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Starting Replication 3 | sidebar: 4 | order: 2 5 | --- 6 | import { Aside, Code, Tabs, TabItem } from '@astrojs/starlight/components'; 7 | 8 | ## Getting Full Data 9 | 10 | When a player joins the game, you will need to send the current state of the world.
11 | This can be done by calling `server_replicator:get_full(player)`. 12 | 13 | This will return two values: 14 | - A `buffer` containing the data 15 | - A `variants` table contaning unserialized component values. 16 | 17 | You will apply this in the client by calling `client_replicator:apply_full(buffer, variants)`. 18 | 19 | 20 | 21 | ```luau 22 | local replicator = require("@server/replicator") 23 | 24 | -- remote function in the server 25 | remotes_server.receive_full:set_callback(function(player) 26 | return replicator:get_full(player) 27 | end) 28 | ``` 29 | 30 | 31 | ```luau 32 | local replicator = require("@client/replicator") 33 | 34 | -- remote function in the client 35 | local buf, variants = remotes_client.receive_full:invoke_server() 36 | replicator:apply_full(buf, variants) 37 | ``` 38 | 39 | 40 | 41 | 42 | ## Activating Players 43 | 44 | Replecs will automatically ignore all players that have joined the game. 45 | This is to prevent players from getting updates when they haven't received the full state of the world yet. 46 | 47 | To activate a player to be replicated you will need to call `server_replicator:mark_player_ready`.
48 | You can check if a player has been marked as ready by calling `server_replicator:is_player_ready`. 49 | 50 | ```luau 51 | local replicator = require("@server/replicator") 52 | 53 | -- remote function in the server 54 | remotes_server.receive_full:set_callback(function(player: Player) 55 | replicator:mark_player_ready(player) 56 | return replicator:get_full(player) 57 | end) 58 | 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /demo/src/shared/components/init.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local world = require(script.Parent.world) 3 | local add_names = require(ReplicatedStorage.code.utils.ecs.add_names) 4 | local add_shared = require(ReplicatedStorage.code.utils.ecs.add_shared) 5 | local ref = require(ReplicatedStorage.code.utils.ecs.ref) 6 | local jecs = require(ReplicatedStorage.packages.jecs) 7 | local replecs = require(ReplicatedStorage.packages.replecs) 8 | 9 | export type TimerInterface = { 10 | start: number, 11 | wait: number, 12 | repeats: boolean?, 13 | callback: (jecs.Entity) -> (), 14 | } 15 | 16 | local components = { 17 | player = world:component() :: jecs.Entity, 18 | client = world:entity(), 19 | r_owner = world:entity(), 20 | setup_player = world:component() :: jecs.Entity<(Player, jecs.Entity) -> ()>, 21 | cleanup = world:component() :: jecs.Entity<(jecs.Entity) -> ()>, 22 | 23 | cube = world:entity(), 24 | wooble = world:entity(), 25 | rainbow = world:entity(), 26 | size = world:component() :: jecs.Entity, 27 | timer = world:component() :: jecs.Entity, 28 | lifetime = world:component() :: jecs.Entity, 29 | transform = world:component() :: jecs.Entity, 30 | color = world:component() :: jecs.Entity, 31 | basepart = world:component() :: jecs.Entity, 32 | unix = world:component() :: jecs.Entity, 33 | } 34 | 35 | world:set(components.player, replecs.custom_handler, function(player: Player) 36 | local entity = ref(player) 37 | return entity 38 | end) 39 | 40 | world:set(components.transform, replecs.serdes, { 41 | bytespan = 12, 42 | serialize = function(transform: CFrame) 43 | local buf = buffer.create(12) 44 | buffer.writef32(buf, 0, transform.X) 45 | buffer.writef32(buf, 4, transform.Y) 46 | buffer.writef32(buf, 8, transform.Z) 47 | return buf 48 | end, 49 | deserialize = function(buf: buffer) 50 | return CFrame.new(buffer.readf32(buf, 0), buffer.readf32(buf, 4), buffer.readf32(buf, 8)) 51 | end, 52 | }) 53 | 54 | world:removed(components.cleanup, function(entity: jecs.Entity) 55 | local callback = world:get(entity, components.cleanup) 56 | 57 | if callback then 58 | callback(entity) 59 | end 60 | end) 61 | 62 | add_names(components) 63 | add_shared(components) 64 | 65 | return components 66 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | on: 4 | push: 5 | tags: ["v*", "workflow_dispatch"] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | unit-tests: 10 | uses: ./.github/workflows/unit-tests.yml 11 | 12 | build: 13 | uses: ./.github/workflows/build.yml 14 | with: 15 | version: ${{ github.ref_name }} 16 | 17 | release: 18 | name: Release 19 | needs: [build, unit-tests] 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: write 23 | steps: 24 | - name: Checkout Project 25 | uses: actions/checkout@v4 26 | 27 | - name: Download Replecs 28 | uses: actions/download-artifact@v4 29 | with: 30 | name: build 31 | path: build 32 | 33 | - name: Rename Build 34 | run: mv build/replecs.rbxm replecs.rbxm 35 | 36 | - name: Create Release 37 | uses: softprops/action-gh-release@v1 38 | with: 39 | name: Replecs ${{ github.ref_name }} 40 | files: | 41 | replecs.rbxm 42 | 43 | wally: 44 | name: Publish to Wally 45 | needs: [release] 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout Project 49 | uses: actions/checkout@v4 50 | 51 | - name: Install Rokit 52 | uses: CompeyDev/setup-rokit@v0.1.2 53 | 54 | - name: Setup Pesde 55 | uses: lumin-org/setup-pesde@v0.4.1 56 | with: 57 | version: v0.7.0-rc.7+registry.0.2.3-rc.3 58 | 59 | - name: Install Packages 60 | run: pesde install 61 | 62 | - name: Darklua Process 63 | run: lune run .lune/process.luau ${{ github.ref_name }} 64 | 65 | - name: Wally Login 66 | run: wally login --token ${{ secrets.WALLY_AUTH }} 67 | 68 | - name: Publish 69 | run: wally publish --project-path serve 70 | 71 | rbxts: 72 | name: Publish to npm 73 | needs: [release] 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Setup JS 77 | uses: sxzz/workflows/setup-js@v1 78 | with: 79 | fetch-all: true 80 | 81 | - name: Install Rokit 82 | uses: CompeyDev/setup-rokit@v0.1.2 83 | 84 | - name: Darklua Process 85 | run: lune run .lune/ts-process.luau 86 | 87 | - name: Publish to NPM 88 | run: npm publish --access public --tag latest 89 | working-directory: ./dist 90 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/custom-ids.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Ids 3 | description: Custom Ids API Reference 4 | sidebar: 5 | order: 4 6 | --- 7 | 8 | ## Types 9 | 10 | --- 11 | 12 |
13 | 14 | ### `Replecs.CustomId` 15 | 16 | Custom id handler, created with `Replecs.create_custom_id`. 17 | 18 | ```ts 19 | type CustomId = { 20 | identifier: string, 21 | handle_callback: (ctx: HandleContext) -> Entity? 22 | 23 | handler: (self: CustomId, handler: (ctx: HandleContext) -> Entity?) -> () 24 | } 25 | ``` 26 | 27 |
28 | 29 | #### Fields 30 | - **identifier**: The identifier of the custom id. 31 | - **handle_callback**: The callback set to handle the custom id. 32 | 33 | #### Methods 34 | - **handler()**: Sets the handler for the custom id. 35 | - **Arguments** 36 | - **handler**: The handler to set. 37 | 38 |
39 | 40 | ### `Replecs.HandleContext` 41 | 42 | Context passed when an entity is handled by a custom id. Used to gather information about the entity. 43 | 44 | ```ts 45 | type HandleContext = { 46 | entity_id: number, 47 | 48 | component: (component: Entity) -> T, 49 | target: (relation: jecs.Id, index: number?) -> Entity?, 50 | pair_value: (relation: jecs.Id, target: Entity) -> T?, 51 | has_pair: (relation: jecs.Id, target: Entity) -> boolean, 52 | 53 | entity: (server_entity: number) -> Entity, 54 | has: (tag: Entity) -> boolean, 55 | } 56 | ``` 57 |
58 | 59 | #### Fields 60 | - **entity_id**: The server id of the entity being handled. 61 | 62 | #### Functions (Not Methods) 63 | - **component()**: Returns a component value from the entity. 64 | - **Arguments** 65 | - **component**: The component to get. 66 | 67 | - **target()**: Returns the target of a relation, will recursively resolve other custom ids. 68 | - **Arguments** 69 | - **relation**: The relation to get the target of. 70 | - **index**: The index of the target to get. defaults to 0. 71 | 72 | - **pair_value()**: Returns the value of a pair. 73 | - **Arguments** 74 | - **relation**: The relation to get the value of. 75 | - **target**: The target of the pair, must be a client id. 76 | 77 | - **has_pair()**: Returns whether the entity has a pair. 78 | - **Arguments** 79 | - **relation**: The relation to check. 80 | - **target**: The target of the pair, must be a client id. 81 | 82 | 83 | - **has()**: Returns whether the entity has a component or tag. 84 | - **Arguments** 85 | - **tag**: The tag to check. 86 | 87 | - **entity()**: Returns an entity from a server entity id, will recursively resolve other custom ids. 88 | - **Arguments** 89 | - **server_entity**: The server entity to get. 90 | 91 |
92 |
-------------------------------------------------------------------------------- /docs/src/content/docs/started/sending-updates.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sending Updates 3 | sidebar: 4 | order: 3 5 | --- 6 | import { Aside, Code, Tabs, TabItem } from '@astrojs/starlight/components'; 7 | import collect_updates from './examples/collect-updates.luau?raw'; 8 | import apply_updates from './examples/apply-updates.luau?raw'; 9 | import collect_all from './examples/collect-all.luau?raw'; 10 | import apply_all from './examples/apply-all.luau?raw'; 11 | 12 | ## Sending Changes 13 | 14 | Once players have received the full state of the world, you will need to constantly send changes to your players. 15 | 16 | Changes should be sent inside a system that will run every frame. 17 | Its recommended that you throttle the updates to a lower interval, a good rate is somewhere around 10-30 times a second. 18 | 19 | In the server, you will call `server_replicator:collect_updates()`. 20 | This will return an iterator that will be executed for every player. 21 | 22 | The iterator will give you the following values: 23 | - player: `Player` - the player to send the updates to 24 | - buf: `buffer` - the buffer containing the updates 25 | - variants: `{any}` - the variants table containing non-serialized values 26 | 27 | 28 | Updates can then be applied in the client using `client_replicator:apply_updates(buf, variants)`. 29 | 30 | 31 | #### Example: 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ## Sending Unreliable Values 43 | 44 | Unreliable updates are sent similarly to normal updates. You can call `server_replicator:collect_unreliable()`. 45 | This will return an iterator that will be executed for every player. 46 | 47 | The iterator will give you the same values as normal updates. 48 | 49 | Updates can then be applied in the client using `client_replicator:apply_unreliable(buf, variants)`. 50 | 51 | Because unreliable values are always sent, 52 | deciding how often you send them is crucial, making it slower to not overload the network or faster for smoother replication. 53 | 54 | 55 | 58 | 59 | ## Summary 60 | 61 | Combining both update types your client/server systems should look something like this: 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/replecs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Replecs 3 | description: Replecs API Reference 4 | sidebar: 5 | order: 1 6 | --- 7 | 8 | ## Replecs 9 | 10 | --- 11 | 12 |
13 | 14 | ### `Replecs.create` 15 | 16 | Creates a new `ReplecsLib`. This contains both the server and client replicators 17 | in one object and provides methods to interact with both of them at the same time. 18 | 19 | ```ts 20 | function Replecs.create(world: jecs.World?): ReplecsLib 21 | ``` 22 | 23 |
24 | 25 | #### Arguments 26 | - **world**: The world to use for the replicators. Can also be provided when initing the replicator. 27 | 28 |
29 | 30 | ### `Replecs.create_server` 31 | 32 | Creates a new server replicator. 33 | 34 | ```ts 35 | function Replecs.create_server(world: jecs.World?): Replecs.Server 36 | ``` 37 | 38 |
39 | #### Arguments 40 | - **world**: The world to use for the replicators. Can also be provided when initing the replicator. 41 |
42 | 43 | ### `Replecs.create_client` 44 | 45 | Creates a new client replicator. 46 | 47 | ```ts 48 | function Replecs.create_client(world: jecs.World?): Replecs.Client 49 | ``` 50 |
51 | #### Arguments 52 | - **world**: The world to use for the replicators. Can also be provided when initing the replicator. 53 |
54 | 55 | ### `Replecs.create_custom_id` 56 | 57 | Creates a new handler for parsing custom entity ids. 58 | 59 | ```ts 60 | function Replecs.create_custom_id( 61 | identifier: string, 62 | handler: ((ctx: HandleContext) -> Entity?)? 63 | ): CustomId 64 | ``` 65 | 66 |
67 | #### Arguments 68 | - **identifier**: A string that will be used to identify the custom id when gets through the network. 69 | Must be the same in both the server and client. 70 | - **handler**: A function handler that will pass use the `HandleContext` to create or return the entity. Optionally, 71 | the handler can be ommited and set later with `customid:handler(handler)` 72 |
73 | 74 |
75 | 76 | ## Types 77 | 78 | --- 79 | 80 |
81 | 82 | ### `Replecs.ReplecsLib` 83 | 84 | Container for both the server and client replicators. 85 | 86 | ```ts 87 | type ReplecsLib = { 88 | server: Replecs.Server?, 89 | client: Replecs.Client?, 90 | 91 | after_replication: (self: ReplecsLib, callback: () -> ()) -> (), 92 | register_custom_id: (self: ReplecsLib, custom_id: CustomId) -> (), 93 | } 94 | ``` 95 | 96 |
97 | 98 | #### Fields 99 | - **server**: The server replicator, nil in the client. 100 | - **client**: The client replicator, nil in the server. 101 | 102 | #### Methods 103 | 104 | - **after_replication()**: Registers a callback to be called after the replication has finished. 105 |
If not replication is in progress, the callback will be called immediately. If this is called in the server, 106 | the callback will always be called immediately. 107 | - **Arguments** 108 | - **callback**: The callback to be called. 109 | 110 |
111 | - **register_custom_id()**: Calls `register_custom_id` on the server and/or client replicators. 112 | - **Arguments** 113 | - **custom_id**: The custom id to register. 114 |
115 | 116 |
-------------------------------------------------------------------------------- /demo/src/shared/stories/repro.story.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 2 | local jecs = require(ReplicatedStorage.packages.jecs) 3 | local replecs = require(ReplicatedStorage.packages.replecs) 4 | 5 | local world = jecs.world() 6 | 7 | local created_components = 0 8 | function create_component() 9 | local component = world:component() 10 | world:add(component, replecs.shared) 11 | world:set(component, jecs.Name, "component " .. created_components) 12 | created_components = created_components + 1 13 | return component 14 | end 15 | 16 | local created_tags = 0 17 | function create_tag() 18 | local tag = world:entity() 19 | world:add(tag, replecs.shared) 20 | world:set(tag, jecs.Name, "tag " .. created_tags) 21 | created_tags = created_tags + 1 22 | return tag 23 | end 24 | 25 | local style = create_component() 26 | local ability = create_component() 27 | local inputAction = create_component() 28 | local abilityOf = create_tag() 29 | 30 | local member = newproxy() 31 | local server = replecs.create_server(world) 32 | server:init() 33 | 34 | server.masking:register_member(member) 35 | server.masking:activate_member(member) 36 | 37 | function create_things() 38 | local playerEntity = world:entity() 39 | world:add(playerEntity, replecs.networked) 40 | 41 | local styleEntity = world:entity() 42 | world:set(styleEntity, style, "style") 43 | world:add(styleEntity, jecs.pair(replecs.reliable, style)) 44 | world:add(styleEntity, jecs.pair(jecs.ChildOf, playerEntity)) 45 | world:add(styleEntity, jecs.pair(replecs.pair, jecs.ChildOf)) 46 | world:add(styleEntity, replecs.Networked) 47 | 48 | local abilityEntity = world:entity() 49 | world:set(abilityEntity, ability, "12312") 50 | world:add(abilityEntity, jecs.pair(replecs.reliable, ability)) 51 | world:set(abilityEntity, inputAction, "steal") 52 | world:add(abilityEntity, jecs.pair(replecs.reliable, inputAction)) 53 | world:add(abilityEntity, jecs.pair(abilityOf, styleEntity)) 54 | world:add(abilityEntity, jecs.pair(replecs.pair, abilityOf)) 55 | world:add(abilityEntity, jecs.pair(jecs.ChildOf, playerEntity)) 56 | world:add(abilityEntity, jecs.pair(replecs.pair, jecs.ChildOf)) 57 | world:add(abilityEntity, replecs.Networked) 58 | end 59 | 60 | local c_world = jecs.world() 61 | local client = replecs.create_client(c_world) 62 | 63 | local c_created_components = 0 64 | function c_create_component() 65 | local component = c_world:component() 66 | c_world:add(component, replecs.shared) 67 | c_world:set(component, jecs.Name, "component " .. c_created_components) 68 | c_created_components = c_created_components + 1 69 | return component 70 | end 71 | 72 | local c_created_tags = 0 73 | function c_create_tag() 74 | local tag = c_world:entity() 75 | c_world:add(tag, replecs.shared) 76 | c_world:set(tag, jecs.Name, "tag " .. c_created_tags) 77 | c_created_tags = c_created_tags + 1 78 | return tag 79 | end 80 | 81 | local c_style = c_create_component() 82 | local c_ability = c_create_component() 83 | local c_inputAction = c_create_component() 84 | local c_abilityOf = c_create_tag() 85 | 86 | client:init() 87 | 88 | return function() 89 | server:collect_updates() 90 | create_things() 91 | 92 | for _, buf, variants in server:collect_updates() do 93 | client:apply_updates(buf, variants) 94 | end 95 | 96 | return function() end 97 | end 98 | -------------------------------------------------------------------------------- /src/masking/mask_generator.luau: -------------------------------------------------------------------------------- 1 | --!native 2 | --!optimize 2 3 | 4 | local utils = require "../utils" 5 | 6 | type ComputeCallback = (generator: MaskGenerator) -> utils.Bitmask 7 | type SubscribeCallback = (generator: MaskGenerator) -> () 8 | 9 | export type MaskGenerator = { 10 | compute_callback: ComputeCallback?, 11 | subscribed: SubscribeCallback?, 12 | result: utils.Bitmask, 13 | bitmask: utils.Bitmask?, 14 | 15 | tracking: { GeneratorInternal }, 16 | tracked_by: { GeneratorInternal }, 17 | tracked_indexes: { [GeneratorInternal]: number }, 18 | 19 | compute: (self: MaskGenerator) -> (), 20 | run_subscribed: (self: MaskGenerator) -> (), 21 | track: (self: MaskGenerator, tracking: MaskGenerator) -> (), 22 | destroy: (self: MaskGenerator) -> (), 23 | } 24 | 25 | export type MaskGeneratorWithBitmask = MaskGenerator & { 26 | bitmask: utils.Bitmask, 27 | } 28 | 29 | local mask_generator = {} 30 | mask_generator.__index = mask_generator 31 | 32 | type GeneratorInternal = MaskGenerator & typeof(mask_generator) 33 | 34 | function mask_generator.track(generator: GeneratorInternal, tracking: GeneratorInternal) 35 | table.insert(generator.tracking, tracking) 36 | tracking:mark_as_tracked(generator) 37 | end 38 | 39 | function mask_generator.mark_as_tracked(generator: GeneratorInternal, tracked_by: GeneratorInternal) 40 | table.insert(generator.tracked_by, tracked_by) 41 | generator.tracked_indexes[tracked_by] = #generator.tracked_by 42 | end 43 | 44 | function mask_generator.unmark_tracked(generator: GeneratorInternal, to_untrack: GeneratorInternal) 45 | local index = generator.tracked_indexes[to_untrack] 46 | if index then 47 | local last = table.remove(generator.tracked_by) 48 | 49 | if last and last ~= to_untrack then 50 | generator.tracked_by[index] = last 51 | generator.tracked_indexes[last :: GeneratorInternal] = index 52 | end 53 | 54 | generator.tracked_indexes[to_untrack] = nil 55 | end 56 | end 57 | 58 | function mask_generator.run_subscribed(generator: GeneratorInternal) 59 | if generator.subscribed then 60 | generator.subscribed(generator :: GeneratorInternal) 61 | end 62 | end 63 | 64 | function mask_generator.compute(generator: GeneratorInternal) 65 | if generator.compute_callback then 66 | local result: utils.Bitmask = generator.compute_callback(generator :: any) 67 | generator.result = result 68 | end 69 | if generator.subscribed then 70 | generator.subscribed(generator :: GeneratorInternal) 71 | end 72 | 73 | for _, tracking in generator.tracked_by do 74 | tracking:compute() 75 | end 76 | end 77 | 78 | function mask_generator.destroy(generator: GeneratorInternal) 79 | for _, tracking in generator.tracking do 80 | tracking:unmark_tracked(generator :: GeneratorInternal) 81 | end 82 | end 83 | 84 | function create(bitmask: utils.Bitmask?, compute: ComputeCallback?): MaskGenerator 85 | local self = {} :: MaskGenerator 86 | 87 | self.bitmask = bitmask 88 | self.result = if compute then compute(self) else bitmask :: utils.Bitmask 89 | self.compute_callback = compute 90 | self.subscribed = nil 91 | self.tracking = {} 92 | self.tracked_by = {} 93 | self.tracked_indexes = {} 94 | 95 | return setmetatable(self, mask_generator) :: any 96 | end 97 | 98 | function follow(leading: MaskGenerator): MaskGenerator 99 | local created = create(leading.result, function() 100 | return leading.result 101 | end) 102 | created:track(leading) 103 | return created 104 | end 105 | 106 | return { create = create, follow = follow } 107 | -------------------------------------------------------------------------------- /src/init.luau: -------------------------------------------------------------------------------- 1 | local jecs = require "./jecs" 2 | local common = require "@self/common" 3 | local types = require "@self/types" 4 | local utils = require "@self/utils" 5 | 6 | local client = require "@self/client" 7 | local server = require "@self/server" 8 | local customid = require "@self/customid" 9 | local VERSION = require "@self/ver" 10 | 11 | export type Server = server.Server 12 | export type Client = client.Client 13 | 14 | type Entity = common.Entity 15 | type World = common.World 16 | 17 | export type HandleContext = customid.HandleContext 18 | export type CustomId = customid.CustomId 19 | export type HandshakeInfo = types.HandshakeInfo 20 | 21 | export type ReplecsLib = { 22 | server: Server, 23 | client: Client, 24 | components: types.Components, 25 | 26 | after_replication: (self: ReplecsLib, callback: () -> ()) -> (), 27 | register_custom_id: (self: ReplecsLib, custom_id: CustomId) -> (), 28 | } 29 | 30 | type Replecs = types.Components & { 31 | __index: any, 32 | VERSION: string, 33 | create: (world: World?) -> ReplecsLib, 34 | 35 | create_server: (world: World?) -> Server, 36 | create_client: (world: World?) -> Client, 37 | create_custom_id: (identifier: string, handler: (ctx: HandleContext) -> Entity?) -> CustomId, 38 | } 39 | 40 | local replecs = {} 41 | replecs.VERSION = VERSION 42 | replecs.__index = replecs 43 | 44 | local PREREGISTRATION = common.preregistration 45 | 46 | if PREREGISTRATION then 47 | local components = replecs :: Replecs 48 | utils.create_components(common.preregister_tag, common.preregister_component, components) 49 | utils.add_component_names(components, common.add_preregistered_name) 50 | 51 | jecs.meta(components.custom, jecs.Exclusive) 52 | jecs.meta(components.global, jecs.Exclusive) 53 | end 54 | 55 | local COMPONENTS = if PREREGISTRATION then replecs :: Replecs else nil 56 | 57 | function replecs.create_server(world: World?): Server 58 | return server.create(world, COMPONENTS) 59 | end 60 | 61 | function replecs.create_client(world: World?): Client 62 | return client.create(world, COMPONENTS) 63 | end 64 | 65 | function replecs.create(world: World?): ReplecsLib 66 | assert(game, "This is a Roblox specific function") 67 | local RunService = game:GetService "RunService" 68 | local self = {} :: ReplecsLib 69 | 70 | if COMPONENTS then 71 | self.components = COMPONENTS 72 | elseif world then 73 | self.components = utils.create_components(utils.tag_factory(world), utils.component_factory(world)) 74 | utils.add_component_names(self.components, function(component, name) 75 | common.add_name(world, component, name) 76 | end) 77 | end 78 | 79 | if RunService:IsServer() then 80 | self.server = server.create(world, self.components) 81 | end 82 | if RunService:IsClient() then 83 | self.client = client.create(world, self.components) 84 | end 85 | 86 | return setmetatable(self, replecs) :: any 87 | end 88 | 89 | function replecs.create_custom_id(identifier: string, handler: (ctx: HandleContext) -> Entity?) 90 | return customid.create(identifier, handler) 91 | end 92 | 93 | function replecs.after_replication(lib: ReplecsLib, callback: () -> ()) 94 | local lib_client = lib.client 95 | if lib_client then 96 | lib_client:after_replication(callback) 97 | else 98 | callback() 99 | end 100 | end 101 | 102 | function replecs.register_custom_id(lib: ReplecsLib, custom_id: CustomId) 103 | if lib.server then 104 | lib.server:register_custom_id(custom_id) 105 | end 106 | if lib.client then 107 | lib.client:register_custom_id(custom_id) 108 | end 109 | end 110 | 111 | return replecs :: Replecs 112 | -------------------------------------------------------------------------------- /tests/utils.luau: -------------------------------------------------------------------------------- 1 | local utils = {} 2 | 3 | local color = { 4 | white_underline = function(s: string): string 5 | return `\27[1;4m{s}\27[0m` 6 | end, 7 | 8 | white = function(s: string): string 9 | return `\27[37;1m{s}\27[0m` 10 | end, 11 | 12 | green = function(s: string): string 13 | return `\27[38;5;78m{s}\27[0m` 14 | end, 15 | 16 | magenta = function(s: string): string 17 | return `\27[38;5;171m{s}\27[0m` 18 | end, 19 | 20 | red = function(s: string): string 21 | return `\27[31;1m{s}\27[0m` 22 | end, 23 | 24 | blue = function(s: string): string 25 | return `\27[38;5;39m{s}\27[0m` 26 | end, 27 | 28 | yellow = function(s: string): string 29 | return `\27[38;5;11m{s}\27[0m` 30 | end, 31 | 32 | red_highlight = function(s: string): string 33 | return `\27[41;1;30m{s}\27[0m` 34 | end, 35 | 36 | green_highlight = function(s: string): string 37 | return `\27[42;1;30m{s}\27[0m` 38 | end, 39 | 40 | gray = function(s: string): string 41 | return `\27[38;1m{s}\27[0m` 42 | end, 43 | } 44 | 45 | function is_valid_identifier(str: string): boolean 46 | if typeof(str) ~= "string" then 47 | return false 48 | end 49 | return str:match "^[A-Za-z_][A-Za-z0-9_]*$" == nil 50 | end 51 | 52 | function table_is_empty(t: any): boolean 53 | for _ in pairs(t) do 54 | return false 55 | end 56 | return true 57 | end 58 | 59 | function table_is_single(t: any): boolean 60 | if type(t) ~= "table" then 61 | return false 62 | end 63 | local count = 0 64 | for _ in t do 65 | if count >= 1 then 66 | return false 67 | end 68 | count += 1 69 | end 70 | return true 71 | end 72 | 73 | function table_is_array(t: any): boolean 74 | if type(t) ~= "table" then 75 | return false 76 | end 77 | return #t > 0 78 | end 79 | 80 | function format_value(value: any, depth: number, indent: number, visited: any): string 81 | local t = typeof(value) 82 | if t == "string" then 83 | return color.green(`"{value}"`) 84 | elseif t == "number" then 85 | return color.blue(tostring(value)) 86 | elseif t == "boolean" then 87 | return color.yellow(tostring(value)) 88 | elseif t == "table" then 89 | if visited[value] then 90 | return color.red "[CYCLE]" 91 | end 92 | if indent >= depth then 93 | return color.gray "{...}" 94 | end 95 | if table_is_empty(value) then 96 | return "{ }" 97 | end 98 | local is_single = table_is_single(value) 99 | local is_array = table_is_array(value) 100 | 101 | visited[value] = true 102 | local parts = {} 103 | local indent_str = string.rep(" ", indent) 104 | for k, val in value do 105 | local val_t = type(val) 106 | local val_str = format_value(val, depth, indent + 1, visited) 107 | if is_array then 108 | if is_single then 109 | if val_t == "table" then 110 | is_single = false 111 | table.insert(parts, indent_str .. val_str) 112 | else 113 | table.insert(parts, val_str) 114 | end 115 | else 116 | table.insert(parts, indent_str .. val_str) 117 | end 118 | else 119 | local key_str = k 120 | if is_valid_identifier(k :: any) then 121 | key_str = "[" .. format_value(k, depth, indent + 1, visited) .. "]" 122 | end 123 | if is_single then 124 | if val_t == "table" then 125 | is_single = false 126 | table.insert(parts, indent_str .. key_str .. color.white " = " .. val_str) 127 | else 128 | table.insert(parts, key_str .. color.white " = " .. val_str) 129 | end 130 | else 131 | table.insert(parts, indent_str .. key_str .. color.white " = " .. val_str) 132 | end 133 | end 134 | end 135 | visited[value] = nil 136 | if is_single then 137 | return "{ " .. table.concat(parts, "") .. " }" 138 | else 139 | return "{\n" .. table.concat(parts, ",\n") .. "\n" .. string.rep(" ", indent - 1) .. "}" 140 | end 141 | else 142 | return color.magenta(`[{t}]: ` .. tostring(value)) 143 | end 144 | end 145 | 146 | function utils.stringify_value(t: any, depth: number?): string 147 | return format_value(t, (depth or 8) + 1, 1, {}) 148 | end 149 | 150 | return utils 151 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/setting-up.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setting Up 3 | sidebar: 4 | order: 1 5 | --- 6 | 7 | import { Code, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; 8 | 9 | import creating_replicator from './examples/creating-replicators.luau?raw'; 10 | import server_replicator from './examples/server-replicator.luau?raw'; 11 | import client_replicator from './examples/client-replicator.luau?raw'; 12 | 13 | ## Creating Replicators 14 | 15 | To start you need to create a `Replicator` for the server and the client. 16 | 17 | Replecs exports `replecs.create`. This creates a holder that will have both `client` and `server` entries. 18 | These entries will only be present on the client and server respectively. 19 | 20 | You can then export each replicator type from their sides. 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | --- 35 | 36 | Having a holder that has both `client` and `server` is useful for methods that would be used in a shared file where you cant 37 | require the client or server replicators directly. 38 | 39 | An example of this is the method for [hooking after replication](/replecs/guides/networking-hooks/#hook-after-replication) 40 | which most of the time will be called inside signals for shared components, but it's exclusive to the client. 41 | 42 | --- 43 | 44 | Additionally, you can create the client and server replicators separately using: 45 | - `replecs.create_client` 46 | - `replecs.create_server`. 47 | 48 | 49 | 50 | ```luau 51 | local replecs = require("@pkg/replecs") 52 | return replecs.create_server(world) 53 | ``` 54 | 55 | 56 | ```luau 57 | local replecs = require("@pkg/replecs") 58 | return replecs.create_client(world) 59 | ``` 60 | 61 | 62 | 63 | --- 64 | 65 | ## Providing Your World 66 | 67 | Your world can be provided to the replicator to be used in two ways: 68 | - Directly to the replicator constructor, *as seen in the previous examples.* 69 | - When initialiazing the replicator calling `replicator:init(world)` 70 | 71 | 77 | 78 | 79 | --- 80 | 81 | ## Marking Shared Components 82 | 83 | After creating your replicators. You must add `replecs.shared` to **every single** component/tag that can be replicated. 84 | These components also need to be named with `jecs.Name`. 85 | 86 | They need to be **equally** marked in both the client and server, This means that a marked component can't be missing from one of the two. 87 | 88 | Easiest way to do this is to save your components in a table and then iterate over them. 89 | 90 | ```luau 91 | local replecs = require("@pkg/replecs") 92 | local world = require("world") 93 | 94 | -- create a table with all your components 95 | local shared_components = { 96 | foo = world:component(), 97 | baz = world:entity(), 98 | } 99 | 100 | for name, component in shared_components do 101 | world:set(component, jecs.Name, name) 102 | world:add(component, replecs.shared) 103 | end 104 | ``` 105 | 106 | 110 | 111 | --- 112 | 113 | ## Initializing Replicators 114 | 115 | After creating your replicators. You need to initialize them by calling `replicator:init()` before using them. 116 | 117 | You can also provide your world here if you did not provide it in the constructor. 118 | This is useful in workflows where the world is not exported as a singleton. 119 | 120 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/component-serialization.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Component Serialization 3 | sidebar: 4 | order: 4 5 | --- 6 | import { Card, Aside } from '@astrojs/starlight/components'; 7 | 8 | 9 | Component values are not saved in the main buffer by default. 10 | The raw values get pushed into the `variants` array and only referenced by their position in the array in the buffer. 11 | 12 | You can provide a serialize/deserialize table to a component. 13 | This will allow you to compress the component value into a buffer 14 | that will be appended into the main buffer, allowing you to reduce bandwidth usage. 15 | 16 | This will also avoid unreliable packets getting over the 1KB limit, which is necessary if you want to use actual unreliable remotes. 17 | 18 | ### Adding a Serializer 19 | 20 | You can add a serializer to a component by adding `replecs.serdes`. It's important that this function is set in both the client and server. 21 | 22 | This value should be a table with this structure: 23 | 24 | ```luau 25 | type SerdesTable = { 26 | serialize: (value: any) -> buffer, 27 | deserialize: (buf: buffer) -> any, 28 | 29 | bytespan: number?, 30 | includes_variants: boolean?, 31 | } 32 | ``` 33 | 34 | Let's see an example of how we can add a serializer to a position component. 35 | 36 | ```luau 37 | local replecs = require("@pkg/replecs") 38 | local world = require("world") 39 | 40 | local position = world:component() :: Entity 41 | 42 | world:set(position, replecs.serdes, { 43 | serialize = function(value: Vector3) 44 | local buf = buffer.create(12) 45 | buffer.writef32(buf, 0, transform.X) 46 | buffer.writef32(buf, 4, transform.Y) 47 | buffer.writef32(buf, 8, transform.Z) 48 | return buf 49 | end, 50 | deserialize = function(buf: buffer) 51 | local x = buffer.readf32(buf, 0) 52 | local y = buffer.readf32(buf, 4) 53 | local z = buffer.readf32(buf, 8) 54 | 55 | return Vector3.new(x, y, z) 56 | end, 57 | } 58 | ``` 59 | 60 | 64 | 65 | ### Setting a bytespan 66 | 67 | If you want to optimize the bandwidth usage a little more, you can hardcode the size of the serialize buffer. 68 | This will skip saving the size of the buffer in the packet which is usually `1 Byte`. 69 | 70 | The `bytespan` should be taken as a micro-optimization, if the size of the buffer is dynamic, don't use this. 71 | 72 | ```luau 73 | local position = world:component() :: Entity 74 | 75 | world:set(position, replecs.serdes, { 76 | bytespan = 12, 77 | serialize = function(value: Vector3) 78 | local buf = buffer.create(12) 79 | ... 80 | end, 81 | deserialize = function(value: Vector3) 82 | ... 83 | end, 84 | }) 85 | ``` 86 | 87 | 90 | 91 | ### Combining Variants and Buffers 92 | 93 | If the value of the component can't be fully serialized into a buffer, 94 | you can return the unknown values in an array after the buffer. These values will be passed to the deserializer as the second argument. 95 | 96 | For replecs to be able to handle this, you need to set `includes_variants` to `true`. 97 | 98 | ```luau 99 | local replecs = require("@pkg/replecs") 100 | local world = require("world") 101 | 102 | type Data = { 103 | can_serialize: number, 104 | cannot_serialize: Instance, 105 | } 106 | 107 | local data = world:component() :: Entity 108 | 109 | world:set(data, replecs.serdes, { 110 | includes_variants = true, 111 | serialize = function(value: Data) 112 | local buf = buffer.create(4) 113 | buffer.writef32(buf, 0, value.can_serialize) 114 | 115 | return buf, { value.cannot_serialize } 116 | end, 117 | deserialize = function(buf: buffer, variants: { Instance }): Data 118 | local can_serialize = buffer.readf32(buf, 0) 119 | local cannot_serialize = variants[1] 120 | return { 121 | can_serialize = can_serialize, 122 | cannot_serialize = cannot_serialize, 123 | } 124 | end, 125 | }) 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /tests/observers.luau: -------------------------------------------------------------------------------- 1 | local jecs = require "@pkg/jecs" 2 | 3 | type Observer = { 4 | callback: (jecs.Entity) -> (), 5 | query: jecs.Query, 6 | } 7 | 8 | export type PatchedWorld = jecs.World & { 9 | added: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> (), 10 | removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (), 11 | changed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> (), 12 | -- deleted: (PatchedWorld, () -> ()) -> () -> (), 13 | observer: (PatchedWorld, Observer) -> (), 14 | monitor: (PatchedWorld, Observer) -> (), 15 | } 16 | 17 | local function observers_new(world, description) 18 | local query = description.query 19 | local callback = description.callback 20 | local terms = query.filter_with :: { jecs.Id } 21 | if not terms then 22 | local ids = query.ids 23 | query.filter_with = ids 24 | terms = ids 25 | end 26 | 27 | local entity_index = world.entity_index :: any 28 | local function emplaced(entity: jecs.Entity) 29 | local r = jecs.entity_index_try_get_fast(entity_index, entity :: any) 30 | 31 | if not r then 32 | return 33 | end 34 | 35 | local archetype = r.archetype 36 | 37 | if jecs.query_match(query, archetype) then 38 | callback(entity) 39 | end 40 | end 41 | 42 | for _, term in terms do 43 | world:added(term, emplaced) 44 | world:changed(term, emplaced) 45 | end 46 | end 47 | 48 | local function monitors_new(world, description) 49 | local query = description.query 50 | local callback = description.callback 51 | local terms = query.filter_with :: { jecs.Id } 52 | if not terms then 53 | local ids = query.ids 54 | query.filter_with = ids 55 | terms = ids 56 | end 57 | 58 | local entity_index = world.entity_index :: any 59 | local function emplaced(entity: jecs.Entity) 60 | local r = jecs.entity_index_try_get_fast(entity_index, entity :: any) 61 | 62 | if not r then 63 | return 64 | end 65 | 66 | local archetype = r.archetype 67 | 68 | if jecs.query_match(query, archetype) then 69 | callback(entity, jecs.OnAdd) 70 | end 71 | end 72 | 73 | local function removed(entity: jecs.Entity, component: jecs.Id) 74 | local r = jecs.entity_index_try_get_fast(entity_index, entity :: any) 75 | 76 | if not r then 77 | return 78 | end 79 | 80 | local archetype = r.archetype 81 | 82 | if jecs.query_match(query, archetype) then 83 | callback(entity, jecs.OnRemove) 84 | end 85 | end 86 | 87 | for _, term in terms do 88 | world:added(term, emplaced) 89 | world:removed(term, removed) 90 | end 91 | end 92 | 93 | local function observers_add(world: jecs.World): PatchedWorld 94 | local signals = { 95 | added = {}, 96 | emplaced = {}, 97 | removed = {}, 98 | deleted = {}, 99 | } 100 | 101 | world = world :: jecs.World & { [string]: any } 102 | 103 | world.added = function(_, component, fn) 104 | local listeners = signals.added[component] 105 | if not listeners then 106 | listeners = {} 107 | signals.added[component] = listeners 108 | 109 | local function on_add(entity: number, id: number, value: any) 110 | for _, listener in listeners do 111 | listener(entity, id, value) 112 | end 113 | end 114 | world:set(component, jecs.OnAdd, on_add) 115 | end 116 | table.insert(listeners, fn) 117 | end 118 | 119 | world.changed = function(_, component, fn) 120 | local listeners = signals.emplaced[component] 121 | if not listeners then 122 | listeners = {} 123 | signals.emplaced[component] = listeners 124 | local function on_change(entity: number, id: number, value: any) 125 | for _, listener in listeners do 126 | listener(entity, id, value) 127 | end 128 | end 129 | world:set(component, jecs.OnChange, on_change) 130 | end 131 | table.insert(listeners, fn) 132 | end 133 | 134 | world.removed = function(_, component, fn) 135 | local listeners = signals.removed[component] 136 | if not listeners then 137 | listeners = {} 138 | signals.removed[component] = listeners 139 | local function on_remove(entity: number, id: number, value: any) 140 | for _, listener in listeners do 141 | listener(entity, id, value) 142 | end 143 | end 144 | world:set(component, jecs.OnRemove, on_remove) 145 | end 146 | table.insert(listeners, fn) 147 | end 148 | 149 | world.signals = signals 150 | 151 | world.observer = observers_new 152 | 153 | world.monitor = monitors_new 154 | 155 | -- local world_delete = world.delete 156 | 157 | -- world.deleted = function(_, fn) 158 | -- local listeners = signals.deleted 159 | -- table.insert(listeners, fn) 160 | -- end 161 | -- world.delete = function(world, entity) 162 | -- world_delete(world, entity) 163 | -- for _, fn in signals.deleted do 164 | -- fn(entity) 165 | -- end 166 | -- end 167 | 168 | return world :: PatchedWorld 169 | end 170 | 171 | return observers_add 172 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/player-filtering.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Player Filtering 3 | sidebar: 4 | order: 2 5 | --- 6 | 7 | ## Filtering Entities 8 | 9 | By default, entities will replicate to all active players. You can decide to which players you want to replicate certain entities. 10 | 11 | This can be done by setting a filter in the `replecs.networked` component. This filter can be a set of players or a single player. 12 | 13 | ```luau 14 | 15 | local entity1 = world:entity() 16 | local entity2 = world:entity() 17 | local entity3 = world:entity() 18 | 19 | world:add(entity1, replecs.networked) -- replicate to all players 20 | world:set(entity3, replecs.networked, Player3) -- replicate to player3 only 21 | world:set(entity2, replecs.networked, { -- replicate to player1 and player2 22 | [Player1] = true, 23 | [Player2] = true, 24 | }) 25 | 26 | ``` 27 | 28 | You can also convert this filter to a blacklist by setting it to `false`. 29 | 30 | ```luau 31 | local entity = world:entity() 32 | 33 | world:set(entity, replecs.networked, { -- replicate to all players except player3 34 | [Player3] = false, 35 | }) 36 | ``` 37 | 38 | You can add a [non-ready](/replecs/started/starting-replication#activating-players) player to the filter, but it wont replicate until the player is marked as ready. 39 | 40 | ```luau 41 | local entity = world:entity() 42 | 43 | world:set(entity, replecs.networked, { -- replicates to no players 44 | [Player1] = true, 45 | }) 46 | 47 | replicator:mark_player_ready(Player1) -- finally will replicate to player1 48 | ``` 49 | 50 | --- 51 | 52 | ## Filtering Components 53 | 54 | By default, components will be replicated to the same players that the entity is replicated to. 55 | Components can also have player filters. This will make a specific component replicate to certain players, while the rest of the entity will replicate to all other players. 56 | 57 | You can set a filter to the component pair. Filters can be set to any type of replication. 58 | 59 | ```luau 60 | local entity = world:entity() 61 | 62 | world:set(entity, jecs.pair(replecs.reliable, component), { 63 | [Player1] = true, 64 | }) 65 | world:set(entity, jecs.pair(replecs.pair, relation), { 66 | [Player2] = true, 67 | }) 68 | world:set(entity, jecs.pair(replecs.unreliable, component), { 69 | [Player3] = true, 70 | }) 71 | ``` 72 | 73 | Example: 74 | 75 | ```luau 76 | local entity = world:entity() 77 | local position = world:component() :: Entity 78 | local password = world:component() :: Entity 79 | 80 | world:set(entity, replecs.networked, { 81 | [Player1] = true, 82 | [Player2] = true, 83 | }) 84 | -- position will replicate to player1 and player2 85 | world:add(entity, jecs.pair(replecs.reliable, position)) 86 | -- password will replicate only to player1 87 | world:set(entity, jecs.pair(replecs.reliable, password), { 88 | [Player1] = true, 89 | }) 90 | 91 | ``` 92 | 93 | Component filters are limited to the entity filter. 94 | This means that if you set a filter that includes a player that the entity does not include, the component will not replicate to that player. 95 | 96 | ```luau 97 | local entity = world:entity() 98 | local password = world:component() :: Entity 99 | 100 | world:set(entity, replecs.networked, { 101 | [Player1] = true, 102 | [Player2] = true, 103 | }) 104 | world:set(entity, jecs.pair(replecs.reliable, password), { 105 | [Player1] = true, -- only player1 will be able to see the password 106 | [Player3] = true, 107 | }) 108 | ``` 109 | 110 | 111 | ## Modifying Filters 112 | 113 | Filters can be modified at any time, but you need to call `world:set` again to apply the changes. 114 | 115 | ```luau 116 | local entity = world:entity() 117 | 118 | world:set(entity, replecs.networked, { 119 | [Player1] = true, 120 | [Player2] = true, 121 | }) 122 | 123 | -- now it only replicates to player1 124 | world:set(entity, replecs.networked, { 125 | [Player1] = true, 126 | }) 127 | ``` 128 | 129 | 130 | ## Player Aliasing 131 | 132 | You can add aliases to refer to players with a different key. 133 | This is useful if you mainly work with player entities, rather than with player objects. 134 | 135 | To set aliases you would call `server_replicator:add_player_alias(player, alias)` 136 | 137 | ```luau 138 | local Players = game:GetService("Players") 139 | local replicator = require("@server/replicator") 140 | 141 | local player = Players.Player1 142 | local player_entity = world:entity() 143 | 144 | replicator:add_player_alias(player, player_entity) 145 | 146 | local entity = world:entity() 147 | 148 | world:set(entity, replecs.networked, { 149 | [player] = true, -- works as normal 150 | }) 151 | world:set(entity, replecs.networked, { 152 | [player_entity] = true, -- works too :D 153 | }) 154 | 155 | ``` 156 | 157 | You can also remove aliases by calling `server_replicator:remove_player_alias(alias)`. 158 | This is important when a player leaves the game. 159 | 160 | ```luau 161 | local Players = game:GetService("Players") 162 | local replicator = require("@server/replicator") 163 | 164 | Players.PlayerRemoving:Connect(function(player) 165 | local player_entity = ref(player) 166 | replicator:remove_player_alias(player_entity) 167 | end) 168 | ``` -------------------------------------------------------------------------------- /docs/src/content/docs/guides/networking-entity.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Replicating Entities 3 | sidebar: 4 | order: 1 5 | --- 6 | import { Card, Aside } from '@astrojs/starlight/components'; 7 | 8 | ## Replicating Entities 9 | 10 | Replecs replication is _per-entity_. This means that you need to mark each entity to be networked.
11 | You can do this by adding `replecs.networked` to the entity. This will allocate an entity creation event to be sent to the client. 12 | 13 | ```luau 14 | local replecs = require("@pkg/replecs") 15 | local world = require("world") 16 | 17 | local npc = world:entity() 18 | world:add(npc, replecs.networked) 19 | ``` 20 | 21 | ## Replicating Components 22 | 23 | After marking an entity as networked, you need to mark each component that you want to replicate and how you want to replicate it. 24 | 25 | This allows for a highly granular control over replication for each entity. 26 | 27 | 28 | #### Replication Types 29 |
30 | 31 | ### Components/Tags 32 | 33 | Components and tags should be marked with `replecs.reliable` with a pair. You will set what component to replicate as the target. 34 | 35 | ```luau 36 | local replecs = require("@pkg/replecs") 37 | local jecs = require("@pkg/jecs") 38 | local world = require("world") 39 | local components = require("@shared/components") 40 | 41 | local npc = world:entity() 42 | world:add(npc, replecs.networked) 43 | 44 | world:set(npc, components.health, 100) 45 | world:add(npc, components.alive) 46 | -- mark components to be replicated 47 | world:add(npc, jecs.pair(replecs.reliable, components.health)) 48 | world:add(npc, jecs.pair(replecs.reliable, components.alive)) 49 | ``` 50 | 51 | ### Unreliable Components 52 | 53 | Components can be marked to be replicated unreliably. 54 | 55 | This will send the latest value of the component to the client no matter whether it changed or not. 56 | This kind of replication should be used when the component value changes too quickly in the server (every frame). 57 | 58 | Replecs will also automatically split buffer packets if they exceed the 1KB maximum unrealible size. 59 | This will ensure that no packets get dropped. 60 | 61 | 62 | ```luau 63 | local replecs = require("@pkg/replecs") 64 | local jecs = require("@pkg/jecs") 65 | local world = require("world") 66 | local components = require("@shared/components") 67 | 68 | local npc = world:entity() 69 | world:add(npc, replecs.networked) 70 | 71 | world:set(npc, components.position, Vector3.new(0, 0, 0)) 72 | world:add(npc, jecs.pair(replecs.unreliable, components.position)) 73 | ``` 74 | 75 | 80 | 81 | ### Relationships 82 | 83 | Pairs should be marked with `replecs.pair` with a pair. You will set what `relation` to replicate as the target.
84 | Note that if the relation target is an entity, this entity also needs to be marked as networked. 85 | 86 | ```luau 87 | local replecs = require("@pkg/replecs") 88 | local jecs = require("@pkg/jecs") 89 | local world = require("world") 90 | 91 | -- create a relation 92 | local npc = world:entity() 93 | local player = world:entity() 94 | 95 | world:add(npc, replecs.networked) 96 | world:add(player, replecs.networked) 97 | 98 | -- create a pair 99 | world:add(npc, jecs.pair(components.attacking, player)) 100 | world:add(npc, jecs.pair(replecs.pair, components.attacking)) 101 | ``` 102 | 103 | --- 104 | 105 | ### Relationship Values 106 | 107 | Pairs that contain values can be marked like normal pairs, but replecs will only replicate pair values if the first id of the pair is a component. 108 | 109 | ```luau 110 | local replecs = require("@pkg/replecs") 111 | local jecs = require("@pkg/jecs") 112 | local world = require("world") 113 | 114 | local value_relation = world:component() 115 | local no_value_relation = world:entity() 116 | 117 | -- this will replicate pair values 118 | world:add(relation, jecs.pair(replecs.pair, value_relation)) 119 | -- this will not replicate pair values 120 | world:add(relation, jecs.pair(replecs.pair, no_value_relation)) 121 | ``` 122 | 123 |
124 | 125 | ## Stopping Replication 126 | 127 | Entities and components can be stopped from being replicated at any time. 128 | 129 |
130 | 131 | ### Entities 132 | 133 | You can stop an entity from being replicated by removing the `replecs.networked` tag. 134 | This will also allocate an event to delete the entity in the client. 135 | 136 | All your components will still remain in the entity. You can reactivate the entity by adding `replecs.networked` again. 137 | 138 | This will correctly replicate back your entity with all its components. This is useful for streaming entities in and out dynamically. 139 | 140 | ### Components 141 | 142 | You can stop a component from being replicated by removing the pair. 143 | This will also allocate an event to remove the component entirely in the client 144 | 145 | ```luau 146 | world:remove(npc, jecs.pair(replecs.reliable, components.health)) 147 | world:remove(npc, jecs.pair(replecs.reliable, components.alive)) 148 | world:remove(npc, jecs.pair(replecs.unreliable, components.position)) 149 | world:remove(npc, jecs.pair(replecs.pair, components.attacking)) 150 | ``` 151 | 152 |
-------------------------------------------------------------------------------- /src/common.luau: -------------------------------------------------------------------------------- 1 | local jecs = require "../jecs" 2 | 3 | export type Entity = jecs.Entity 4 | export type World = jecs.World 5 | export type Component = jecs.Entity 6 | 7 | local common = {} 8 | 9 | local WILDCARD = jecs.Wildcard 10 | local ECS_NAME = jecs.Name 11 | local PREREGISTRATION = true 12 | local RELATIONSHIPS = true 13 | 14 | local IMPLICITLY_SHARED: { [string]: Entity } = { 15 | ["jecs.ChildOf"] = jecs.ChildOf, 16 | } 17 | 18 | function LOGERROR(...: string): never 19 | error("REPLECS ERROR - " .. table.concat({ ... }, " ")) 20 | end 21 | function LOGWARN(...: string) 22 | warn("REPLECS WARN - " .. table.concat({ ... }, " ")) 23 | end 24 | 25 | function LOG_COMPONENT(world: World, component: Entity) 26 | return ` name: {world:get(component, ECS_NAME) or "(no name)"}, id: {component}` 27 | end 28 | 29 | local function IS_PAIR(id: Component): boolean 30 | return jecs.IS_PAIR(id) 31 | end 32 | 33 | local function PAIR(relation: Entity, target: Entity): Entity 34 | return jecs.pair(relation, target) 35 | end 36 | 37 | local function PAIR_SECOND(world: World, id: Entity): Entity 38 | if not IS_PAIR(id) then 39 | LOGERROR(`expected a pair, got: {LOG_COMPONENT(world, id)}`) 40 | end 41 | return jecs.pair_second(world, id) 42 | end 43 | 44 | local function ADD_NAME(world: World, component: Component, name: string) 45 | world:set(component, ECS_NAME, name) 46 | end 47 | 48 | local function ADD_PREREGISTERED_NAME(component: Component, name: string) 49 | jecs.meta(component, ECS_NAME, name) 50 | end 51 | 52 | local function ITERATE_ALL_RELATIONS(world: World, entity: Entity, relation: Component, callback: (id: Component) -> ()) 53 | local entity_index = jecs.entity_index_try_get_fast(world.entity_index :: any, entity :: any) 54 | if not entity_index then 55 | return 56 | end 57 | local archetype = entity_index.archetype 58 | local wildcard = PAIR(relation, WILDCARD) :: any 59 | local idr = world.component_index[wildcard] 60 | if not idr then 61 | return 62 | end 63 | 64 | local archetype_id = archetype.id 65 | local count = idr.counts[archetype_id] 66 | if not count then 67 | return 68 | end 69 | 70 | local start = idr.records[archetype_id] 71 | if not start then 72 | return 73 | end 74 | 75 | for i = start, start + count - 1 do 76 | local pair = archetype.types[i] 77 | callback(pair) 78 | end 79 | end 80 | 81 | local function WORLD_ADD(world: World, entity: Entity, id: Component) 82 | world:add(entity, id) 83 | end 84 | 85 | local function WORLD_SET(world: World, entity: Entity, id: Component, value: T) 86 | world:set(entity, id, value) 87 | end 88 | 89 | local function WORLD_REMOVE(world: World, entity: Entity, id: Component) 90 | world:remove(entity, id) 91 | end 92 | 93 | local function WORLD_DELETE(world: World, entity: Entity) 94 | world:delete(entity) 95 | end 96 | 97 | local function WORLD_GET(world: World, entity: Entity, id: Component): T 98 | return world:get(entity, id) 99 | end 100 | 101 | local function WORLD_TARGET(world: World, entity: Entity, rel: Component, index: number?): jecs.Entity? 102 | return world:target(entity, rel, index) 103 | end 104 | 105 | local function WORLD_HAS(world: World, entity: Entity, id: Component) 106 | return world:has(entity, id) 107 | end 108 | 109 | local function WORLD_ENTITY(world: World) 110 | return world:entity() 111 | end 112 | 113 | local function WORLD_TAG(world: World) 114 | return world:entity() 115 | end 116 | local function WORLD_COMPONENT(world: World) 117 | return world:component() 118 | end 119 | 120 | local PREREGISTER_COMPONENT = jecs.component 121 | local PREREGISTER_TAG = jecs.tag 122 | 123 | local function IS_COMPONENT(world: World, id: Component) 124 | return world:has(id, jecs.Component) 125 | end 126 | 127 | local function HOOK_ADDED(world: World, id: Component, callback: (e: Entity, id: Component, value: any) -> ()): () -> () 128 | return world:added(id, callback) 129 | end 130 | 131 | local function HOOK_CHANGED( 132 | world: World, 133 | id: Component, 134 | callback: (e: Entity, id: Component, value: any) -> () 135 | ): () -> () 136 | return world:changed(id, callback) 137 | end 138 | 139 | local function HOOK_REMOVED(world: World, id: Component, callback: (e: Entity, id: Component) -> ()): () -> () 140 | return world:removed(id, callback) 141 | end 142 | 143 | common.preregistration = PREREGISTRATION 144 | common.relationships = RELATIONSHIPS 145 | common.wildcard = WILDCARD 146 | common.ecs_name = ECS_NAME 147 | common.implicitly_shared = IMPLICITLY_SHARED 148 | 149 | common.is_pair = IS_PAIR 150 | common.pair = PAIR 151 | common.pair_second = PAIR_SECOND 152 | common.add_name = ADD_NAME 153 | common.add_preregistered_name = ADD_PREREGISTERED_NAME 154 | common.iterate_all_relations = ITERATE_ALL_RELATIONS 155 | 156 | common.world_set = WORLD_SET 157 | common.world_remove = WORLD_REMOVE 158 | common.world_delete = WORLD_DELETE 159 | common.world_add = WORLD_ADD 160 | 161 | common.world_get = WORLD_GET 162 | common.world_target = WORLD_TARGET 163 | common.world_has = WORLD_HAS 164 | 165 | common.world_entity = WORLD_ENTITY 166 | common.world_tag = WORLD_TAG 167 | common.world_component = WORLD_COMPONENT 168 | common.preregister_component = PREREGISTER_COMPONENT 169 | common.preregister_tag = PREREGISTER_TAG 170 | 171 | common.is_component = IS_COMPONENT 172 | 173 | common.hook_added = HOOK_ADDED 174 | common.hook_changed = HOOK_CHANGED 175 | common.hook_removed = HOOK_REMOVED 176 | 177 | common.log_error = LOGERROR 178 | common.log_warn = LOGWARN 179 | common.log_component = LOG_COMPONENT 180 | 181 | return common 182 | -------------------------------------------------------------------------------- /demo/src/shared/utils/ecs/scheduler.luau: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | 3 | local ReplicatedStorage = game:GetService "ReplicatedStorage" 4 | local RunService = game:GetService "RunService" 5 | local Jecs = require(ReplicatedStorage.packages.jecs) 6 | local Jabby = require(ReplicatedStorage.packages.jabby) 7 | 8 | local world = require(ReplicatedStorage.code.world) 9 | 10 | type Map = { [K]: T } 11 | type Set = { [T]: true } 12 | type Array = { T } 13 | 14 | local HIDDEN = world:entity() 15 | local DEPENDS_ON = world:entity() 16 | local PHASE = world:entity() 17 | 18 | local SYSTEM = world:component() :: Jecs.Entity<{ name: string, callback: () -> (), group: string? }> 19 | local EVENT = world:component() 20 | 21 | local PRE_RENDER = world:entity() 22 | local HEARBEAT = world:entity() 23 | local PRE_ANIMATION = world:entity() 24 | local PRE_SIMULATION = world:entity() 25 | local POST_SIMULATION = world:entity() 26 | 27 | type System = { 28 | callback: (...any) -> (), 29 | name: string, 30 | id: any?, 31 | } 32 | type Systems = Array 33 | 34 | world:add(PRE_RENDER, PHASE) 35 | world:set(PRE_RENDER, EVENT, RunService.PreRender) 36 | 37 | world:add(HEARBEAT, PHASE) 38 | world:set(HEARBEAT, EVENT, RunService.Heartbeat) 39 | 40 | world:add(PRE_ANIMATION, PHASE) 41 | world:set(PRE_ANIMATION, EVENT, RunService.PreAnimation) 42 | 43 | world:add(PRE_SIMULATION, PHASE) 44 | world:set(PRE_SIMULATION, EVENT, RunService.PreSimulation) 45 | 46 | world:add(POST_SIMULATION, PHASE) 47 | world:set(POST_SIMULATION, EVENT, RunService.PostSimulation) 48 | 49 | local StepPhases = { 50 | Hearbeat = HEARBEAT, 51 | PreRender = PRE_RENDER, 52 | PreAnimation = PRE_ANIMATION, 53 | PreSimulation = PRE_SIMULATION, 54 | PostSimulation = POST_SIMULATION, 55 | } 56 | 57 | local CurrentSystem = {} :: System 58 | 59 | local SCHEDULER = Jabby.scheduler.create() 60 | 61 | function Run(...) 62 | local id = CurrentSystem.id 63 | if id ~= nil then 64 | SCHEDULER:run(id, CurrentSystem.callback, ...) 65 | else 66 | CurrentSystem.callback(...) 67 | end 68 | end 69 | 70 | function ConnectEvent(event: any, callback: (...any) -> ()): any 71 | if typeof(event) == "RBXScriptSignal" then 72 | return event:Connect(callback) 73 | elseif event["Connect"] then 74 | return event:Connect(callback) 75 | elseif event["connect"] then 76 | return event:connect(callback) 77 | elseif event["subscribe"] then 78 | return event.subscribe(callback) 79 | else 80 | error "Event-like object does not have a supported connect method." 81 | end 82 | end 83 | 84 | local OTHER = "__other__" 85 | 86 | type OrderedSystems = Array ())> 87 | 88 | function Initialize(events: Map>, orderTable: OrderedSystems?) 89 | local connections: Map = {} 90 | 91 | if orderTable then 92 | local old_events = events 93 | events = {} 94 | 95 | for event, systems in old_events do 96 | local ordered = table.clone(systems) 97 | table.sort(ordered, function(a, b) 98 | local a_order = table.find(orderTable, a.callback) or table.find(orderTable, OTHER) or 0 99 | local b_order = table.find(orderTable, b.callback) or table.find(orderTable, OTHER) or 0 100 | return a_order < b_order 101 | end) 102 | events[event] = ordered 103 | end 104 | end 105 | 106 | for event, systems in events do 107 | if not event then 108 | continue 109 | end 110 | 111 | local eventName = tostring(event) 112 | local connection = ConnectEvent(event, function(...) 113 | debug.profilebegin("JECS - " .. eventName) 114 | for _, system in systems do 115 | CurrentSystem = system 116 | 117 | debug.profilebegin("JECS - " .. system.name) 118 | Run(...) 119 | debug.profileend() 120 | end 121 | debug.profileend() 122 | end) 123 | connections[event] = connection 124 | end 125 | 126 | return function() 127 | for event, connection in connections do 128 | connection:disconnect() 129 | end 130 | end 131 | end 132 | 133 | function CollectSystemsUnderEventRecursive(systems: Systems, phase: Jecs.Entity) 134 | local depends = Jecs.pair(DEPENDS_ON, phase) 135 | local phaseHidden = world:has(phase, HIDDEN) 136 | 137 | for systemId, system in world:query(SYSTEM):with(depends):iter() do 138 | local systemEntry: System = { 139 | name = system.name, 140 | callback = system.callback, 141 | } 142 | 143 | local hidden = phaseHidden or world:has(systemId, HIDDEN) 144 | if not hidden then 145 | systemEntry.id = SCHEDULER:register_system { 146 | name = system.name, 147 | phase = system.group or "main", 148 | } 149 | end 150 | systems[#systems + 1] = systemEntry 151 | end 152 | 153 | for after in world:query(PHASE):with(depends):iter() do 154 | CollectSystemsUnderEventRecursive(systems, after) 155 | end 156 | end 157 | 158 | function CollectSystemsUnderEvent(event: Jecs.Entity) 159 | local systems: Systems = {} 160 | CollectSystemsUnderEventRecursive(systems, event) 161 | return systems 162 | end 163 | 164 | function CollectSystems() 165 | local events: { [any]: Array } = {} 166 | for phase, event in world:query(EVENT):with(PHASE):iter() do 167 | events[event] = CollectSystemsUnderEvent(phase) 168 | end 169 | return events 170 | end 171 | 172 | function Phase(event: any?, after: Jecs.Entity, hidden: boolean?) 173 | local phase = world:entity() 174 | world:add(phase, PHASE) 175 | 176 | if after then 177 | local dependency = Jecs.pair(DEPENDS_ON, after) 178 | world:add(phase, dependency) 179 | end 180 | if event then 181 | world:set(phase, EVENT, event) 182 | end 183 | if hidden then 184 | world:add(phase, HIDDEN) 185 | end 186 | end 187 | 188 | function System(callback: (...any) -> (), phase: Jecs.Entity?, group: string?, hidden: boolean?) 189 | local system = world:entity() 190 | world:set(system, SYSTEM, { 191 | callback = callback, 192 | name = debug.info(callback, "n"), 193 | group = group, 194 | }) 195 | 196 | phase = phase or HEARBEAT 197 | if hidden then 198 | world:add(system, HIDDEN) 199 | end 200 | 201 | world:add(system, Jecs.pair(DEPENDS_ON, phase :: any)) 202 | return system 203 | end 204 | 205 | return { 206 | StepPhases = StepPhases, 207 | Initialize = Initialize, 208 | CollectSystems = CollectSystems, 209 | System = System, 210 | Phase = Phase, 211 | Phases = StepPhases, 212 | Scheduler = SCHEDULER, 213 | } 214 | -------------------------------------------------------------------------------- /tests/bench/replecs.bench.luau: -------------------------------------------------------------------------------- 1 | local replecs = require "@replecs" 2 | local jecs = require "@pkg/jecs" 3 | local testkit = require "@testkit" 4 | 5 | local BENCH, START = testkit.benchmark() 6 | 7 | function TITLE(str: string) 8 | print() 9 | print(testkit.color.white(str)) 10 | end 11 | 12 | local AMOUNT = 2 ^ 17 13 | local FILTER_AMOUNT = 200 14 | local MEMBER_AMOUNT = 62 15 | 16 | do 17 | TITLE "player masking" 18 | 19 | local world = jecs.world() 20 | local server = replecs.create_server(world) 21 | local masking = server.masking 22 | local entities = table.create(AMOUNT) :: { jecs.Entity } 23 | 24 | local function register_members(amount: number?) 25 | local members = {} :: { any } 26 | for i = 1, amount or MEMBER_AMOUNT do 27 | local member = "member" .. i 28 | masking:register_client(member) 29 | members[i] = member 30 | end 31 | return members 32 | end 33 | 34 | local members = register_members() 35 | 36 | local function write_storage_count() 37 | local count = 0 38 | for i in masking.storages do 39 | count += 1 40 | end 41 | print(testkit.color.green "storage count: " .. count) 42 | end 43 | 44 | local function POST() 45 | write_storage_count() 46 | server:destroy() 47 | server = replecs.create_server(world) 48 | masking = server.masking 49 | members = register_members() 50 | end 51 | 52 | local function activate_members() 53 | for member in masking.member_indexes do 54 | masking:activate_client(member) 55 | end 56 | end 57 | 58 | local function randomize_filter(members: { any }, bool: boolean?, amount: number?) 59 | local filter = {} 60 | bool = if bool == nil then true else bool 61 | for i = 1, amount or (#members / 2) do 62 | filter[members[math.random(1, #members)]] = bool 63 | end 64 | return filter 65 | end 66 | 67 | local function create_random_filters(bool: boolean?, amount: number?) 68 | local filters = table.create(amount or FILTER_AMOUNT) :: { { [any]: boolean } } 69 | for i = 1, amount or FILTER_AMOUNT do 70 | filters[i] = randomize_filter(members, bool) 71 | end 72 | return filters 73 | end 74 | 75 | local random_include_filters = create_random_filters(true, FILTER_AMOUNT) 76 | local random_exclude_filters = create_random_filters(false, FILTER_AMOUNT) 77 | 78 | for i = 1, AMOUNT do 79 | entities[i] = world:entity() 80 | end 81 | 82 | for i = 1, FILTER_AMOUNT do 83 | random_include_filters[i] = randomize_filter(members) 84 | end 85 | for i = 1, FILTER_AMOUNT do 86 | random_exclude_filters[i] = randomize_filter(members, false) 87 | end 88 | 89 | --[[do 90 | register_members() 91 | activate_members() 92 | 93 | BENCH("start entity with no filter", function() 94 | for i = 1, START(AMOUNT) do 95 | masking:start_entity(entities[i]) 96 | end 97 | end) 98 | POST() 99 | end 100 | 101 | do 102 | activate_members() 103 | local filter = random_include_filters[1] 104 | 105 | BENCH("start entity with include filter", function() 106 | for i = 1, START(AMOUNT) do 107 | masking:start_entity(entities[i], filter) 108 | end 109 | end) 110 | POST() 111 | end 112 | 113 | do 114 | activate_members() 115 | local filter = random_include_filters[1] 116 | 117 | BENCH("start entity with exclude filter", function() 118 | for i = 1, START(AMOUNT) do 119 | masking:start_entity(entities[i], filter) 120 | end 121 | end) 122 | POST() 123 | end 124 | 125 | do 126 | activate_members() 127 | local component = world:component() 128 | local filter = random_include_filters[1] 129 | 130 | for i = 1, AMOUNT do 131 | masking:start_entity(entities[i], filter) 132 | end 133 | 134 | BENCH("component with no filter", function() 135 | for i = 1, START(AMOUNT) do 136 | masking:start_component(entities[i], component, 1) 137 | end 138 | end) 139 | POST() 140 | end 141 | do 142 | activate_members() 143 | local component = world:component() 144 | local filter = random_include_filters[1] 145 | 146 | for i = 1, AMOUNT do 147 | masking:start_entity(entities[i], filter) 148 | end 149 | 150 | BENCH("component with same filter as entity", function() 151 | for i = 1, START(AMOUNT) do 152 | masking:start_component(entities[i], component, 1, filter) 153 | end 154 | end) 155 | POST() 156 | end 157 | do 158 | activate_members() 159 | local component = world:component() 160 | local e_filter = random_include_filters[1] 161 | local c_filter = random_include_filters[2] 162 | 163 | for i = 1, AMOUNT do 164 | masking:start_entity(entities[i], e_filter) 165 | end 166 | 167 | BENCH("component with random filter", function() 168 | for i = 1, START(AMOUNT) do 169 | masking:start_component(entities[i], component, 1, c_filter) 170 | end 171 | end) 172 | POST() 173 | end 174 | do 175 | activate_members() 176 | 177 | BENCH("random entity filters (high fragmentation)", function() 178 | for i = 1, START(AMOUNT) do 179 | masking:start_entity(entities[i], random_include_filters[i % FILTER_AMOUNT]) 180 | end 181 | end) 182 | POST() 183 | end 184 | 185 | do 186 | activate_members() 187 | local component = world:component() 188 | 189 | for i = 1, AMOUNT do 190 | masking:start_entity(entities[i]) 191 | end 192 | 193 | BENCH("random component filters (high fragmentation)", function() 194 | for i = 1, START(AMOUNT) do 195 | masking:start_component(entities[i], component, 1, random_include_filters[i % FILTER_AMOUNT]) 196 | end 197 | end) 198 | POST() 199 | end 200 | 201 | do 202 | activate_members() 203 | local component = world:component() 204 | local component_filters = create_random_filters(true, FILTER_AMOUNT) 205 | 206 | for i = 1, AMOUNT do 207 | masking:start_entity(entities[i], random_include_filters[i % FILTER_AMOUNT]) 208 | end 209 | 210 | BENCH("random filters in both entity and component", function() 211 | for i = 1, START(AMOUNT) do 212 | masking:start_component(entities[i], component, 1, component_filters[i % FILTER_AMOUNT]) 213 | end 214 | end) 215 | POST() 216 | end 217 | ]] 218 | do 219 | masking:activate_client(members[1]) 220 | 221 | for i = 1, 10000 do 222 | masking:start_entity(entities[i], { [members[1]] = true }) 223 | end 224 | 225 | BENCH("move 10,000 entities by activating a member", function() 226 | for i = 2, START(MEMBER_AMOUNT) do 227 | masking:activate_client(members[i]) 228 | end 229 | end) 230 | POST() 231 | end 232 | end 233 | 234 | do 235 | TITLE "serialization" 236 | 237 | TITLE "deserialization" 238 | end 239 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/server-replicator.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server Replicator 3 | description: Server Replicator API Reference 4 | sidebar: 5 | order: 2 6 | --- 7 | 8 | 9 | ## Types 10 | 11 | --- 12 | 13 |
14 | 15 | ### `Iterator` 16 | 17 | ```ts 18 | type Iterator = () -> (T...) 19 | ``` 20 | 21 |
22 | 23 | ## Members 24 | 25 | --- 26 | 27 |
28 | 29 | ### `Server.masking` 30 | 31 | The masking controller. This handles player filtering, as well as excluding non-ready players. 32 | 33 | ```ts 34 | type Server = { 35 | masking: MaskingController 36 | } 37 | ``` 38 | 39 |
40 | 41 | ## Methods 42 | 43 | --- 44 | 45 |
46 | 47 | ### `server:init()` 48 | 49 | Initializes the server replicator. World can be provided here 50 | if it wasn't provided when creating the replicator. 51 | 52 | ```ts 53 | type Server = { 54 | init: (self: Server, world: jecs.World?) -> () 55 | } 56 | ``` 57 | 58 |
59 | #### Arguments 60 | - **world**: The world to use for the replicators. Can also be provided when initing the replicator. 61 |
62 | 63 | ### `server:destroy()` 64 | 65 | Destroys the server replicator. This will disconnect jecs hooks and connections. 66 | 67 | ```ts 68 | type Server = { 69 | destroy: (self: Server) -> () 70 | } 71 | ``` 72 | 73 | 74 | ### `server:collect_updates()` 75 | 76 | Returns an iterator that will send batched updates. 77 | 78 | ```ts 79 | type Server = { 80 | collect_updates: (self: Server) -> Iterator 81 | } 82 | ``` 83 | 84 |
85 | #### Iterator Returns 86 | - **player**: The player to send the updates to. 87 | - **buf**: The buffer containing the updates. 88 | - **variants**: The variants table containing non-serialized values. 89 |
90 | 91 | ### `server:collect_unreliable()` 92 | 93 | Returns an iterator that will send the values of unrealible components. 94 | 95 | ```ts 96 | type Server = { 97 | collect_unreliable: (self: Server) -> Iterator 98 | } 99 | ``` 100 | 101 |
102 | #### Iterator Returns 103 | - **player**: The player to send the updates to. 104 | - **buf**: The buffer containing the updates. 105 | - **variants**: The variants table containing non-serialized values. 106 |
107 | 108 | ### `server:get_full()` 109 | 110 | Gets the full state of the world for the provided player. 111 | 112 | ```ts 113 | type Server = { 114 | get_full: (self: Server, player: Player) -> (buffer, {{any}}) 115 | } 116 | ``` 117 | 118 |
119 | #### Arguments 120 | - **player**: The player to get the full state of the world for. 121 | #### Returns 122 | - **buf**: The buffer containing the full state of the world. 123 | - **variants**: The variants table containing non-serialized values. 124 |
125 | 126 | 127 | ### `server:enconde_component()` 128 | 129 | Encodes a component for sending through the network. This returns a number between 1 to 255. 130 | 131 | ```ts 132 | type Server = { 133 | encode_component: (self: Server, component: Entity) -> number 134 | } 135 | ``` 136 | 137 |
138 | #### Arguments 139 | - **component**: The component to encode. 140 | #### Returns 141 | - **number**: The encoded component. 142 |
143 | 144 | 145 | ### `server:decode_component()` 146 | 147 | Decodes a component from a number. 148 | 149 | ```ts 150 | type Server = { 151 | decode_component: (self: Server, encoded: number) -> Entity 152 | } 153 | ``` 154 | 155 |
156 | #### Arguments 157 | - **number**: The number to decode, This is number from `encode_component`. 158 | #### Returns 159 | - **component**: The decoded component. 160 |
161 | 162 | ### `server:mark_player_ready()` 163 | 164 | Marks a player as ready to receive updates. Non ready players will not receive updates until this is called. 165 | 166 | ```ts 167 | type Server = { 168 | mark_player_ready: (self: Server, player: Player) -> () 169 | } 170 | ``` 171 | 172 |
173 | #### Arguments 174 | - **player**: The player to mark as ready. 175 |
176 | 177 | ### `server:is_player_ready()` 178 | 179 | Checks if a player has been marked as ready to receive updates. 180 | 181 | ```ts 182 | type Server = { 183 | is_player_ready: (self: Server, player: Player) -> boolean 184 | } 185 | ``` 186 | 187 |
188 | #### Arguments 189 | - **player**: The player to check. 190 | #### Returns 191 | - **boolean**: Whether the player is ready. 192 |
193 | 194 | ### `server:add_player_alias()` 195 | 196 | Adds an alias for a player. This can be used to use different values like entities in player filters. 197 | 198 | ```ts 199 | type Server = { 200 | add_player_alias: (self: Server, player: Player, alias: any) -> () 201 | } 202 | ``` 203 | 204 |
205 | #### Arguments 206 | - **player**: The player to add the alias for. 207 | - **alias**: The alias to add. 208 |
209 | 210 | ### `server:remove_player_alias()` 211 | 212 | Removes an alias for a player. This is important to do when a player leaves the server. 213 | 214 | ```ts 215 | type Server = { 216 | remove_player_alias: (self: Server, alias: any) -> () 217 | } 218 | ``` 219 | 220 |
221 | #### Arguments 222 | - **alias**: The alias to remove. 223 |
224 | 225 | ### `server:generate_handshake()` 226 | 227 | Generates a handshake info. This can be used to verify that the server and client are setup correctly. 228 | 229 | ```ts 230 | type Server = { 231 | generate_handshake: (self: Server) -> HandshakeInfo 232 | } 233 | ``` 234 | 235 |
236 | #### Returns 237 | - **handshake**: The handshake info. This can be passed to `client:verify_handshake()` 238 |
239 | 240 | 241 | ### `server:verify_handshake()` 242 | 243 | Verifies a handshake info from `client:generate_handshake()`. 244 | 245 | ```ts 246 | type Server = { 247 | verify_handshake: (self: Server, handshake: HandshakeInfo) -> (success: boolean, err: string?) 248 | } 249 | ``` 250 | 251 |
252 | #### Arguments 253 | - **handshake**: The handshake info to verify. 254 | #### Returns 255 | - **success**: Whether the handshake info is valid. 256 | - **err**: The error message if the handshake was not successful. 257 |
258 | 259 | 260 | ### `server:register_custom_id()` 261 | 262 | Registers a custom id handler. Custom ids can only be used if they are registered. 263 | 264 | 265 | ```ts 266 | type Server = { 267 | register_custom_id: (self: Server, custom_id: CustomId) -> () 268 | } 269 | ``` 270 | 271 |
272 | #### Arguments 273 | - **custom_id**: The custom id to register. 274 |
275 | 276 | 277 |
-------------------------------------------------------------------------------- /docs/src/content/docs/guides/networking-hooks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Networking Hooks 3 | sidebar: 4 | order: 3 5 | --- 6 | 7 | 8 | Replecs allows you to intercept to the networking process in the client. 9 | 10 | 11 | ## Hook After Replication 12 | 13 | You can set a callback to be called after replication has been completed using `replicator:after_replication(callback)`. 14 | This is useful for jecs signals because they will most likely be called in the middle of the networking process. 15 | 16 | Hooking after replication will ensure that all the components are available to be used. 17 | 18 | - This is not a signal, so it doesn't need to be disconnected. 19 | - Calling this when no replication is taking place will run the callback immediately. 20 | - This can also be called in the shared replicator that contains both the client and server replicators. 21 | Calling this in the server will always run the callback immediately. 22 | 23 | 24 | ```luau 25 | local replicator = require("@shared/replicator") 26 | 27 | world:added(components.car, function(entity) 28 | replicator:after_replication(function() 29 | -- safe to get the components since the car entity has been replicated entirely. 30 | local car_model = world:get(entity, components.car_model) 31 | local speed = world:get(entity, components.speed) 32 | end) 33 | end) 34 | ``` 35 | 36 | ## Entity Creation 37 | 38 | You can hook when a networked entity is created in the client by calling `replicator:added(callback)`. 39 | The callback is run immediately right after creation, because of this, the entity will be empty when the callback runs. 40 | You can hook to `after_replication` to wait for the components. The entity you get is the client entity. 41 | 42 | Unlike `after_replication`, this is a signal that needs to be disconnected, and this can only be called in the client replicator. 43 | 44 | ```luau 45 | local replicator = require("@client/replicator") 46 | 47 | local disconnect = replicator:added(function(entity) 48 | replicator:after_replication(function() 49 | local server_entity = replicator:get_server_entity(entity) 50 | print("entity created from the server", server_entity) 51 | end) 52 | end) 53 | disconnect() -- disconnect the signal 54 | ``` 55 | 56 | ## Network Hooks 57 | 58 | You can set hooks to when an action is performed. To do this, you need to call 59 | `replicator:hook(action, ...)`. The arguments of this method will change depending on the action. 60 | 61 | This method returns a disconnect function that you can call to remove the hook. 62 | 63 |
64 | ### Component Changes 65 | Setting the action to "changed" will make the callback run right after a component has been changed. 66 | This does not distinguish component additions and changes. 67 | 68 | The arguments for the hook method will be: 69 | - action: `"changed"` - "changed" action 70 | - replication_pair: `Pair` - what component to hook to. This pair should mimic how you track components [in the server](/replecs/guides/networking-entity/#replication-types) 71 | - callback: `(entity: Entity, id: Id, component: Component)` - callback to run when the component is changed 72 | 73 | ```luau 74 | local replicator = require("@client/replicator") 75 | 76 | replicator:hook("changed", jecs.pair(replecs.reliable, components.speed), function(entity, id, speed) 77 | print(entity, "changed speed to", speed) 78 | end) 79 | replicator:hook("changed", jecs.pair(replecs.pair, components.attacking), function(entity, id) 80 | print(entity, "is now attacking", jecs.pair_second(world, id)) 81 | end) 82 | ``` 83 | 84 | ### Component Removals 85 | Setting the action to "removed" will make the callback run right before a component or pair gets removed. 86 | You can still access the component or pair right at this point. 87 | 88 | The arguments for the hook method will be: 89 | - action: `"removed"` - "removed" action 90 | - replication_pair: `Pair` - what component to hook to. This pair should mimic how you track components [in the server](/replecs/guides/networking-entity/#replication-types) 91 | - callback: `(entity: Entity, id: Id)` - callback to run when the component is removed 92 | 93 | ```luau 94 | local replicator = require("@client/replicator") 95 | 96 | replicator:hook("removed", jecs.pair(replecs.reliable, components.speed), function(entity, id) 97 | print("removed speed for:", entity, world:get(entity, components.speed)) 98 | end) 99 | replicator:hook("removed", jecs.pair(replecs.pair, components.attacking), function(entity, id) 100 | print(entity, "is no longer attacking", jecs.pair_second(world, id)) 101 | end) 102 | ``` 103 | 104 | 105 | ### Entity Deletions 106 | Setting the action to "deleted" will make the callback run right before an entity gets deleted. You need to connect this once per-entity. 107 | All hooks get disconnected automatically after the entity is deleted. 108 | 109 | The arguments for the hook method will be: 110 | - action: `"deleted"` - "deleted" action 111 | - entity: `Entity` - entity to hook 112 | - callback: `(entity: Entity)` - callback to run when the entity is deleted 113 | 114 | ```luau 115 | local replicator = require("@client/replicator") 116 | 117 | replicator:hook("deleted", entity, function(entity) 118 | print(entity, "was deleted") 119 | end) 120 | ``` 121 |
122 | 123 | ## Intercepting Actions 124 | 125 | You can override actions. When this happens, replecs will not perform the action, but instead run the callback. 126 | This allows you add your own logic for changing/removing/deleting entities and components. 127 | 128 | You override actions the same way you would hook to them, 129 | but calling `replicator:override(entity, action, ...)` instead, you cant override and hook at the same time for the same component and action. 130 | 131 | All actions behave the same way with the exception of "changed" since the component will never change. 132 | 133 | ```luau 134 | local replicator = require("@client/replicator") 135 | 136 | replicator:override("changed", jecs.pair(replecs.reliable, components.speed), function(entity, _, speed) 137 | -- double the speed in the client (for some reason?) 138 | world:set(entity, components.speed, speed * 2) 139 | end) 140 | replicator:override("removed", jecs.pair(replecs.reliable, components.speed), function(entity) 141 | -- set speed to 0 instead of removing it 142 | world:set(car, components.speed, 0) 143 | end) 144 | replicator:override("deleted", enemy, function(_) 145 | -- fade out the enemy and add lifetime to be deleted after 4 seconds 146 | world:add(enemy, components.fade_out) 147 | world:set(enemy, components.lifetime, 4) 148 | end) 149 | ``` 150 | 151 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Entity, Id, Pair, Tag, World } from "@rbxts/jecs"; 2 | 3 | declare namespace Replecs { 4 | export type SerdesTable = 5 | | { 6 | bytespan?: number; 7 | includes_variants?: false; 8 | serialize: (value: any) => buffer; 9 | deserialize: (buffer: buffer) => any; 10 | } 11 | | { 12 | bytespan?: number; 13 | includes_variants: true; 14 | serialize: (value: any) => LuaTuple<[buffer, defined[] | undefined]>; 15 | deserialize: (buffer: buffer, blobs: defined[] | undefined) => any; 16 | }; 17 | 18 | type MemberFilterMap = Map; 19 | type MemberFilter = Player | MemberFilterMap | undefined; 20 | type Member = unknown; 21 | 22 | export interface SharedInfo { 23 | lookup: Record; 24 | keys: string[]; 25 | indexes: [T]; 26 | members: Map; 27 | } 28 | export interface HandleContext { 29 | entity_id: number; 30 | component: (component: Entity) => T; 31 | target: (relation: Id, index?: number) => Entity | undefined; 32 | pair_value: (relation: Id, target: Entity) => T | undefined; 33 | has_pair: (relation: Id, target: Entity) => boolean; 34 | 35 | entity: (server_entity: number) => Entity | undefined; 36 | has: (tag: Entity) => boolean; 37 | } 38 | export interface CustomId { 39 | identifier: string; 40 | handle_callback: (ctx: HandleContext) => Entity; 41 | handle(handler: (ctx: HandleContext) => Entity): void; 42 | } 43 | 44 | export interface Shared { 45 | components: SharedInfo; 46 | custom_ids: SharedInfo; 47 | } 48 | interface HandshakeSerdesInfo { 49 | includes_variants?: boolean; 50 | bytespan?: number; 51 | } 52 | 53 | export interface HandshakeInfo { 54 | components: Record; 55 | custom_ids: Record; 56 | serdes: Record; 57 | } 58 | 59 | export interface Components { 60 | shared: Tag; 61 | networked: Entity; 62 | reliable: Entity; 63 | unreliable: Entity; 64 | pair: Entity; 65 | 66 | serdes: Entity; 67 | custom: Entity; 68 | custom_handler: Entity<(value: any) => Entity>; 69 | global: Entity; 70 | 71 | Shared: Tag; 72 | Networked: Entity; 73 | Reliable: Entity; 74 | Unreliable: Entity; 75 | Pair: Entity; 76 | 77 | Serdes: Entity; 78 | Custom: Entity; 79 | CustomHandler: Entity<(value: any) => Entity>; 80 | Global: Entity; 81 | } 82 | 83 | export interface Client { 84 | world: World; 85 | inited?: boolean; 86 | 87 | is_relicating: boolean; 88 | after_relication_callbacks: [() => void]; 89 | 90 | components: Components; 91 | 92 | init(world?: World): void; 93 | destroy(): void; 94 | 95 | handle_global(handler: (id: number) => Entity): void; 96 | 97 | get_server_entity(client_entity: Entity): number | undefined; 98 | get_client_entity(server_entity: number): Entity | undefined; 99 | 100 | register_entity(entity: Entity, server_entity: number): void; 101 | unregister_entity(entity: Entity): void; 102 | 103 | after_replication(callback: () => void): void; 104 | added(callback: (entity: Entity) => void): () => void; 105 | 106 | hook( 107 | action: "changed", 108 | relation: Pair, 109 | callback: (entity: Entity, id: Id, value: T) => void 110 | ): () => void; 111 | hook( 112 | action: "removed", 113 | relation: Pair, 114 | callback: (entity: Entity, id: Id) => void 115 | ): () => void; 116 | hook( 117 | action: "deleted", 118 | entity: Entity, 119 | callback: (entity: Entity) => void 120 | ): () => void; 121 | 122 | override( 123 | action: "changed", 124 | relation: Pair, 125 | callback: (entity: Entity, id: Id, value: any) => void 126 | ): () => void; 127 | override( 128 | action: "removed", 129 | relation: Pair, 130 | callback: (entity: Entity, id: Id) => void 131 | ): () => void; 132 | override( 133 | action: "deleted", 134 | entity: Entity, 135 | callback: (entity: Entity) => void 136 | ): () => void; 137 | 138 | encode_component(component: Entity): number; 139 | decode_component(encoded: number): Entity; 140 | register_custom_id(custom_id: CustomId): void; 141 | 142 | apply_updates(buf: buffer, all_variants?: unknown[][]): void; 143 | apply_unreliable(buf: buffer, all_variants?: unknown[][]): void; 144 | apply_full(buf: buffer, all_variants?: unknown[][]): void; 145 | 146 | generate_handshake(): HandshakeInfo; 147 | verify_handshake( 148 | handshake: HandshakeInfo 149 | ): LuaTuple<[true]> | LuaTuple<[false, string]>; 150 | } 151 | 152 | export interface ServerImp { 153 | set_networked(entity: Entity, filter?: MemberFilter): void; 154 | stop_networked(entity: Entity): void; 155 | 156 | set_reliable( 157 | entity: Entity, 158 | component: Entity, 159 | filter?: MemberFilter 160 | ): void; 161 | set_unreliable( 162 | entity: Entity, 163 | component: Entity, 164 | filter?: MemberFilter 165 | ): void; 166 | set_pair(entity: Entity, relation: Entity, filter?: MemberFilter): void; 167 | set_custom(entity: Entity, handler: Entity | CustomId): void; 168 | 169 | stop_reliable(entity: Entity, component: Entity): void; 170 | stop_unreliable(entity: Entity, component: Entity): void; 171 | stop_pair(entity: Entity, relation: Entity): void; 172 | remove_custom(entity: Entity): void; 173 | } 174 | 175 | export interface Server extends ServerImp { 176 | world: World; 177 | inited?: boolean; 178 | 179 | init(world?: World): void; 180 | destroy(): void; 181 | 182 | encode_component(component: Entity): number; 183 | decode_component(encoded: number): Entity; 184 | register_custom_id(custom_id: CustomId): void; 185 | 186 | get_full(player: Player): LuaTuple<[buffer, unknown[][]]>; 187 | collect_updates(): IterableFunction< 188 | LuaTuple<[Player, buffer, unknown[][]]> 189 | >; 190 | collect_unreliable(): IterableFunction< 191 | LuaTuple<[Player, buffer, unknown[][]]> 192 | >; 193 | 194 | mark_player_ready(player: Player): void; 195 | is_player_ready(player: Player): boolean; 196 | 197 | add_player_alias(client: Player, alias: defined): void; 198 | remove_player_alias(alias: defined): void; 199 | 200 | generate_handshake(): HandshakeInfo; 201 | verify_handshake( 202 | handshake: HandshakeInfo 203 | ): LuaTuple<[true]> | LuaTuple<[false, string]>; 204 | } 205 | 206 | export interface ReplecsLib { 207 | client: Client; 208 | server: Server; 209 | 210 | after_replication(callback: () => void): void; 211 | register_custom_id(custom_id: CustomId): void; 212 | } 213 | 214 | export interface Replecs extends Components { 215 | VERSION: string; 216 | 217 | create: (world?: World) => ReplecsLib; 218 | create_server: (world?: World) => Server; 219 | create_client: (world?: World) => Client; 220 | create_custom_id: ( 221 | identifier: string, 222 | handler?: (ctx: HandleContext) => Entity 223 | ) => CustomId; 224 | } 225 | } 226 | 227 | declare const Replecs: Replecs.Replecs; 228 | 229 | export = Replecs; 230 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "luau-lsp.completion.imports.enabled": true, 3 | "luau-lsp.completion.imports.suggestServices": true, 4 | "luau-lsp.completion.imports.suggestRequires": true, 5 | "luau-lsp.completion.autocompleteEnd": true, 6 | "luau-lsp.sourcemap.rojoProjectFile": "serve.project.json", 7 | "[luau]": { 8 | "editor.defaultFormatter": "JohnnyMorganz.stylua" 9 | }, 10 | "files.eol": "\n", 11 | "luau-lsp.sourcemap.generatorCommand": "rojo sourcemap serve.project.json -o sourcemap.json --watch", 12 | "luau-lsp.sourcemap.sourcemapFile": "sourcemap.json", 13 | "luau-lsp.fflags.enableNewSolver": true, 14 | "luau-lsp.fflags.sync": false, 15 | "luau-lsp.fflags.override": { 16 | "FFlagLuauAllowMixedTables": "true", 17 | "FFlagLuauAllowNilAssignmentToIndexer": "true", 18 | "FFlagLuauAllowNonSharedTableTypesInLiteral": "true", 19 | "FFlagLuauAlwaysFillInFunctionCallDiscriminantTypes": "true", 20 | "FFlagLuauAlwaysResolveAstTypes": "true", 21 | "FFlagLuauAutocompleteMissingFollows": "true", 22 | "FFlagLuauAutocompleteUnionCopyPreviousSeen": "true", 23 | "FFlagLuauAutocompleteUseLimits": "true", 24 | "FFlagLuauAvoidDoubleNegation": "true", 25 | "FFlagLuauAvoidExcessiveTypeCopying": "true", 26 | "FFlagLuauAvoidGenericsLeakingDuringFunctionCallCheck": "true", 27 | "FFlagLuauBidirectionalFailsafe": "true", 28 | "FFlagLuauBidirectionalInferenceCollectIndexerTypes": "true", 29 | "FFlagLuauBidirectionalInferenceElideAssert": "true", 30 | "FFlagLuauCacheDuplicateHasPropConstraints": "true", 31 | "FFlagLuauCacheInferencePerAstExpr": "true", 32 | "FFlagLuauClipNestedAndRecursiveUnion": "true", 33 | "FFlagLuauCloneReturnTypePack": "true", 34 | "FFlagLuauCodegen": "true", 35 | "FFlagLuauCodeGenBetterBytecodeAnalysis": "true", 36 | "FFlagLuauCodeGenSimplifyImport": "false", 37 | "FFlagLuauDeferBidirectionalInferenceForTableAssignment": "true", 38 | "FFlagLuauDfgAllowUpdatesInLoops": "true", 39 | "FFlagLuauDfgForwardNilFromAndOr": "true", 40 | "FFlagLuauDfgIfBlocksShouldRespectControlFlow": "true", 41 | "FFlagLuauDfgScopeStackNotNull": "true", 42 | "FFlagLuauDfgScopeStackTrueReset": "true", 43 | "FFlagLuauDisablePrimitiveInferenceInLargeTables": "true", 44 | "FFlagLuauDoNotAddUpvalueTypesToLocalType": "true", 45 | "FFlagLuauDoNotBlockOnStuckTypeFunctions": "true", 46 | "FFlagLuauDoNotGeneralizeInTypeFunctions": "true", 47 | "FFlagLuauDoNotLeakNilInRefinement": "true", 48 | "FFlagLuauDoNotPrototypeTableIndex": "true", 49 | "FFlagLuauDontDynamicallyCreateRedundantSubtypeConstraints": "true", 50 | "FFlagLuauDontForgetToReduceUnionFunc": "true", 51 | "FFlagLuauDontInPlaceMutateTableType": "true", 52 | "FFlagLuauEagerGeneralization4": "true", 53 | "FFlagLuauEmptyStringInKeyOf": "true", 54 | "FFlagLuauExpectedTypeVisitor": "true", 55 | "FFlagLuauExplicitSkipBoundTypes": "true", 56 | "FFlagLuauFixCyclicIndexInIndexer": "true", 57 | "FFlagLuauFixInfiniteRecursionInNormalization": "true", 58 | "FFlagLuauFixNormalizedIntersectionOfNegatedClass": "true", 59 | "FFlagLuauForceSimplifyConstraint2": "true", 60 | "FFlagLuauFormatUseLastPosition": "true", 61 | "FFlagLuauFragmentAcMemoryLeak": "true", 62 | "FFlagLuauFragmentAcSupportsReporter": "true", 63 | "FFlagLuauFragmentAutocompleteTakesInnermostRefinement": "true", 64 | "FFlagLuauGeneralizationRemoveRecursiveUpperBound": "false", 65 | "FFlagLuauGeneralizationRemoveRecursiveUpperBound2": "true", 66 | "FFlagLuauGetClassTypeForClassNameDefaults": "true", 67 | "FFlagLuauGlobalVariableModuleIsolation": "true", 68 | "FFlagLuauGuardAgainstMalformedTypeAliasExpansion2": "true", 69 | "FFlagLuauIceLess": "true", 70 | "FFlagLuauImplicitTableIndexerKeys3": "true", 71 | "FFlagLuauImproveTypePathsInErrors": "true", 72 | "FFlagLuauIndexAnyIsAny": "true", 73 | "FFlagLuauIndexDeferPendingIndexee": "true", 74 | "FFlagLuauIndexTypeFunctionFunctionMetamethods": "true", 75 | "FFlagLuauIndexTypeFunctionImprovements": "true", 76 | "FFlagLuauInferActualIfElseExprType2": "true", 77 | "FFlagLuauInferPolarityOfReadWriteProperties": "true", 78 | "FFlagLuauInitializeDefaultGenericParamsAtProgramPoint": "true", 79 | "FFlagLuauInsertErrorTypesIntoIndexerResult": "true", 80 | "FFlagLuauInstantiateResolvedTypeFunctions": "true", 81 | "FFlagLuauIntersectNotNil": "true", 82 | "FFlagLuauLimitDynamicConstraintSolving3": "true", 83 | "FFlagLuauLimitIterationWhenCheckingArgumentCounts": "true", 84 | "FFlagLuauMagicFreezeCheckBlocked2": "true", 85 | "FFlagLuauMetatableTypeFunctions": "true", 86 | "FFlagLuauMissingFollowInAssignIndexConstraint": "true", 87 | "FFlagLuauMissingFollowMappedGenericPacks": "true", 88 | "FFlagLuauMissingSeenSetRelate": "true", 89 | "FFlagLuauMorePreciseExternTableRelation": "true", 90 | "FFlagLuauNativeCodegenBetaFeature": "false", 91 | "FFlagLuauNewNonStrictSuppressSoloConstraintSolvingIncomplete": "true", 92 | "FFlagLuauNewNonStrictVisitTypes": "false", 93 | "FFlagLuauNewNonStrictVisitTypes2": "true", 94 | "FFlagLuauNewNonStrictWarnOnUnknownGlobals": "true", 95 | "FFlagLuauNewSolverPopulateTableLocations": "true", 96 | "FFlagLuauNewSolverPrePopulateClasses": "true", 97 | "FFlagLuauNewSolverVisitErrorExprLvalues": "true", 98 | "FFlagLuauNoConstraintGenRecursionLimitIce": "true", 99 | "FFlagLuauNoMoreInjectiveTypeFunctions": "true", 100 | "FFlagLuauNonReentrantGeneralization2": "false", 101 | "FFlagLuauNonStrictVisitorImprovements": "true", 102 | "FFlagLuauNormalizationCatchMetatableCycles": "true", 103 | "FFlagLuauNormalizationLimitTyvarUnionSize": "true", 104 | "FFlagLuauNormalizationReorderFreeTypeIntersect": "true", 105 | "FFlagLuauNumericUnaryOpsDontProduceNegationRefinements": "true", 106 | "FFlagLuauOccursCheckForRefinement": "true", 107 | "FFlagLuauOptimizeFalsyAndTruthyIntersect": "true", 108 | "FFlagLuauPrecalculateMutatedFreeTypes2": "true", 109 | "FFlagLuauPreprocessTypestatedArgument": "true", 110 | "FFlagLuauPreventReentrantTypeFunctionReduction": "true", 111 | "FFlagLuauPropagateExpectedTypesForCalls": "true", 112 | "FFlagLuauPushFunctionTypesInFunctionStatement": "true", 113 | "FFlagLuauRawGetHandlesNil": "true", 114 | "FFlagLuauReduceCheckBinaryExprStackPressure": "true", 115 | "FFlagLuauReduceSetTypeStackPressure": "true", 116 | "FFlagLuauReduceUnionFollowUnionType": "true", 117 | "FFlagLuauRefineOccursCheckDirectRecursion": "true", 118 | "FFlagLuauRefineTablesWithReadType": "true", 119 | "FFlagLuauRefineWaitForBlockedTypesInTarget": "true", 120 | "FFlagLuauRelateTablesAreNeverDisjoint": "true", 121 | "FFlagLuauRemoveGenericErrorForParams": "true", 122 | "FFlagLuauRemoveNotAnyHack": "true", 123 | "FFlagLuauReportAutocompleteICEToUser": "true", 124 | "FFlagLuauResetConditionalContextProperly": "true", 125 | "FFlagLuauRetrySubtypingWithoutHiddenPack": "true", 126 | "FFlagLuauReturnMappedGenericPacksFromSubtyping": "false", 127 | "FFlagLuauSearchForRefineableType": "true", 128 | "FFlagLuauSimplifyAnyAndUnion": "true", 129 | "FFlagLuauSimplifyOutOfLine2": "true", 130 | "FFlagLuauSimplyRefineNotNil": "true", 131 | "FFlagLuauSkipLvalueForCompoundAssignment": "true", 132 | "FFlagLuauSkipMalformedTypeAliases": "true", 133 | "FFlagLuauSolverAgnosticClone": "true", 134 | "FFlagLuauStuckTypeFunctionsStillDispatch": "true", 135 | "FFlagLuauSubtypeGenericsAndNegations": "true", 136 | "FFlagLuauSubtypingGenericPacksDoesntUseVariance": "true", 137 | "FFlagLuauSuggestHotComments": "true", 138 | "FFlagLuauTableLiteralSubtypeCheckFunctionCalls": "true", 139 | "FFlagLuauTableLiteralSubtypeSpecificCheck2": "true", 140 | "FFlagLuauTrackFreeInteriorTypePacks": "true", 141 | "FFlagLuauTrackInferredFunctionTypeFromCall": "true", 142 | "FFlagLuauTrackInteriorFreeTablesOnScope": "true", 143 | "FFlagLuauTrackInteriorFreeTypesOnScope": "true", 144 | "FFlagLuauTypeFunReadWriteParents": "true", 145 | "FFlagLuauTypestateBuiltins2": "true", 146 | "FFlagLuauUngeneralizedTypesForRecursiveFunctions": "true", 147 | "FFlagLuauUnifyMetatableWithAny": "true", 148 | "FFlagLuauUseOrderedTypeSetsInConstraints": "true", 149 | "FFlagLuauUserTypeFunFixNoReadWrite": "true", 150 | "FFlagLuauWeakNilRefinementType": "true", 151 | "FFlagLuauWriteOnlyPropertyMangling": "true" 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/client-replicator.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client Replicator 3 | description: Client Replicator API Reference 4 | sidebar: 5 | order: 3 6 | --- 7 | 8 | ## Types 9 | 10 | --- 11 | 12 |
13 | 14 | ### `Disconnect` 15 | 16 | ```ts 17 | type Disconnect = () -> () 18 | ``` 19 | 20 |
21 | 22 | ## Methods 23 | 24 | --- 25 | 26 |
27 | 28 | ### `client:init()` 29 | 30 | Initializes the client replicator. World can be provided here 31 | if it wasn't provided when creating the replicator. 32 | 33 | ```ts 34 | type Client = { 35 | init: (self: Client, world: jecs.World?) -> () 36 | } 37 | ``` 38 | 39 |
40 | #### Arguments 41 | - **world**: The world to use for the replicators. Can also be provided when initing the replicator. 42 |
43 | 44 | ### `client:destroy()` 45 | 46 | Destroys the client replicator. This will disconnect jecs hooks and connections. 47 | 48 | ```ts 49 | type Client = { 50 | destroy: (self: Client) -> () 51 | } 52 | ``` 53 | 54 | ### `client:apply_updates()` 55 | 56 | Applies the updates from the server. 57 | 58 | ```ts 59 | type Client = { 60 | apply_updates: (self: Client, buf: buffer, variants: {{any}}) -> () 61 | } 62 | ``` 63 | 64 |
65 | #### Arguments 66 | - **buf**: The buffer containing the updates. 67 | - **variants**: The variants table containing non-serialized values. 68 |
69 | 70 | ### `client:apply_unreliable()` 71 | 72 | Applies the unreliable values from the server. 73 | 74 | ```ts 75 | type Client = { 76 | apply_unreliable: (self: Client, buf: buffer, variants: {{any}}) -> () 77 | } 78 | ``` 79 | 80 |
81 | #### Arguments 82 | - **buf**: The buffer containing the updates. 83 | - **variants**: The variants table containing non-serialized values. 84 |
85 | 86 | ### `client:apply_full()` 87 | 88 | Applies the full state of the world from the server. 89 | 90 | ```ts 91 | type Client = { 92 | apply_full: (self: Client, buf: buffer, variants: {{any}}) -> () 93 | } 94 | ``` 95 | 96 |
97 | #### Arguments 98 | - **buf**: The buffer containing the updates. 99 | - **variants**: The variants table containing non-serialized values. 100 |
101 | 102 | 103 | ### `client:hook()` 104 | 105 | Hooks to a networking process. The arguments depend on the action type 106 | 107 | ```ts 108 | type Client = { 109 | hook: (self: Client, action: "changed", pair: Pair, callback: (entity: Entity, id: Id, value: any) -> ()) -> Disconnect, 110 | hook: (self: Client, action: "removed", pair: Pair, callback: (entity: Entity, id: Id) -> ()) -> Disconnect, 111 | hook: (self: Client, action: "deleted", entity: Entity, callback: (entity: Entity) -> ()) -> Disconnect, 112 | } 113 | ``` 114 | 115 |
116 | #### Arguments 117 | - **action**: The action to hook to. 118 | - **...**: Hook arguments. 119 | #### Returns 120 | - **disconnect**: A function that disconnects the hook when called. 121 |
122 | 123 | 124 | ### `client:override()` 125 | 126 | Special hook that overrides the networking process entirely, allowing you to customize or abort actions. 127 | The usage is identical to `client:hook()`. 128 | 129 | ```ts 130 | type Client = { 131 | override: (self: Client, action: "changed", pair: Pair, callback: (entity: Entity, id: Id, value: any) -> ()) -> Disconnect, 132 | override: (self: Client, action: "removed", pair: Pair, callback: (entity: Entity, id: Id) -> ()) -> Disconnect, 133 | override: (self: Client, action: "deleted", entity: Entity, callback: (entity: Entity) -> ()) -> Disconnect, 134 | } 135 | ``` 136 | 137 | ### `client:added()` 138 | 139 | Hook that gets called when replecs creates a new entity, 140 | this is called right after the entity is created so it will be empty. 141 | Use `client:after_replication()` to wait for any useful info from it. 142 | 143 | This hook is not called if the entity was created with a custom id. 144 | 145 | ```ts 146 | type Client = { 147 | added: (self: Client, callback: (entity: Entity) -> ()) -> Disconnect, 148 | } 149 | ``` 150 | 151 |
152 | #### Arguments 153 | - **callback**: Function that gets called when the entity is created. 154 | #### Returns 155 | - **disconnect**: A function that disconnects the hook when called. 156 |
157 | 158 | 159 | ### `client:get_server_entity()` 160 | 161 | Gets the equivalent server entity for a client entity. 162 | 163 | ```ts 164 | type Client = { 165 | get_server_entity: (self: Client, client_entity: Entity) -> number? 166 | } 167 | ``` 168 | 169 |
170 | #### Arguments 171 | - **entity**: The client entity. 172 | #### Returns 173 | - **server_entity**: The server entity, This is typed a number to avoid using this in the client. 174 |
175 | 176 | ### `client:get_client_entity()` 177 | 178 | Gets the equivalent client entity from a server entity. 179 | 180 | ```ts 181 | type Client = { 182 | get_client_entity: (self: Client, server_entity: number) -> Entity? 183 | } 184 | ``` 185 | 186 |
187 | #### Arguments 188 | - **server_entity**: The server entity. 189 | #### Returns 190 | - **entity**: The client entity. 191 |
192 | 193 | 194 | ### `client:register_entity()` 195 | 196 | Binds a client entity to a server id. This would modify what `get_server_entity` and `get_client_entity` return. 197 | 198 | ```ts 199 | type Client = { 200 | register_entity: (self: Client, entity: Entity, server_entity: number) -> () 201 | } 202 | ``` 203 | 204 |
205 | #### Arguments 206 | - **entity**: The client entity to bind. 207 | - **server_entity**: The server entity to bind. 208 |
209 | 210 | ### `client:unregister_entity()` 211 | 212 | Unbinds a client entity from a server id. This would also cause replecs to recreate a client entity for the server entity, 213 | and can possibly be used for force replecs to re-run custom ids. 214 | 215 | ```ts 216 | type Client = { 217 | unregister_entity: (self: Client, entity: Entity) -> () 218 | } 219 | ``` 220 | 221 |
222 | #### Arguments 223 | - **entity**: The client entity to unbind. 224 |
225 | 226 | ### `client:enconde_component()` 227 | 228 | Encodes a component for sending through the network. This returns a number between 1 to 255. 229 | 230 | ```ts 231 | type Client = { 232 | encode_component: (self: Client, component: Entity) -> number 233 | } 234 | ``` 235 | 236 |
237 | #### Arguments 238 | - **component**: The component to encode. 239 | #### Returns 240 | - **number**: The encoded component. 241 |
242 | 243 | 244 | ### `client:decode_component()` 245 | 246 | Decodes a component from a number. 247 | 248 | ```ts 249 | type Client = { 250 | decode_component: (self: Client, encoded: number) -> Entity 251 | } 252 | ``` 253 | 254 |
255 | #### Arguments 256 | - **number**: The number to decode, This is number from `encode_component`. 257 | #### Returns 258 | - **component**: The decoded component. 259 |
260 | 261 | 262 | ### `client:register_custom_id()` 263 | 264 | Registers a custom id handler. Custom ids can only be used if they are registered. 265 | 266 | 267 | ```ts 268 | type Client = { 269 | register_custom_id: (self: Client, custom_id: CustomId) -> () 270 | } 271 | ``` 272 | 273 | ### `client:generate_handshake()` 274 | 275 | Generates a handshake info. This can be used to verify that the server and client are setup correctly. 276 | Relevant for components with `shared`,`serdes` and `custom_ids`, 277 | as a mistmatch in these between server and client would cause hard to debug issues. 278 | 279 | ```ts 280 | type Client = { 281 | generate_handshake: (self: Client) -> HandshakeInfo 282 | } 283 | ``` 284 | 285 |
286 | #### Returns 287 | - **handshake**: The handshake info. This can be passed to `server:verify_handshake()` 288 |
289 | 290 | 291 | ### `client:verify_handshake()` 292 | 293 | Verifies a handshake info from `server:generate_handshake()`. 294 | 295 | ```ts 296 | type Client = { 297 | verify_handshake: (self: Client, handshake: HandshakeInfo) -> (success: boolean, err: string?) 298 | } 299 | ``` 300 | 301 |
302 | #### Arguments 303 | - **handshake**: The handshake info to verify. 304 | #### Returns 305 | - **success**: Whether the handshake info is valid. 306 | - **err**: The error message if the handshake was not successful. 307 |
308 | 309 | 310 |
311 | -------------------------------------------------------------------------------- /demo/pesde.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by pesde. 2 | # It is not intended for manual editing. 3 | format = 2 4 | name = "pepeeltoro41/replecs_demo" 5 | version = "0.0.1" 6 | target = "roblox" 7 | 8 | [overrides."jabby>jecs"] 9 | wally = "wally#ukendio/jecs" 10 | version = "^0.9.0" 11 | index = "default" 12 | 13 | [graph."0x5eal/unzip@0.2.0 luau".pkg_ref] 14 | ref_ty = "pesde" 15 | index_url = "https://github.com/pesde-pkg/index" 16 | 17 | [graph."0x5eal/unzip@0.2.0 luau".pkg_ref.dependencies] 18 | asciitable = [{ name = "kimpure/asciitable", version = "^0.2.3", index = "https://github.com/pesde-pkg/index" }, "dev"] 19 | dirs = [{ name = "jiwonz/dirs", version = "^0.6.0", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] 20 | frktest = [{ name = "itsfrank/frktest", version = "^0.0.4", index = "https://github.com/pesde-pkg/index" }, "dev"] 21 | luau-lsp = [{ name = "pesde/luau_lsp", version = "=1.53.2", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] 22 | stylua = [{ name = "pesde/stylua", version = "=2.1.0", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] 23 | 24 | [graph."jiwonz/dirs@0.6.0 lune".dependencies] 25 | greentea = ["jiwonz/greentea_luau@0.2.0 luau", "standard"] 26 | pathfs = ["jiwonz/pathfs@0.6.0 lune", "standard"] 27 | 28 | [graph."jiwonz/dirs@0.6.0 lune".pkg_ref] 29 | ref_ty = "pesde" 30 | index_url = "https://github.com/pesde-pkg/index" 31 | 32 | [graph."jiwonz/dirs@0.6.0 lune".pkg_ref.dependencies] 33 | greentea = [{ name = "jiwonz/greentea_luau", version = "^0.2.0", index = "https://github.com/pesde-pkg/index", target = "luau" }, "standard"] 34 | pathfs = [{ name = "jiwonz/pathfs", version = "^0.6.0", index = "https://github.com/pesde-pkg/index" }, "standard"] 35 | 36 | [graph."jiwonz/greentea_luau@0.2.0 luau".pkg_ref] 37 | ref_ty = "pesde" 38 | index_url = "https://github.com/pesde-pkg/index" 39 | 40 | [graph."jiwonz/luau_disk@0.2.0 luau".pkg_ref] 41 | ref_ty = "pesde" 42 | index_url = "https://github.com/pesde-pkg/index" 43 | 44 | [graph."jiwonz/luau_path@0.2.0 luau".dependencies] 45 | luau_disk = ["jiwonz/luau_disk@0.2.0 luau", "standard"] 46 | 47 | [graph."jiwonz/luau_path@0.2.0 luau".pkg_ref] 48 | ref_ty = "pesde" 49 | index_url = "https://github.com/pesde-pkg/index" 50 | 51 | [graph."jiwonz/luau_path@0.2.0 luau".pkg_ref.dependencies] 52 | luau_disk = [{ name = "jiwonz/luau_disk", version = "^0.2.0", index = "https://github.com/pesde-pkg/index" }, "standard"] 53 | 54 | [graph."jiwonz/pathfs@0.6.0 lune".dependencies] 55 | greentea = ["jiwonz/greentea_luau@0.2.0 luau", "standard"] 56 | luau_path = ["jiwonz/luau_path@0.2.0 luau", "standard"] 57 | 58 | [graph."jiwonz/pathfs@0.6.0 lune".pkg_ref] 59 | ref_ty = "pesde" 60 | index_url = "https://github.com/pesde-pkg/index" 61 | 62 | [graph."jiwonz/pathfs@0.6.0 lune".pkg_ref.dependencies] 63 | frktest = [{ name = "itsfrank/frktest", version = "^0.0.4", index = "https://github.com/pesde-pkg/index", target = "luau" }, "dev"] 64 | greentea = [{ name = "jiwonz/greentea_luau", version = "^0.2.0", index = "https://github.com/pesde-pkg/index", target = "luau" }, "standard"] 65 | luau_path = [{ name = "jiwonz/luau_path", version = "^0.2.0", index = "https://github.com/pesde-pkg/index", target = "luau" }, "standard"] 66 | 67 | [graph."lukadev_0/option@1.2.1 lune".pkg_ref] 68 | ref_ty = "pesde" 69 | index_url = "https://github.com/pesde-pkg/index" 70 | 71 | [graph."lukadev_0/result@1.2.1 lune".pkg_ref] 72 | ref_ty = "pesde" 73 | index_url = "https://github.com/pesde-pkg/index" 74 | 75 | [graph."pesde/rojo@7.6.0 lune".dependencies] 76 | option = ["lukadev_0/option@1.2.1 lune", "standard"] 77 | result = ["lukadev_0/result@1.2.1 lune", "standard"] 78 | toolchainlib = ["pesde/toolchainlib@0.2.0 lune", "standard"] 79 | 80 | [graph."pesde/rojo@7.6.0 lune".pkg_ref] 81 | ref_ty = "pesde" 82 | index_url = "https://github.com/pesde-pkg/index" 83 | 84 | [graph."pesde/rojo@7.6.0 lune".pkg_ref.dependencies] 85 | option = [{ name = "lukadev_0/option", version = "^1.2.1", index = "https://github.com/pesde-pkg/index" }, "standard"] 86 | result = [{ name = "lukadev_0/result", version = "^1.2.1", index = "https://github.com/pesde-pkg/index" }, "standard"] 87 | toolchainlib = [{ name = "pesde/toolchainlib", version = "^0.2.0", index = "https://github.com/pesde-pkg/index", target = "lune" }, "standard"] 88 | 89 | [graph."pesde/scripts_core@0.2.0 lune".pkg_ref] 90 | ref_ty = "pesde" 91 | index_url = "https://github.com/pesde-pkg/index" 92 | 93 | [graph."pesde/scripts_core@0.2.0 lune".pkg_ref.dependencies] 94 | argon = [{ name = "pesde/argon", version = "^2.0.26", index = "https://github.com/pesde-pkg/index" }, "dev"] 95 | frktest = [{ name = "itsfrank/frktest", version = "^0.0.4", index = "https://github.com/pesde-pkg/index", target = "luau" }, "dev"] 96 | luau-lsp = [{ name = "pesde/luau_lsp", version = "^1.53.5", index = "https://github.com/pesde-pkg/index" }, "dev"] 97 | pathfs = [{ name = "jiwonz/pathfs", version = "^0.6.0", index = "https://github.com/pesde-pkg/index" }, "dev"] 98 | rojo = [{ name = "pesde/rojo", version = "^7.5.1", index = "https://github.com/pesde-pkg/index" }, "dev"] 99 | stylua = [{ name = "pesde/stylua", version = "^2.2.0", index = "https://github.com/pesde-pkg/index" }, "dev"] 100 | 101 | [graph."pesde/scripts_rojo@0.2.0 lune"] 102 | direct = ["scripts", { name = "pesde/scripts_rojo", version = "^0.2.0", index = "default", target = "lune" }, "dev"] 103 | 104 | [graph."pesde/scripts_rojo@0.2.0 lune".dependencies] 105 | rojo = ["pesde/rojo@7.6.0 lune", "peer"] 106 | scripts_core = ["pesde/scripts_core@0.2.0 lune", "standard"] 107 | 108 | [graph."pesde/scripts_rojo@0.2.0 lune".pkg_ref] 109 | ref_ty = "pesde" 110 | index_url = "https://github.com/pesde-pkg/index" 111 | 112 | [graph."pesde/scripts_rojo@0.2.0 lune".pkg_ref.dependencies] 113 | rojo = [{ name = "pesde/rojo", version = "^7.5.1", index = "https://github.com/pesde-pkg/index" }, "peer"] 114 | scripts_core = [{ name = "pesde/scripts_core", version = "^0.2.0", index = "https://github.com/pesde-pkg/index", target = "lune" }, "standard"] 115 | 116 | [graph."pesde/toolchainlib@0.2.0 lune".dependencies] 117 | dirs = ["jiwonz/dirs@0.6.0 lune", "standard"] 118 | option = ["lukadev_0/option@1.2.1 lune", "peer"] 119 | pathfs = ["jiwonz/pathfs@0.6.0 lune", "standard"] 120 | result = ["lukadev_0/result@1.2.1 lune", "peer"] 121 | unzip = ["0x5eal/unzip@0.2.0 luau", "standard"] 122 | 123 | [graph."pesde/toolchainlib@0.2.0 lune".pkg_ref] 124 | ref_ty = "pesde" 125 | index_url = "https://github.com/pesde-pkg/index" 126 | 127 | [graph."pesde/toolchainlib@0.2.0 lune".pkg_ref.dependencies] 128 | dirs = [{ name = "jiwonz/dirs", version = "^0.6.0", index = "https://github.com/pesde-pkg/index" }, "standard"] 129 | option = [{ name = "lukadev_0/option", version = "^1.2.1", index = "https://github.com/pesde-pkg/index" }, "peer"] 130 | pathfs = [{ name = "jiwonz/pathfs", version = "^0.6.0", index = "https://github.com/pesde-pkg/index" }, "standard"] 131 | result = [{ name = "lukadev_0/result", version = "^1.2.1", index = "https://github.com/pesde-pkg/index" }, "peer"] 132 | unzip = [{ name = "0x5eal/unzip", version = "^0.2.0", index = "https://github.com/pesde-pkg/index", target = "luau" }, "standard"] 133 | 134 | [graph."wally#alicesaidhi/jabby@0.3.1 roblox"] 135 | direct = ["jabby", { wally = "wally#alicesaidhi/jabby", version = "^0.3.1", index = "default" }, "standard"] 136 | 137 | [graph."wally#alicesaidhi/jabby@0.3.1 roblox".dependencies] 138 | jecs = ["wally#ukendio/jecs@0.9.0 roblox", "standard"] 139 | pebble = ["wally#alicesaidhi/pebble@0.1.2 roblox", "standard"] 140 | vide = ["wally#centau/vide@0.3.1 roblox", "standard"] 141 | 142 | [graph."wally#alicesaidhi/jabby@0.3.1 roblox".pkg_ref] 143 | ref_ty = "wally" 144 | index_url = "https://github.com/UpliftGames/wally-index" 145 | 146 | [graph."wally#alicesaidhi/jabby@0.3.1 roblox".pkg_ref.dependencies] 147 | jecs = [{ wally = "wally#ukendio/jecs", version = ">=0.8.1, <0.9.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"] 148 | pebble = [{ wally = "wally#alicesaidhi/pebble", version = ">=0.1.2, <0.2.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"] 149 | vide = [{ wally = "wally#centau/vide", version = ">=0.3.1, <0.4.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"] 150 | 151 | [graph."wally#alicesaidhi/pebble@0.1.2 roblox".dependencies] 152 | vide = ["wally#centau/vide@0.3.1 roblox", "standard"] 153 | 154 | [graph."wally#alicesaidhi/pebble@0.1.2 roblox".pkg_ref] 155 | ref_ty = "wally" 156 | index_url = "https://github.com/UpliftGames/wally-index" 157 | 158 | [graph."wally#alicesaidhi/pebble@0.1.2 roblox".pkg_ref.dependencies] 159 | vide = [{ wally = "wally#centau/vide", version = ">=0.3.1, <0.4.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"] 160 | 161 | [graph."wally#centau/vide@0.3.1 roblox".pkg_ref] 162 | ref_ty = "wally" 163 | index_url = "https://github.com/UpliftGames/wally-index" 164 | 165 | [graph."wally#ukendio/jecs@0.9.0 roblox"] 166 | direct = ["jecs", { wally = "wally#ukendio/jecs", version = "^0.9.0", index = "default" }, "standard"] 167 | 168 | [graph."wally#ukendio/jecs@0.9.0 roblox".pkg_ref] 169 | ref_ty = "wally" 170 | index_url = "https://github.com/UpliftGames/wally-index" 171 | -------------------------------------------------------------------------------- /docs/src/content/docs/started/summary.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Summary 3 | sidebar: 4 | order: 4 5 | --- 6 | 7 | import { FileTree, Steps, Code, Tabs, TabItem } from '@astrojs/starlight/components'; 8 | 9 | This page will summarize the previous guides and create a basic project setup for replecs. 10 | 11 | 12 | 13 | 14 | 1. Create your world and require replecs. This is to avoid creating a world [before requiring replecs](/replecs/started/setting-up#providing-your-world). 15 | 16 | - src 17 | - shared 18 | - **world.lua** 19 | 20 | 21 | 22 | 23 | ```luau 24 | local jecs = require("@pkg/jecs") 25 | require("@pkg/replecs") 26 | 27 | return jecs.world() 28 | ``` 29 | 30 | 31 | --- 32 | 33 | 2. Create a `Replicator` in shared and re-export the `client` and `server` entries: 34 | 35 | 36 | - src 37 | - client 38 | - **replicator.lua** 39 | - server 40 | - **replicator.lua** 41 | - shared 42 | - **replicator.lua** 43 | - world.lua 44 | 45 | 46 | 47 | 48 | ```luau 49 | local replecs = require("@pkg/replecs") 50 | local world = require("@shared/world") 51 | 52 | return replecs.create(world) 53 | ``` 54 | 55 | 56 | ```luau 57 | local replicator = require("@shared/replicator") 58 | 59 | return replicator.server 60 | ``` 61 | 62 | 63 | ```luau 64 | local replicator = require("@shared/replicator") 65 | 66 | return replicator.client 67 | ``` 68 | 69 | 70 | --- 71 | 72 | 3. Create your components and mark them as shared: 73 | 74 | 75 | - src 76 | - client 77 | - replicator.lua 78 | - server 79 | - replicator.lua 80 | - shared 81 | - **components.lua** 82 | - replicator.lua 83 | - world.lua 84 | 85 | 86 | 87 | 88 | ```luau 89 | local jecs = require("@pkg/jecs") 90 | local replecs = require("@pkg/replecs") 91 | local world = require("@shared/world") 92 | 93 | local components = { 94 | position = world:component(), 95 | velocity = world:component(), 96 | player = world:component(), 97 | health = world:component(), 98 | alive = world:tag(), 99 | } 100 | 101 | for name, component in components do 102 | world:add(component, replecs.shared) 103 | world:set(component, jecs.Name, name) 104 | end 105 | ``` 106 | 107 | 108 | --- 109 | 110 | 4. Initialize replicators and get the full state of the world: 111 | 112 | 113 | - src 114 | - client 115 | - replicator.lua 116 | - **init.client.lua** 117 | - server 118 | - replicator.lua 119 | - **init.server.lua** 120 | - shared 121 | - components.lua 122 | - replicator.lua 123 | - world.lua 124 | 125 | 126 | 127 | 128 | ```luau 129 | local replicator = require("@server/replicator") 130 | local remotes_server = require("@shared/remotes").server 131 | 132 | replicator:init() 133 | 134 | remotes_server.receive_full:set_callback(function(player) 135 | replicator:mark_player_ready(player) 136 | return replicator:get_full(player) 137 | end) 138 | ``` 139 | 140 | 141 | ```luau 142 | local replicator = require("@client/replicator") 143 | local remotes_client = require("@shared/remotes").client 144 | 145 | replicator:init() 146 | 147 | local buf, variants = remotes_client.receive_full:invoke_server() 148 | replicator:apply_full(buf, variants) 149 | ``` 150 | 151 | 152 | 153 | 5. Create systems to send updates and unreliable values: 154 | 155 | 156 | - src 157 | - client 158 | - systems 159 | - **replecs-client.lua** 160 | - replicator.lua 161 | - server 162 | - systems 163 | - **replecs-server.lua** 164 | - replicator.lua 165 | - shared 166 | - utils 167 | - **collect.lua** 168 | - **interval.lua** 169 | - components.lua 170 | - replicator.lua 171 | - world.lua 172 | 173 | 174 | 175 | 176 | ```luau 177 | local replicator = require("@server/replicator") 178 | local interval = require("@utils/interval") 179 | 180 | local updates_interval = interval(1 / 20) 181 | local unreliables_interval = interval(1 / 30) 182 | 183 | function replecs_server() 184 | if updates_interval() then 185 | for player, buf, variants in replicator:collect_updates() do 186 | remotes_server.send_updates:fire(player, buf, variants) 187 | end 188 | end 189 | 190 | if unreliables_interval() then 191 | for player, buf, variants in replicator:collect_unreliable() do 192 | remotes_server.send_unreliables:fire(player, buf, variants) 193 | end 194 | end 195 | end 196 | 197 | return replecs_server 198 | ``` 199 | 200 | 201 | ```luau 202 | local replicator = require("@client/replicator") 203 | local collect = require("@utils/collect") 204 | 205 | local updates = collect(remotes_client.send_updates) 206 | local unreliables = collect(remotes_client.send_unreliables) 207 | 208 | function replecs_client() 209 | for buf, variants in updates do 210 | replicator:apply_updates(buf, variants) 211 | end 212 | for buf, variants in unreliables do 213 | replicator:apply_unreliable(buf, variants) 214 | end 215 | end 216 | 217 | return replecs_client 218 | ``` 219 | 220 | 221 | ```luau 222 | local function collect(event) 223 | local storage = {} 224 | local mt = {} 225 | local iter = function() 226 | local n = #storage 227 | return function() 228 | if n <= 0 then 229 | mt.__iter = nil 230 | return nil 231 | end 232 | 233 | n -= 1 234 | return n + 1, unpack(table.remove(storage, 1) :: any) 235 | end 236 | end 237 | 238 | if type(event) == "function" then 239 | event(function(...) 240 | table.insert(storage, { ... }) 241 | mt.__iter = iter 242 | end) 243 | else 244 | event:Connect(function(...) 245 | table.insert(storage, { ... }) 246 | mt.__iter = iter 247 | end) 248 | end 249 | 250 | setmetatable(storage, mt) 251 | return (storage :: any) :: () -> (number, ...any) 252 | end 253 | 254 | return collect 255 | ``` 256 | 257 | 258 | ```luau 259 | local function interval(s: number) 260 | local pin: number = nil :: any 261 | 262 | local function throttle() 263 | if not pin then 264 | pin = os.clock() 265 | end 266 | 267 | local elapsed = os.clock() - pin > s 268 | if elapsed then 269 | pin = os.clock() 270 | end 271 | 272 | return elapsed 273 | end 274 | return throttle 275 | end 276 | 277 | return interval 278 | ``` 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/entity-remapping.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Entity Remapping 3 | sidebar: 4 | order: 5 5 | --- 6 | import { Card, Aside } from '@astrojs/starlight/components'; 7 | 8 | Replecs will automatically create entities from the server when they dont exist 9 | and have a lookup table to translate server entities to client entities. 10 | 11 | But there will be cases where you already created the equivalent entity in the client separately, 12 | and you want to tell replecs to use that entity for the server entity. Let's see how we can do this: 13 | 14 | ## Custom Ids 15 | 16 |
17 | Customs Ids will let you get information about the entity the server is trying to create. And determine which entity to use. 18 | 19 | There are two ways you can use custom ids: 20 | 21 | ### Using a component value. 22 | 23 | If you can use a component value to determine which entity to use, you can set a custom id with the component. 24 | 25 | - You can use `server_replicator:set_custom(entity, component)` to set a custom id for an entity. 26 | 27 | - You can also add a pair `replecs.custom` with the component as the target. 28 | 29 | ```luau 30 | local Players = game:GetService("Players") 31 | local replicator = require("@server/replicator") 32 | local components = require("@shared/components") 33 | 34 | local player_entity = world:entity() 35 | world:add(player_entity, replecs.networked) 36 | world:set(player_entity, components.player, Players.Player1) 37 | 38 | -- this will use the value of player_component in the client 39 | replicator:set_custom(player_entity, components.player) 40 | 41 | -- or you can alternatively use a pair 42 | world:add(player_entity, jecs.pair(replecs.custom, components.player)) 43 | ``` 44 | 45 | 49 | 50 | --- 51 | 52 | Now, we can set a handler using `replecs.custom_handler` in the client for the `player` component. 53 | This function will receive the value of the component and return the client entity. 54 | 55 | ```luau 56 | local components = require("@shared/components") 57 | 58 | local player_refs: {[Player]: Entity} = {} 59 | 60 | local player_entity = world:entity() 61 | player_refs[Players.Player1] = player_entity 62 | world:set(player, components.player, Players.Player1) 63 | 64 | world:set(components.player, replecs.custom_handler, function(player: Player) 65 | return player_refs[player] 66 | end) 67 | ``` 68 | 69 | 92 | 93 | ### Complex Custom Ids 94 | 95 | If you need more complex logic or more information about the entity, you can use a custom parser. 96 | This will allow you to get any information about the entity its trying to create, 97 | as well as information about other entities. 98 | 99 | 100 | To start you can create a custom handler by caling `replecs.create_custom_id(identifier, handler)`. 101 | 102 | - The first argument is the identifier. This is a string that will be used so the client and server can agree on what to use. 103 | - The second argument is the handler callback. This will be called in the client when the entity is trying to be created. 104 | 105 | The handler callback will receive a `HandleContext` object. This can be used to get entity information. 106 | You can find all the functions in the [API Reference](/replecs/reference/custom-ids#replecshandlecontext). 107 | 108 | After that, you need to register the custom id in both the client and server by calling `replicator:register_custom_id(custom_id)`. 109 | The shared replicator has a method that will call `replicator:register_custom_id` for both the server and client. 110 | 111 | ```luau 112 | local new_custom = replecs.create_custom_id("identifier") 113 | 114 | new_custom:handler(function(ctx) 115 | -- do something (called in server) 116 | print(ctx.component(c.foo)) 117 | print(ctx.target(c.bar)) 118 | print(ctx.entity(some_server_entity)) 119 | end) 120 | 121 | -- register for both the server and client 122 | replicator:register_custom_id(new_custom) 123 | 124 | --- server 125 | local entity = world:entity() 126 | server_replicator:set_custom(entity, new_custom) 127 | ``` 128 | 129 | 138 | 139 | --- 140 | 141 | #### Cancelling Custom Ids 142 | 143 | Replecs supports cancelling the creation of a custom id. You can do this by returning `nil` from the handler. 144 | This is not recommended, and if you use it, its worth nothing: 145 | 146 | - This will cause ctx.target() and ctx.entity() from other custom id to return nil 147 | if the requested entity aborted the handler. This is not reflected in the types for simplicity. You can return `nil` 148 | again to abort the current one too. 149 | 150 | - The custom handler will be called again if replecs needs the entity again for applying server updates. 151 | 152 | Here's an example of how to use custom ids to create a building prediction system. 153 | This will create a building entity instantly in the client without waiting for the server. 154 | This makes the game feel more responsive. 155 | 156 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 157 | 158 | 159 | 160 | ```luau 161 | local replecs = require("@pkg/replecs") 162 | local world = require("@shared/world") 163 | local replicator = require("@shared/replicator") 164 | local c = require("@shared/components") 165 | 166 | local building_custom = replecs.create_custom_id("building-prediction") 167 | replicator:register_custom_id(building_custom) 168 | 169 | building_custom:handler(function(ctx) 170 | for building, building_uuid in world:query(c.building_uuid):with(c.predicted) do 171 | if building_uuid == ctx.component(c.building_uuid) then 172 | -- building is no longer predicted 173 | world:remove(building, c.predicted) 174 | return building 175 | end 176 | end 177 | 178 | -- default to normal behaviour 179 | return world:entity() 180 | end) 181 | 182 | return building_custom 183 | ``` 184 | 185 | 186 | ```luau 187 | local HttpService = game:GetService("HttpService") 188 | 189 | local building_custom = require("@shared/custom/building") 190 | 191 | local c = require("@shared/components") 192 | local world = require("@shared/world") 193 | local replicator = require("@server/replicator") 194 | local remotes = require("@shared/remotes") 195 | 196 | function create_building(player: Player, transform: CFrame) 197 | local building_uuid = HttpService:GenerateGUID(false) 198 | 199 | local building = world:entity() 200 | world:set(building, c.building_uuid, building_uuid) 201 | world:set(building, c.transform, transform) 202 | 203 | replicator:set_networked(building) 204 | replicator:set_reliable(building, c.transform, transform) 205 | replicator:set_reliable(building, c.building_uuid) 206 | 207 | -- setting custom id 208 | replicator:set_custom(building, building_custom) 209 | 210 | -- return the building uuid so the client can know which server entity to use 211 | return building_uuid 212 | end 213 | 214 | remotes.create_building:set_callback(create_building) 215 | ``` 216 | 217 | 218 | ```luau 219 | local world = require("@shared/world") 220 | local c = require("@shared/components") 221 | local remotes = require("@shared/remotes") 222 | local replicator = require("@client/replicator") 223 | 224 | -- called when trying to add a building 225 | function build(building: Model, transform: CFrame) 226 | local predicted_building = world:entity() 227 | world:set(predicted_building, c.transform, transform) 228 | world:set(predicted_building, c.model, building:Clone()) 229 | world:add(predicted_building, c.predicted) 230 | 231 | -- this is to hold the predicted building until the server confirms it 232 | world:add(predicted_building, c.prediction_active) 233 | 234 | task.spawn(function() 235 | local building_uuid = remotes.create_building:invoke_server(transform) 236 | if building_uuid then 237 | world:set(predicted_building, c.building_uuid, building_uuid) 238 | end 239 | 240 | world:remove(predicted_building, c.prediction_active) 241 | end) 242 | end 243 | 244 | function delete_predicted() 245 | for building in world:query(c.predicted):without(c.prediction_active) do 246 | world:delete(building) 247 | end 248 | end 249 | 250 | local updates = collect(remotes.send_updates) 251 | 252 | function replecs_client() 253 | for buf, variants in updates do 254 | replicator:apply_updates(buf, variants) 255 | -- if there was a predicted building, the custom id should've claimed it 256 | -- delete what wasn't claimed 257 | delete_predicted() 258 | end 259 | end 260 | 261 | system(replecs_client) 262 | ``` 263 | 264 | 265 | 266 |
267 | 268 | ## Global Ids 269 | 270 | In essence, global ids are a worse version of custom ids. These are entities that get marked with a single number from `1 - 245`. 271 | There is no extra data associated like a component in Custom Ids. 272 | 273 | The reason why they exist is because they are a lot more bandwidth efficient. 274 | They can be represented with a single byte rather than `6 bytes` for normal entities. Making it a good fit for cases where bandwidth is crucial. 275 | 276 | If packet size is not a concern, custom ids should be **always** prefered. Its the responsability of the user to give a meaning to the number 277 | 278 | To use global ids you need to set `replecs.global` to the entity with the number you want to use. 279 | 280 | ```luau 281 | local replecs = require("@pkg/replecs") 282 | 283 | local entity = world:entity() 284 | world:set(entity, replecs.global, 1) 285 | ``` 286 | 287 | In the client, you set a handler globally by doing `client_replicator:handle_global(handler)`. 288 | 289 | This handler will receive the number, and should return an entity. 290 | 291 | ```luau 292 | local entity = world:entity() 293 | 294 | client_replicator:handle_global(function(id: number) 295 | if id == 1 then 296 | return entity 297 | else 298 | error("invalid global id") 299 | end 300 | end) --------------------------------------------------------------------------------