From 0a1a7c1955a5298ece6c39e048f55529243fd937 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 29 Jul 2024 00:32:23 +0200 Subject: [PATCH] Initial commit --- example.project.json | 71 +++++++ example/.gitignore | 6 + example/README.md | 17 ++ example/src/client/init.client.luau | 1 + example/src/server/init.server.luau | 1 + example/src/shared/common.luau | 277 ++++++++++++++++++++++++++++ src/init.luau | 2 +- test/tests.luau | 8 +- 8 files changed, 378 insertions(+), 5 deletions(-) create mode 100644 example.project.json create mode 100644 example/.gitignore create mode 100644 example/README.md create mode 100644 example/src/client/init.client.luau create mode 100644 example/src/server/init.server.luau create mode 100644 example/src/shared/common.luau diff --git a/example.project.json b/example.project.json new file mode 100644 index 0000000..c3c66c8 --- /dev/null +++ b/example.project.json @@ -0,0 +1,71 @@ +{ + "name": "example", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "Shared": { + "$path": "example/src/shared" + }, + "ecs": { + "$path": "src" + } + }, + "ServerScriptService": { + "Server": { + "$path": "example/src/server" + } + }, + "StarterPlayer": { + "StarterPlayerScripts": { + "Client": { + "$path": "example/src/client" + } + } + }, + "Workspace": { + "$properties": { + "FilteringEnabled": true + }, + "Baseplate": { + "$className": "Part", + "$properties": { + "Anchored": true, + "Color": [ + 0.38823, + 0.37254, + 0.38823 + ], + "Locked": true, + "Position": [ + 0, + -10, + 0 + ], + "Size": [ + 512, + 20, + 512 + ] + } + } + }, + "Lighting": { + "$properties": { + "Ambient": [ + 0, + 0, + 0 + ], + "Brightness": 2, + "GlobalShadows": true, + "Outlines": false, + "Technology": "Voxel" + } + }, + "SoundService": { + "$properties": { + "RespectFilteringEnabled": true + } + } + } +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..cf9d94d --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,6 @@ +# Project place file +/example.rbxlx + +# Roblox Studio lock files +/*.rbxlx.lock +/*.rbxl.lock \ No newline at end of file diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..5223a3e --- /dev/null +++ b/example/README.md @@ -0,0 +1,17 @@ +# example +Generated by [Rojo](https://github.com/rojo-rbx/rojo) 7.4.1. + +## Getting Started +To build the place from scratch, use: + +```bash +rojo build -o "example.rbxlx" +``` + +Next, open `example.rbxlx` in Roblox Studio and start the Rojo server: + +```bash +rojo serve +``` + +For more help, check out [the Rojo documentation](https://rojo.space/docs). \ No newline at end of file diff --git a/example/src/client/init.client.luau b/example/src/client/init.client.luau new file mode 100644 index 0000000..505f71c --- /dev/null +++ b/example/src/client/init.client.luau @@ -0,0 +1 @@ +print("Hello world, from client!") \ No newline at end of file diff --git a/example/src/server/init.server.luau b/example/src/server/init.server.luau new file mode 100644 index 0000000..feebef1 --- /dev/null +++ b/example/src/server/init.server.luau @@ -0,0 +1 @@ +print("Hello world, from server!") \ No newline at end of file diff --git a/example/src/shared/common.luau b/example/src/shared/common.luau new file mode 100644 index 0000000..1842550 --- /dev/null +++ b/example/src/shared/common.luau @@ -0,0 +1,277 @@ +--!optimize 2 +--!native + +local jecs = require(game:GetService("ReplicatedStorage").ecs) + +type World = jecs.WorldShim +type Entity = jecs.Entity + +local function LOOP_ERROR(str) + -- We don't want to interrupt the loop when we error + task.spawn(error, str) +end + +local Scheduler: (World, ...ModuleScript) -> (number) -> () +do + local systems + local N + local w + local dt + local systemsNames + + local function run(system) + local name = systemsNames[system] + + return function() + debug.profilebegin(name) + debug.setmemorycategory(name) + system(w, dt) + debug.profileend() + end + end + + local function loop(sinceLastFrame) + debug.profilebegin("loop()") + + for i = N, 1, -1 do + local system = systems[i] + + dt = sinceLastFrame + + local didNotYield, why = xpcall(function() + for _ in run(system) do end + end, debug.traceback) + + if didNotYield then + continue + end + + if string.find(why, "thread is not yieldable") then + N -= 1 + table.remove(systems, i) + LOOP_ERROR("Not allowed to yield in the systems." + .. "\n" + .. "System: " + .. debug.info(system, "n") + .. " has been ejected" + ) + continue + end + LOOP_ERROR(why) + end + + debug.profileend() + debug.resetmemorycategory() + end + + function Scheduler(world, ...) + systems = { ... } + systemsNames = {} + N = #systems + w = world + + for i, system in systems do + systems[i] = require(system) + systemsNames[system] = debug.info(system, "n") + end + + return loop + end +end + +type Tracker = { track: (world: World, fn: (changes: { + added: () -> () -> (number, T), + removed: () -> () -> number, + changed: () -> () -> (number, T, T) + }) -> ()) -> () +} +local ChangeTracker: (world: any, component: Entity) -> Tracker + +do + local world: World + local T + local PreviousT + local addedComponents + local removedComponents + local isTrivial + local added + local removed + + local function changes_added() + added = true + local q = world:query(T):without(PreviousT) + return function() + local id, data = q:next() + if not id then + return nil + end + + if isTrivial == nil then + isTrivial = typeof(data) ~= "table" + end + + if not isTrivial then + data = table.clone(data) + end + + addedComponents[id] = data + return id, data + end + end + + local function shallowEq(a, b) + for k, v in a do + if b[k] ~= v then + return false + end + end + return true + end + + local function changes_changed() + local q = world:query(T, PreviousT) + + return function() + local id, new, old = q:next() + while true do + if not id then + return nil + end + + if not isTrivial then + if not shallowEq(new, old) then + break + end + elseif new ~= old then + break + end + + id, new, old = q:next() + end + + addedComponents[id] = new + + return id, old, new + end + end + + local function changes_removed() + removed = true + + local q = world:query(PreviousT):without(T) + return function() + local id = q:next() + if id then + table.insert(removedComponents, id) + end + return id + end + end + + local changes = { + added = changes_added, + changed = changes_changed, + removed = changes_removed, + } + + local function track(fn) + added = true + removed = true + + fn(changes) + + if not added then + for _ in changes_added() do + end + end + + if not removed then + for _ in changes_removed() do + end + end + + for e, data in addedComponents do + world:set(e, PreviousT, if isTrivial then data else table.clone(data)) + end + + for _, e in removedComponents do + world:remove(e, PreviousT) + end + end + + local tracker = { track = track } + + function ChangeTracker(worldToTrack: World, component: Entity): Tracker + world = worldToTrack + T = component + -- We just use jecs.Rest because people will probably not use it anyways + PreviousT = jecs.pair(jecs.Rest, T) + addedComponents = {} + removedComponents = {} + + return tracker + end +end + +local Allocator: (World, Entity, (T) -> ()) -> { alloc: (Entity) -> (), free: (Entity) -> (), deinit: () -> () } + +do + local arena: {} + local world + local cleanup + local id + + local function alloc(entity: Entity) + table.insert(arena, entity) + end + + local function deinit() + for _, e in arena do + cleanup(world:get(e, id), e) + world:clear(e) + end + end + + local function free(entity: Entity) + local e = table.remove(arena, table.find(arena, entity)) + if e then + cleanup(world:get(e, id), e) + world:clear(e) + end + end + + local handle = { + alloc = alloc, + deinit = deinit, + free = free, + } + + setmetatable(handle, handle) + + function Allocator(w: World, T: Entity, fn: (T) -> ()) + arena = {} + world = w + cleanup = fn + id = T + + return handle + end +end + +local world = jecs.World.new() +local Model = world:component() :: Entity + +local ModelAllocator = Allocator(world, + Model, function(model) model:Destroy() end) + +local e = world:entity() +world:set(e, Model, Instance.new("Model")) + +ModelAllocator.alloc(e) +ModelAllocator.free(e) + +return { + Scheduler = Scheduler, + ChangeTracker = ChangeTracker, + Allocator = Allocator +} diff --git a/src/init.luau b/src/init.luau index a497ebe..9b16140 100644 --- a/src/init.luau +++ b/src/init.luau @@ -1207,7 +1207,7 @@ return { Component = EcsComponent, Wildcard = EcsWildcard :: Entity, w = EcsWildcard :: Entity, - Rest = EcsRest, + Rest = EcsRest :: Entity, pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> number, diff --git a/test/tests.luau b/test/tests.luau index 56c6779..94fc6ad 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -594,10 +594,10 @@ do end if is_trivial == nil then - isTrivial = typeof(data) ~= "table" + is_trivial = typeof(data) ~= "table" end - if not isTrivial then + if not is_trivial then data = table.clone(data) end @@ -635,7 +635,7 @@ do return nil end - if isTrivial and new ~= old then + if is_trivial and new ~= old then elseif diff(new, old) then break end @@ -685,7 +685,7 @@ do end for e, data in add do - world:set(e, PreviousT, if isTrivial then data else table.clone(data)) + world:set(e, PreviousT, if is_trivial then data else table.clone(data)) end end