From cab13e42138103c907024a5508911b86d5cc9d35 Mon Sep 17 00:00:00 2001 From: Mark Marks Date: Tue, 24 Sep 2024 15:35:21 +0200 Subject: [PATCH] chore: Make jecs a dependency, bump jecs to v0.3.0 + Make jecs a wally dependency instead of a file in lib. This wasn't done earlier due to wally packages not being able to be used outside of Roblox without dynamically requiring them. Now, a script is used to download the latest source of jecsfrom Github associated with the version in the manifest. + Bump jecs-utils to v0.1.3 and release --- .darklua.json | 2 +- .github/workflows/ci.yml | 9 + .gitignore | 4 + .luaurc | 2 +- .lune/analyze.luau | 3 +- .lune/build.luau | 1 + .lune/check.luau | 1 + .lune/dev.luau | 8 +- .lune/dist.luau | 1 + .lune/download-jecs.luau | 67 ++ .lune/util/watch.luau | 14 +- .zed/settings.json | 2 +- dev.project.json | 1 + lib/command_buffer.luau | 2 +- lib/handle.luau | 2 +- lib/init.luau | 2 +- lib/jecs.luau | 1894 -------------------------------------- lib/ref.luau | 4 +- lib/replicator.luau | 2 +- lib/world.luau | 2 +- luau_lsp_settings.json | 5 +- test/tests.luau | 2 +- wally.toml | 3 +- 23 files changed, 117 insertions(+), 1916 deletions(-) create mode 100644 .lune/download-jecs.luau delete mode 100644 lib/jecs.luau diff --git a/.darklua.json b/.darklua.json index 1630968..970498b 100644 --- a/.darklua.json +++ b/.darklua.json @@ -5,7 +5,7 @@ "current": { "name": "path", "sources": { - "@jecs": "lib/jecs" + "@jecs": "Packages/jecs" } }, "target": { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 279db27..ce00454 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,15 @@ jobs: - name: Install Luau uses: encodedvenom/install-luau@v2.1 + - name: Install Rokit + uses: CompeyDev/setup-rokit@v0.1.2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download Jecs + run: | + lune run download-jecs + - name: Run Unit Tests run: | output=$(luau test/tests.luau) diff --git a/.gitignore b/.gitignore index a2ce70d..cdd52d9 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ WallyPatches roblox.toml sourcemap.json globalTypes.d.luau +# Used for testing, unfortunately we can't just install it with wally and expect it to work in vanilla luau +jecs.luau +# Stores Github PAT +.env diff --git a/.luaurc b/.luaurc index 6809eec..332de00 100644 --- a/.luaurc +++ b/.luaurc @@ -3,6 +3,6 @@ "aliases": { "jecs_utils": "lib", "testkit": "test/testkit", - "jecs": "lib/jecs" + "jecs": "jecs" } } diff --git a/.lune/analyze.luau b/.lune/analyze.luau index 3dffcb5..6f884d6 100644 --- a/.lune/analyze.luau +++ b/.lune/analyze.luau @@ -1,7 +1,8 @@ --!strict local spawn = require("util/spawn") +spawn.start("lune run install-packages") spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") spawn.start( - "luau-lsp analyze --base-luaurc=.luaurc --sourcemap=sourcemap.json --settings=luau_lsp_settings.json --no-strict-dm-types --ignore Packages/**/*.lua --ignore Packages/**/*.luau --ignore lib/jecs.luau lib/" + "luau-lsp analyze --base-luaurc=.luaurc --sourcemap=sourcemap.json --settings=luau_lsp_settings.json --no-strict-dm-types --ignore Packages/**/*.lua --ignore Packages/**/*.luau --ignore Packages/*.lua --ignore Packages/*.luau lib/" ) diff --git a/.lune/build.luau b/.lune/build.luau index 6bfc7a7..8be60df 100644 --- a/.lune/build.luau +++ b/.lune/build.luau @@ -1,6 +1,7 @@ --!strict local spawn = require("util/spawn") +spawn.start("lune run install-packages") spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") spawn.start("darklua process --config .darklua.json lib/ dist/", { env = { ROBLOX_DEV = "false" } }) spawn.start("rojo build default.project.json -o build.rbxm") diff --git a/.lune/check.luau b/.lune/check.luau index e4dc495..61712a2 100644 --- a/.lune/check.luau +++ b/.lune/check.luau @@ -4,4 +4,5 @@ local spawn = require("util/spawn") spawn.start("lune run analyze") spawn.start("stylua lib/") spawn.start("selene lib/") +spawn.start("lune run download-jecs") spawn.start("luau test/tests.luau") diff --git a/.lune/dev.luau b/.lune/dev.luau index b135888..188dffd 100644 --- a/.lune/dev.luau +++ b/.lune/dev.luau @@ -4,11 +4,17 @@ local stdio = require("@lune/stdio") local task = require("@lune/task") local spawn = require("util/spawn") +local watch = require("util/watch") + +task.spawn(watch, "wally.toml", function() + spawn.spawn("lune run install-packages") +end, false) +spawn.start("lune run install-packages") spawn.spawn("rojo sourcemap dev.project.json -o sourcemap.json --watch") spawn.spawn("darklua process --config .darklua.json --watch lib/ dist/", { env = { ROBLOX_DEV = "true" } }) -task.wait(1) +task.wait(2.5) while true do local start_commit = stdio.prompt("confirm", "Start commit? -- `y` to start a commit, `n` to exit the script") diff --git a/.lune/dist.luau b/.lune/dist.luau index ccd0bd8..77e901d 100644 --- a/.lune/dist.luau +++ b/.lune/dist.luau @@ -2,4 +2,5 @@ local spawn = require("util/spawn") spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") +spawn.start("lune run install-packages") spawn.start("darklua process --config .darklua.json lib/ dist/", { env = { ROBLOX_DEV = "false" } }) diff --git a/.lune/download-jecs.luau b/.lune/download-jecs.luau new file mode 100644 index 0000000..2d23e65 --- /dev/null +++ b/.lune/download-jecs.luau @@ -0,0 +1,67 @@ +--!strict +local fs = require("@lune/fs") +local net = require("@lune/net") +local process = require("@lune/process") +local serde = require("@lune/serde") +local spawn = require("util/spawn") + +type wally_manifest = { + package: { + name: string, + version: string, + registry: string, + realm: string, + license: string?, + exclude: { string }?, + include: { string }?, + }, + dependencies: { + [string]: string, + }, +} + +local github_token: string = process.args[1] + +if not github_token then + local env_exists = fs.metadata(".env").exists + if not env_exists then + error("Usage: lune run download-jecs [GITHUB_PAT]\nAlternatively, put the PAT in an .env file under GITHUB_PAT") + end + + local env = serde.decode("toml", fs.readFile(".env")) + local pat = env.GITHUB_PAT or error("Couldn't read GITHUB_PAT from .env") + github_token = pat +end + +local manifest_contents = fs.readFile("wally.toml") or error("Couldn't read manifest.") +local manifest: wally_manifest = serde.decode("toml", manifest_contents) or error("Couldn't decode manifest.") +local jecs_version = string.match(manifest.dependencies.jecs, "%d.%d.%d") or error("Couldn't find jecs version.") + +type gh_api_tag = { + ref: string, + node_id: string, + url: string, + object: { + sha: string, + type: string, + url: string, + }, +} + +local response = net.request({ + url = `https://api.github.com/repos/ukendio/jecs/git/refs/tags/v{jecs_version}`, + method = "GET", + headers = { + Accept = "application/vnd.github+json", + Authorization = `Bearer {github_token}`, + ["X-GitHub-Api-Version"] = "2022-11-28", + }, +}) + +if not response.ok then + error(`Github api response not ok:\n{response.statusCode} @ {response.statusMessage}\n{response.body}`) +end + +local gh_api_tag: gh_api_tag = serde.decode("json", response.body) + +spawn.start(`curl https://raw.githubusercontent.com/ukendio/jecs/{gh_api_tag.object.sha}/src/init.luau -o jecs.luau`) diff --git a/.lune/util/watch.luau b/.lune/util/watch.luau index be2cdf8..2b300c7 100644 --- a/.lune/util/watch.luau +++ b/.lune/util/watch.luau @@ -2,18 +2,20 @@ local fs = require("@lune/fs") local task = require("@lune/task") -local function watch(path: string, on_change: (path: string, contents: string) -> ()) +local function watch(path: string, on_change: (path: string, contents: string) -> (), run_initially: boolean?) local initial_metadata = fs.metadata(path) if not initial_metadata.exists then return end - local initial_contents = fs.readFile(path) + if run_initially then + local initial_contents = fs.readFile(path) - local initial_success, why = pcall(on_change :: any, path, initial_contents) -- :: any because otherwise it shits itself and the type doesn't give (boolean, string)?? - if not initial_success then - warn(`There was an error while trying to start the watcher thread:\n{why}`) - return + local initial_success, why = pcall(on_change :: any, path, initial_contents) -- :: any because otherwise it shits itself and the type doesn't give (boolean, string)?? + if not initial_success then + warn(`There was an error while trying to start the watcher thread:\n{why}`) + return + end end local last_modification = initial_metadata.modifiedAt diff --git a/.zed/settings.json b/.zed/settings.json index 665e15a..f815196 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -21,7 +21,7 @@ "mode": "relativeToFile", "fileAliases": { "@jecs_utils": "lib", - "@jecs": "lib/jecs", + "@jecs": "Packages/jecs", "@testkit": "test/testkit" }, "directoryAliases": { diff --git a/dev.project.json b/dev.project.json index 8a1bf5a..7496e83 100644 --- a/dev.project.json +++ b/dev.project.json @@ -6,6 +6,7 @@ "ReplicatedStorage": { "Packages": { "$className": "Folder", + "$path": "Packages", "jecs_utils": { "$path": "lib" } diff --git a/lib/command_buffer.luau b/lib/command_buffer.luau index 28e4155..60aff53 100644 --- a/lib/command_buffer.luau +++ b/lib/command_buffer.luau @@ -1,6 +1,6 @@ --!strict --!optimize 2 -local jecs = require("./jecs") +local jecs = require("@jecs") type entity = jecs.Entity type id = jecs.Id diff --git a/lib/handle.luau b/lib/handle.luau index 90107f5..b57f4cb 100644 --- a/lib/handle.luau +++ b/lib/handle.luau @@ -1,6 +1,6 @@ --!strict --!optimize 2 -local jecs = require("./jecs") +local jecs = require("@jecs") type entity = jecs.Entity type id = entity | jecs.Pair diff --git a/lib/init.luau b/lib/init.luau index 67d06da..4031c64 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -1,6 +1,6 @@ --!strict --!optimize 2 -local jecs = require("./jecs") +local jecs = require("@jecs") local WORLD = require("./world") local collect = require("./collect") diff --git a/lib/jecs.luau b/lib/jecs.luau deleted file mode 100644 index bd92479..0000000 --- a/lib/jecs.luau +++ /dev/null @@ -1,1894 +0,0 @@ ---!optimize 2 ---!native ---!strict ---draft 4 - -type i53 = number -type i24 = number - -type Ty = { i53 } -type ArchetypeId = number - -type Column = { any } - -type Map = { [K]: V } - -type GraphEdge = { - from: Archetype, - to: Archetype?, - prev: GraphEdge?, - next: GraphEdge?, - id: number, -} - -type GraphEdges = Map - -type GraphNode = { - add: GraphEdges, - remove: GraphEdges, - add_ref: GraphEdge?, - remove_ref: GraphEdge?, -} - -export type Archetype = { - id: number, - node: GraphNode, - types: Ty, - type: string, - entities: { number }, - columns: { Column }, - records: { ArchetypeRecord }, -} -type Record = { - archetype: Archetype, - row: number, - dense: i24, -} - -type EntityIndex = { - dense: Map, - sparse: Map, -} - -type ArchetypeRecord = { - count: number, - column: number, -} - -type IdRecord = { - cache: { ArchetypeRecord }, - flags: number, - size: number, -} - -type ComponentIndex = Map - -type Archetypes = { [ArchetypeId]: Archetype } - -type ArchetypeDiff = { - added: Ty, - removed: Ty, -} - -local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 - -local EcsOnAdd = HI_COMPONENT_ID + 1 -local EcsOnRemove = HI_COMPONENT_ID + 2 -local EcsOnSet = HI_COMPONENT_ID + 3 -local EcsWildcard = HI_COMPONENT_ID + 4 -local EcsChildOf = HI_COMPONENT_ID + 5 -local EcsComponent = HI_COMPONENT_ID + 6 -local EcsOnDelete = HI_COMPONENT_ID + 7 -local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 -local EcsDelete = HI_COMPONENT_ID + 9 -local EcsRemove = HI_COMPONENT_ID + 10 -local EcsName = HI_COMPONENT_ID + 11 -local EcsRest = HI_COMPONENT_ID + 12 - -local ECS_PAIR_FLAG = 0x8 -local ECS_ID_FLAGS_MASK = 0x10 -local ECS_ENTITY_MASK = bit32.lshift(1, 24) -local ECS_GENERATION_MASK = bit32.lshift(1, 16) - -local ECS_ID_DELETE = 0b0000_0001 -local ECS_ID_IS_TAG = 0b0000_0010 -local ECS_ID_HAS_ON_ADD = 0b0000_0100 -local ECS_ID_HAS_ON_SET = 0b0000_1000 -local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 -local ECS_ID_MASK = 0b0000_0000 - -local NULL_ARRAY = table.freeze({}) - -local function FLAGS_ADD(is_pair: boolean): number - local flags = 0x0 - - if is_pair then - flags = bit32.bor(flags, ECS_PAIR_FLAG) -- HIGHEST bit in the ID. - end - if false then - flags = bit32.bor(flags, 0x4) -- Set the second flag to true - end - if false then - flags = bit32.bor(flags, 0x2) -- Set the third flag to true - end - if false then - flags = bit32.bor(flags, 0x1) -- LAST BIT in the ID. - end - - return flags -end - -local function ECS_COMBINE(source: number, target: number): i53 - return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) -end - -local function ECS_IS_PAIR(e: number): boolean - return if e > ECS_ENTITY_MASK then (e % ECS_ID_FLAGS_MASK) // ECS_PAIR_FLAG ~= 0 else false -end - --- HIGH 24 bits LOW 24 bits -local function ECS_GENERATION(e: i53): i24 - return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 -end - -local function ECS_GENERATION_INC(e: i53) - if e > ECS_ENTITY_MASK then - local flags = e // ECS_ID_FLAGS_MASK - local id = flags // ECS_ENTITY_MASK - local generation = flags % ECS_GENERATION_MASK - - return ECS_COMBINE(id, generation + 1) + flags - end - return ECS_COMBINE(e, 1) -end - --- FIRST gets the high ID -local function ECS_ENTITY_T_HI(e: i53): i24 - return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_ENTITY_MASK else e -end - --- SECOND -local function ECS_ENTITY_T_LO(e: i53): i24 - return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e -end - -local function _STRIP_GENERATION(e: i53): i24 - return ECS_ENTITY_T_LO(e) -end - -local function ECS_PAIR(pred: i53, obj: i53): i53 - return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + FLAGS_ADD(--[[isPair]] true) :: i53 -end - -local ERROR_ENTITY_NOT_ALIVE = "Entity is not alive" -local ERROR_GENERATION_INVALID = "INVALID GENERATION" - -local function entity_index_get_alive(index: EntityIndex, e: i24): i53 - local denseArray = index.dense - local id = denseArray[ECS_ENTITY_T_LO(e)] - - if id then - local currentGeneration = ECS_GENERATION(id) - local gen = ECS_GENERATION(e) - if gen == currentGeneration then - return id - end - - error(ERROR_GENERATION_INVALID) - end - - error(ERROR_ENTITY_NOT_ALIVE) -end - -local function _entity_index_sparse_get(entityIndex, id) - return entityIndex.sparse[entity_index_get_alive(entityIndex, id)] -end - --- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits -local function ecs_pair_first(world, e) - return entity_index_get_alive(world.entityIndex, ECS_ENTITY_T_HI(e)) -end - --- ECS_PAIR_SECOND gets the relationship / pred / LOW bits -local function ecs_pair_second(world, e) - return entity_index_get_alive(world.entityIndex, ECS_ENTITY_T_LO(e)) -end - -local function entity_index_new_id(entityIndex: EntityIndex, index: i24): i53 - --local id = ECS_COMBINE(index, 0) - local id = index - entityIndex.sparse[id] = { - dense = index, - } :: Record - entityIndex.dense[index] = id - - return id -end - -local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) - local src_columns = from.columns - local dst_columns = to.columns - local dst_entities = to.entities - local src_entities = from.entities - - local last = #src_entities - local types = from.types - local records = to.records - - for i, column in src_columns do - if column == NULL_ARRAY then - continue - end - -- Retrieves the new column index from the source archetype's record from each component - -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. - local tr = records[types[i]] - - -- Sometimes target column may not exist, e.g. when you remove a component. - if tr then - dst_columns[tr.column][dst_row] = column[src_row] - end - -- If the entity is the last row in the archetype then swapping it would be meaningless. - if src_row ~= last then - -- Swap rempves columns to ensure there are no holes in the archetype. - column[src_row] = column[last] - end - column[last] = nil - end - - local sparse = entity_index.sparse - local moved = #src_entities - - -- Move the entity from the source to the destination archetype. - -- Because we have swapped columns we now have to update the records - -- corresponding to the entities' rows that were swapped. - local e1 = src_entities[src_row] - local e2 = src_entities[moved] - - if src_row ~= moved then - src_entities[src_row] = e2 - end - - src_entities[moved] = nil :: any - dst_entities[dst_row] = e1 - - local record1 = sparse[e1] - local record2 = sparse[e2] - - record1.row = dst_row - record2.row = src_row -end - -local function archetype_append(entity: number, archetype: Archetype): number - local entities = archetype.entities - local length = #entities + 1 - entities[length] = entity - return length -end - -local function new_entity(entityId: i53, record: Record, archetype: Archetype): Record - local row = archetype_append(entityId, archetype) - record.archetype = archetype - record.row = row - return record -end - -local function entity_move(entity_index: EntityIndex, entityId: i53, record: Record, to: Archetype) - local sourceRow = record.row - local from = record.archetype - local dst_row = archetype_append(entityId, to) - archetype_move(entity_index, to, dst_row, from, sourceRow) - record.archetype = to - record.row = dst_row -end - -local function hash(arr: { number }): string - return table.concat(arr, "_") -end - -local world_get: (world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) -> ...any -do - -- Keeping the function as small as possible to enable inlining - local records - local columns - local row - - local function fetch(id) - local tr = records[id] - - if not tr then - return nil - end - - return columns[tr.column][row] - end - - function world_get(world: World, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any - local record = world.entityIndex.sparse[entity] - if not record then - return nil - end - - local archetype = record.archetype - if not archetype then - return nil - end - - records = archetype.records - columns = archetype.columns - row = record.row - - local va = fetch(a) - - if not b then - return va - elseif not c then - return va, fetch(b) - elseif not d then - return va, fetch(b), fetch(c) - elseif not e then - return va, fetch(b), fetch(c), fetch(d) - else - error("args exceeded") - end - end -end - -local function world_get_one_inline(world: World, entity: i53, id: i53) - local record = world.entityIndex.sparse[entity] - if not record then - return nil - end - - local archetype = record.archetype - if not archetype then - return nil - end - - local tr = archetype.records[id] - if not tr then - return nil - end - return archetype.columns[tr.column][record.row] -end - -local function world_has_one_inline(world: World, entity: number, id: i53): boolean - local record = world.entityIndex.sparse[entity] - if not record then - return false - end - - local archetype = record.archetype - if not archetype then - return false - end - - local records = archetype.records - - return records[id] ~= nil -end - -local function world_has(world: World, entity: number, ...: i53): boolean - local record = world.entityIndex.sparse[entity] - if not record then - return false - end - - local archetype = record.archetype - if not archetype then - return false - end - - local records = archetype.records - - for i = 1, select("#", ...) do - if not records[select(i, ...)] then - return false - end - end - - return true -end - -local function world_target(world: World, entity: i53, relation: i24, index: number): i24? - local record = world.entityIndex.sparse[entity] - local archetype = record.archetype - if not archetype then - return nil - end - - local idr = world.componentIndex[ECS_PAIR(relation, EcsWildcard)] - if not idr then - return nil - end - - local tr = idr.cache[archetype.id] - if not tr then - return nil - end - - local count = tr.count - if index >= count then - index = index + count + 1 - end - - local nth = archetype.types[index + tr.column] - - if not nth then - return nil - end - - return ecs_pair_second(world, nth) -end - -local function ECS_ID_IS_WILDCARD(e: i53): boolean - local first = ECS_ENTITY_T_HI(e) - local second = ECS_ENTITY_T_LO(e) - return first == EcsWildcard or second == EcsWildcard -end - -local function id_record_ensure(world: World, id: number): IdRecord - local componentIndex = world.componentIndex - local idr = componentIndex[id] - - if not idr then - local flags = ECS_ID_MASK - local relation = ECS_ENTITY_T_HI(id) - - local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) - local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) - - local has_delete = false - - if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then - has_delete = true - end - - local on_add, on_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, EcsOnRemove) - - local is_tag = not world_has_one_inline(world, relation, EcsComponent) - - flags = bit32.bor( - flags, - if on_add then ECS_ID_HAS_ON_ADD else 0, - if on_remove then ECS_ID_HAS_ON_REMOVE else 0, - if on_set then ECS_ID_HAS_ON_SET else 0, - if has_delete then ECS_ID_DELETE else 0, - if is_tag then ECS_ID_IS_TAG else 0 - ) - - idr = { - size = 0, - cache = {}, - flags = flags, - } :: IdRecord - componentIndex[id] = idr - end - - return idr -end - -local function archetype_append_to_records( - idr: IdRecord, - archetype_id: number, - records: Map, - id: number, - index: number -) - local tr = idr.cache[archetype_id] - if not tr then - tr = { column = index, count = 1 } - idr.cache[archetype_id] = tr - idr.size += 1 - records[id] = tr - else - tr.count += 1 - end -end - -local function archetype_create(world: World, types: { i24 }, prev: i53?): Archetype - local ty = hash(types) - - local archetype_id = (world.nextArchetypeId :: number) + 1 - world.nextArchetypeId = archetype_id - - local length = #types - local columns = (table.create(length) :: any) :: { Column } - - local records: { ArchetypeRecord } = {} - for i, componentId in types do - local idr = id_record_ensure(world, componentId) - archetype_append_to_records(idr, archetype_id, records, componentId, i) - - if ECS_IS_PAIR(componentId) then - local relation = ecs_pair_first(world, componentId) - local object = ecs_pair_second(world, componentId) - - local r = ECS_PAIR(relation, EcsWildcard) - local idr_r = id_record_ensure(world, r) - archetype_append_to_records(idr_r, archetype_id, records, r, i) - - local t = ECS_PAIR(EcsWildcard, object) - local idr_t = id_record_ensure(world, t) - archetype_append_to_records(idr_t, archetype_id, records, t, i) - end - if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then - columns[i] = {} - else - columns[i] = NULL_ARRAY - end - end - - local archetype: Archetype = { - columns = columns, - node = { add = {}, remove = {} }, - entities = {}, - id = archetype_id, - records = records, - type = ty, - types = types, - } - - world.archetypeIndex[ty] = archetype - world.archetypes[archetype_id] = archetype - - return archetype -end - -local function world_entity(world: World): i53 - local entityId = (world.nextEntityId :: number) + 1 - world.nextEntityId = entityId - return entity_index_new_id(world.entityIndex, entityId + EcsRest) -end - -local function world_parent(world: World, entity: i53) - return world_target(world, entity, EcsChildOf, 0) -end - -local function archetype_ensure(world: World, types): Archetype - if #types < 1 then - return world.ROOT_ARCHETYPE - end - - local ty = hash(types) - local archetype = world.archetypeIndex[ty] - if archetype then - return archetype - end - - return archetype_create(world, types) -end - -local function find_insert(types: { i53 }, toAdd: i53): number - for i, id in types do - if id == toAdd then - return -1 - end - if id > toAdd then - return i - end - end - return #types + 1 -end - -local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype - local types = node.types - -- Component IDs are added incrementally, so inserting and sorting - -- them each time would be expensive. Instead this insertion sort can find the insertion - -- point in the types array. - - local dst = table.clone(node.types) :: { i53 } - local at = find_insert(types, id) - if at == -1 then - -- If it finds a duplicate, it just means it is the same archetype so it can return it - -- directly instead of needing to hash types for a lookup to the archetype. - return node - end - table.insert(dst, at, id) - - return archetype_ensure(world, dst) -end - -local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype - local types = node.types - local at = table.find(types, id) - if at == nil then - return node - end - - local dst = table.clone(types) - table.remove(dst, at) - - return archetype_ensure(world, dst) -end - -local function archetype_init_edge(archetype: Archetype, edge: GraphEdge, id: i53, to: Archetype) - edge.from = archetype - edge.to = to - edge.id = id -end - -local function archetype_ensure_edge(world, edges, id): GraphEdge - local edge = edges[id] - if not edge then - edge = { - from = nil :: any, - to = nil :: any, - id = id, - prev = nil, - next = nil, - } :: GraphEdge - edges[id] = edge - end - - return edge -end - -local function init_edge_for_add(world, archetype, edge, id, to) - archetype_init_edge(archetype, edge, id, to) - archetype_ensure_edge(world, archetype.node.add, id) - if archetype ~= to then - local to_add_ref = to.node.add_ref - edge.next = to_add_ref - edge.prev = nil - if to_add_ref then - to_add_ref.prev = edge - end - to.node.add_ref = edge - end -end - -local function init_edge_for_remove(world, archetype, edge, id, to) - archetype_init_edge(archetype, edge, id, to) - archetype_ensure_edge(world, archetype.node.remove, id) - if archetype ~= to then - local to_remove_ref = to.node.remove_ref - local prev - if to_remove_ref then - prev = to_remove_ref.prev - to_remove_ref.prev = edge - edge.next = to_remove_ref - else - to.node.remove_ref = edge - edge.next = nil - end - - edge.prev = prev - if prev then - prev.next = edge - end - end -end - -local function create_edge_for_add(world: World, node: Archetype, edge: GraphEdge, id: i53): Archetype - local to = find_archetype_with(world, node, id) - init_edge_for_add(world, node, edge, id, to) - return to -end - -local function create_edge_for_remove(world: World, node: Archetype, edge: GraphEdge, id: i53): Archetype - local to = find_archetype_without(world, node, id) - init_edge_for_remove(world, node, edge, id, to) - return to -end - -local function archetype_traverse_add(world: World, id: i53, from: Archetype): Archetype - from = from or world.ROOT_ARCHETYPE - local edge = archetype_ensure_edge(world, from.node.add, id) - - local to = edge.to - if not to then - to = create_edge_for_add(world, from, edge, id) - end - - return to :: Archetype -end - -local function archetype_traverse_remove(world: World, id: i53, from: Archetype): Archetype - from = from or world.ROOT_ARCHETYPE - - local edge = archetype_ensure_edge(world, from.node.add, id) - - local to = edge.to - if not to then - to = create_edge_for_remove(world, from, edge, id) - end - - return to :: Archetype -end - -local function invoke_hook(world: World, hook_id: number, id: i53, entity: i53, data: any?) - local hook = world_get_one_inline(world, id, hook_id) - if hook then - hook(entity, data) - end -end - -local function world_add(world: World, entity: i53, id: i53): () - local entityIndex = world.entityIndex - local record = entityIndex.sparse[entity] - local from = record.archetype - local to = archetype_traverse_add(world, id, from) - if from == to then - return - end - if from then - entity_move(entityIndex, entity, record, to) - else - if #to.types > 0 then - new_entity(entity, record, to) - end - end - - local idr = world.componentIndex[id] - local has_on_add = bit32.band(idr.flags, ECS_ID_HAS_ON_ADD) ~= 0 - - if has_on_add then - invoke_hook(world, EcsOnAdd, id, entity) - end -end - -local function world_set(world: World, entity: i53, id: i53, data: unknown): () - local entityIndex = world.entityIndex - local record = entityIndex.sparse[entity] - local from = record.archetype - local to = archetype_traverse_add(world, id, from) - local idr = world.componentIndex[id] - local flags = idr.flags - local is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0 - local has_on_set = bit32.band(flags, ECS_ID_HAS_ON_SET) ~= 0 - - if from == to then - if is_tag then - return - end - -- If the archetypes are the same it can avoid moving the entity - -- and just set the data directly. - local tr = to.records[id] - from.columns[tr.column][record.row] = data - if has_on_set then - invoke_hook(world, EcsOnSet, id, entity, data) - end - - return - end - - if from then - -- If there was a previous archetype, then the entity needs to move the archetype - entity_move(entityIndex, entity, record, to) - else - if #to.types > 0 then - -- When there is no previous archetype it should create the archetype - new_entity(entity, record, to) - end - end - - local has_on_add = bit32.band(flags, ECS_ID_HAS_ON_ADD) ~= 0 - - if has_on_add then - invoke_hook(world, EcsOnAdd, id, entity) - end - - if is_tag then - return - end - - local tr = to.records[id] - local column = to.columns[tr.column] - - column[record.row] = data - - if has_on_set then - invoke_hook(world, EcsOnSet, id, entity, data) - end -end - -local function world_component(world: World): i53 - local componentId = (world.nextComponentId :: number) + 1 - if componentId > HI_COMPONENT_ID then - -- IDs are partitioned into ranges because component IDs are not nominal, - -- so it needs to error when IDs intersect into the entity range. - error("Too many components, consider using world:entity() instead to create components.") - end - world.nextComponentId = componentId - local id = entity_index_new_id(world.entityIndex, componentId) - world_add(world, id, EcsComponent) - return id -end - -local function world_remove(world: World, entity: i53, id: i53) - local entity_index = world.entityIndex - local record = entity_index.sparse[entity] - local from = record.archetype - if not from then - return - end - local to = archetype_traverse_remove(world, id, from) - - if from and not (from == to) then - local idr = world.componentIndex[id] - local flags = idr.flags - local has_on_remove = bit32.band(flags, ECS_ID_HAS_ON_REMOVE) ~= 0 - if has_on_remove then - invoke_hook(world, EcsOnRemove, id, entity) - end - - entity_move(entity_index, entity, record, to) - end -end - -local function world_clear(world: World, entity: i53) - --TODO: use sparse_get (stashed) - local record = world.entityIndex.sparse[entity] - if not record then - return - end - - local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE - local archetype = record.archetype - - if archetype == nil or archetype == ROOT_ARCHETYPE then - return - end - - entity_move(world.entityIndex, entity, record, ROOT_ARCHETYPE) -end - -local function archetype_fast_delete_last(columns: { Column }, column_count: number, types: { i53 }, entity: i53) - for i, column in columns do - if column ~= NULL_ARRAY then - column[column_count] = nil - end - end -end - -local function archetype_fast_delete(columns: { Column }, column_count: number, row, types, entity) - for i, column in columns do - if column ~= NULL_ARRAY then - column[row] = column[column_count] - column[column_count] = nil - end - end -end - -local function archetype_disconnect_edge(edge: GraphEdge) - local edge_next = edge.next - local edge_prev = edge.prev - if edge_next then - edge_next.prev = edge_prev - end - if edge_prev then - edge_prev.next = edge_next - end -end - -local function archetype_remove_edge(edges: Map, id: i53, edge: GraphEdge) - archetype_disconnect_edge(edge) - edges[id] = nil -end - -local function archetype_clear_edges(archetype: Archetype) - local node = archetype.node - local add = node.add - local remove = node.remove - for _, edge in add do - archetype_disconnect_edge(edge) - end - for _, edge in remove do - archetype_disconnect_edge(edge) - end - local node_add_ref = node.add_ref - if node_add_ref then - local current = node_add_ref.next - while current do - local edge = current - current = current.next - local node_add = edge.from.node.add - if node_add then - archetype_remove_edge(node_add, edge.id, edge) - end - end - end - - local node_remove_ref = node.remove_ref - if node_remove_ref then - local current = node_remove_ref.prev - while current do - local edge = current - current = current.prev - local node_remove = edge.from.node.remove - if node_remove then - archetype_remove_edge(node_remove, edge.id, edge) - end - end - end - - node.add = nil :: any - node.remove = nil :: any - node.add_ref = nil :: any - node.remove_ref = nil :: any -end - -local function archetype_destroy(world: World, archetype: Archetype) - local component_index = world.componentIndex - archetype_clear_edges(archetype) - local archetype_id = archetype.id - world.archetypes[archetype_id] = nil - world.archetypeIndex[archetype.type] = nil - local records = archetype.records - - for id in records do - local idr = component_index[id] - idr.cache[archetype_id] = nil - idr.size -= 1 - records[id] = nil - if idr.size == 0 then - component_index[id] = nil - end - end -end - -local function world_cleanup(world) - for _, archetype in world.archetypes do - if #archetype.entities == 0 then - archetype_destroy(world, archetype) - end - end -end - -local world_delete: (world: World, entity: i53, destruct: boolean?) -> () -do - local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) - local entityIndex = world.entityIndex - local columns = archetype.columns - local types = archetype.types - local entities = archetype.entities - local column_count = #entities - local last = #entities - local move = entities[last] - local delete = entities[row] - entities[row] = move - entities[last] = nil - - if row ~= last then - -- TODO: should be "entity_index_sparse_get(entityIndex, move)" - local record_to_move = entityIndex.sparse[move] - if record_to_move then - record_to_move.row = row - end - end - - -- TODO: if last == 0 then deactivate table - - for _, id in types do - invoke_hook(world, EcsOnRemove, id, delete) - end - - if row == last then - archetype_fast_delete_last(columns, column_count, types, delete) - else - archetype_fast_delete(columns, column_count, row, types, delete) - end - end - - function world_delete(world: World, entity: i53, destruct: boolean?) - local entityIndex = world.entityIndex - - local record = entityIndex.sparse[entity] - if not record then - return - end - - local archetype = record.archetype - local row = record.row - - if archetype then - -- In the future should have a destruct mode for - -- deleting archetypes themselves. Maybe requires recycling - archetype_delete(world, archetype, row, destruct) - end - - local delete = entity - local component_index = world.componentIndex - local archetypes = world.archetypes - local tgt = ECS_PAIR(EcsWildcard, delete) - local idr_t = component_index[tgt] - local idr = component_index[delete] - - if idr then - local children = {} - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] - - for i, child in idr_archetype.entities do - table.insert(children, child) - end - end - local flags = idr.flags - if bit32.band(flags, ECS_ID_DELETE) ~= 0 then - for _, child in children do - -- Cascade deletion to children - world_delete(world, child) - end - else - for _, child in children do - world_remove(world, child, delete) - end - end - end - - if idr_t then - for archetype_id in idr_t.cache do - local children = {} - local idr_t_archetype = archetypes[archetype_id] - - local idr_t_types = idr_t_archetype.types - - for _, child in idr_t_archetype.entities do - table.insert(children, child) - end - - for _, id in idr_t_types do - if not ECS_IS_PAIR(id) then - continue - end - local object = ECS_ENTITY_T_LO(id) - if object == delete then - local id_record = component_index[id] - local flags = id_record.flags - if bit32.band(flags, ECS_ID_DELETE) ~= 0 then - for _, child in children do - -- Cascade deletions of it has Delete as component trait - world_delete(world, child, destruct) - end - - break - else - for _, child in children do - world_remove(world, child, id) - end - end - end - end - - archetype_destroy(world, idr_t_archetype) - end - end - - record.archetype = nil :: any - entityIndex.sparse[entity] = nil - end -end - -local function world_contains(world: World, entity): boolean - return world.entityIndex.sparse[entity] ~= nil -end - -local function NOOP() end - -local function ARM(query, ...) - return query -end - -local EMPTY_LIST = {} -local EMPTY_QUERY = { - __iter = function() - return NOOP - end, - iter = function() - return NOOP - end, - drain = ARM, - next = NOOP, - replace = NOOP, - with = ARM, - without = ARM, - archetypes = function() - return EMPTY_LIST - end, -} - -setmetatable(EMPTY_QUERY, EMPTY_QUERY) - -local function query_iter(query) - local world_query_iter_next - - if query.should_drain then - world_query_iter_next = query.iter_next - if world_query_iter_next then - return world_query_iter_next - end - end - - local compatible_archetypes = query.compatible_archetypes - local lastArchetype = 1 - local archetype = compatible_archetypes[1] - if not archetype then - return EMPTY_QUERY - end - local columns = archetype.columns - local entities = archetype.entities - local i = #entities - local records = archetype.records - - local ids = query.ids - local A, B, C, D, E, F, G, H, I = unpack(ids) - local a, b, c, d, e, f, g, h - - if not B then - a = columns[records[A].column] - elseif not C then - a = columns[records[A].column] - b = columns[records[B].column] - elseif not D then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - elseif not E then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - elseif not F then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - elseif not G then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - elseif not H then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - elseif not I then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - h = columns[records[H].column] - end - - if not B then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - end - - local row = i - i -= 1 - - return entityId, a[row] - end - elseif not C then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - b = columns[records[B].column] - end - - local row = i - i -= 1 - - return entityId, a[row], b[row] - end - elseif not D then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - end - - local row = i - i -= 1 - - return entityId, a[row], b[row], c[row] - end - elseif not E then - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - end - - local row = i - i -= 1 - - return entityId, a[row], b[row], c[row], d[row] - end - else - local queryOutput = {} - function world_query_iter_next(): any - local entityId = entities[i] - while entityId == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entityId = entities[i] - columns = archetype.columns - local records = archetype.records - - if not F then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - elseif not G then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - elseif not H then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - elseif not I then - a = columns[records[A].column] - b = columns[records[B].column] - c = columns[records[C].column] - d = columns[records[D].column] - e = columns[records[E].column] - f = columns[records[F].column] - g = columns[records[G].column] - h = columns[records[H].column] - end - end - - local row = i - i -= 1 - - if not F then - return entityId, a[row], b[row], c[row], d[row], e[row] - elseif not G then - return entityId, a[row], b[row], c[row], d[row], e[row], f[row] - elseif not H then - return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - elseif not I then - return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] - end - - local records = archetype.records - for j, id in ids do - queryOutput[j] = columns[records[id].column][row] - end - - return entityId, unpack(queryOutput) - end - end - - query.iter_next = world_query_iter_next - return world_query_iter_next -end - -local function query_drain(query) - local query_iter_next = query_iter(query) - query.next = query_iter_next - query.should_drain = true - return query -end - -local function query_next(query) - error("Did you forget to call drain?") -end - -local function query_without(query, ...) - local compatible_archetypes = query.compatible_archetypes - local N = select("#", ...) - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local records = archetype.records - local shouldRemove = false - - for j = 1, N do - local id = select(j, ...) - if records[id] then - shouldRemove = true - break - end - end - - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil - end - end - - if #compatible_archetypes == 0 then - return EMPTY_QUERY - end - - return query -end - -local function query_with(query, ...) - local compatible_archetypes = query.compatible_archetypes - local N = select("#", ...) - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local records = archetype.records - local shouldRemove = false - - for j = 1, N do - local id = select(j, ...) - if not records[id] then - shouldRemove = true - break - end - end - - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil - end - end - if #compatible_archetypes == 0 then - return EMPTY_QUERY - end - return query -end - -local function columns_replace_values(row, columns, ...) - for i, column in columns do - column[row] = select(i, ...) - end -end - -local function query_replace(query, fn: (...any) -> ...any) - local compatible_archetypes = query.compatible_archetypes - local ids = query.ids - local A, B, C, D, E = unpack(ids, 1, 5) - local queryOutput = {} - for i, archetype in compatible_archetypes do - local columns = archetype.columns - local records = archetype.records - for row in archetype.entities do - if not B then - local va = columns[records[A].column] - local pa = fn(va[row]) - - va[row] = pa - elseif not C then - local va = columns[records[A].column] - local vb = columns[records[B].column] - - va[row], vb[row] = fn(va[row], vb[row]) - elseif not D then - local va = columns[records[A].column] - local vb = columns[records[B].column] - local vc = columns[records[C].column] - - va[row], vb[row], vc[row] = fn(va[row], vb[row], vc[row]) - elseif not E then - local va = columns[records[A].column] - local vb = columns[records[B].column] - local vc = columns[records[C].column] - local vd = columns[records[D].column] - - va[row], vb[row], vc[row], vd[row] = fn(va[row], vb[row], vc[row], vd[row]) - else - for j, id in ids do - local tr = records[id] - queryOutput[j] = columns[tr.column][row] - end - columns_replace_values(row, columns, fn(unpack(queryOutput))) - end - end - end -end - --- Meant for directly iterating over archetypes to minimize --- function call overhead. Should not be used unless iterating over --- hundreds of thousands of entities in bulk. -local function query_archetypes(query) - return query.compatible_archetypes -end - -local Query = {} -Query.__index = Query -Query.__iter = query_iter -Query.iter = query_iter -Query.without = query_without -Query.with = query_with -Query.archetypes = query_archetypes -Query.drain = query_drain -Query.next = query_next -Query.replace = query_replace - -local function world_query(world: World, ...) - local compatible_archetypes = {} - local length = 0 - - local ids = { ... } - - local archetypes = world.archetypes - - local idr: IdRecord - local componentIndex = world.componentIndex - - for _, id in ids do - local map = componentIndex[id] - if not map then - return EMPTY_QUERY - end - - if idr == nil or map.size < idr.size then - idr = map - end - end - - for archetype_id in idr.cache do - local compatibleArchetype = archetypes[archetype_id] - if #compatibleArchetype.entities == 0 then - continue - end - local records = compatibleArchetype.records - - local skip = false - - for i, id in ids do - local tr = records[id] - if not tr then - skip = true - break - end - end - - if skip then - continue - end - - length += 1 - compatible_archetypes[length] = compatibleArchetype - end - - if length == 0 then - return EMPTY_QUERY - end - - local q = setmetatable({ - compatible_archetypes = compatible_archetypes, - ids = ids, - }, Query) :: any - - return q -end - -local World = {} -World.__index = World - -World.entity = world_entity -World.query = world_query -World.remove = world_remove -World.clear = world_clear -World.delete = world_delete -World.component = world_component -World.add = world_add -World.set = world_set -World.get = world_get -World.has = world_has -World.target = world_target -World.parent = world_parent -World.contains = world_contains -World.cleanup = world_cleanup - -if _G.__JECS_DEBUG then - -- taken from https://github.com/centau/ecr/blob/main/src/ecr.luau - -- error but stack trace always starts at first callsite outside of this file - local function throw(msg: string) - local s = 1 - repeat - s += 1 - until debug.info(s, "s") ~= debug.info(1, "s") - if warn then - error(msg, s) - else - print(`[jecs] error: {msg}\n`) - end - end - - local function ASSERT(v: T, msg: string) - if v then - return - end - throw(msg) - end - - local function get_name(world, id): string - local name: string | nil - if ECS_IS_PAIR(id) then - name = `pair({get_name(world, ECS_ENTITY_T_HI(id))}, {get_name(world, ECS_ENTITY_T_LO(id))})` - else - local _1 = world_get_one_inline(world, id, EcsName) - if _1 then - name = `${_1}` - end - end - if name then - return name - else - return `${id}` - end - end - - local function ID_IS_TAG(world, id) - return not world_has_one_inline(world, ECS_ENTITY_T_HI(id), EcsComponent) - end - - local original_invoke_hook = invoke_hook - local invoked_hook = false - invoke_hook = function(...) - invoked_hook = true - original_invoke_hook(...) - invoked_hook = false - end - - World.query = function(world: World, ...) - ASSERT((...), "Requires at least a single component") - return world_query(world, ...) - end - - World.set = function(world: World, entity: i53, id: i53, value: any): () - local is_tag = ID_IS_TAG(world, id) - if is_tag and value == nil then - world_add(world, entity, id) - local _1 = get_name(world, entity) - local _2 = get_name(world, id) - local why = "cannot set component value to nil" - throw(why) - return - elseif value ~= nil and is_tag then - world_add(world, entity, id) - local _1 = get_name(world, entity) - local _2 = get_name(world, id) - local why = `cannot set a component value because {_2} is a tag` - why ..= `\n[jecs] note: consider using "world:add({_1}, {_2})" instead` - throw(why) - return - end - - if world_has_one_inline(world, entity, id) then - if invoked_hook then - local file, line = debug.info(2, "sl") - local hook_fn = `{file}::{line}` - local why = `cannot call world:set inside {hook_fn} because it adds the component {get_name(world, id)}` - why ..= `\n[jecs note]: consider handling this logic inside of a system` - throw(why) - return - end - end - - world_set(world, entity, id, value) - end - - World.add = function(world: World, entity: i53, id: i53, value: nil) - if value ~= nil then - local _1 = get_name(world, entity) - local _2 = get_name(world, id) - throw("You provided a value when none was expected. " .. `Did you mean to use "world:add({_1}, {_2})"`) - return - end - - if invoked_hook then - local hook_fn = debug.info(2, "sl") - throw(`Cannot call world:add when the hook {hook_fn} is in process`) - end - world_add(world, entity, id) - end - - World.get = function(world: World, entity: i53, ...) - local length = select("#", ...) - ASSERT(length < 5, "world:get does not support more than 4 components") - local _1 - for i = 1, length do - local id = select(i, ...) - local id_is_tag = not world_has(world, id, EcsComponent) - if id_is_tag then - local name = get_name(world, id) - if not _1 then - _1 = get_name(world, entity) - end - throw( - `cannot get (#{i}) component {name} value because it is a tag.` - .. `\n[jecs] note: If this was intentional, use "world:has({_1}, {name}) instead"` - ) - end - end - - return world_get(world, entity, ...) - end - - World.target = function(world, entity, relation, index) - if index == nil then - local _1 = get_name(world, entity) - local _2 = get_name(world, relation) - - throw( - "We have changed the function call to require an index parameter," - .. ` please use world:target({_1}, {_2}, 0)` - ) - end - return world_target(world, entity, relation, index) - end - - World.remove = function() end -end - -function World.new() - local self = setmetatable({ - archetypeIndex = {} :: { [string]: Archetype }, - archetypes = {} :: Archetypes, - componentIndex = {} :: ComponentIndex, - entityIndex = { - dense = {} :: { [i24]: i53 }, - sparse = {} :: { [i53]: Record }, - } :: EntityIndex, - nextArchetypeId = 0 :: number, - nextComponentId = 0 :: number, - nextEntityId = 0 :: number, - ROOT_ARCHETYPE = (nil :: any) :: Archetype, - }, World) :: any - - self.ROOT_ARCHETYPE = archetype_create(self, {}) - - for i = HI_COMPONENT_ID + 1, EcsRest do - -- Initialize built-in components - entity_index_new_id(self.entityIndex, i) - end - - world_add(self, EcsName, EcsComponent) - world_add(self, EcsOnSet, EcsComponent) - world_add(self, EcsOnAdd, EcsComponent) - world_add(self, EcsOnRemove, EcsComponent) - world_add(self, EcsWildcard, EcsComponent) - world_add(self, EcsRest, EcsComponent) - - world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd") - world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove") - world_set(self, EcsOnSet, EcsName, "jecs.OnSet") - world_set(self, EcsWildcard, EcsName, "jecs.Wildcard") - world_set(self, EcsChildOf, EcsName, "jecs.ChildOf") - world_set(self, EcsComponent, EcsName, "jecs.Component") - world_set(self, EcsOnDelete, EcsName, "jecs.OnDelete") - world_set(self, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") - world_set(self, EcsDelete, EcsName, "jecs.Delete") - world_set(self, EcsRemove, EcsName, "jecs.Remove") - world_set(self, EcsName, EcsName, "jecs.Name") - world_set(self, EcsRest, EcsRest, "jecs.Rest") - - world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) - - return self -end - -export type Id = Entity | Pair - -export type Pair = number - -type Item = (self: Query) -> (Entity, T...) - -export type Entity = number & { __T: T } - -type Iter = (query: Query) -> () -> (Entity, T...) - -type Query = typeof(setmetatable({}, { - __iter = (nil :: any) :: Iter, -})) & { - iter: Iter, - next: Item, - drain: (self: Query) -> Query, - with: (self: Query, ...i53) -> Query, - without: (self: Query, ...i53) -> Query, - replace: (self: Query, (T...) -> U...) -> (), - archetypes: () -> { Archetype }, -} - -export type World = { - archetypeIndex: { [string]: Archetype }, - archetypes: Archetypes, - componentIndex: ComponentIndex, - entityIndex: EntityIndex, - ROOT_ARCHETYPE: Archetype, - - nextComponentId: number, - nextEntityId: number, - nextArchetypeId: number, -} & { - --- Creates a new entity - entity: (self: World) -> Entity, - --- Creates a new entity located in the first 256 ids. - --- These should be used for static components for fast access. - component: (self: World) -> Entity, - --- Gets the target of an relationship. For example, when a user calls - --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. - target: (self: World, id: Entity, relation: Entity, nth: number) -> Entity?, - --- Deletes an entity and all it's related components and relationships. - delete: (self: World, id: Entity) -> (), - - --- Adds a component to the entity with no value - add: (self: World, id: Entity, component: Id) -> (), - --- Assigns a value to a component on the given entity - set: (self: World, id: Entity, component: Id, data: T) -> (), - - -- Clears an entity from the world - clear: (self: World, id: Entity) -> (), - --- Removes a component from the given entity - remove: (self: World, id: Entity, component: Id) -> (), - --- Retrieves the value of up to 4 components. These values may be nil. - get: ((self: World, id: any, Id) -> A?) - & ((self: World, id: Entity, Id, Id) -> (A?, B?)) - & ((self: World, id: Entity, Id, Id, Id) -> (A?, B?, C?)) - & (self: World, id: Entity, Id, Id, Id, Id) -> (A?, B?, C?, D?), - - --- Returns whether the entity has the ID. - has: (self: World, entity: Entity, ...Id) -> boolean, - - --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. - parent: (self: World, entity: Entity) -> Entity, - - --- Checks if the world contains the given entity - contains: (self: World, entity: Entity) -> boolean, - - --- Searches the world for entities that match a given query - query: ((self: World, Id) -> Query) - & ((self: World, Id, Id) -> Query) - & ((self: World, Id, Id, Id) -> Query) - & ((self: World, Id, Id, Id, Id) -> Query) - & ((self: World, Id, Id, Id, Id, Id) -> Query) - & (( - self: World, - Id, - Id, - Id, - Id, - Id, - Id - ) -> Query) - & (( - self: World, - Id, - Id, - Id, - Id, - Id, - Id, - Id - ) -> Query) - & (( - self: World, - Id, - Id, - Id, - Id, - Id, - Id, - Id, - Id, - ...Id - ) -> Query), -} - -return { - World = World :: { new: () -> World }, - - OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, - OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, - OnSet = EcsOnSet :: Entity<(entity: Entity, data: any) -> ()>, - ChildOf = EcsChildOf :: Entity, - Component = EcsComponent :: Entity, - Wildcard = EcsWildcard :: Entity, - w = EcsWildcard :: Entity, - OnDelete = EcsOnDelete :: Entity, - OnDeleteTarget = EcsOnDeleteTarget :: Entity, - Delete = EcsDelete :: Entity, - Remove = EcsRemove :: Entity, - Name = EcsName :: Entity, - Rest = EcsRest :: Entity, - - pair = ECS_PAIR, - - -- Inwards facing API for testing - ECS_ID = ECS_ENTITY_T_LO, - ECS_GENERATION_INC = ECS_GENERATION_INC, - ECS_GENERATION = ECS_GENERATION, - ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD, - - IS_PAIR = ECS_IS_PAIR, - pair_first = ecs_pair_first, - pair_second = ecs_pair_second, - entity_index_get_alive = entity_index_get_alive, -} diff --git a/lib/ref.luau b/lib/ref.luau index 9a1e5f4..7adfb71 100644 --- a/lib/ref.luau +++ b/lib/ref.luau @@ -1,10 +1,10 @@ --!strict --!optimize 2 local handle = require("./handle") -local jecs = require("./jecs") +local jecs = require("@jecs") local WORLD = require("./world").get -local refs: { [jecs.World]: { [any]: jecs.Entity } } = {} +local refs: { [jecs.World]: { [any]: jecs.Entity } } = {} --- Gets an entity the given key references to. --- If the key is nil, an entirely new entity is created and returned. diff --git a/lib/replicator.luau b/lib/replicator.luau index 7890732..9e47e4c 100644 --- a/lib/replicator.luau +++ b/lib/replicator.luau @@ -1,6 +1,6 @@ --!strict --!optimize 2 -local jecs = require("./jecs") +local jecs = require("@jecs") type entity = jecs.Entity type i53 = number diff --git a/lib/world.luau b/lib/world.luau index 9d5de0b..75f4acd 100644 --- a/lib/world.luau +++ b/lib/world.luau @@ -1,6 +1,6 @@ --!strict --!optimize 2 -local jecs = require("./jecs") +local jecs = require("@jecs") local WORLD: jecs.World diff --git a/luau_lsp_settings.json b/luau_lsp_settings.json index 79d0f9f..613b218 100644 --- a/luau_lsp_settings.json +++ b/luau_lsp_settings.json @@ -4,6 +4,7 @@ }, "luau-lsp.require.mode": "relativeToFile", "luau-lsp.require.fileAliases": { - "@jecs": "lib/jecs" - } + "@jecs": "Packages/jecs" + }, + "luau-lsp.platform.type": "roblox" } diff --git a/test/tests.luau b/test/tests.luau index 528bd78..8236117 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,6 +1,6 @@ --!strict -- stylua: ignore start -local jecs = require("@jecs") +local jecs = require("../jecs") local jecs_utils = require("@jecs_utils") local testkit = require("@testkit") diff --git a/wally.toml b/wally.toml index 8341d61..4e97b0d 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "mark-marks/jecs-utils" -version = "0.1.2" +version = "0.1.3" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" license = "MIT" @@ -15,3 +15,4 @@ include = [ ] [dependencies] +jecs = "ukendio/jecs@0.3.0"