diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b485f17..3b90293 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -49,9 +49,7 @@ jobs: - name: Create Release uses: softprops/action-gh-release@v1 with: - name: Matter ${{ github.ref_name }} - body: | - Matter ${{ github.ref_name }} is now available! + name: Jecs ${{ github.ref_name }} files: | jecs.rbxm @@ -70,4 +68,4 @@ jobs: run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }} - name: Publish - run: wally publish \ No newline at end of file + run: wally publish diff --git a/.gitignore b/.gitignore index a43fa5f..f3d15ef 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,10 @@ Packages wally.lock WallyPatches +# Typescript +/node_modules +/include + # Misc roblox.toml sourcemap.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6fd615a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,120 @@ +# Jecs Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog][kac], and this project adheres to +[Semantic Versioning][semver]. + +[kac]: https://keepachangelog.com/en/1.1.0/ +[semver]: https://semver.org/spec/v2.0.0.html + +## [Unreleased] + +### Changed + +- Iterator now goes backwards instead to prevent common cases of iterator invalidation + +## [0.2.1] - 2024-07-06 + +### Added + +- Added `jecs.Component` built-in component which will be added to ids created with `world:component()`. + - Used to find every component id with `query(jecs.Component) + +## [0.2.0] - 2024-07-03 + +### Added + +- Added `world:parent(entity)` and `jecs.ChildOf` respectively as first class citizen for building parent-child relationships. + - Give a parent to an entity with `world:add($source, pair(ChildOf, $target))` + - Use `world:parent(entity)` to find the target of the relationship +- Added user-facing Luau types + +### Changed +- Improved iteration speeds 20-40% by manually indexing rather than using `next()` :scream: + + +## [0.1.1] - 2024-05-19 + +### Added + +- Added `world:clear(entity)` for removing the components to the corresponding entity +- Added Typescript Types + +## [0.1.0] - 2024-05-13 + +### Changed +- Optimized iterator + +## [0.1.0-rc.6] - 2024-05-13 + +### Added + +- Added a `jecs.Wildcard` term + - it lets you query any partially matched pairs + +## [0.1.0-rc.5] - 2024-05-10 + +### Added + +- Added Entity relationships for creating logical connections between entities +- Added `world:__iter method` which allows for iteration over the whole world to get every entity + - used for reconciling whole worlds such as via replication, saving/loading, etc +- Added `world:add(entity, component)` which adds a component to the entity + - it is an idempotent function, so calling it twice and in any order should be fine + +### Fixed +- Fixed component overriding when in disorder + - Previously setting the components in different order results in it overriding component data because it incorrectly mapped the index of the column. So it took the index from the source archetype rather than the destination archetype + +## [0.0.0-prototype.rc.3] - 2024-05-01 + +### Added + +- Added observers +- Added an arm to query `query:without()` for chaining invariants. + +### Changed +- Separates ranges for components and entity IDs. + - IDs created with `world:component()` will promote array lookups rather than map lookups in the `componentIndex` which is a significant boost + +- No longer caches the column pointers directly and instead the column indices which stay persistent even when data is reallocated during swap-removals + - This was an issue with the iterator being invalidated when you move an entity to a different archetype. + +### Fixedhttps://github.com/Ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3 + +- Fixed a bug where changing an existing component would be slow because it was always appending changing the row of the entity record + - The fix dramatically improves times where it is basically down to just the speed of setting a field in a table + +## [0.0.0-prototype.rc.2] - 2024-04-26 + +### Changed +- Optimized the creation of the query + - It will now finds the smallest archetype map to iterate over +- Optimized the query iterator + - It will now populates iterator with columns for faster indexing + +- Renamed the insertion method from world:add to world:set to better reflect what it does. + +## [0.0.0-prototype.rc.2] - 2024-04-23 +- Initial release + +[unreleased]: https://github.com/ukendio/jecs/compare/v0.0.0.0-prototype.rc.2...HEAD +[0.2.1]: https://github.com/ukendio/jecs/releases/tag/v0.2.1 +[0.2.0]: https://github.com/ukendio/jecs/releases/tag/v0.2.0 +[0.1.1]: https://github.com/ukendio/jecs/releases/tag/v0.1.1 +[0.1.0]: https://github.com/ukendio/jecs/releases/tag/v0.1.0 +[0.1.0-rc.6]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.6 +[0.1.0-rc.5]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.5 +[0.0.0-prototype-rc.3]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3 +[0.0.0-prototype.rc.2]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.2 +[0.0.0-prototype-rc.1]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.1 + + + + + + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..605eef8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 jecs authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 059e172..665ea7b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Just an ECS jecs is a stupidly fast Entity Component System (ECS). - Entity Relationships as first class citizens -- Iterate 350,000 entities at 60 frames per second +- Iterate 500,000 entities at 60 frames per second - Type-safe [Luau](https://luau-lang.org/) API - Zero-dependency package - Optimized for column-major operations @@ -31,7 +31,7 @@ local Name = world:component() local function parent(entity) return world:target(entity, ChildOf) end -local function name(entity) +local function getName(entity) return world:get(entity, Name) end @@ -58,7 +58,7 @@ end -- sara is the child of alice ``` -125 archetypes, 4 random components queried. +21,000 entities 125 archetypes 4 random components queried. ![Queries](image-3.png) Can be found under /benches/query.lua diff --git a/bench.project.json b/bench.project.json index e55b3ec..33ded9b 100644 --- a/bench.project.json +++ b/bench.project.json @@ -15,7 +15,7 @@ "$path": "lib" }, "rgb": { - "$path": "rgb.lua" + "$path": "rgb.luau" }, "benches": { "$path": "benches" diff --git a/benches/query.lua b/benches/query.luau similarity index 100% rename from benches/query.lua rename to benches/query.luau diff --git a/benches/visual/insertion.bench.lua b/benches/visual/insertion.bench.luau similarity index 80% rename from benches/visual/insertion.bench.lua rename to benches/visual/insertion.bench.luau index 8e24f29..40bea46 100644 --- a/benches/visual/insertion.bench.lua +++ b/benches/visual/insertion.bench.luau @@ -54,8 +54,9 @@ return { Functions = { Matter = function() - for i = 1, 500 do - newWorld:spawn( + local e = newWorld:spawn() + for i = 1, 5000 do + newWorld:insert(e, A1({ value = true }), A2({ value = true }), A3({ value = true }), @@ -71,7 +72,7 @@ return { ECR = function() local e = registry2.create() - for i = 1, 500 do + for i = 1, 5000 do registry2:set(e, B1, {value = false}) registry2:set(e, B2, {value = false}) registry2:set(e, B3, {value = false}) @@ -85,11 +86,8 @@ return { Jecs = function() - local e = ecs:entity() - - for i = 1, 500 do - + for i = 1, 5000 do ecs:set(e, C1, {value = false}) ecs:set(e, C2, {value = false}) ecs:set(e, C3, {value = false}) @@ -101,23 +99,5 @@ return { end end, - Mirror = function() - - local e = ecs:entity() - - for i = 1, 500 do - - mcs:set(e, E1, {value = false}) - mcs:set(e, E2, {value = false}) - mcs:set(e, E3, {value = false}) - mcs:set(e, E4, {value = false}) - mcs:set(e, E5, {value = false}) - mcs:set(e, E6, {value = false}) - mcs:set(e, E7, {value = false}) - mcs:set(e, E8, {value = false}) - - end - end - }, } diff --git a/benches/visual/query.bench.lua b/benches/visual/query.bench.luau similarity index 92% rename from benches/visual/query.bench.lua rename to benches/visual/query.bench.luau index e8f948a..3d00783 100644 --- a/benches/visual/query.bench.lua +++ b/benches/visual/query.bench.luau @@ -3,8 +3,8 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local rgb = require(ReplicatedStorage.rgb) -local Matter = require(ReplicatedStorage.DevPackages.Matter) -local ecr = require(ReplicatedStorage.DevPackages.ecr) +local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matter@0.8.1"].matter) +local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr) local newWorld = Matter.World.new() local jecs = require(ReplicatedStorage.Lib) @@ -177,6 +177,13 @@ return { end end, + Matter = function() + local matched = 0 + for entityId, firstComponent in newWorld:query(A1, A4, A6, A8) do + matched += 1 + end + end, + ECR = function() local matched = 0 for entityId, firstComponent in registry2:view(B1, B4, B6, B8) do diff --git a/benches/visual/spawn.bench.lua b/benches/visual/spawn.bench.luau similarity index 100% rename from benches/visual/spawn.bench.lua rename to benches/visual/spawn.bench.luau diff --git a/docs/api-types.md b/docs/api-types.md new file mode 100644 index 0000000..cce3856 --- /dev/null +++ b/docs/api-types.md @@ -0,0 +1,45 @@ +# World + +A World contains all ECS data +Games can have multiple worlds, although typically only one is necessary. These worlds are isolated from each other, meaning they donot share the same entities nor component IDs. + +--- + +# Entity + +An unique id. + +Entities consist out of a number unique to the entity in the lower 32 bits, and a counter used to track entity liveliness in the upper 32 bits. When an id is recycled, its generation count is increased. This causes recycled ids to be very large (>4 billion), which is normal. + +--- + +# QueryIter + +A result from the `World:query` function. + +Queries are used to iterate over entities that match against the set collection of components. + +Calling it in a loop will allow iteration over the results. + +```lua +for id, enemy, charge, model in world:query(Enemy, Charge, Model) do + -- Do something +end +``` + +### QueryIter.without + +QueryIter.without(iter: QueryIter + ...: [Entity](#Entity)): QueryIter + + +Create a new Query Iterator from the filter + +#### Parameters + world The world. + ... The collection of components to filter archetypes against. + +#### Returns + +The new query iterator. + diff --git a/docs/api/world.md b/docs/api/world.md new file mode 100644 index 0000000..0a019c3 --- /dev/null +++ b/docs/api/world.md @@ -0,0 +1,187 @@ +# World + +### World.new + +World.new(): [World](../api-types.md#World) + +Create a new world. + +#### Returns +A new world + +--- + +### World.entity + +World.entity(world: [World](../api-types.md#World)): [Entity](../api-types.md#Entity) + +Creates an entity in the world. + +#### Returns +A new entiity id + +--- + +### World.target + +World.target(world: [World](../api-types.md#World), + entity: [Entity](../api-types.md#Entity), + rel: [Entity](../api-types.md#Entity)): [Entity](../api-types.md#Entity) + +Get the target of a relationship. + +This will return a target (second element of a pair) of the entity for the specified relationship. + +#### Parameters + world The world. + entity The entity. + rel The relationship between the entity and the target. + +#### Returns + +The first target for the relationship + +--- + +### World.add + +World.add(world: [World](../api-types.md#World), + entity: [Entity](../api-types.md#Entity), + id: [Entity](../api-types.md#Entity)): [Entity](..#api-types.md#Entity) + +Add a (component) id to an entity. + +This operation adds a single (component) id to an entity. +If the entity already has the id, this operation will have no side effects. + +#### Parameters + world The world. + entity The entity. + id The id to add. + +--- + +### World.remove + +World.remove(world: [World](../api-types#World), + entity: [Entity](../api-types#Entity), + id: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) + +Remove a (component) id to an entity. + +This operation removes a single (component) id to an entity. +If the entity already has the id, this operation will have no side effects. + +#### Parameters + world The world. + entity The entity. + id The id to add. + +--- + +### World.get + +World.get(world: [World](../api-types.md#World), + entity: [Entity](../api-types.md#Entity), + id: [Entity](../api-types.md#Entity)): any + +Gets the component data. + +#### Parameters + world The world. + entity The entity. + id The id of component to get. + +#### Returns +The component data, nil if the entity does not have the componnet. + +--- + +### World.set + +World.set(world: [World](../api-types.md#World), + entity: [Entity](../api-types.md#Entity), + id: [Entity](../api-types.md#Entity) + data: any) + +Set the value of a component. + +#### Parameters + world The world. + entity The entity. + id The id of the componment set. + data The data to the component. + +--- + +### World.query + +World.query(world: [World](../api-types.md#World), + ...: [Entity](../api-types.mdEntity)): [QueryIter](../api-types.md#QueryIter) + +Create a QueryIter from the list of filters. + +#### Parameters + world The world. + ... The collection of components to match entities against. + +#### Returns + +The query iterator. + +--- + +# Pair + +### pair + +pair(first: [Entity](../api-types#Entity), second: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) + +Creates a composite key. + +#### Parameters + first The first element. + second The second element. + +#### Returns + +The pair of the two elements + +--- + +### IS_PAIR + +jecs.IS_PAIR(id: [Entity](../api-types#Entity)): boolean + +Creates a composite key. + +#### Parameters + id The id to check. + +#### Returns + +If id is a pair. + +--- + +# Constants + +### OnAdd + +--- + +### OnRemove + +--- + +### Rest + +--- + +### OnSet + +--- + +### Wildcard + +Matches any id, returns all matches. diff --git a/docs/tutorials/quick-start/getting-started.md b/docs/tutorials/quick-start/getting-started.md new file mode 100644 index 0000000..bd702d2 --- /dev/null +++ b/docs/tutorials/quick-start/getting-started.md @@ -0,0 +1,19 @@ +# Getting Started +This section will provide a walk through setting up your development environment and a quick overview of the different features and concepts in Jecs with short examples. + +## Installing Jecs + +To use Jecs, you will need to add the library to your project's source folder. + +## Installing as standalone +Head over to the [Releases](https://github.com/ukendio/jecs/releases/latest) page and install the rbxm file. +![jecs.rbxm](rbxm.png) + +## Installing with Wally +Jecs is available as a package on [wally.run](https://wally.run/package/ukendio/jecs) + +Add it to your project's Wally.toml like this: +```toml +[dependencies] +jecs = "0.1.0" # Make sure this is the latest version +``` \ No newline at end of file diff --git a/docs/tutorials/quick-start/rbxm.png b/docs/tutorials/quick-start/rbxm.png new file mode 100644 index 0000000..ad8f38d Binary files /dev/null and b/docs/tutorials/quick-start/rbxm.png differ diff --git a/image-3.png b/image-3.png index 9db2297..1039142 100644 Binary files a/image-3.png and b/image-3.png differ diff --git a/lib/init.lua b/lib/init.lua deleted file mode 100644 index 27cb078..0000000 --- a/lib/init.lua +++ /dev/null @@ -1,938 +0,0 @@ ---!optimize 2 ---!native ---!strict ---draft 4 - -type i53 = number -type i24 = number - -type Ty = {i53} -type ArchetypeId = number - -type Column = {any} - -type Archetype = { - id: number, - edges: { - [i53]: { - add: Archetype, - remove: Archetype, - }, - }, - types: Ty, - type: string | number, - entities: {number}, - columns: {Column}, - records: {}, -} - - -type Record = { - archetype: Archetype, - row: number, - dense: i24, - componentRecord: ArchetypeMap -} - -type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}} - -type ArchetypeRecord = number ---[[ -TODO: -{ - index: number, - count: number, - column: number -} - -]] - -type ArchetypeMap = { - cache: {[number]: ArchetypeRecord}, - first: ArchetypeMap, - second: ArchetypeMap, - parent: ArchetypeMap, - size: number -} - -type ComponentIndex = {[i24]: ArchetypeMap} - -type Archetypes = {[ArchetypeId]: Archetype} - -type ArchetypeDiff = { - added: Ty, - removed: Ty, -} - -local FLAGS_PAIR = 0x8 -local HI_COMPONENT_ID = 256 -local ON_ADD = HI_COMPONENT_ID + 1 -local ON_REMOVE = HI_COMPONENT_ID + 2 -local ON_SET = HI_COMPONENT_ID + 3 -local WILDCARD = HI_COMPONENT_ID + 4 -local REST = HI_COMPONENT_ID + 5 - -local ECS_ID_FLAGS_MASK = 0x10 -local ECS_ENTITY_MASK = bit32.lshift(1, 24) -local ECS_GENERATION_MASK = bit32.lshift(1, 16) - -local function addFlags(isPair: boolean) - local typeFlags = 0x0 - - if isPair then - typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. - end - if false then - typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true - end - if false then - typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true - end - if false then - typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. - end - - return typeFlags -end - -local function ECS_COMBINE(source: number, target: number): i53 - local e = source * 2^28 + target * ECS_ID_FLAGS_MASK - return e -end - -local function ECS_IS_PAIR(e: number) - return (e % 2^4) // FLAGS_PAIR ~= 0 -end - -function separate(entity: number) - local _typeFlags = entity % 0x10 - entity //= ECS_ID_FLAGS_MASK - return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags -end - --- HIGH 24 bits LOW 24 bits -local function ECS_GENERATION(e: i53) - e //= 0x10 - return e % ECS_GENERATION_MASK -end - --- SECOND -local function ECS_ENTITY_T_LO(e: i53) - e //= 0x10 - return e // ECS_ENTITY_MASK -end - -local function ECS_GENERATION_INC(e: i53) - local id, generation, flags = separate(e) - - return ECS_COMBINE(id, generation + 1) + flags -end - --- FIRST gets the high ID -local function ECS_ENTITY_T_HI(entity: i53): i24 - entity //= 0x10 - local first = entity % ECS_ENTITY_MASK - return first -end - -local function ECS_PAIR(pred: number, obj: number) - local first - local second: number = WILDCARD - - if pred == WILDCARD then - first = obj - elseif obj == WILDCARD then - first = pred - else - first = obj - second = ECS_ENTITY_T_LO(pred) - end - - return ECS_COMBINE( - ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true) -end - -local function getAlive(entityIndex: EntityIndex, id: i24) - local entityId = entityIndex.dense[id] - local record = entityIndex.sparse[entityIndex.dense[id]] - if not record then - error(id.." is not alive") - end - return entityId -end - --- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits -local function ECS_PAIR_RELATION(entityIndex, e) - assert(ECS_IS_PAIR(e)) - return getAlive(entityIndex, ECS_ENTITY_T_HI(e)) -end - --- ECS_PAIR_SECOND gets the relationship / pred / LOW bits -local function ECS_PAIR_OBJECT(entityIndex, e) - assert(ECS_IS_PAIR(e)) - return getAlive(entityIndex, ECS_ENTITY_T_LO(e)) -end - -local function nextEntityId(entityIndex, index: i24): i53 - local id = ECS_COMBINE(index, 0) - entityIndex.sparse[id] = { - dense = index - } :: Record - entityIndex.dense[index] = id - - return id -end - -local function transitionArchetype( - entityIndex: EntityIndex, - to: Archetype, - destinationRow: i24, - from: Archetype, - sourceRow: i24 -) - local columns = from.columns - local sourceEntities = from.entities - local destinationEntities = to.entities - local destinationColumns = to.columns - local tr = to.records - local types = from.types - - for i, column in columns do - -- 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 targetColumn = destinationColumns[tr[types[i]]] - - -- Sometimes target column may not exist, e.g. when you remove a component. - if targetColumn then - targetColumn[destinationRow] = column[sourceRow] - end - -- If the entity is the last row in the archetype then swapping it would be meaningless. - local last = #column - if sourceRow ~= last then - -- Swap rempves columns to ensure there are no holes in the archetype. - column[sourceRow] = column[last] - end - column[last] = nil - end - - local sparse = entityIndex.sparse - local movedAway = #sourceEntities - - -- 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 = sourceEntities[sourceRow] - local e2 = sourceEntities[movedAway] - - if sourceRow ~= movedAway then - sourceEntities[sourceRow] = e2 - end - - sourceEntities[movedAway] = nil - destinationEntities[destinationRow] = e1 - - local record1 = sparse[e1] - local record2 = sparse[e2] - - record1.row = destinationRow - record2.row = sourceRow -end - -local function archetypeAppend(entity: number, archetype: Archetype): number - local entities = archetype.entities - local length = #entities + 1 - entities[length] = entity - return length -end - -local function newEntity(entityId: i53, record: Record, archetype: Archetype) - local row = archetypeAppend(entityId, archetype) - record.archetype = archetype - record.row = row - return record -end - -local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype) - local sourceRow = record.row - local from = record.archetype - local destinationRow = archetypeAppend(entityId, to) - transitionArchetype(entityIndex, to, destinationRow, from, sourceRow) - record.archetype = to - record.row = destinationRow -end - -local function hash(arr): string | number - return table.concat(arr, "_") -end - -local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId, componentId, i): ArchetypeMap - local archetypesMap = componentIndex[componentId] - - if not archetypesMap then - archetypesMap = {size = 0, cache = {}, first = {}, second = {}} :: ArchetypeMap - componentIndex[componentId] = archetypesMap - end - - archetypesMap.cache[archetypeId] = i - archetypesMap.size += 1 - - return archetypesMap -end - -local function ECS_ID_IS_WILDCARD(e) - assert(ECS_IS_PAIR(e)) - local first = ECS_ENTITY_T_HI(e) - local second = ECS_ENTITY_T_LO(e) - return first == WILDCARD or second == WILDCARD -end - - -local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype - local ty = hash(types) - - local id = world.nextArchetypeId + 1 - world.nextArchetypeId = id - - local length = #types - local columns = table.create(length) - local componentIndex = world.componentIndex - - local records = {} - for i, componentId in types do - ensureComponentRecord(componentIndex, id, componentId, i) - records[componentId] = i - if ECS_IS_PAIR(componentId) then - local relation = ECS_PAIR_RELATION(world.entityIndex, componentId) - local object = ECS_PAIR_OBJECT(world.entityIndex, componentId) - - local idr_r = ECS_PAIR(relation, WILDCARD) - ensureComponentRecord( - componentIndex, id, idr_r, i) - records[idr_r] = i - - local idr_t = ECS_PAIR(WILDCARD, object) - ensureComponentRecord( - componentIndex, id, idr_t, i) - records[idr_t] = i - end - columns[i] = {} - end - - local archetype = { - columns = columns; - edges = {}; - entities = {}; - id = id; - records = records; - type = ty; - types = types; - } - world.archetypeIndex[ty] = archetype - world.archetypes[id] = archetype - - return archetype -end - -local World = {} -World.__index = World -function World.new() - local self = setmetatable({ - archetypeIndex = {}; - archetypes = {} :: Archetypes; - componentIndex = {} :: ComponentIndex; - entityIndex = { - dense = {}, - sparse = {} - } :: EntityIndex; - hooks = { - [ON_ADD] = {}; - }; - nextArchetypeId = 0; - nextComponentId = 0; - nextEntityId = 0; - ROOT_ARCHETYPE = (nil :: any) :: Archetype; - }, World) - self.ROOT_ARCHETYPE = archetypeOf(self, {}) - return self -end - -function World.component(world: World) - local componentId = world.nextComponentId + 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 - return nextEntityId(world.entityIndex, componentId) -end - -function World.entity(world: World) - local entityId = world.nextEntityId + 1 - world.nextEntityId = entityId - return nextEntityId(world.entityIndex, entityId + REST) -end - --- TODO: --- should have an additional `index` parameter which selects the nth target --- this is important when an entity can have multiple relationships with the same target -function World.target(world: World, entity: i53, relation: i24): i24? - local entityIndex = world.entityIndex - local record = entityIndex.sparse[entity] - local archetype = record.archetype - if not archetype then - return nil - end - local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)] - if not componentRecord then - return nil - end - - local archetypeRecord = componentRecord.cache[archetype.id] - if not archetypeRecord then - return nil - end - - return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord]) -end - --- should reuse this logic in World.set instead of swap removing in transition archetype -local function destructColumns(columns, count, row) - if row == count then - for _, column in columns do - column[count] = nil - end - else - for _, column in columns do - column[row] = column[count] - column[count] = nil - end - end -end - -local function archetypeDelete(world: World, id: i53) - local componentIndex = world.componentIndex - local archetypesMap = componentIndex[id] - local archetypes = world.archetypes - if archetypesMap then - for archetypeId in archetypesMap.cache do - for _, entity in archetypes[archetypeId].entities do - world:remove(entity, id) - end - end - - componentIndex[id] = nil - end -end - -function World.delete(world: World, entityId: i53) - local record = world.entityIndex.sparse[entityId] - if not record then - return - end - local entityIndex = world.entityIndex - local sparse, dense = entityIndex.sparse, entityIndex.dense - local archetype = record.archetype - local row = record.row - - archetypeDelete(world, entityId) - -- TODO: should traverse linked )component records to pairs including entityId - archetypeDelete(world, ECS_PAIR(entityId, WILDCARD)) - archetypeDelete(world, ECS_PAIR(WILDCARD, entityId)) - - if archetype then - local entities = archetype.entities - local last = #entities - - if row ~= last then - local entityToMove = entities[last] - dense[record.dense] = entityToMove - sparse[entityToMove] = record - end - - entities[row], entities[last] = entities[last], nil - - local columns = archetype.columns - - destructColumns(columns, last, row) - end - - sparse[entityId] = nil - dense[#dense] = nil -end - -export type World = typeof(World.new()) - -local function ensureArchetype(world: World, types, prev) - 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 archetypeOf(world, types, prev) -end - -local function findInsert(types: {i53}, toAdd: i53) - 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 findArchetypeWith(world: World, node: Archetype, componentId: i53) - 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 destinationType = table.clone(node.types) - local at = findInsert(types, componentId) - 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(destinationType, at, componentId) - - return ensureArchetype(world, destinationType, node) -end - -local function ensureEdge(archetype: Archetype, componentId: i53) - local edges = archetype.edges - local edge = edges[componentId] - if not edge then - edge = {} :: any - edges[componentId] = edge - end - return edge -end - -local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype - from = from or world.ROOT_ARCHETYPE - - local edge = ensureEdge(from, componentId) - local add = edge.add - if not add then - -- Save an edge using the component ID to the archetype to allow - -- faster traversals to adjacent archetypes. - add = findArchetypeWith(world, from, componentId) - edge.add = add :: never - end - - return add -end - -function World.add(world: World, entityId: i53, componentId: i53) - local entityIndex = world.entityIndex - local record = entityIndex.sparse[entityId] - local from = record.archetype - local to = archetypeTraverseAdd(world, componentId, from) - if from and not (from == world.ROOT_ARCHETYPE) then - moveEntity(entityIndex, entityId, record, to) - else - if #to.types > 0 then - newEntity(entityId, record, to) - end - end -end - --- Symmetric like `World.add` but idempotent -function World.set(world: World, entityId: i53, componentId: i53, data: unknown) - local record = world.entityIndex.sparse[entityId] - local from = record.archetype - local to = archetypeTraverseAdd(world, componentId, from) - - if from == to then - -- If the archetypes are the same it can avoid moving the entity - -- and just set the data directly. - local archetypeRecord = to.records[componentId] - from.columns[archetypeRecord][record.row] = data - -- Should fire an OnSet event here. - return - end - - if from then - -- If there was a previous archetype, then the entity needs to move the archetype - moveEntity(world.entityIndex, entityId, record, to) - else - if #to.types > 0 then - -- When there is no previous archetype it should create the archetype - newEntity(entityId, record, to) - end - end - - local archetypeRecord = to.records[componentId] - to.columns[archetypeRecord][record.row] = data -end - -local function archetypeTraverseRemove(world: World, componentId: i53, from: Archetype): Archetype - local edge = ensureEdge(from, componentId) - - local remove = edge.remove - if not remove then - local to = table.clone(from.types) - local at = table.find(to, componentId) - if not at then - return from - end - table.remove(to, at) - remove = ensureArchetype(world, to, from) - edge.remove = remove :: never - end - - return remove -end - -function World.remove(world: World, entityId: i53, componentId: i53) - local entityIndex = world.entityIndex - local record = entityIndex.sparse[entityId] - local sourceArchetype = record.archetype - local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) - - if sourceArchetype and not (sourceArchetype == destinationArchetype) then - moveEntity(entityIndex, entityId, record, destinationArchetype) - end -end - --- Keeping the function as small as possible to enable inlining -local function get(record: Record, componentId: i24) - local archetype = record.archetype - if not archetype then - return nil - end - - local archetypeRecord = archetype.records[componentId] - - if not archetypeRecord then - return nil - end - - return archetype.columns[archetypeRecord][record.row] -end - -function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) - local id = entityId - local record = world.entityIndex.sparse[id] - if not record then - return nil - end - - local va = get(record, a) - - if b == nil then - return va - elseif c == nil then - return va, get(record, b) - elseif d == nil then - return va, get(record, b), get(record, c) - elseif e == nil then - return va, get(record, b), get(record, c), get(record, d) - else - error("args exceeded") - end -end - -local function noop() end -local function iterNoop(_self: Query, ...: i53): () -> (number, ...any) - return noop :: any -end - -local EmptyQuery -EmptyQuery = { - __iter = iterNoop, - next = noop, - patch = noop, - without = function() - return EmptyQuery - end -} - -EmptyQuery.__index = EmptyQuery -setmetatable(EmptyQuery, EmptyQuery) - -export type Query = typeof(EmptyQuery) - -local function replace(row, columns, ...) - for i, column in columns do - column[row] = select(i, ...) - end -end - -function World.query(world: World, ...): Query - -- breaking? - if (...) == nil then - error("Missing components") - end - - local compatibleArchetypes = {} - local length = 0 - - local components = {...} - local archetypes = world.archetypes - local queryLength = #components - - local firstArchetypeMap - local componentIndex = world.componentIndex - - for _, componentId in components do - local map = componentIndex[componentId] - if not map then - return EmptyQuery - end - - if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then - firstArchetypeMap = map - end - end - - for id in firstArchetypeMap.cache do - local archetype = archetypes[id] - local archetypeRecords = archetype.records - - local indices = {} - local skip = false - - for i, componentId in components do - local index = archetypeRecords[componentId] - if not index then - skip = true - break - end - -- index should be index.offset - indices[i] = index - end - - if skip then - continue - end - - length += 1 - compatibleArchetypes[length] = { - archetype = archetype, - indices = indices - } - end - - local lastArchetype, compatibleArchetype = next(compatibleArchetypes) - if not lastArchetype then - return EmptyQuery - end - - local preparedQuery = {} - preparedQuery.__index = preparedQuery - - function preparedQuery:without(...) - local withoutComponents = {...} - for i = #compatibleArchetypes, 1, -1 do - local archetype = compatibleArchetypes[i].archetype - local records = archetype.records - local shouldRemove = false - - for _, componentId in withoutComponents do - if records[componentId] then - shouldRemove = true - break - end - end - - if shouldRemove then - table.remove(compatibleArchetypes, i) - end - end - - lastArchetype, compatibleArchetype = next(compatibleArchetypes) - if not lastArchetype then - return EmptyQuery - end - - return self - end - - local lastRow - local queryOutput = {} - - - function preparedQuery:patch(fn: any) - for _, compatibleArchetype in compatibleArchetypes do - local archetype = compatibleArchetype.archetype - local tr = compatibleArchetype.indices - local columns = archetype.columns - - for row in archetype.entities do - if queryLength == 1 then - local a = columns[tr[1]] - local pa = fn(a[row]) - - a[row] = pa - elseif queryLength == 2 then - local a = columns[tr[1]] - local b = columns[tr[2]] - - a[row], b[row] = fn(a[row], b[row]) - elseif queryLength == 3 then - local a = columns[tr[1]] - local b = columns[tr[2]] - local c = columns[tr[3]] - - a[row], b[row], c[row] = fn(a[row], b[row], c[row]) - elseif queryLength == 4 then - local a = columns[tr[1]] - local b = columns[tr[2]] - local c = columns[tr[3]] - local d = columns[tr[4]] - - a[row], b[row], c[row], d[row] = fn( - a[row], b[row], c[row], d[row]) - else - for i = 1, queryLength do - queryOutput[i] = columns[tr[i]][row] - end - replace(row, columns, fn(unpack(queryOutput))) - end - end - end - end - - local function iter() - local archetype = compatibleArchetype.archetype - local row: number = next(archetype.entities, lastRow) :: number - while row == nil do - lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) - if lastArchetype == nil then - return - end - archetype = compatibleArchetype.archetype - row = next(archetype.entities, row) :: number - end - lastRow = row - - local entityId = archetype.entities[row :: number] - local columns = archetype.columns - local tr = compatibleArchetype.indices - - if queryLength == 1 then - return entityId, columns[tr[1]][row] - elseif queryLength == 2 then - return entityId, columns[tr[1]][row], columns[tr[2]][row] - elseif queryLength == 3 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] - elseif queryLength == 4 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] - elseif queryLength == 5 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row], - columns[tr[5]][row] - elseif queryLength == 6 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row] - elseif queryLength == 7 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row], - columns[tr[7]][row] - elseif queryLength == 8 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row], - columns[tr[7]][row], - columns[tr[8]][row] - end - - for i in components do - queryOutput[i] = columns[tr[i]][row] - end - - return entityId, unpack(queryOutput, 1, queryLength) - end - - function preparedQuery:__iter() - return iter - end - - function preparedQuery:next() - return iter() - end - - return setmetatable({}, preparedQuery) :: any -end - -function World.__iter(world: World): () -> (number?, unknown?) - local dense = world.entityIndex.dense - local sparse = world.entityIndex.sparse - local last - - return function() - local lastEntity, entityId = next(dense, last) - if not lastEntity then - return - end - last = lastEntity - - local record = sparse[entityId] - local archetype = record.archetype - if not archetype then - -- Returns only the entity id as an entity without data should not return - -- data and allow the user to get an error if they don't handle the case. - return entityId - end - - local row = record.row - local types = archetype.types - local columns = archetype.columns - local entityData = {} - for i, column in columns do - -- We use types because the key should be the component ID not the column index - entityData[types[i]] = column[row] - end - - return entityId, entityData - end -end - -return table.freeze({ - World = World; - - OnAdd = ON_ADD; - OnRemove = ON_REMOVE; - OnSet = ON_SET; - Wildcard = WILDCARD, - w = WILDCARD, - Rest = REST, - - IS_PAIR = ECS_IS_PAIR, - ECS_ID = ECS_ENTITY_T_LO, - ECS_PAIR = ECS_PAIR, - ECS_GENERATION_INC = ECS_GENERATION_INC, - ECS_GENERATION = ECS_GENERATION, - ECS_PAIR_RELATION = ECS_PAIR_RELATION, - ECS_PAIR_OBJECT = ECS_PAIR_OBJECT, - - pair = ECS_PAIR, - getAlive = getAlive, -}) diff --git a/mirror/init.lua b/mirror/init.luau similarity index 100% rename from mirror/init.lua rename to mirror/init.luau diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..1a47333 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,186 @@ +site_name: Jecs +site_url: jecs.github.io/jecs +repo_name: ukendio/jecs +repo_url: https://github.com/ukendio/jecs + +extra: + version: + provider: mike + +theme: + name: material + custom_dir: docs/assets/overrides + logo: assets/logo + favicon: assets/logo-dark.svg + palette: + - media: "(prefers-color-scheme: dark)" + scheme: fusiondoc-dark + toggle: + icon: octicons/sun-24 + title: Switch to light theme + - media: "(prefers-color-scheme: light)" + scheme: fusiondoc-light + toggle: + icon: octicons/moon-24 + title: Switch to dark theme + font: + text: Plus Jakarta Sans + code: JetBrains Mono + features: + - navigation.tabs + - navigation.top + - navigation.sections + - navigation.instant + - navigation.indexes + - search.suggest + - search.highlight + icon: + repo: octicons/mark-github-16 + +extra_css: + - assets/theme/fusiondoc.css + - assets/theme/colours.css + - assets/theme/code.css + - assets/theme/paragraph.css + - assets/theme/page.css + - assets/theme/admonition.css + - assets/theme/404.css + - assets/theme/api-reference.css + - assets/theme/dev-tools.css + +extra_javascript: + - assets/scripts/smooth-scroll.js + +nav: + - Home: index.md + - Tutorials: + - Get Started: tutorials/index.md + - Installing Fusion: tutorials/get-started/installing-fusion.md + - Developer Tools: tutorials/get-started/developer-tools.md + - Getting Help: tutorials/get-started/getting-help.md + - Fundamentals: + - Scopes: tutorials/fundamentals/scopes.md + - Values: tutorials/fundamentals/values.md + - Observers: tutorials/fundamentals/observers.md + - Computeds: tutorials/fundamentals/computeds.md + - Tables: + - ForValues: tutorials/tables/forvalues.md + - ForKeys: tutorials/tables/forkeys.md + - ForPairs: tutorials/tables/forpairs.md + - Animation: + - Tweens: tutorials/animation/tweens.md + - Springs: tutorials/animation/springs.md + - Roblox: + - Hydration: tutorials/roblox/hydration.md + - New Instances: tutorials/roblox/new-instances.md + - Parenting: tutorials/roblox/parenting.md + - Events: tutorials/roblox/events.md + - Change Events: tutorials/roblox/change-events.md + - Outputs: tutorials/roblox/outputs.md + - References: tutorials/roblox/references.md + - Best Practices: + - Components: tutorials/best-practices/components.md + - Instance Handling: tutorials/best-practices/instance-handling.md + - Callbacks: tutorials/best-practices/callbacks.md + - State: tutorials/best-practices/state.md + - Sharing Values: tutorials/best-practices/sharing-values.md + - Error Safety: tutorials/best-practices/error-safety.md + - Optimisation: tutorials/best-practices/optimisation.md + + - Examples: + - Home: examples/index.md + - Cookbook: + - examples/cookbook/index.md + - Player List: examples/cookbook/player-list.md + - Animated Computed: examples/cookbook/animated-computed.md + - Fetch Data From Server: examples/cookbook/fetch-data-from-server.md + - Light & Dark Theme: examples/cookbook/light-and-dark-theme.md + - Button Component: examples/cookbook/button-component.md + - Loading Spinner: examples/cookbook/loading-spinner.md + - Drag & Drop: examples/cookbook/drag-and-drop.md + - API Reference: + - api-reference/index.md + - General: + - Errors: api-reference/general/errors.md + - Types: + - Contextual: api-reference/general/types/contextual.md + - Version: api-reference/general/types/version.md + - Members: + - Contextual: api-reference/general/members/contextual.md + - Safe: api-reference/general/members/safe.md + - version: api-reference/general/members/version.md + - Memory: + - Types: + - Scope: api-reference/memory/types/scope.md + - ScopedObject: api-reference/memory/types/scopedobject.md + - Task: api-reference/memory/types/task.md + - Members: + - deriveScope: api-reference/memory/members/derivescope.md + - doCleanup: api-reference/memory/members/docleanup.md + - scoped: api-reference/memory/members/scoped.md + - State: + - Types: + - UsedAs: api-reference/state/types/usedas.md + - Computed: api-reference/state/types/computed.md + - Dependency: api-reference/state/types/dependency.md + - Dependent: api-reference/state/types/dependent.md + - For: api-reference/state/types/for.md + - Observer: api-reference/state/types/observer.md + - StateObject: api-reference/state/types/stateobject.md + - Use: api-reference/state/types/use.md + - Value: api-reference/state/types/value.md + - Members: + - Computed: api-reference/state/members/computed.md + - ForKeys: api-reference/state/members/forkeys.md + - ForPairs: api-reference/state/members/forpairs.md + - ForValues: api-reference/state/members/forvalues.md + - Observer: api-reference/state/members/observer.md + - peek: api-reference/state/members/peek.md + - Value: api-reference/state/members/value.md + - Roblox: + - Types: + - Child: api-reference/roblox/types/child.md + - PropertyTable: api-reference/roblox/types/propertytable.md + - SpecialKey: api-reference/roblox/types/specialkey.md + - Members: + - Attribute: api-reference/roblox/members/attribute.md + - AttributeChange: api-reference/roblox/members/attributechange.md + - AttributeOut: api-reference/roblox/members/attributeout.md + - Children: api-reference/roblox/members/children.md + - Hydrate: api-reference/roblox/members/hydrate.md + - New: api-reference/roblox/members/new.md + - OnChange: api-reference/roblox/members/onchange.md + - OnEvent: api-reference/roblox/members/onevent.md + - Out: api-reference/roblox/members/out.md + - Ref: api-reference/roblox/members/ref.md + - Animation: + - Types: + - Animatable: api-reference/animation/types/animatable.md + - Spring: api-reference/animation/types/spring.md + - Tween: api-reference/animation/types/tween.md + - Members: + - Tween: api-reference/animation/members/tween.md + - Spring: api-reference/animation/members/spring.md + - Extras: + - Home: extras/index.md + - Backgrounds: extras/backgrounds.md + - Brand Guidelines: extras/brand-guidelines.md + +markdown_extensions: + - admonition + - attr_list + - meta + - md_in_html + - pymdownx.superfences + - pymdownx.betterem + - pymdownx.details + - pymdownx.tabbed: + alternate_style: true + - pymdownx.inlinehilite + - toc: + permalink: true + - pymdownx.highlight: + guess_lang: false + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..964ddec --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2199 @@ +{ + "name": "@rbxts/jecs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@rbxts/jecs", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@rbxts/compiler-types": "^2.3.0-types.1", + "@rbxts/types": "^1.0.781", + "@typescript-eslint/eslint-plugin": "^5.8.0", + "@typescript-eslint/parser": "^5.8.0", + "eslint": "^8.5.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-roblox-ts": "^0.0.32", + "prettier": "^2.5.1", + "roblox-ts": "^2.3.0", + "typescript": "^5.4.2" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rbxts/compiler-types": { + "version": "2.3.0-types.1", + "resolved": "https://registry.npmjs.org/@rbxts/compiler-types/-/compiler-types-2.3.0-types.1.tgz", + "integrity": "sha512-NZWNo+fC4icfJte+NiDSDdWJo1KwzD0MDQ2iBi70YhNYlkA7+Xc+B1udbVqxLJuBur2JxG5bbKrpMAfHqfCtbw==", + "dev": true + }, + "node_modules/@rbxts/types": { + "version": "1.0.781", + "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.781.tgz", + "integrity": "sha512-q8NwgHqyKiyhl3q22tSxCD8S796T88hh/itUJim+XYC010f7GZv2jNKzJcIjp+2d+iKVxgRFvx9go8nHBDjwDA==", + "dev": true + }, + "node_modules/@roblox-ts/luau-ast": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@roblox-ts/luau-ast/-/luau-ast-1.0.11.tgz", + "integrity": "sha512-+maoLYpqY0HK8ugLFHS3qz0phMyDaN3i21jjW75T2ZaqJg84heKDUo98iXClvnx3mUDhW10IxqH+cYJ2iftYhQ==", + "dev": true + }, + "node_modules/@roblox-ts/path-translator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@roblox-ts/path-translator/-/path-translator-1.0.0.tgz", + "integrity": "sha512-Lp6qVUqjmXIrICy2KPKRiX8IkJ+lNqn6RqoUplLiTr+4JehIN+mJv0tTnE72XRyIfcx0VWl5nKrRwUuqcOj1yg==", + "dev": true, + "dependencies": { + "ajv": "^8.12.0", + "fs-extra": "^11.2.0" + } + }, + "node_modules/@roblox-ts/path-translator/node_modules/ajv": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", + "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@roblox-ts/path-translator/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@roblox-ts/rojo-resolver": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@roblox-ts/rojo-resolver/-/rojo-resolver-1.0.6.tgz", + "integrity": "sha512-+heTECMo6BdH3a3h4DCj+8kJvwKuxWqBevcW/m2BzQaVtmo1GtLa4V4bJCMvDuAMeEqYKQZUB7546nN2dcqqAA==", + "dev": true, + "dependencies": { + "ajv": "^8.12.0", + "fs-extra": "^11.1.1" + } + }, + "node_modules/@roblox-ts/rojo-resolver/node_modules/ajv": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", + "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@roblox-ts/rojo-resolver/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.18.98", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.98.tgz", + "integrity": "sha512-fpiC20NvLpTLAzo3oVBKIqBGR6Fx/8oAK/SSf7G+fydnXMY1x4x9RZ6sBXhqKlCU21g2QapUsbLlhv3+a7wS+Q==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", + "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-roblox-ts": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/eslint-plugin-roblox-ts/-/eslint-plugin-roblox-ts-0.0.32.tgz", + "integrity": "sha512-zbwahPiQha5KGwY/J3pVXtyR4ORBSP8qouc4DGfnyGcdz0HOFFu+sACWX2u7/c4HVymtZlKRkTL4uR5qZ+THgg==", + "dev": true, + "dependencies": { + "@types/node": "^16.10.4", + "@typescript-eslint/experimental-utils": "^5.0.0", + "typescript": "^4.4.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-roblox-ts/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roblox-ts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/roblox-ts/-/roblox-ts-2.3.0.tgz", + "integrity": "sha512-swz+3sxHcB1ww5iUkwxzPFqrbWYmjD9uDriLhta5MAShvRFW4Vdku/aBSU4KiLqtVWYvYo32G+5bXg1Pw2yvIA==", + "dev": true, + "dependencies": { + "@roblox-ts/luau-ast": "^1.0.11", + "@roblox-ts/path-translator": "^1.0.0", + "@roblox-ts/rojo-resolver": "^1.0.6", + "chokidar": "^3.6.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "resolve": "^1.22.6", + "typescript": "=5.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "rbxtsc": "out/CLI/cli.js" + } + }, + "node_modules/roblox-ts/node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ce2b802 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "@rbxts/jecs", + "version": "0.1.0", + "description": "Stupidly fast Entity Component System", + "main": "lib/init.lua", + "repository": { + "type": "git", + "url": "https://github.com/ukendio/jecs.git" + }, + "scripts": { + "build": "rbxtsc", + "watch": "rbxtsc -w", + "prepublishOnly": "npm run build" + }, + "keywords": [], + "author": "Ukendio", + "contributors": [ + "Ukendio", + "EncodedVenom" + ], + "homepage": "https://github.com/ukendio/jecs", + "license": "MIT", + "types": "lib/index.d.ts", + "files": [ + "lib/" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@rbxts/compiler-types": "^2.3.0-types.1", + "@rbxts/types": "^1.0.781", + "@typescript-eslint/eslint-plugin": "^5.8.0", + "@typescript-eslint/parser": "^5.8.0", + "eslint": "^8.5.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-roblox-ts": "^0.0.32", + "prettier": "^2.5.1", + "roblox-ts": "^2.3.0", + "typescript": "^5.4.2" + } +} diff --git a/rgb.lua b/rgb.luau similarity index 100% rename from rgb.lua rename to rgb.luau diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..66ae62d --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,153 @@ +type Query = { + without: (...components: Entity[]) => Query; +} & IterableFunction>; + +// Utility Types +export type Entity = number & { __nominal_type_dont_use: T }; +export type EntityType = T extends Entity ? A : never; +export type InferComponents = { + [K in keyof A]: EntityType; +}; +type Nullable = { + [K in keyof T]: T[K] | undefined; +}; + +export class World { + /** + * Creates a new World + */ + constructor(); + + /** + * Creates a new entity + * @returns Entity + */ + entity(): Entity; + + /** + * Creates a new entity located in the first 256 ids. + * These should be used for static components for fast access. + * @returns Entity + */ + component(): Entity; + + /** + * Gets the target of a relationship. For example, when a user calls + * `world.target(id, ChildOf(parent))`, you will obtain the parent entity. + * @param id Entity + * @param relation The Relationship + * @returns The Parent Entity if it exists + */ + target(id: Entity, relation: Entity): Entity | undefined; + + /** + * Deletes an entity and all its related components and relationships. + * @param id Entity to be destroyed + */ + delete(id: Entity): void; + + /** + * Adds a component to the entity with no value + * @param id Target Entity + * @param component Component + */ + add(id: Entity, component: Entity): void; + + /** + * Assigns a value to a component on the given entity + * @param id Target Entity + * @param component Target Component + * @param data Component Data + */ + set(id: Entity, component: Entity, data: T): void; + + /** + * Removes a component from the given entity + * @param id Target Entity + * @param component Target Component + */ + remove(id: Entity, component: Entity): void; + + // Manually typed out get since there is a hard limit. + + /** + * Retrieves the value of one component. This value may be undefined. + * @param id Target Entity + * @param component Target Component + * @returns Data associated with the component if it exists + */ + get(id: number, component: Entity): A | undefined; + + /** + * Retrieves the value of two components. This value may be undefined. + * @param id Target Entity + * @param component Target Component 1 + * @param component2 Target Component 2 + * @returns Data associated with the components if it exists + */ + get( + id: number, + component: Entity, + component2: Entity + ): LuaTuple>; + + /** + * Retrieves the value of three components. This value may be undefined. + * @param id Target Entity + * @param component Target Component 1 + * @param component2 Target Component 2 + * @param component3 Target Component 3 + * @returns Data associated with the components if it exists + */ + get( + id: number, + component: Entity, + component2: Entity, + component3: Entity + ): LuaTuple>; + + /** + * Retrieves the value of four components. This value may be undefined. + * @param id Target Entity + * @param component Target Component 1 + * @param component2 Target Component 2 + * @param component3 Target Component 3 + * @param component4 Target Component 4 + * @returns Data associated with the components if it exists + */ + get( + id: number, + component: Entity, + component2: Entity, + component3: Entity, + component4: Entity + ): LuaTuple>; + + /** + * Searches the world for entities that match a given query + * @param components Queried Components + * @returns Iterable function + */ + query(...components: T): Query>; +} + +/** + * Creates a composite key. + * @param pred The first entity + * @param obj The second entity + * @returns The composite key + */ +export const pair: (pred: Entity, obj: Entity) => Entity; + +/** + * Checks if the entity is a composite key + * @param e The entity to check + * @returns If the entity is a pair + */ +export const IS_PAIR: (e: Entity) => boolean; + +export const OnAdd: Entity; +export const OnRemove: Entity; +export const OnSet: Entity; +export const Wildcard: Entity; +export const Rest: Entity; \ No newline at end of file diff --git a/src/init.luau b/src/init.luau new file mode 100644 index 0000000..d89e406 --- /dev/null +++ b/src/init.luau @@ -0,0 +1,1053 @@ +--!optimize 2 +--!native +--!strict +--draft 4 + +type i53 = number +type i24 = number + +type Ty = { i53 } +type ArchetypeId = number + +type Column = { any } + +type ArchetypeEdge = { + add: Archetype, + remove: Archetype, +} + +type Archetype = { + id: number, + edges: { [i53]: ArchetypeEdge }, + types: Ty, + type: string | number, + entities: { number }, + columns: { Column }, + records: { [number]: number }, +} + +type Record = { + archetype: Archetype, + row: number, + dense: i24, + componentRecord: ArchetypeMap, +} + +type EntityIndex = { dense: { [i24]: i53 }, sparse: { [i53]: Record } } + +type ArchetypeRecord = number +--[[ +TODO: +{ + index: number, + count: number, + column: number +} + +]] + +type ArchetypeMap = { + cache: { ArchetypeRecord }, + first: ArchetypeMap, + second: ArchetypeMap, + parent: ArchetypeMap, + size: number, +} + +type ComponentIndex = { [i24]: ArchetypeMap } + +type Archetypes = { [ArchetypeId]: Archetype } + +type ArchetypeDiff = { + added: Ty, + removed: Ty, +} + +local HI_COMPONENT_ID = 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 EcsRest = HI_COMPONENT_ID + 7 + +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 function addFlags(isPair: boolean): number + local typeFlags = 0x0 + + if isPair then + typeFlags = bit32.bor(typeFlags, ECS_PAIR_FLAG) -- HIGHEST bit in the ID. + end + if false then + typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. + end + + return typeFlags +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)) + addFlags(--[[isPair]] true) :: i53 +end + +local ERROR_ENTITY_NOT_ALIVE = "Entity is not alive" +local ERROR_GENERATION_INVALID = "INVALID GENERATION" + +local function getAlive(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 sparseGet(entityIndex, id) + return entityIndex.sparse[getAlive(entityIndex, id)] +end + +-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits +local function ECS_PAIR_RELATION(entityIndex, e) + return getAlive(entityIndex, ECS_ENTITY_T_HI(e)) +end + +-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits +local function ECS_PAIR_OBJECT(entityIndex, e) + return getAlive(entityIndex, ECS_ENTITY_T_LO(e)) +end + +local function nextEntityId(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 transitionArchetype(entityIndex: EntityIndex, to: Archetype, + destinationRow: i24, from: Archetype, sourceRow: i24) + + local columns = from.columns + local sourceEntities = from.entities + local destinationEntities = to.entities + local destinationColumns = to.columns + local tr = to.records + local types = from.types + + for i, column in columns do + -- 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 targetColumn = destinationColumns[tr[types[i]]] + + -- Sometimes target column may not exist, e.g. when you remove a component. + if targetColumn then + targetColumn[destinationRow] = column[sourceRow] + end + -- If the entity is the last row in the archetype then swapping it would be meaningless. + local last = #column + if sourceRow ~= last then + -- Swap rempves columns to ensure there are no holes in the archetype. + column[sourceRow] = column[last] + end + column[last] = nil + end + + local sparse = entityIndex.sparse + local movedAway = #sourceEntities + + -- 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 = sourceEntities[sourceRow] + local e2 = sourceEntities[movedAway] + + if sourceRow ~= movedAway then + sourceEntities[sourceRow] = e2 + end + + sourceEntities[movedAway] = nil :: any + destinationEntities[destinationRow] = e1 + + local record1 = sparse[e1] + local record2 = sparse[e2] + + record1.row = destinationRow + record2.row = sourceRow +end + +local function archetypeAppend(entity: number, archetype: Archetype): number + local entities = archetype.entities + local length = #entities + 1 + entities[length] = entity + return length +end + +local function newEntity(entityId: i53, record: Record, archetype: Archetype): Record + local row = archetypeAppend(entityId, archetype) + record.archetype = archetype + record.row = row + return record +end + +local function moveEntity(entityIndex: EntityIndex, entityId: i53, record: Record, to: Archetype) + local sourceRow = record.row + local from = record.archetype + local destinationRow = archetypeAppend(entityId, to) + transitionArchetype(entityIndex, to, destinationRow, from, sourceRow) + record.archetype = to + record.row = destinationRow +end + +local function hash(arr: { number }): string + return table.concat(arr, "_") +end + +local function ensureComponentRecord( + componentIndex: ComponentIndex, + archetypeId: number, + componentId: number, + i: number +): ArchetypeMap + local archetypesMap = componentIndex[componentId] + + if not archetypesMap then + archetypesMap = ({ size = 0, cache = {} } :: any) :: ArchetypeMap + componentIndex[componentId] = archetypesMap + end + + archetypesMap.cache[archetypeId] = i + archetypesMap.size += 1 + + return archetypesMap +end + +local function ECS_ID_IS_WILDCARD(e: i53): boolean + assert(ECS_IS_PAIR(e)) + local first = ECS_ENTITY_T_HI(e) + local second = ECS_ENTITY_T_LO(e) + return first == EcsWildcard or second == EcsWildcard +end + +local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archetype + local ty = hash(types) + + local id = world.nextArchetypeId + 1 + world.nextArchetypeId = id + + local length = #types + local columns = (table.create(length) :: any) :: { Column } + local componentIndex = world.componentIndex + + local records = {} + for i, componentId in types do + ensureComponentRecord(componentIndex, id, componentId, i) + records[componentId] = i + if ECS_IS_PAIR(componentId) then + local relation = ECS_PAIR_RELATION(world.entityIndex, componentId) + local object = ECS_PAIR_OBJECT(world.entityIndex, componentId) + + local idr_r = ECS_PAIR(relation, EcsWildcard) + ensureComponentRecord(componentIndex, id, idr_r, i) + records[idr_r] = i + + local idr_t = ECS_PAIR(EcsWildcard, object) + ensureComponentRecord(componentIndex, id, idr_t, i) + records[idr_t] = i + end + columns[i] = {} + end + + local archetype: Archetype = { + columns = columns, + edges = {}, + entities = {}, + id = id, + records = records, + type = ty, + types = types, + } + + world.archetypeIndex[ty] = archetype + world.archetypes[id] = archetype + + return archetype +end + +export type World = { + archetypeIndex: { [string]: Archetype }, + archetypes: Archetypes, + componentIndex: ComponentIndex, + entityIndex: EntityIndex, + nextArchetypeId: number, + nextComponentId: number, + nextEntityId: number, + ROOT_ARCHETYPE: Archetype +} + +local function entity(world: World): i53 + local entityId = world.nextEntityId + 1 + world.nextEntityId = entityId + return nextEntityId(world.entityIndex, entityId + EcsRest) +end + +-- TODO: +-- should have an additional `nth` parameter which selects the nth target +-- this is important when an entity can have multiple relationships with the same target +local function target(world: World, entity: i53, relation: i24--[[, nth: number]]): i24? + local entityIndex = world.entityIndex + local record = entityIndex.sparse[entity] + local archetype = record.archetype + if not archetype then + return nil + end + + local componentRecord = world.componentIndex[ECS_PAIR(relation, EcsWildcard)] + if not componentRecord then + return nil + end + + local archetypeRecord = componentRecord.cache[archetype.id] + if not archetypeRecord then + return nil + end + + return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord]) +end + +local function parent(world: World, entity: i53) + return target(world, entity, EcsChildOf) +end + +local function ensureArchetype(world: World, types, prev): 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 archetypeOf(world, types, prev) +end + +local function findInsert(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 findArchetypeWith(world: World, node: Archetype, componentId: 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 destinationType = table.clone(node.types) :: { i53 } + local at = findInsert(types, componentId) + 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(destinationType, at, componentId) + + return ensureArchetype(world, destinationType, node) +end + +local function ensureEdge(archetype: Archetype, componentId: i53): ArchetypeEdge + local edges = archetype.edges + local edge = edges[componentId] + if not edge then + edge = {} :: any + edges[componentId] = edge + end + return edge +end + +local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype + from = from or world.ROOT_ARCHETYPE + + local edge = ensureEdge(from, componentId) + local add = edge.add + if not add then + -- Save an edge using the component ID to the archetype to allow + -- faster traversals to adjacent archetypes. + add = findArchetypeWith(world, from, componentId) + edge.add = add :: never + end + + return add +end + +local function add(world: World, entityId: i53, componentId: i53) + local entityIndex = world.entityIndex + local record = entityIndex.sparse[entityId] + local from = record.archetype + local to = archetypeTraverseAdd(world, componentId, from) + if from and not (from == world.ROOT_ARCHETYPE) then + moveEntity(entityIndex, entityId, record, to) + else + if #to.types > 0 then + newEntity(entityId, record, to) + end + end +end + +-- Symmetric like `World.add` but idempotent +local function set(world: World, entityId: i53, componentId: i53, data: unknown) + local record = world.entityIndex.sparse[entityId] + local from = record.archetype + local to = archetypeTraverseAdd(world, componentId, from) + + if from == to then + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. + local archetypeRecord = to.records[componentId] + from.columns[archetypeRecord][record.row] = data + -- Should fire an OnSet event here. + return + end + + if from then + -- If there was a previous archetype, then the entity needs to move the archetype + moveEntity(world.entityIndex, entityId, record, to) + else + if #to.types > 0 then + -- When there is no previous archetype it should create the archetype + newEntity(entityId, record, to) + end + end + + local archetypeRecord = to.records[componentId] + to.columns[archetypeRecord][record.row] = data +end + +local function newComponent(world: World): i53 + local componentId = world.nextComponentId + 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 = nextEntityId(world.entityIndex, componentId) + add(world, id, EcsComponent) + return id +end + + +local function archetypeTraverseRemove(world: World, componentId: i53, from: Archetype): Archetype + local edge = ensureEdge(from, componentId) + + local remove = edge.remove + if not remove then + local to = table.clone(from.types) :: { i53 } + local at = table.find(to, componentId) + if not at then + return from + end + table.remove(to, at) + remove = ensureArchetype(world, to, from) + edge.remove = remove :: never + end + + return remove +end + +local function remove(world: World, entityId: i53, componentId: i53) + local entityIndex = world.entityIndex + local record = entityIndex.sparse[entityId] + local sourceArchetype = record.archetype + local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) + + if sourceArchetype and not (sourceArchetype == destinationArchetype) then + moveEntity(entityIndex, entityId, record, destinationArchetype) + end +end + +-- should reuse this logic in World.set instead of swap removing in transition archetype +local function destructColumns(columns: { Column }, count: number, row: number) + if row == count then + for _, column in columns do + column[count] = nil + end + else + for _, column in columns do + column[row] = column[count] + column[count] = nil + end + end +end + +local function archetypeDelete(world: World, id: i53) + local componentIndex = world.componentIndex + local archetypesMap = componentIndex[id] + local archetypes = world.archetypes + + if archetypesMap then + for archetypeId in archetypesMap.cache do + for _, entity in archetypes[archetypeId].entities do + remove(world, entity, id) + end + end + + componentIndex[id] = nil :: any + end +end + +local function delete(world: World, entityId: i53) + local record = world.entityIndex.sparse[entityId] + if not record then + return + end + local entityIndex = world.entityIndex + local sparse, dense = entityIndex.sparse, entityIndex.dense + local archetype = record.archetype + local row = record.row + + archetypeDelete(world, entityId) + -- TODO: should traverse linked )component records to pairs including entityId + archetypeDelete(world, ECS_PAIR(entityId, EcsWildcard)) + archetypeDelete(world, ECS_PAIR(EcsWildcard, entityId)) + + if archetype then + local entities = archetype.entities + local last = #entities + + if row ~= last then + local entityToMove = entities[last] + dense[record.dense] = entityToMove + sparse[entityToMove] = record + end + + entities[row], entities[last] = entities[last], nil :: any + + local columns = archetype.columns + + destructColumns(columns, last, row) + end + + sparse[entityId] = nil :: any + dense[#dense] = nil :: any + +end + +local function clear(world: World, entityId: i53) + --TODO: use sparse_get (stashed) + local record = world.entityIndex.sparse[entityId] + 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 + + moveEntity(world.entityIndex, entityId, record, ROOT_ARCHETYPE) +end + +-- Keeping the function as small as possible to enable inlining +local function fetch(record: Record, componentId: i24): any + local archetype = record.archetype + if not archetype then + return nil + end + + local archetypeRecord = archetype.records[componentId] + + if not archetypeRecord then + return nil + end + + return archetype.columns[archetypeRecord][record.row] +end + +local function get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any + local id = entityId + local record = world.entityIndex.sparse[id] + if not record then + return nil + end + + local va = fetch(record, a) + + if b == nil then + return va + elseif c == nil then + return va, fetch(record, b) + elseif d == nil then + return va, fetch(record, b), fetch(record, c) + elseif e == nil then + return va, fetch(record, b), fetch(record, c), fetch(record, d) + else + error("args exceeded") + end +end + +-- the less creation the better +local function actualNoOperation() end +local function noop(_self: Query, ...): () -> () + return actualNoOperation :: any +end + +local EmptyQuery = { + __iter = noop, + without = noop, +} +EmptyQuery.__index = EmptyQuery +setmetatable(EmptyQuery, EmptyQuery) + +export type Query = typeof(EmptyQuery) + +type CompatibleArchetype = { archetype: Archetype, indices: { number } } + +local function preparedQuery(compatibleArchetypes: { Archetype }, + components: { i53? }, indices: { { number } }) + + local queryLength = #components + + local lastArchetype = 1 + local archetype: Archetype = compatibleArchetypes[lastArchetype] + + if not archetype then + return EmptyQuery + end + + local queryOutput = {} + + local entities = archetype.entities + local i = #entities + + local function queryNext(): ...any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatibleArchetypes[lastArchetype] + + if not archetype then + return + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + end + + local row = i + i-=1 + + local columns = archetype.columns + local tr = indices[lastArchetype] + + if queryLength == 1 then + return entityId, columns[tr[1]][row] + elseif queryLength == 2 then + return entityId, columns[tr[1]][row], columns[tr[2]][row] + elseif queryLength == 3 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] + elseif queryLength == 4 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] + elseif queryLength == 5 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row] + elseif queryLength == 6 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row] + elseif queryLength == 7 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row], + columns[tr[7]][row] + elseif queryLength == 8 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row], + columns[tr[7]][row], + columns[tr[8]][row] + end + + for i in components do + queryOutput[i] = columns[tr[i]][row] + end + + return entityId, unpack(queryOutput, 1, queryLength) + end + + local function without(self, ...): Query + local withoutComponents = { ... } + for i = #compatibleArchetypes, 1, -1 do + local archetype = compatibleArchetypes[i] + local records = archetype.records + local shouldRemove = false + + for _, componentId in withoutComponents do + if records[componentId] then + shouldRemove = true + break + end + end + + if shouldRemove then + table.remove(compatibleArchetypes, i) + end + end + + return self + end + + local it = { + __iter = function() + lastArchetype = 1 + archetype = compatibleArchetypes[1] + entities = archetype.entities + i = #entities + + return queryNext + end, + next = queryNext, + without = without + } + + return setmetatable(it, it) :: any +end + +local function query(world: World, ...: number): Query + -- breaking? + if (...) == nil then + error("Missing components") + end + + local indices: { { number } } = {} + local compatibleArchetypes: { Archetype } = {} + local length = 0 + + local components: { number } = { ... } + local archetypes: { Archetype } = world.archetypes :: any + + local firstArchetypeMap: ArchetypeMap + local componentIndex = world.componentIndex + + for _, componentId in components do + local map: ArchetypeMap = componentIndex[componentId] :: any + if not map then + return EmptyQuery + end + + if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size < map.size then + firstArchetypeMap = map + end + end + + for id in firstArchetypeMap.cache do + local archetype = archetypes[id] + local archetypeRecords = archetype.records + + local records: { number } = {} + local skip = false + + for i, componentId in components do + local index = archetypeRecords[componentId] + if not index then + skip = true + break + end + -- index should be index.offset + records[i] = index + end + + if skip then + continue + end + + length += 1 + compatibleArchetypes[length] = archetype + indices[length] = records + end + + return preparedQuery(compatibleArchetypes, components, indices) +end + +type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) +-- __nominal_type_dont_use could not be any or T as it causes a type error +-- or produces a union +export type Entity = number & { __nominal_type_dont_use: T } +export type Pair = number + +export type QueryShim = typeof(setmetatable({ + without = function(...): QueryShim + return nil :: any + end, +}, { + __iter = function(): () -> (number, T...) + return nil :: any + end, +})) + +export type WorldShim = typeof(setmetatable( + {} :: { + + --- Creates a new entity + entity: (WorldShim) -> Entity, + --- Creates a new entity located in the first 256 ids. + --- These should be used for static components for fast access. + component: (WorldShim) -> Entity, + --- Gets the target of an relationship. For example, when a user calls + --- `world:target(id, ChildOf(parent))`, you will obtain the parent entity. + target: (WorldShim, id: Entity, relation: Entity) -> Entity?, + --- Deletes an entity and all it's related components and relationships. + delete: (WorldShim, id: Entity) -> (), + + --- Adds a component to the entity with no value + add: (WorldShim, id: Entity, component: Entity) -> (), + --- Assigns a value to a component on the given entity + set: (WorldShim, id: Entity, component: Entity, data: T) -> (), + --- Removes a component from the given entity + remove: (WorldShim, id: Entity, component: Entity) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: ((WorldShim, id: any, Entity) -> A) + & ((WorldShim, id: Entity, Entity, Entity) -> (A, B)) + & ((WorldShim, id: Entity, Entity, Entity, Entity) -> (A, B, C)) + & (WorldShim, id: Entity, Entity, Entity, Entity, Entity) -> (A, B, C, D), + + --- Searches the world for entities that match a given query + query: ((WorldShim, Entity) -> QueryShim) + & ((WorldShim, Entity, Entity) -> QueryShim) + & ((WorldShim, Entity, Entity, Entity) -> QueryShim) + & ((WorldShim, Entity, Entity, Entity, Entity) -> QueryShim) + & (( + WorldShim, + Entity, + Entity, + Entity, + Entity, + Entity + ) -> QueryShim) + & (( + WorldShim, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity + ) -> QueryShim) + & (( + WorldShim, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity + ) -> QueryShim) + & (( + WorldShim, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity + ) -> QueryShim) + & (( + WorldShim, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity + ) -> QueryShim) + & (( + WorldShim, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity + ) -> QueryShim) + & (( + WorldShim, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + ...Entity + ) -> QueryShim), + }, + {} :: { + __iter: (world: WorldShim) -> () -> (number, { [unknown]: unknown? }), + } +)) + +local World = {} +World.__index = World + +function World.new() + local self = setmetatable({ + archetypeIndex = {} :: { [string]: Archetype }, + archetypes = {} :: Archetypes, + componentIndex = {} :: ComponentIndex, + entityIndex = { + dense = {} :: { [i24]: i53 }, + sparse = {} :: { [i53]: Record }, + } :: EntityIndex, + hooks = { + [EcsOnAdd] = {}, + }, + nextArchetypeId = 0, + nextComponentId = 0, + nextEntityId = 0, + ROOT_ARCHETYPE = (nil :: any) :: Archetype, + }, World) + self.ROOT_ARCHETYPE = archetypeOf(self, {}) + + -- Initialize built-in components + nextEntityId(self.entityIndex, EcsChildOf) + + return self +end + +World.entity = entity +World.query = query +World.remove = remove +World.clear = clear +World.delete = delete +World.component = newComponent +World.add = add +World.set = set +World.get = get +World.target = target +World.parent = parent + +return { + World = World :: { new: () -> WorldShim }, + + OnAdd = EcsOnAdd :: Entity, + OnRemove = EcsOnRemove :: Entity, + OnSet = EcsOnSet :: Entity, + + Wildcard = EcsWildcard :: Entity, + w = EcsWildcard :: Entity, + ChildOf = EcsChildOf, + Component = EcsComponent, + + Rest = EcsRest, + + IS_PAIR = ECS_IS_PAIR, + ECS_ID = ECS_ENTITY_T_LO, + ECS_PAIR = ECS_PAIR, + ECS_GENERATION_INC = ECS_GENERATION_INC, + ECS_GENERATION = ECS_GENERATION, + ECS_PAIR_RELATION = ECS_PAIR_RELATION, + ECS_PAIR_OBJECT = ECS_PAIR_OBJECT, + + pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> number, + getAlive = getAlive, +} diff --git a/testez.d.lua b/testez.d.luau similarity index 100% rename from testez.d.lua rename to testez.d.luau diff --git a/testkit.lua b/testkit.luau similarity index 100% rename from testkit.lua rename to testkit.luau diff --git a/tests/world.lua b/tests/world.lua deleted file mode 100644 index f0eff7d..0000000 --- a/tests/world.lua +++ /dev/null @@ -1,339 +0,0 @@ -local testkit = require("../testkit") -local jecs = require("../lib/init") -local __ = jecs.Wildcard -local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION -local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC -local IS_PAIR = jecs.IS_PAIR -local ECS_PAIR = jecs.ECS_PAIR -local getAlive = jecs.getAlive -local ECS_PAIR_RELATION = jecs.ECS_PAIR_RELATION -local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT - -local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() -local function CHECK_NO_ERR(s: string, fn: (T...) -> (), ...: T...) - local ok, err: string? = pcall(fn, ...) - - if not CHECK(not ok, 2) then - local i = string.find(err :: string, " ") - assert(i) - local msg = string.sub(err :: string, i+1) - CHECK(msg == s, 2) - end -end -local N = 10 - -TEST("world", function() - do CASE "should be iterable" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - local eA = world:entity() - world:set(eA, A, true) - local eB = world:entity() - world:set(eB, B, true) - local eAB = world:entity() - world:set(eAB, A, true) - world:set(eAB, B, true) - - local count = 0 - for id, data in world do - count += 1 - if id == eA then - CHECK(data[A] == true) - CHECK(data[B] == nil) - elseif id == eB then - CHECK(data[A] == nil) - CHECK(data[B] == true) - elseif id == eAB then - CHECK(data[A] == true) - CHECK(data[B] == true) - end - end - - -- components are registered in the entity index as well - -- so this test has to add 2 to account for them - CHECK(count == 3 + 2) - end - - do CASE "should query all matching entities" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local entities = {} - for i = 1, N do - local id = world:entity() - - world:set(id, A, true) - if i > 5 then world:set(id, B, true) end - entities[i] = id - end - - for id in world:query(A) do - table.remove(entities, CHECK(table.find(entities, id))) - end - - CHECK(#entities == 0) - - end - - do CASE "should query all matching entities when irrelevant component is removed" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - local C = world:component() - - local entities = {} - for i = 1, N do - local id = world:entity() - - -- specifically put them in disorder to track regression - -- https://github.com/Ukendio/jecs/pull/15 - world:set(id, B, true) - world:set(id, A, true) - if i > 5 then world:remove(id, B) end - entities[i] = id - end - - local added = 0 - for id in world:query(A) do - added += 1 - table.remove(entities, CHECK(table.find(entities, id))) - end - - CHECK(added == N) - end - - do CASE "should query all entities without B" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local entities = {} - for i = 1, N do - local id = world:entity() - - world:set(id, A, true) - if i < 5 then - entities[i] = id - else - world:set(id, B, true) - end - - end - - for id in world:query(A):without(B) do - table.remove(entities, CHECK(table.find(entities, id))) - end - - CHECK(#entities == 0) - - end - - do CASE "should allow setting components in arbitrary order" - local world = jecs.World.new() - - local Health = world:entity() - local Poison = world:component() - - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) - - CHECK(world:get(id, Poison) == 5) - end - - do CASE "should allow deleting components" - local world = jecs.World.new() - - local Health = world:entity() - local Poison = world:component() - - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) - local id1 = world:entity() - world:set(id1, Poison, 500) - world:set(id1, Health, 50) - - world:delete(id) - - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == nil) - CHECK(world:get(id1, Poison) == 500) - CHECK(world:get(id1, Health) == 50) - - end - - do CASE "should allow remove that doesn't exist on entity" - local world = jecs.World.new() - - local Health = world:entity() - local Poison = world:component() - - local id = world:entity() - world:set(id, Health, 50) - world:remove(id, Poison) - - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == 50) - end - - do CASE "should increment generation" - local world = jecs.World.new() - local e = world:entity() - CHECK(ECS_ID(e) == 1 + jecs.Rest) - CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) - CHECK(ECS_GENERATION(e) == 0) -- 0 - e = ECS_GENERATION_INC(e) - CHECK(ECS_GENERATION(e) == 1) -- 1 - end - - do CASE "should get alive from index in the dense array" - local world = jecs.World.new() - local _e = world:entity() - local e2 = world:entity() - local e3 = world:entity() - - CHECK(IS_PAIR(world:entity()) == false) - - local pair = ECS_PAIR(e2, e3) - CHECK(IS_PAIR(pair) == true) - CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2) - CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3) - end - - do CASE "should allow querying for relations" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() - - world:set(bob, ECS_PAIR(Eats, Apples), true) - for e, bool in world:query(ECS_PAIR(Eats, Apples)) do - CHECK(e == bob) - CHECK(bool) - end - end - - do CASE "should allow wildcards in queries" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() - - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - - local w = jecs.Wildcard - for e, data in world:query(ECS_PAIR(Eats, w)) do - CHECK(e == bob) - CHECK(data == "bob eats apples") - end - for e, data in world:query(ECS_PAIR(w, Apples)) do - CHECK(e == bob) - CHECK(data == "bob eats apples") - end - end - - do CASE "should match against multiple pairs" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local Oranges =world:entity() - local bob = world:entity() - local alice = world:entity() - - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") - - local w = jecs.Wildcard - local count = 0 - for e, data in world:query(ECS_PAIR(Eats, w)) do - count += 1 - if e == bob then - CHECK(data == "bob eats apples") - else - CHECK(data == "alice eats oranges") - end - end - - CHECK(count == 2) - count = 0 - - for e, data in world:query(ECS_PAIR(w, Apples)) do - count += 1 - CHECK(data == "bob eats apples") - end - CHECK(count == 1) - end - - do CASE "should only relate alive entities" - - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local Oranges = world:entity() - local bob = world:entity() - local alice = world:entity() - - world:set(bob, Apples, "apples") - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") - - world:delete(Apples) - local Wildcard = jecs.Wildcard - - local count = 0 - for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do - count += 1 - end - - world:delete(ECS_PAIR(Eats, Apples)) - - CHECK(count == 0) - CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) - end - - do CASE "should error when setting invalid pair" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() - - world:delete(Apples) - - CHECK_NO_ERR("Apples should be dead", function() - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - end) - end - - do CASE "should find target for ChildOf" - local world = jecs.World.new() - - local ChildOf = world:component() - local Name = world:component() - - local function parent(entity) - return world:target(entity, ChildOf) - end - - local bob = world:entity() - local alice = world:entity() - local sara = world:entity() - - world:add(bob, ECS_PAIR(ChildOf, alice)) - world:set(bob, Name, "bob") - world:add(sara, ECS_PAIR(ChildOf, alice)) - world:set(sara, Name, "sara") - CHECK(parent(bob) == alice) -- O(1) - - local count = 0 - for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do - print(name) - count += 1 - end - CHECK(count == 2) - end -end) - -FINISH() \ No newline at end of file diff --git a/tests/world.luau b/tests/world.luau new file mode 100644 index 0000000..1aaa04c --- /dev/null +++ b/tests/world.luau @@ -0,0 +1,457 @@ +local jecs = require("../lib/init") +local testkit = require("../testkit") +local __ = jecs.Wildcard +local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION +local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC +local IS_PAIR = jecs.IS_PAIR +local ECS_PAIR = jecs.ECS_PAIR +local getAlive = jecs.getAlive +local ECS_PAIR_RELATION = jecs.ECS_PAIR_RELATION +local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT + +local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() +local function CHECK_NO_ERR(s: string, fn: (T...) -> (), ...: T...) + local ok, err: string? = pcall(fn, ...) + + if not CHECK(not ok, 2) then + local i = string.find(err :: string, " ") + assert(i) + local msg = string.sub(err :: string, i + 1) + CHECK(msg == s, 2) + end +end +local N = 10 + +type World = jecs.WorldShim + +TEST("world", function() + do CASE("should find every component id") + local world = jecs.World.new() :: World + local A = world:component() + local B = world:component() + world:entity() + world:entity() + world:entity() + + local count = 0 + for componentId in world:query(jecs.Component) do + if componentId ~= A and componentId ~= B then + error("found entity") + end + count += 1 + end + + CHECK(count == 2) + end + + do CASE("should remove its components") + local world = jecs.World.new() :: World + local A = world:component() + local B = world:component() + + local e = world:entity() + + world:set(e, A, true) + world:set(e, B, true) + + CHECK(world:get(e, A)) + CHECK(world:get(e, B)) + + world:clear(e) + CHECK(world:get(e, A) == nil) + CHECK(world:get(e, B) == nil) + + end + + do CASE("iterator should not drain the query") + local world = jecs.World.new() :: World + local A = world:component() + local B = world:component() + local eA = world:entity() + world:set(eA, A, true) + local eB = world:entity() + world:set(eB, B, true) + local eAB = world:entity() + world:set(eAB, A, true) + world:set(eAB, B, true) + + local q = world:query(A) + + local i = 0 + local j = 0 + for _ in q do + i+=1 + end + for _ in q do + j+=1 + end + CHECK(i == j) + end + + do CASE("should be able to get next results") + local world = jecs.World.new() :: World + world:component() + local A = world:component() + local B = world:component() + local eA = world:entity() + world:set(eA, A, true) + local eB = world:entity() + world:set(eB, B, true) + local eAB = world:entity() + world:set(eAB, A, true) + world:set(eAB, B, true) + + local q = world:query(A) + + local e, data = q:next() + while e do + CHECK( + if e == eA then data == true + elseif e == eAB then data == true + else false + ) + e, data = q:next() + end + end + + do CASE("should query all matching entities") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + + local entities = {} + for i = 1, N do + local id = world:entity() + + world:set(id, A, true) + if i > 5 then + world:set(id, B, true) + end + entities[i] = id + end + + for id in world:query(A) do + table.remove(entities, CHECK(table.find(entities, id))) + end + + CHECK(#entities == 0) + end + + do CASE("should query all matching entities when irrelevant component is removed") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() + + local entities = {} + for i = 1, N do + local id = world:entity() + + -- specifically put them in disorder to track regression + -- https://github.com/Ukendio/jecs/pull/15 + world:set(id, B, true) + world:set(id, A, true) + if i > 5 then + world:remove(id, B) + end + entities[i] = id + end + + local added = 0 + for id in world:query(A) do + added += 1 + table.remove(entities, CHECK(table.find(entities, id))) + end + + CHECK(added == N) + end + + do CASE("should query all entities without B") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + + local entities = {} + for i = 1, N do + local id = world:entity() + + world:set(id, A, true) + if i < 5 then + entities[i] = id + else + world:set(id, B, true) + end + end + + for id in world:query(A):without(B) do + table.remove(entities, CHECK(table.find(entities, id))) + end + + CHECK(#entities == 0) + end + + do CASE("should allow setting components in arbitrary order") + local world = jecs.World.new() + + local Health = world:entity() + local Poison = world:component() + + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) + + CHECK(world:get(id, Poison) == 5) + end + + do CASE("should allow deleting components") + local world = jecs.World.new() + + local Health = world:entity() + local Poison = world:component() + + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) + local id1 = world:entity() + world:set(id1, Poison, 500) + world:set(id1, Health, 50) + + world:delete(id) + + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == nil) + CHECK(world:get(id1, Poison) == 500) + CHECK(world:get(id1, Health) == 50) + end + + do CASE("should allow remove that doesn't exist on entity") + local world = jecs.World.new() + + local Health = world:entity() + local Poison = world:component() + + local id = world:entity() + world:set(id, Health, 50) + world:remove(id, Poison) + + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == 50) + end + + do CASE("should increment generation") + local world = jecs.World.new() + local e = world:entity() + CHECK(ECS_ID(e) == 1 + jecs.Rest) + CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) + CHECK(ECS_GENERATION(e) == 0) -- 0 + e = ECS_GENERATION_INC(e) + CHECK(ECS_GENERATION(e) == 1) -- 1 + end + + do CASE("should get alive from index in the dense array") + local world = jecs.World.new() + local _e = world:entity() + local e2 = world:entity() + local e3 = world:entity() + + CHECK(IS_PAIR(world:entity()) == false) + + local pair = ECS_PAIR(e2, e3) + CHECK(IS_PAIR(pair) == true) + + CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2) + CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3) + end + + do CASE("should allow querying for relations") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:set(bob, ECS_PAIR(Eats, Apples), true) + for e, bool in world:query(ECS_PAIR(Eats, Apples)) do + CHECK(e == bob) + CHECK(bool) + end + end + + do CASE("should allow wildcards in queries") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + + local w = jecs.Wildcard + for e, data in world:query(ECS_PAIR(Eats, w)) do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + for e, data in world:query(ECS_PAIR(w, Apples)) do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + end + + do CASE("should match against multiple pairs") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local Oranges = world:entity() + local bob = world:entity() + local alice = world:entity() + + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") + + local w = jecs.Wildcard + local count = 0 + for e, data in world:query(ECS_PAIR(Eats, w)) do + count += 1 + if e == bob then + CHECK(data == "bob eats apples") + else + CHECK(data == "alice eats oranges") + end + end + + CHECK(count == 2) + count = 0 + + for e, data in world:query(ECS_PAIR(w, Apples)) do + count += 1 + CHECK(data == "bob eats apples") + end + CHECK(count == 1) + end + + do CASE("should only relate alive entities") + + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local Oranges = world:entity() + local bob = world:entity() + local alice = world:entity() + + world:set(bob, Apples, "apples") + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") + + world:delete(Apples) + local Wildcard = jecs.Wildcard + + local count = 0 + for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do + count += 1 + end + + world:delete(ECS_PAIR(Eats, Apples)) + + CHECK(count == 0) + CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) + end + + do CASE("should error when setting invalid pair") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:delete(Apples) + + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + end + + do CASE("should find target for ChildOf") + local world = jecs.World.new() + local ChildOf = jecs.ChildOf + local pair = ECS_PAIR + + local Name = world:component() + + local bob = world:entity() + local alice = world:entity() + local sara = world:entity() + + world:add(bob, pair(ChildOf, alice)) + world:set(bob, Name, "bob") + world:add(sara, pair(ChildOf, alice)) + world:set(sara, Name, "sara") + CHECK(world:parent(bob) == alice) -- O(1) + + local count = 0 + for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do + count += 1 + end + CHECK(count == 2) + end + + do CASE "should be able to add/remove matching entity during iteration" + local world = jecs.World.new() + local Name = world:component() + for i = 1, 5 do + local e = world:entity() + world:set(e, Name, tostring(e)) + end + local count = 0 + for id, name in world:query(Name) do + count += 1 + CHECK(id == tonumber(name)) + + world:remove(id, Name) + local e = world:entity() + world:set(e, Name, tostring(e)) + end + CHECK(count == 5) + end + + do CASE "should allow adding a matching entity during iteration" + local world = jecs.World.new() + local A = world:component() + local B = world:component() + + local e1 = world:entity() + local e2 = world:entity() + world:add(e1, A) + world:add(e2, A) + world:add(e2, B) + + local count = 0 + for id in world:query(A) do + local e = world:entity() + world:add(e, A) + world:add(e, B) + count += 1 + end + + CHECK(count == 3) + end + + + do CASE "should not iterate same entity when adding component" + local world = jecs.World.new() + local A = world:component() + local B = world:component() + + local e1 = world:entity() + local e2 = world:entity() + world:add(e1, A) + world:add(e2, A) + world:add(e2, B) + + local count = 0 + for id in world:query(A) do + world:add(id, B) + + count += 1 + end + + print(count) + CHECK(count == 2) + end +end) + +FINISH() diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e0c819c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + // required + "allowSyntheticDefaultImports": true, + "downlevelIteration": true, + "jsx": "react", + "jsxFactory": "Roact.createElement", + "jsxFragmentFactory": "Roact.Fragment", + "module": "commonjs", + "moduleResolution": "Node", + "noLib": true, + "resolveJsonModule": true, + "strict": true, + "target": "ESNext", + "typeRoots": ["node_modules/@rbxts"], + + // configurable + "rootDir": "lib", + "outDir": "out", + "baseUrl": "lib", + "incremental": true, + "tsBuildInfoFile": "out/tsconfig.tsbuildinfo", + + "moduleDetection": "force" + } + } \ No newline at end of file diff --git a/wally.toml b/wally.toml index a19b86f..c81bb8a 100644 --- a/wally.toml +++ b/wally.toml @@ -1,7 +1,8 @@ [package] name = "ukendio/jecs" -version = "0.1.0" +version = "0.2.1" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" -include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] +include = ["default.project.json", "src/**", + "src", "wally.toml", "README.md", "CHANGELOG.md"] exclude = ["**"] \ No newline at end of file