From 0567856a5998fc8e9c0a0bfb8debe0ff16ccba92 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 27 May 2024 03:39:20 +0200 Subject: [PATCH 01/37] Add docs (#45) * Initial commit * Add section for standalone * Fix docs * Add pages to docs * Remove redundant files --- README.md | 2 +- docs/api-types.md | 45 ++ docs/tutorials/quick-start/getting-started.md | 19 + docs/tutorials/quick-start/rbxm.png | Bin 0 -> 34402 bytes lib/init.lua | 273 +++++---- mkdocs.yml | 186 ++++++ tests/world.lua | 552 +++++++++--------- 7 files changed, 665 insertions(+), 412 deletions(-) create mode 100644 docs/api-types.md create mode 100644 docs/tutorials/quick-start/getting-started.md create mode 100644 docs/tutorials/quick-start/rbxm.png create mode 100644 mkdocs.yml diff --git a/README.md b/README.md index 059e172..c5ece64 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/api-types.md b/docs/api-types.md new file mode 100644 index 0000000..b446999 --- /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](../api-types/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/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 0000000000000000000000000000000000000000..ad8f38d5b842c87789249317e03a70c24f45ab48 GIT binary patch literal 34402 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

J;wRKgqu9eJayK1e z&$AVytU`@SAnaioMNr@;{ouuamPluxB~H|%&-?gOOH5F|#~1C+>D+ViJ>nTRXxkbt zIyHasbYvF$#9}I6QXIVsJfkJ%YoVW{aw7kU(F~YIAbQdbIDZ_A#%uC;Pd>q@*h?$q zM~olW;yp`L6RFy6fTwpI68_=r%=2#(al7Q%p6<)mru)FCgu7=WC3mc|V>rV%nN(9m z-Stm5P6($YvOortT#dB~Rj7F#?vPTF)U4TBBID`%-BXA}xc_h^lKe#e#!dUzMd;nfpLyjmu*jPWfq_Sb(~V;vA5AbQYCi8?PmI2zrUE%e61hY zN1TH7X{3hey*k}XOS?yEB09Iq%=t>#ob;V+5WStfns$`7D7BR`&LmjKdX()`JC%e} z)3WgfkqR>S*99TsL00vpqBy1{_bdD>H*+?(4f72Zew5j-Lp=KVe?R!R7L6oa|9xj& z|CQ6}hSJ1+dm!785Y=}5$jH_Yuk{AjAvGSXl8_ZJg)J)5u4-?c6UA>LU!q=m;}@$G z(7WoSbk1*z2(Y$q=DMJ>Hz`u~QMx(?o@-V7KPv{Zbmw8m?`n8`AfRe}Do;f3pC9WN zSDCiTNjbaK@1tWWK7vMlkt6!$A`DC85q0S@QUn+^c+#iBm6@&qe; zZ-q4nlgt7;v?clAD5)v^=?hp7{lNOPchSU6K%24-Qln&fr>h?>3hHk5!L%82-HHf@ zbagbBW`y-B9tlQq4pL7b)c@fS()lLp+~%KT!ewnZtKA;5pN12$gl|9Ng8G4RA=5z)H9uFX zCpxQSL}PL<>^F~HGm%rM7#!34p@H8gslH!oI>OqaFA-;AQS8?J>!OpR5GeZhG$A;@ zB3AF*p$QUTYmrDAlyG#+X%y1!4{6ph+1a08w0CZPs3-!aewW%RzhC@9ToSE2-r8GP zCH;?w=LtJ-Dp3maeK}D&WsmkL;Q{)we(!qxI?~M43r1~B6uARB9=OZRo zcVz=aiRQNME2YDSjXQuR1JV7-R+rdTL+N<(?lIF=lAcJ(WpCx#oP3>I+n(|slpLK+ zOgx@xs6VnUWqeNWOyQ-|_R>wr`k!uQ z@Ac^HEepgVS~NQ|oo=G*fMK(7RYi~7Oee6NwM|Hf9Z1<$^brq9e0UDK~^OyxR|)B>e7Fy2MHR z#xp<*!f`AVIh)qmvj-GLvcdfx zfKusWY$=f`)b6B^RU5vJ_RCHE$Av{0B_7uUdChy}RF2OO$O5^(55kxmM*cxPkNyS; zY^lV2y3FwGC!qDW!x%KId|{aw&p|(!|8}6i+TWM>M_-xz?Y!PH5xez`{~wL}x8vA< zb^W5vNrJ^0zer$~-hVZ#!@CM24@~M%_zls8{0}g!yOfS749u?GWco?vV##yL!l{Vk zADpJp4WDmepaK&3HHZWEGkwApHAE;J>QX5~9{3r;G7kR|8Ee8L~|w4;n(n9e!RX1lJ`4QzdWfq1$eXwlqP@A zcK{DO0rQtB9!K9T#TMU8A=?q^1Atk+LpSf!FL#!(rm*hl96z+kM7RCs)ic1qXVOYi J`A-br{vYTowmAR* literal 0 HcmV?d00001 diff --git a/lib/init.lua b/lib/init.lua index 5cf38ea..9ef0b10 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -6,10 +6,10 @@ type i53 = number type i24 = number -type Ty = {i53} +type Ty = { i53 } type ArchetypeId = number -type Column = {any} +type Column = { any } type Archetype = { id: number, @@ -21,20 +21,19 @@ type Archetype = { }, types: Ty, type: string | number, - entities: {number}, - columns: {Column}, + entities: { number }, + columns: { Column }, records: {}, } - type Record = { archetype: Archetype, row: number, dense: i24, - componentRecord: ArchetypeMap + componentRecord: ArchetypeMap, } -type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}} +type EntityIndex = { dense: { [i24]: i53 }, sparse: { [i53]: Record } } type ArchetypeRecord = number --[[ @@ -48,16 +47,16 @@ TODO: ]] type ArchetypeMap = { - cache: {[number]: ArchetypeRecord}, + cache: { [number]: ArchetypeRecord }, first: ArchetypeMap, second: ArchetypeMap, parent: ArchetypeMap, - size: number + size: number, } -type ComponentIndex = {[i24]: ArchetypeMap} +type ComponentIndex = { [i24]: ArchetypeMap } -type Archetypes = {[ArchetypeId]: Archetype} +type Archetypes = { [ArchetypeId]: Archetype } type ArchetypeDiff = { added: Ty, @@ -70,114 +69,102 @@ 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 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 +local function addFlags(isPair: boolean) + local typeFlags = 0x0 - if isPair then - typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. - end + 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 + 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 + return typeFlags end local function ECS_COMBINE(source: number, target: number): i53 - local e = source * 2^28 + target * ECS_ID_FLAGS_MASK - return e + local e = source * 268435456 + 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 +local function ECS_IS_PAIR(e: number) + return (e % 2 ^ 4) // FLAGS_PAIR ~= 0 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 + e = e // 0x10 + return e % ECS_GENERATION_MASK end local function ECS_GENERATION_INC(e: i53) - local id, generation, flags = separate(e) + local flags = e // 0x10 + local id = flags // ECS_ENTITY_MASK + local generation = flags % ECS_GENERATION_MASK - return ECS_COMBINE(id, generation + 1) + flags + 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 +local function ECS_ENTITY_T_HI(e: i53): i24 + e = e // 0x10 + return e % ECS_ENTITY_MASK end -local function ECS_PAIR(pred: number, obj: number) - local first +-- SECOND +local function ECS_ENTITY_T_LO(e: i53) + e = e // 0x10 + return e // ECS_ENTITY_MASK +end + +local function ECS_PAIR(pred: i53, obj: i53): i53 + local first local second: number = WILDCARD - if pred == WILDCARD then + if pred == WILDCARD then first = obj elseif obj == WILDCARD then first = pred else - first = obj + first = obj second = ECS_ENTITY_T_LO(pred) end - return ECS_COMBINE( - ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true) -end + return ECS_COMBINE(ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true) +end -local function getAlive(entityIndex: EntityIndex, id: i24) +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 + 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)) +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) - assert(ECS_IS_PAIR(e)) - return getAlive(entityIndex, ECS_ENTITY_T_LO(e)) +local function ECS_PAIR_OBJECT(entityIndex, 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 + dense = index, + } :: Record entityIndex.dense[index] = id return id @@ -224,7 +211,7 @@ local function transitionArchetype( local e1 = sourceEntities[sourceRow] local e2 = sourceEntities[movedAway] - if sourceRow ~= movedAway then + if sourceRow ~= movedAway then sourceEntities[sourceRow] = e2 end @@ -252,7 +239,7 @@ local function newEntity(entityId: i53, record: Record, archetype: Archetype) return record end -local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype) +local function moveEntity(entityIndex: EntityIndex, entityId: i53, record: Record, to: Archetype) local sourceRow = record.row local from = record.archetype local destinationRow = archetypeAppend(entityId, to) @@ -265,11 +252,16 @@ local function hash(arr): string | number return table.concat(arr, "_") end -local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId, componentId, i): ArchetypeMap +local function ensureComponentRecord( + componentIndex: ComponentIndex, + archetypeId: number, + componentId: number, + i: number +): ArchetypeMap local archetypesMap = componentIndex[componentId] if not archetypesMap then - archetypesMap = {size = 0, cache = {}, first = {}, second = {}} :: ArchetypeMap + archetypesMap = { size = 0, cache = {}, first = {}, second = {} } :: ArchetypeMap componentIndex[componentId] = archetypesMap end @@ -279,15 +271,14 @@ local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId return archetypesMap end -local function ECS_ID_IS_WILDCARD(e) +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 function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archetype local ty = hash(types) local id = world.nextArchetypeId + 1 @@ -301,31 +292,29 @@ local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetyp for i, componentId in types do ensureComponentRecord(componentIndex, id, componentId, i) records[componentId] = i - if ECS_IS_PAIR(componentId) then + 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) + ensureComponentRecord(componentIndex, id, idr_r, i) records[idr_r] = i - + local idr_t = ECS_PAIR(WILDCARD, object) - ensureComponentRecord( - componentIndex, id, idr_t, i) + 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; + columns = columns, + edges = {}, + entities = {}, + id = id, + records = records, + type = ty, + types = types, } world.archetypeIndex[ty] = archetype world.archetypes[id] = archetype @@ -337,20 +326,20 @@ local World = {} World.__index = World function World.new() local self = setmetatable({ - archetypeIndex = {}; - archetypes = {} :: Archetypes; - componentIndex = {} :: ComponentIndex; + archetypeIndex = {}, + archetypes = {} :: Archetypes, + componentIndex = {} :: ComponentIndex, entityIndex = { dense = {}, - sparse = {} - } :: EntityIndex; + sparse = {}, + } :: EntityIndex, hooks = { - [ON_ADD] = {}; - }; - nextArchetypeId = 0; - nextComponentId = 0; - nextEntityId = 0; - ROOT_ARCHETYPE = (nil :: any) :: Archetype; + [ON_ADD] = {}, + }, + nextArchetypeId = 0, + nextComponentId = 0, + nextEntityId = 0, + ROOT_ARCHETYPE = (nil :: any) :: Archetype, }, World) self.ROOT_ARCHETYPE = archetypeOf(self, {}) return self @@ -380,16 +369,16 @@ 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 + if not archetype then return nil end local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)] - if not componentRecord then + if not componentRecord then return nil end local archetypeRecord = componentRecord.cache[archetype.id] - if not archetypeRecord then + if not archetypeRecord then return nil end @@ -397,37 +386,37 @@ function World.target(world: World, entity: i53, relation: i24): i24? 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 +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 + 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 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 + 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) +function World.delete(world: World, entityId: i53) local record = world.entityIndex.sparse[entityId] - if not record then + if not record then return end local entityIndex = world.entityIndex @@ -439,12 +428,12 @@ function World.delete(world: World, entityId: i53) -- 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 + + if archetype then local entities = archetype.entities local last = #entities - if row ~= last then + if row ~= last then local entityToMove = entities[last] dense[record.dense] = entityToMove sparse[entityToMove] = record @@ -477,7 +466,7 @@ local function ensureArchetype(world: World, types, prev) return archetypeOf(world, types, prev) end -local function findInsert(types: {i53}, toAdd: i53) +local function findInsert(types: { i53 }, toAdd: i53) for i, id in types do if id == toAdd then return -1 @@ -494,7 +483,7 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53 -- 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 @@ -532,7 +521,7 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet return add end -function World.add(world: World, entityId: i53, componentId: i53) +function World.add(world: World, entityId: i53, componentId: i53) local entityIndex = world.entityIndex local record = entityIndex.sparse[entityId] local from = record.archetype @@ -582,7 +571,7 @@ local function archetypeTraverseRemove(world: World, componentId: i53, from: Arc if not remove then local to = table.clone(from.types) local at = table.find(to, componentId) - if not at then + if not at then return from end table.remove(to, at) @@ -607,7 +596,7 @@ 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 + if not archetype then return nil end @@ -644,20 +633,20 @@ end -- the less creation the better local function actualNoOperation() end -local function noop(_self: Query, ...: i53): () -> (number, ...any) +local function noop(_self: Query, ...): () -> () return actualNoOperation :: any end local EmptyQuery = { - __iter = noop; - without = noop; + __iter = noop, + without = noop, } EmptyQuery.__index = EmptyQuery setmetatable(EmptyQuery, EmptyQuery) export type Query = typeof(EmptyQuery) -function World.query(world: World, ...: i53): Query +function World.query(world: World, ...): Query -- breaking? if (...) == nil then error("Missing components") @@ -666,7 +655,7 @@ function World.query(world: World, ...: i53): Query local compatibleArchetypes = {} local length = 0 - local components = {...} + local components = { ... } local archetypes = world.archetypes local queryLength = #components @@ -707,8 +696,8 @@ function World.query(world: World, ...: i53): Query length += 1 compatibleArchetypes[length] = { - archetype = archetype, - indices = indices + archetype = archetype, + indices = indices, } end @@ -721,7 +710,7 @@ function World.query(world: World, ...: i53): Query preparedQuery.__index = preparedQuery function preparedQuery:without(...) - local withoutComponents = {...} + local withoutComponents = { ... } for i = #compatibleArchetypes, 1, -1 do local archetype = compatibleArchetypes[i].archetype local records = archetype.records @@ -828,16 +817,16 @@ function World.__iter(world: World): () -> (number?, unknown?) local sparse = world.entityIndex.sparse local last - return function() + return function() local lastEntity, entityId = next(dense, last) - if not lastEntity then + if not lastEntity then return end last = lastEntity local record = sparse[entityId] local archetype = record.archetype - if not archetype then + 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 @@ -851,17 +840,17 @@ function World.__iter(world: World): () -> (number?, unknown?) -- 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; + World = World, - OnAdd = ON_ADD; - OnRemove = ON_REMOVE; - OnSet = ON_SET; + OnAdd = ON_ADD, + OnRemove = ON_REMOVE, + OnSet = ON_SET, Wildcard = WILDCARD, w = WILDCARD, Rest = REST, diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..5029861 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,186 @@ +site_name: Fusion +site_url: https://elttob.uk/Fusion/ +repo_name: dphfox/Fusion +repo_url: https://github.com/dphfox/Fusion + +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/tests/world.lua b/tests/world.lua index f0eff7d..79a2686 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -1,5 +1,5 @@ -local testkit = require("../testkit") 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 @@ -11,329 +11,343 @@ 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, ...) + 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 + 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) +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 + 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 + -- 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() + 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() + 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 + 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 + for id in world:query(A) do + table.remove(entities, CHECK(table.find(entities, id))) + end - CHECK(#entities == 0) + CHECK(#entities == 0) + end - 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() - 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() - 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 - -- 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 - 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 - 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() - 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() - 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 - 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 - for id in world:query(A):without(B) do - table.remove(entities, CHECK(table.find(entities, id))) - end + CHECK(#entities == 0) + end - CHECK(#entities == 0) + do + CASE("should allow setting components in arbitrary order") + local world = jecs.World.new() - end + local Health = world:entity() + local Poison = world:component() - do CASE "should allow setting components in arbitrary order" - local world = jecs.World.new() + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) - local Health = world:entity() - local Poison = world:component() + CHECK(world:get(id, Poison) == 5) + end - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) + do + CASE("should allow deleting components") + local world = jecs.World.new() - CHECK(world:get(id, Poison) == 5) - end + local Health = world:entity() + local Poison = world:component() - do CASE "should allow deleting components" - local world = jecs.World.new() + 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) - local Health = world:entity() - local Poison = world:component() + world:delete(id) - 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) + 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 - world:delete(id) + do + CASE("should allow remove that doesn't exist on entity") + local world = jecs.World.new() - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == nil) - CHECK(world:get(id1, Poison) == 500) - CHECK(world:get(id1, Health) == 50) + local Health = world:entity() + local Poison = world:component() - end + local id = world:entity() + world:set(id, Health, 50) + world:remove(id, Poison) - do CASE "should allow remove that doesn't exist on entity" - local world = jecs.World.new() + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == 50) + end - local Health = world:entity() - local Poison = world:component() + 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 - local id = world:entity() - world:set(id, Health, 50) - world:remove(id, Poison) + 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(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == 50) - end + CHECK(IS_PAIR(world:entity()) == false) - 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 + 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 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() + do + CASE("should allow querying for relations") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() - CHECK(IS_PAIR(world:entity()) == false) + 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 - 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 wildcards in queries") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() - 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 + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - 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 + 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 - CHECK(count == 2) - count = 0 + 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() - for e, data in world:query(ECS_PAIR(w, Apples)) do - count += 1 - CHECK(data == "bob eats apples") - end - CHECK(count == 1) - end + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") - 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") + 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 - world:delete(Apples) - local Wildcard = jecs.Wildcard - - local count = 0 - for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do - count += 1 - end + CHECK(count == 2) + count = 0 - world:delete(ECS_PAIR(Eats, Apples)) - - CHECK(count == 0) - CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) - end + 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 error when setting invalid pair" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() + do + CASE("should only relate alive entities") - world:delete(Apples) + 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() - CHECK_NO_ERR("Apples should be dead", function() - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - end) - end + 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") - do CASE "should find target for ChildOf" - local world = jecs.World.new() + world:delete(Apples) + local Wildcard = jecs.Wildcard - local ChildOf = world:component() - local Name = world:component() + local count = 0 + for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do + count += 1 + end - local function parent(entity) - return world:target(entity, ChildOf) - end + world:delete(ECS_PAIR(Eats, Apples)) - 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) + CHECK(count == 0) + CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) + end - local count = 0 - for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do - print(name) - count += 1 - end - CHECK(count == 2) - 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 = 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 +FINISH() + From c0594c7e75b97a156d031a29f91cbc1cef63e3f8 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 27 May 2024 03:50:46 +0200 Subject: [PATCH 02/37] Fix docs --- docs/api-types.md | 2 +- docs/api/world.md | 188 ++++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 8 +- 3 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 docs/api/world.md diff --git a/docs/api-types.md b/docs/api-types.md index b446999..cce3856 100644 --- a/docs/api-types.md +++ b/docs/api-types.md @@ -30,7 +30,7 @@ end ### QueryIter.without QueryIter.without(iter: QueryIter - ...: [Entity](../api-types/Entity)): QueryIter + ...: [Entity](#Entity)): QueryIter Create a new Query Iterator from the filter diff --git a/docs/api/world.md b/docs/api/world.md new file mode 100644 index 0000000..06a8a66 --- /dev/null +++ b/docs/api/world.md @@ -0,0 +1,188 @@ +# World + +### World.new + +World.new(): [World](../api-types/World) + +Create a new world. + +#### Returns +A new world + +--- + +### World.entity + +World.entity(world: [World](../api-types/World)): [Entity](../api-types/Entity) + +Creates an entity in the world. + +#### Returns +A new entiity id + +--- + +### World.target + +World.target(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + rel: [Entity](../api-types/Entity)): [Entity](../api-types/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/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/Entity)): [Entity](../api-types/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/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/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/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/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/World), + ...: [Entity](../api-types/Entity)): [QueryIter](../api-types/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. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 5029861..1a47333 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ -site_name: Fusion -site_url: https://elttob.uk/Fusion/ -repo_name: dphfox/Fusion -repo_url: https://github.com/dphfox/Fusion +site_name: Jecs +site_url: jecs.github.io/jecs +repo_name: ukendio/jecs +repo_url: https://github.com/ukendio/jecs extra: version: From 3d689ebdd84e9aa115115ca33215a49eada3fd4d Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 27 May 2024 11:33:59 +0200 Subject: [PATCH 03/37] Fix links --- docs/api/world.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/api/world.md b/docs/api/world.md index 06a8a66..39097bc 100644 --- a/docs/api/world.md +++ b/docs/api/world.md @@ -2,7 +2,7 @@ ### World.new -World.new(): [World](../api-types/World) +World.new(): [World](../api-types#World) Create a new world. @@ -13,7 +13,7 @@ A new world ### World.entity -World.entity(world: [World](../api-types/World)): [Entity](../api-types/Entity) +World.entity(world: [World](../api-types#World)): [Entity](../api-types#Entity) Creates an entity in the world. @@ -24,9 +24,9 @@ A new entiity id ### World.target -World.target(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - rel: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) +World.target(world: [World](../api-types#World), + entity: [Entity](../api-types#Entity), + rel: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) Get the target of a relationship. @@ -45,9 +45,9 @@ The first target for the relationship ### World.add -World.add(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - id: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) +World.add(world: [World](../api-types#World), + entity: [Entity](../api-types#Entity), + id: [Entity](../api-types#Entity)): [Entity](..#api-types/Entity) Add a (component) id to an entity. @@ -63,9 +63,9 @@ If the entity already has the id, this operation will have no side effects. ### World.remove -World.remove(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - id: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) +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. @@ -135,8 +135,8 @@ The query iterator. ### pair -pair(first: [Entity](../api-types/Entity), - second: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) +pair(first: [Entity](../api-types#Entity), + second: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) Creates a composite key. @@ -152,7 +152,7 @@ The pair of the two elements ### IS_PAIR -jecs.IS_PAIR(id: [Entity](../api-types/Entity)): boolean +jecs.IS_PAIR(id: [Entity](../api-types#Entity)): boolean Creates a composite key. @@ -185,4 +185,4 @@ If id is a pair. ### Wildcard -Matches any id, returns all matches. \ No newline at end of file +Matches any id, returns all matches. From 94cc3ee8ea8f87bba1e96b5b82a80bc6f6742a80 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 27 May 2024 11:45:41 +0200 Subject: [PATCH 04/37] Fix links --- docs/api/world.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/api/world.md b/docs/api/world.md index 39097bc..fa52403 100644 --- a/docs/api/world.md +++ b/docs/api/world.md @@ -2,7 +2,7 @@ ### World.new -World.new(): [World](../api-types#World) +World.new(): [World](../api-typesk.md#World) Create a new world. @@ -13,7 +13,7 @@ A new world ### World.entity -World.entity(world: [World](../api-types#World)): [Entity](../api-types#Entity) +World.entity(world: [World](../api-types.md#World)): [Entity](../api-types.md#Entity) Creates an entity in the world. @@ -24,9 +24,9 @@ A new entiity id ### World.target -World.target(world: [World](../api-types#World), - entity: [Entity](../api-types#Entity), - rel: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) +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. @@ -45,9 +45,9 @@ The first target for the relationship ### World.add -World.add(world: [World](../api-types#World), - entity: [Entity](../api-types#Entity), - id: [Entity](../api-types#Entity)): [Entity](..#api-types/Entity) +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. @@ -81,9 +81,9 @@ If the entity already has the id, this operation will have no side effects. ### World.get -World.get(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - id: [Entity](../api-types/Entity)): any +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. @@ -99,9 +99,9 @@ The component data, nil if the entity does not have the componnet. ### World.set -World.set(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - id: [Entity](../api-types/Entity) +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. @@ -116,8 +116,8 @@ Set the value of a component. ### World.query -World.query(world: [World](../api-types/World), - ...: [Entity](../api-types/Entity)): [QueryIter](../api-types/QueryIter) +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. From 1498a28c3fd665b4a7215789031eb6e9a6d550e0 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 27 May 2024 11:46:42 +0200 Subject: [PATCH 05/37] Update world.md --- docs/api/world.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/world.md b/docs/api/world.md index fa52403..93f4e7c 100644 --- a/docs/api/world.md +++ b/docs/api/world.md @@ -2,7 +2,7 @@ ### World.new -World.new(): [World](../api-typesk.md#World) +World.new(): [World](../api-types.md#World) Create a new world. From 05d61bd6da988b079ebb0fb210773f9d7b044d7e Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 27 May 2024 20:09:22 +0200 Subject: [PATCH 06/37] Fix doc --- docs/api/world.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api/world.md b/docs/api/world.md index fa52403..c7f251b 100644 --- a/docs/api/world.md +++ b/docs/api/world.md @@ -135,8 +135,7 @@ The query iterator. ### pair -pair(first: [Entity](../api-types#Entity), - second: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) +pair(first: [Entity](../api-types#Entity), second: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) Creates a composite key. From 63f40a22c00dbdfe20b32573438c647178c3ab7c Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 5 Jun 2024 23:34:24 +0200 Subject: [PATCH 07/37] Add Types (#21) * Add Types * Fix return type of without * implement types * make types more specific * reduce query to 10 generics * Remove second type pack --------- Co-authored-by: alice <166900055+alicesaidhi@users.noreply.github.com> --- lib/init.lua | 83 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index 9ef0b10..ebcfd0e 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -845,14 +845,79 @@ function World.__iter(world: World): () -> (number?, unknown?) end end -return table.freeze({ - World = World, +-- __nominal_type_dont_use could not be any or T as it causes a type error +-- or produces a union +export type Component = T & {__nominal_type_dont_use: never} +export type Entity = Component +export type Relationship = Component +type ctype = Component - OnAdd = ON_ADD, - OnRemove = ON_REMOVE, - OnSet = ON_SET, - Wildcard = WILDCARD, - w = WILDCARD, +export type QueryShim = typeof(setmetatable( + {} :: { + --- Excludes the given selection from the query + without: (QueryShim, U...) -> QueryShim + }, + {} :: { + __iter: (QueryShim) -> () -> (Entity, T...) + } +)) + +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) -> Component, + --- 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: Relationship) -> 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: Component) -> (), + --- Assigns a value to a component on the given entity + set: (WorldShim, id: Entity, component: Component, data: T) -> (), + --- Removes a component from the given entity + remove: (WorldShim, id: Entity, component: Component) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: + ((WorldShim, id: any, ctype) -> A) + & ((WorldShim, id: Entity, ctype, ctype) -> (A, B)) + & ((WorldShim, id: Entity, ctype, ctype, ctype) -> (A, B, C)) + & (WorldShim, id: Entity, ctype, ctype, ctype, ctype) -> (A, B, C, D), + + --- Searches the world for entities that match a given query + query: + ((WorldShim, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ...ctype) -> QueryShim) + + }, + {} :: { + __iter: (world: WorldShim) -> () -> (number, {[unknown]: unknown?}) + } + +)) + +return table.freeze({ + World = (World :: any) :: {new: () -> WorldShim}, + + OnAdd = (ON_ADD :: any) :: Component, + OnRemove = (ON_REMOVE :: any) :: Component, + OnSet = (ON_SET :: any) :: Component, + Wildcard = (WILDCARD :: any) :: Component, + w = (WILDCARD :: any) :: Component, Rest = REST, IS_PAIR = ECS_IS_PAIR, @@ -863,6 +928,6 @@ return table.freeze({ ECS_PAIR_RELATION = ECS_PAIR_RELATION, ECS_PAIR_OBJECT = ECS_PAIR_OBJECT, - pair = ECS_PAIR, - getAlive = getAlive, + pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> Relationship, + getAlive = getAlive }) From 818dfdd216b45e4a5070ef0679416cdbd3f6fb32 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 6 Jun 2024 00:38:27 +0200 Subject: [PATCH 08/37] Low foot print ids --- lib/init.lua | 56 +++++++++++++++++++++++++++---------------------- tests/world.lua | 2 +- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index ebcfd0e..b83d988 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -100,49 +100,52 @@ local function ECS_COMBINE(source: number, target: number): i53 end local function ECS_IS_PAIR(e: number) - return (e % 2 ^ 4) // FLAGS_PAIR ~= 0 + if e > ECS_ENTITY_MASK then + return (e % 2 ^ 4) // FLAGS_PAIR ~= 0 + end + return false end -- HIGH 24 bits LOW 24 bits local function ECS_GENERATION(e: i53) - e = e // 0x10 - return e % ECS_GENERATION_MASK + if e > ECS_ENTITY_MASK then + e = e // 0x10 + return e % ECS_GENERATION_MASK + end + return 0 end local function ECS_GENERATION_INC(e: i53) - local flags = e // 0x10 - local id = flags // ECS_ENTITY_MASK - local generation = flags % ECS_GENERATION_MASK + if e > ECS_ENTITY_MASK then + local flags = e // 0x10 + local id = flags // ECS_ENTITY_MASK + local generation = flags % ECS_GENERATION_MASK - return ECS_COMBINE(id, generation + 1) + flags + 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 - e = e // 0x10 - return e % ECS_ENTITY_MASK + if e > ECS_ENTITY_MASK then + e = e // 0x10 + return e % ECS_ENTITY_MASK + end + return e end -- SECOND local function ECS_ENTITY_T_LO(e: i53) - e = e // 0x10 - return e // ECS_ENTITY_MASK + if e > ECS_ENTITY_MASK then + e = e // 0x10 + return e // ECS_ENTITY_MASK + end + return e end local function ECS_PAIR(pred: i53, obj: i53): i53 - 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) + return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + addFlags(--[[isPair]] true) end local function getAlive(entityIndex: EntityIndex, id: i24) @@ -161,7 +164,8 @@ local function ECS_PAIR_OBJECT(entityIndex, e) end local function nextEntityId(entityIndex, index: i24): i53 - local id = ECS_COMBINE(index, 0) + --local id = ECS_COMBINE(index, 0) + local id = index entityIndex.sparse[id] = { dense = index, } :: Record @@ -372,6 +376,7 @@ function World.target(world: World, entity: i53, relation: i24): i24? if not archetype then return nil end + local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)] if not componentRecord then return nil @@ -822,6 +827,7 @@ function World.__iter(world: World): () -> (number?, unknown?) if not lastEntity then return end + last = lastEntity local record = sparse[entityId] diff --git a/tests/world.lua b/tests/world.lua index 79a2686..7ca883c 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -208,6 +208,7 @@ TEST("world", function() 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 @@ -350,4 +351,3 @@ TEST("world", function() end) FINISH() - From a62cb370cff5fbed0a38d9fa34470518068b13b1 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 9 Jun 2024 23:58:01 +0200 Subject: [PATCH 09/37] Everything are entities! --- lib/init.lua | 81 ++++++++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index b83d988..402473c 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -853,77 +853,72 @@ end -- __nominal_type_dont_use could not be any or T as it causes a type error -- or produces a union -export type Component = T & {__nominal_type_dont_use: never} -export type Entity = Component -export type Relationship = Component -type ctype = Component - -export type QueryShim = typeof(setmetatable( - {} :: { - --- Excludes the given selection from the query - without: (QueryShim, U...) -> QueryShim - }, - {} :: { - __iter: (QueryShim) -> () -> (Entity, T...) - } -)) +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, + 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) -> Component, + 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: Relationship) -> 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: Component) -> (), + add: (WorldShim, id: Entity, component: Entity) -> (), --- Assigns a value to a component on the given entity - set: (WorldShim, id: Entity, component: Component, data: T) -> (), + set: (WorldShim, id: Entity, component: Entity, data: T) -> (), --- Removes a component from the given entity - remove: (WorldShim, id: Entity, component: Component) -> (), + remove: (WorldShim, id: Entity, component: Entity) -> (), --- Retrieves the value of up to 4 components. These values may be nil. get: - ((WorldShim, id: any, ctype) -> A) - & ((WorldShim, id: Entity, ctype, ctype) -> (A, B)) - & ((WorldShim, id: Entity, ctype, ctype, ctype) -> (A, B, C)) - & (WorldShim, id: Entity, ctype, ctype, ctype, ctype) -> (A, B, C, D), + ((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, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ...ctype) -> QueryShim) + 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?}) } - )) return table.freeze({ World = (World :: any) :: {new: () -> WorldShim}, - OnAdd = (ON_ADD :: any) :: Component, - OnRemove = (ON_REMOVE :: any) :: Component, - OnSet = (ON_SET :: any) :: Component, - Wildcard = (WILDCARD :: any) :: Component, - w = (WILDCARD :: any) :: Component, + OnAdd = (ON_ADD :: any) :: Entity, + OnRemove = (ON_REMOVE :: any) :: Entity, + OnSet = (ON_SET :: any) :: Entity, + Wildcard = (WILDCARD :: any) :: Entity, + w = (WILDCARD :: any) :: Entity, Rest = REST, IS_PAIR = ECS_IS_PAIR, @@ -934,6 +929,6 @@ return table.freeze({ ECS_PAIR_RELATION = ECS_PAIR_RELATION, ECS_PAIR_OBJECT = ECS_PAIR_OBJECT, - pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> Relationship, + pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> number, getAlive = getAlive }) From 1553362014da14d27e1959f129425241b2428ecb Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 11 Jun 2024 01:39:43 +0200 Subject: [PATCH 10/37] Please the type solver overlord --- lib/init.lua | 146 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 108 insertions(+), 38 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index 402473c..ae3c1e1 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -23,7 +23,7 @@ type Archetype = { type: string | number, entities: { number }, columns: { Column }, - records: {}, + records: { [number]: number }, } type Record = { @@ -47,7 +47,7 @@ TODO: ]] type ArchetypeMap = { - cache: { [number]: ArchetypeRecord }, + cache: { ArchetypeRecord }, first: ArchetypeMap, second: ArchetypeMap, parent: ArchetypeMap, @@ -136,7 +136,7 @@ local function ECS_ENTITY_T_HI(e: i53): i24 end -- SECOND -local function ECS_ENTITY_T_LO(e: i53) +local function ECS_ENTITY_T_LO(e: i53): i24 if e > ECS_ENTITY_MASK then e = e // 0x10 return e // ECS_ENTITY_MASK @@ -145,7 +145,7 @@ local function ECS_ENTITY_T_LO(e: i53) 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) + return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + addFlags(--[[isPair]] true) :: i53 end local function getAlive(entityIndex: EntityIndex, id: i24) @@ -163,7 +163,7 @@ local function ECS_PAIR_OBJECT(entityIndex, e) return getAlive(entityIndex, ECS_ENTITY_T_LO(e)) end -local function nextEntityId(entityIndex, index: i24): i53 +local function nextEntityId(entityIndex: EntityIndex, index: i24): i53 --local id = ECS_COMBINE(index, 0) local id = index entityIndex.sparse[id] = { @@ -219,7 +219,7 @@ local function transitionArchetype( sourceEntities[sourceRow] = e2 end - sourceEntities[movedAway] = nil + sourceEntities[movedAway] = nil :: any destinationEntities[destinationRow] = e1 local record1 = sparse[e1] @@ -265,7 +265,7 @@ local function ensureComponentRecord( local archetypesMap = componentIndex[componentId] if not archetypesMap then - archetypesMap = { size = 0, cache = {}, first = {}, second = {} } :: ArchetypeMap + archetypesMap = { size = 0, cache = {} } componentIndex[componentId] = archetypesMap end @@ -289,7 +289,7 @@ local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archet world.nextArchetypeId = id local length = #types - local columns = table.create(length) + local columns = (table.create(length) :: any) :: { Column } local componentIndex = world.componentIndex local records = {} @@ -311,7 +311,7 @@ local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archet columns[i] = {} end - local archetype = { + local archetype: Archetype = { columns = columns, edges = {}, entities = {}, @@ -320,6 +320,7 @@ local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archet type = ty, types = types, } + world.archetypeIndex[ty] = archetype world.archetypes[id] = archetype @@ -330,12 +331,12 @@ local World = {} World.__index = World function World.new() local self = setmetatable({ - archetypeIndex = {}, + archetypeIndex = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, componentIndex = {} :: ComponentIndex, entityIndex = { - dense = {}, - sparse = {}, + dense = {} :: { [i24]: i53 }, + sparse = {} :: { [i53]: Record }, } :: EntityIndex, hooks = { [ON_ADD] = {}, @@ -349,6 +350,8 @@ function World.new() return self end +export type World = typeof(World.new()) + function World.component(world: World) local componentId = world.nextComponentId + 1 if componentId > HI_COMPONENT_ID then @@ -391,7 +394,7 @@ function World.target(world: World, entity: i53, relation: i24): i24? end -- should reuse this logic in World.set instead of swap removing in transition archetype -local function destructColumns(columns, count, row) +local function destructColumns(columns: { Column }, count: number, row: number) if row == count then for _, column in columns do column[count] = nil @@ -415,7 +418,7 @@ local function archetypeDelete(world: World, id: i53) end end - componentIndex[id] = nil + componentIndex[id] = nil :: any end end @@ -444,20 +447,18 @@ function World.delete(world: World, entityId: i53) sparse[entityToMove] = record end - entities[row], entities[last] = entities[last], nil + entities[row], entities[last] = entities[last], nil :: any local columns = archetype.columns destructColumns(columns, last, row) end - sparse[entityId] = nil - dense[#dense] = nil + sparse[entityId] = nil :: any + dense[#dense] = nil :: any end -export type World = typeof(World.new()) - -local function ensureArchetype(world: World, types, prev) +local function ensureArchetype(world: World, types, prev): Archetype if #types < 1 then return world.ROOT_ARCHETYPE end @@ -853,17 +854,17 @@ end -- __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 Entity = number & { __nominal_type_dont_use: T } export type Pair = number export type QueryShim = typeof(setmetatable({ without = function(...): QueryShim return nil :: any - end + end, }, { __iter = function(): () -> (number, T...) return nil :: any - end + end, })) export type WorldShim = typeof(setmetatable( {} :: { @@ -878,7 +879,7 @@ export type WorldShim = typeof(setmetatable( 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 @@ -886,33 +887,102 @@ export type WorldShim = typeof(setmetatable( --- 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) + 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) - + & (( + 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?}) + __iter: (world: WorldShim) -> () -> (number, { [unknown]: unknown? }), } )) return table.freeze({ - World = (World :: any) :: {new: () -> WorldShim}, + World = (World :: any) :: { new: () -> WorldShim }, OnAdd = (ON_ADD :: any) :: Entity, OnRemove = (ON_REMOVE :: any) :: Entity, @@ -930,5 +1000,5 @@ return table.freeze({ ECS_PAIR_OBJECT = ECS_PAIR_OBJECT, pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> number, - getAlive = getAlive + getAlive = getAlive, }) From c3d745a88b9f7750498583a5b5c0c66efaefa200 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 11 Jun 2024 21:06:27 +0200 Subject: [PATCH 11/37] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE 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. From 1344398ffe57d95c339254cfa6639c70122a57df Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sat, 15 Jun 2024 20:03:12 +0200 Subject: [PATCH 12/37] Fix types --- lib/init.lua | 92 +++++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index ae3c1e1..bc86403 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -252,7 +252,7 @@ local function moveEntity(entityIndex: EntityIndex, entityId: i53, record: Recor record.row = destinationRow end -local function hash(arr): string | number +local function hash(arr): string return table.concat(arr, "_") end @@ -262,10 +262,10 @@ local function ensureComponentRecord( componentId: number, i: number ): ArchetypeMap - local archetypesMap = componentIndex[componentId] + local archetypesMap = componentIndex[componentId] if not archetypesMap then - archetypesMap = { size = 0, cache = {} } + archetypesMap = ({ size = 0, cache = {} } :: any) :: ArchetypeMap componentIndex[componentId] = archetypesMap end @@ -490,7 +490,7 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53 -- 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 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 @@ -575,7 +575,7 @@ local function archetypeTraverseRemove(world: World, componentId: i53, from: Arc local remove = edge.remove if not remove then - local to = table.clone(from.types) + local to = table.clone(from.types) :: { i53 } local at = table.find(to, componentId) if not at then return from @@ -615,7 +615,7 @@ local function get(record: Record, componentId: i24) return archetype.columns[archetypeRecord][record.row] end -function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) +function World.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 @@ -658,14 +658,15 @@ function World.query(world: World, ...): Query error("Missing components") end - local compatibleArchetypes = {} + type CompatibleArchetype = { archetype: Archetype, indices: { number } } + local compatibleArchetypes = {} :: { CompatibleArchetype } local length = 0 local components = { ... } local archetypes = world.archetypes local queryLength = #components - local firstArchetypeMap + local firstArchetypeMap: ArchetypeMap local componentIndex = world.componentIndex for _, componentId in components do @@ -707,7 +708,8 @@ function World.query(world: World, ...): Query } end - local lastArchetype, compatibleArchetype = next(compatibleArchetypes) + local lastArchetype, compatibleArchetype: CompatibleArchetype = next(compatibleArchetypes :: any) + if not lastArchetype then return EmptyQuery end @@ -715,51 +717,24 @@ function World.query(world: World, ...): Query 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:__iter() - return function() + return function() local archetype = compatibleArchetype.archetype - local row: number = next(archetype.entities, lastRow) :: number + local row: number = next(archetype.entities, lastRow) while row == nil do lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) if lastArchetype == nil then return end archetype = compatibleArchetype.archetype - row = next(archetype.entities, row) :: number + row = next(archetype.entities, row) end lastRow = row - local entityId = archetype.entities[row :: number] + local entityId = archetype.entities[row] local columns = archetype.columns local tr = compatibleArchetype.indices @@ -811,22 +786,49 @@ function World.query(world: World, ...): Query queryOutput[i] = columns[tr[i]][row] end - return entityId, unpack(queryOutput, 1, queryLength) + return entityId, unpack(queryOutput :: any, 1, queryLength) end end + 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 :: any) + if not lastArchetype then + return EmptyQuery + end + + return self + end + return setmetatable({}, preparedQuery) :: any end -function World.__iter(world: World): () -> (number?, unknown?) +function World.__iter(world: World): () -> any 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 + local lastEntity: number?, entityId: number = next(dense, last) + if not lastEntity then + return end last = lastEntity From ea12df96a495b875a48fd615be7c6965805dafac Mon Sep 17 00:00:00 2001 From: EncodedVenom <32179912+EncodedVenom@users.noreply.github.com> Date: Sat, 15 Jun 2024 16:03:04 -0400 Subject: [PATCH 13/37] Add Typescript Types (#51) * Add package.json, tsconfig.json, and change gitignore * Typescript Types * Make World a single class, refactor of all query types * Fix InferComponents types due to typo / misunderstanding * Bump dependencies * Fix get type to include undefined and add documentation * Remove non-user facing types * Rename PossiblyUndefinedPack -> Nullable * Some changes to how EntityIndex is handled * Consistent formatting * Remove EntityIndex related type definitions * Match API * Doc comments --- .gitignore | 4 + lib/index.d.ts | 153 ++++ package-lock.json | 2199 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 43 + tsconfig.json | 26 + 5 files changed, 2425 insertions(+) create mode 100644 lib/index.d.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tsconfig.json 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/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..66ae62d --- /dev/null +++ b/lib/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/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/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 From 5bd43bddd4786df4aabf12fb2f3515cd18a0bbe2 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sat, 22 Jun 2024 00:15:03 +0200 Subject: [PATCH 14/37] Add queryNext --- lib/init.lua | 157 ++++++++++++++++++++++++++++----------------------- 1 file changed, 85 insertions(+), 72 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index bc86403..cfde17f 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -652,14 +652,15 @@ setmetatable(EmptyQuery, EmptyQuery) export type Query = typeof(EmptyQuery) +type CompatibleArchetype = { archetype: Archetype, indices: { number } } + function World.query(world: World, ...): Query -- breaking? if (...) == nil then error("Missing components") end - type CompatibleArchetype = { archetype: Archetype, indices: { number } } - local compatibleArchetypes = {} :: { CompatibleArchetype } + local compatibleArchetypes: { CompatibleArchetype } = {} local length = 0 local components = { ... } @@ -708,86 +709,98 @@ function World.query(world: World, ...): Query } end - local lastArchetype, compatibleArchetype: CompatibleArchetype = next(compatibleArchetypes :: any) + local lastArchetype = 1 + local compatibleArchetype: CompatibleArchetype = compatibleArchetypes[lastArchetype] - if not lastArchetype then + if not compatibleArchetype then return EmptyQuery end local preparedQuery = {} preparedQuery.__index = preparedQuery - - local lastRow + local queryOutput = {} - function preparedQuery:__iter() - return function() - local archetype = compatibleArchetype.archetype - local row: number = next(archetype.entities, lastRow) - while row == nil do - lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) - if lastArchetype == nil then - return - end - archetype = compatibleArchetype.archetype - row = next(archetype.entities, row) + local i = 1 + + local function queryNext() + local archetype = compatibleArchetype.archetype + local entityId = archetype.entities[i] + + while entityId == nil do + lastArchetype += 1 + if lastArchetype > #compatibleArchetypes then + return end - lastRow = row - - local entityId = archetype.entities[row] - 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 :: any, 1, queryLength) + compatibleArchetype = compatibleArchetypes[lastArchetype] + archetype = compatibleArchetype.archetype + i = 1 + entityId = archetype.entities[i] end + + local row = i + i+=1 + + 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 :: any, 1, queryLength) + end + + function preparedQuery:__iter() + return queryNext + end + + function preparedQuery:next() + return queryNext() end function preparedQuery:without(...) From eae51988a9e3ca45e39ebcfdbea0f9f8706bd3cd Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sat, 22 Jun 2024 01:08:51 +0200 Subject: [PATCH 15/37] Update benchmarks image --- benches/visual/query.bench.lua | 13 ++++++++++--- image-3.png | Bin 123556 -> 77539 bytes 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/benches/visual/query.bench.lua b/benches/visual/query.bench.lua index e8f948a..57eddaa 100644 --- a/benches/visual/query.bench.lua +++ b/benches/visual/query.bench.lua @@ -3,11 +3,11 @@ 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) +local jecs = require(ReplicatedStorage.Shim) local mirror = require(ReplicatedStorage.mirror) local mcs = mirror.World.new() local ecs = jecs.World.new() @@ -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/image-3.png b/image-3.png index 9db2297237b2aee1f769651c577029a3e7885bde..103914224c9f3f8c55fe2254891c254dae3c56ae 100644 GIT binary patch literal 77539 zcmbSz1yodBzc;p^sE>$98+3<&GzdzE!~jEybcup=3xW#L-O@2b&(NSC-7zyXDBax* ze0#+AxzGE(>)!jV@2urwz%w&v_SyTtfAybF%8F8i1Y`twczA>|(vm88c$W|cyudI-Hnto#6Mm&tnsB4bVIdu(_!Gh7U1PH* zw#nD~^t%$83p}ugvFE{s;0}hj7+uH22xbk%t_1!^9xKC$M}fGD1c#SONNg;ldLAS) zO)6NH-;(<5&v+@m8JU@%>+0&X=>K&p-p%{}{X;MR$Kgjw73@o%!r#O7uYp%#ps3db zKa6_UH%Xo8Oi3B`c=AjrFMvNO-@a;i4iE3;5G!@fZd`R{yZ*0dF7MKd?|}cqz+83~ zWy0wcU~rvErC?%zPdF5zSBwVdzv=uOmPz?&X8VxRR)w!&QM#F2VkX3}YRH{yy*H^Z z!%)f`iq$wU@L#p2+as)2PhE6~s1>e~ZVpmt^xV5?d;djpivO0VR0rnPX{N1){@9wE zN3A6foUM+EjlN7|KYM#QY{%_%Y}3d7r-c^Ay=TVKJZ$Ho-C6wngW)mf@YH znKx?e6@tfChTd{5@#_jm&-+CdZ(|X~BchO1^RS0YJfj11Lbo1mc*fGN!8%>KNJVAj zuk+|X+HOW|VvgsQFpibz0()24VDj!3Kh#>wk?5h^vakY8BY|H-YMadQ(VA&Z)Y$Fu z4aFS8rx14!(@&rD8Mnp{m|RB)qSIEFXhY+p+v@g=g%64+0=BkxlE8IOLNg7Pf+z*2 zKal8!=vFz%sHsJ0C36{nwi>HM2`sfGG-wXv?&GDTTu)I+|#h>_e+YL=tYxoQjd z>&gyWPiiHIb5u~hN)BbqqYm}3#3$}eLQGo;W8~BA%M!M~d=bB9kF6@26%z#>nf`M8 zJS6lMn$RuuRyKXZ{etl%cU|-i^;~JAbo={D2jmp`Jt{j$?>-6M7Ez?JjsAjb>!}-f zdz}%a)zCvQjlxLu-FnqpYd71NH>ZL{p;7@s9CyJ9YzZNW8xOybX^YRS2E#&lU9}eB zJu1xFuH!7lb_iIP3nldma<^(gSwQl2$QRp^(YeuMG}{F3*5&j27J(lO*-&ot(d^F& z{O(T`mjk1@32k`Qi-($XKN+GDkGpfwup#1PRff<|l^OZ!oD|`&1x23`6B%Qhy`sq0 zX#Hln$aouW{kkV-uc^xlE#(k?MX#7(#$=f6dE#90(~lgM_Uvr7$Vn_1xVxRkPE4?b zZg{qyIGuiCH(ro3P48VR2_{?~)O=VF@zO-t6wzS7UafcqM<{ z@{PsnKG56n;iEA{u` zglE9=|MSobw|uzZ_J5u$h9B_hQ>cXhjTfZ2!N(m*Vt>CS)xSMl^5Nn(-}-afK3Tmc zb%r+#S88y_OV8i84fk;JoCl~XZK_b`pY@kyWMmj4Kg)@VisBOxgsrc?u~f?MJFcvq zx7}+S6YiVCJpmkr(J?Xi_}|Sdk8?mSyzun&{1h1YIGC1ISXdX#RAZ_{iRai#Qc}{} zw{D50nDu_+VP#cli)7ZgA42=Ds_EwcD6q3XQ;bM!f5ce3}$4T8{<^#Q|sR#ikZViNd($dl&5$Z^O|H6yK zL$e*RkAD9AiH2+hQVBzI)$>fOBSqY6x`npnyV&o2ZtE`~IeXbg)p-s`gGgdFv#_wR zX|Ma&uU}1i?dJ?>`ExKD`tG#J?z^#C4$FfNa&mGucQE>LP-vX}b2e~$EaA(xwFR{H z!J*7go(w{_04h&SMi>SOhBY1mb07-`JOdA@aM|QX% zscSugjJ(SVmF~|}$Z$W}y-h^)=&!&2>Pr#7U_My*^|8&k4jGRHO(c_g7?_-~yyp)R zgsAcwegzSxI-*cnIAVAX{D9TkqtXh%uy9D1e{a`0-n0>xz4Rqebb|3uVbSu>W?(9iyZmB5R{6SoCyl?p?SsOjP5{#&XN z{m1TKbU#dMs0?71R(YP(@AFN#^_NQZ>_zw$lc9NEj}9iJ)minft9$l-ol2iL^1g7T zgY#_i-%`BD zNC?E@QYoa^A_I2$y$dD0YhV&3-XKAEohJn4k$<$i!X0qJD|U1WMiT>TZf9dhSJCU=XTR_BPIFxdsVb;!%1bKS|^Ib`-yo{$sln&(_jrM*-)Y7tV?Jq(rb zyb8(rHkF&z^Tx@B&%jIl(%oc%Ga%NvG0GH#!+etL;AH$ zTp9dni>ZmjEXn5??Fc}JUkDYlg(@|FFZRvE#(+I-c$bJ8)80cgvYNd zX6|kkIVL)_$3mn9pqJ;}Q7@H`A_e-_6-6uag`cfeZ*?V{RO6VAy`L-DuEkA#!d83O zV;a`3=H0PghpRyrV-qL4WppnGFnfnf1@RU-@Idw92BhoWYUY=xR#q^WZWVf2zwC8l zERCG&ugWx3x+NMGN}}f^PUU(0_F&EP)RK~s!clyGCf}8(023PoSG>1-G;yk@=eokP z#e-=4*tZuj{)o^pGN>ph2NH8OwI&z$5nw6P!v*aZ)x2M>+CKhKv5*q}0b{Vx=0|*JFqN=Htsg{o(Z~es z2|ROVG4MGv&AjhW+s^PV6@H=r`*Ph^M{fJLP zZD9Z#wxk7%SkQqgMSgboje{Qr*5vYDBHJ`KPG-{}I94uB@LA7OAN)KUma1L?_ymtA zH?`aPcOLQ|Oo`?4BY)lcaRz51+>;c3DnHcuP#EOih;L?pJAbY2bbR{?tF?eP>iA$w zp$!6^l}8` zejWz3w1jz^Ibv>Ze(|H?w%)vOt7kz()sfl&_Uv>#A$p~5P5XZ#O+2MMAs(IUH*c2h zPBV2DECsMR7O?46n*}YYNxsZx^l_y5A%Iv*x_k?g#FOA6(F!v#!@Vd);qI;Dm@g zv8elhK6)X09Rv;yos4`BmfptRUK#Q;J%F}-eSLt4ld$RB0{ECwBR|2pdi2+1by8T^ zt9&`9fMQ9{rX6<@Fc{;H8vmlAdJR7t+lYiSuzbtF%DhKx{#7Trkz@Bl*j<&OVeQA~ zWK1)DE^^*>nQlJs4SN)bnvwN}`55Z@XzJd)c8$CHQrqXi!0R&d@{j9`%HJ)7spUaB z1yr5@YVwu?=>Ty9dg4(NAh!9<@&ID z6YO?#U6J@ibkb?hzrbpSwy^?le@QN23X&00=>WKKfjJw?V;P03$HQmLA^#a0v!O(r zMp*NR8;kde0h(&>K>leP)yaJ8@2FC4ytdnf`Uo;4sAsmIZpZHYx>%g0n8<}H z0yv+}7lj95?nDUGc%GhYqP1;eKkZR4spryw`c;Z7PS&rmH3k=<%Xn?#Y(*r^7At%g z{-PH;`$Cw8n_HrbmO4_tZdCP{3rq8`+;9ING8|4X8g_Fkxh&}0Pg`q(dZpD6*HqQ% zaMiE2ywymf%NG|-GvR5MK)ragI?7LU@}!LZ4Jub`#xq^vTwCR_HNoR|s0@I3!*=Ep zhxYoFL|f}lb_x>qLYd^_&#&MM8nxU42-_A#Fa&UxY=S6ZO{sF`4(Ic$=%f znM-t(ZTw6;Q?obD-F-T?=3$y^Bq)ODwmPcZsQCrwxg%9oI0_M)FvhZgrKA&3Kg@$b zOWv*<0ZU74p*QON_@tTYn;{NfTGkgqdiz7e@p;dk+Mf!R1Aqz+*8!6~ z%-8p#Wu8^J&OPm=dA`ktaK5(175W*}mn@?K&(icoN&R!;Xi{fgwm_5@&uQ9L>Za)J zrc@ZHGnC>mzm!8eY6=RgAX<9*_^ok|qm=|PB!_{O$5hjo@C>==I4_86i@*`sMrZt~ zJwhB-hFepg1$5MgK9Xt!SbkyeWv%61jEdi?O%jfZcTFcR=#~n!DLbEr^LEw}6C9@aC|zng+LbP0i7w*u{R- zxp^}UK_}Q+w~92Lp~d`v@(jmEl>K%aOOJ?1Kipur-QRTfR<1x0K5P z=p_7WtnQv>k(MuCrVKeqH4NkU@5PF;ahJS-d|hocOv~`_$6~T+rCD}f-OIyv{ga2L zT?wIAsNH(p;9zdIj5$uQ0w5ElBc!38i_WI`8p)=qv8$k8ftM{ zmQURvdNVKOwIc*~I%*a@c>oGUN&ZXrXn3G-@rxV6Bj1PHb_@3VH;+xFjO@fNv|jHZ zkEqNGL>*0K&)dP4eorhF(tu(%&-nNCO!CGsDh!jSwZuALbS=rJ$EN*P(6NACpuS36 zaR$^>{^lHxzZ}Qj)h$31`%LsH*SucE2-UKqpzXwJB4w3)Gb*o{KlsyE-XO>ctPxOa zx+{tDs^Os8{jP}{0d_DPcN|%=16e7n`Idc(3Q25&{L{G5GZ*!43}?QaC?MZrNnS~2 zRM6&q9QiU)&=J|iB9hpV2~Z$i0wuReAU?zE@}HOOSh%?QE;0jAoHx?M=gQ5fnfB;* zm{v(TAk4EAMkPx|x@Dqw*wV&fbUb96+V}TGu*OYG1$AsnLL0u$9n?Q^3CVeK z_Dp-L#0y%w7ZpQq}MdF)BIqF99sSFD|#i_n(EhkBqd^aQf>D59FY$RLP=+r~M zy%fd2x*fi%B-zr__=3yHA$|0CWHh$<4}{a7;=%@8H-8DAsf>sk>t1aL771zazZq{o zv$n?WbD59}P`o0;<{SM{VnUDo9dk?!}St*l5f-S@T-zJUJYyFyV5?KF*jX<@rc~FX&*PTVGtg^ zfMhts8Di$lx0>`wdLgOx1b#&~wq`rSgGCNohTAg56BUu_6lANcTE&u7PLt>QuR-Yd z2yU<)HUmi7s%Ji;eVZ8-S(jYEYRaU~9y{YBpJDZ);%KmNOX_pLQSYn77vuaPsJFe1 z{2x(v1f2^bc@qN4Z3h9PLv`?&zj?<`aZ)|vOlC|eJpg1TS2|}5?0&4emG44<2&U;OIS$B2 zfBxi%O&wxaXXO3+_tRAdw07*no!0D(jLytY?YT;Tv=;kJ!lXH^>KQ*3qEQU6&d$Rb z-wz*Jfuv)`6o5L|n!85%0}`|wO;>hNZnTaqPFB-ee0_GyG~K`|WYpF(2A1Fkxv^Gl zl*nqUTEJ0q>Z66fDW@~O#DbwdlBFV74=$w|EogHnMEGo@O-E7;H}0GZ6itOY9EO56);$>SPRyebARL z6?5^!p$tOcTqgVSICaTh*|EaMA^okx<&fN05q0>6803>K2WIuRygr7eRK4t(62|aF=WTuL@ z&oq3eTq^!Nrh12yc=*npA5!HwAb{Jc{vKlS`1gI^{~p;V!*^~qMcpPe!eR?n4R;n6 z7B0Qle0J|zxzxQSDg&71_+c=oftC32GZc7*f5#QRMHxPOez-02SLX(UL-$!3#={fq zeL<>C0ItUAW@ORo3(}>yG7oN9V=Hkr!rwb-;0N5M0=8ADx6SX{2LEv}%B0$#ONGCq zKy^>f!;}w0{z&L}%ioRfk*m~(1uhoqC9UjF5l1gQ9ENtkFUiAyMJfA>?CfU!K5Zm- zxaRgeH63Xz>89o8*Yg3Z8uaX|Q9g6LvG)6ytu;bILxnxWw9-UJf}$uKqGDrnPNxnU zWAmnBwf5Ld-wxmW{aUiV?Hs&ZrQXiR2m7K%y?eq0ETYN#S^OSM8zfsF>$j>RSWZZ~ zq1s7VSSunTB7CD4v-91hCss?{Pkuk&i?Sl&+fvf=0_m-6s<7DD2XkS;vYGm;j2^Kb zxBt?SI6iJ`MTb+j`fqe=OXYX;W+P$OfJw6E=Wb~|L+oMeKT4GNFgbxBvg9X%mixYP zyE=f+i1-mbU=P9IRvO&i(S0)2Ud3HqL{91js;cK}IK4G$?t{g)KVI#bpPlmU5FMvD ziD-rr9gMZ-dpb8<)YI_CuO@h9djYr9aNBoN1MZeGtZH(2Yb;FG7*K;7<76j9Q891a zFc?uP$?x*h)Y2xO>?bQN!+n^vft94BuYAIj7foNtO>;4A#qA%85J4V~M1`EwVB;Pt zdSgzqyN6P8xx0RZErc=&k&t^!IX(EJHcEEK#oipq`}LY&T2dR<55C_I$o&soan(@n z${HbVhBd}d{k&?Zi)4H==)PN5Y`Zj7{A$UNKj5_u|Ase(^dL>EIX*g8!vQb}Qhr;b z*qg5k7`Rna>wTtsk%(#0(}GVzcf;=!cT?V}o3_(xHlzGAJ0jGh2@MefyEiHM+Dj-s zJG7{33_iFyzf7twF;0PvaCZu9wUxTqZTxEFUjp7lH~_i&v)#$;s+sbk^YdnQEF2ul z&jas00Di|~_0rJLP!ms_A04To7wWt-$UB(m`pS)zMdG zTiU_o-ea+I;fboV&tu^;S?0qU{n=?F6mVCi@V!=_Z&piWL{v2nX~h71%8GZZqeB8E z^N)>C7DE6dK#?{RY&eVPNW0YR!O6)9m)rguErdlR4ImOB2!OJ<44bZ#;&7Ig4Gw43 z`jwNg>pecVI#eV}&OhRPm%%Zf(!OJCvv(fh|ZZf{7-No+uigb?mlDjqw=Mj)Isks2N zE6Mj>_zi*r?`k}M{=Dm=DwI{wypLw6!Y=DN#S`V>Qgg%I0{uF@%h&IPl(-#lG@I0Q zw;qZp=}Cu*+6b^X)>cT|rOai({|h5y@lB_ntb+X4TYVA?JfNRJCoB(8J*4rc`^a^p0IU`Gke}kKV^Y92%LA?G40%9Xz zNZckPgJxUXNMFrn0I2Xmx0Q6*eqm-9nVv4qeMi5o;-}1s)UL~%q`X!!2zqV*8)}nIbuaw1)ygF{{2LCZp&n9^@PoN9cC>XeN}(>iHKen=TwOhGGpM{rfB!pnbiOZP0g6?`Q8S#FLnFB0y3M6FOLr! zYK=GtQt+!lV44QGt&xx*!Q2)wNK8PT2jy9E{RC9?_573#cZf)ZUQZfk8(@PWhKjxW%ZxP3&tHAd9xzB>FR zASWZk>1}I3GUKG?LcQtlPP+ndSR~-4GGHNgRQ4^JU@M1*pVDhvj+PhU-^K|{7&QsY zm|@^$4wb)6!=JWyvwwvgd0nLdWFL#eD}tq#m@eNUu*A5(!!Cb;zQ`5jcCgDhT3M}~ zXQ!js)-x+Gs>vb)<@9J}b0%89?Yqwv1-YA8(q7lz5>uru?)*8yd3L#Nia8DlvjrG4 zCS1S>IEb>tWj5?m!Bjj;0#t`_d&@(HdeN&(6LodYi2_4OiZQ09hr^MV2cLcQL`Uo% z)jz0KxfG4UX0TSPX*|1EK0Z#nSojld;OD-CU<_StdqTe8#YzD6w8nBO3+)R8VH&Sx9LMG?3 z9*@O9AyU!uF8UcZ z1#+HCfxH(GbU!S8Y>AeC_~=n*ytJqIdLxPc9W&j@YolW%ricC?P98oT;WvvvPxy+b z&>mfBZ%LG5ans}q{0TZm-DmuGdwjutz>D4 zw~;@1mgV#86PceRn(y!b^rwzvdbvUh^Uf#%JXLxGY27KciwJ`hpn%qR9)8U&H18<2 z4zHK@QUQo)<=nzp7|gXP!QY_vW`W}g7an??G6t;Hir@ot9yrooT2?k}f8U8)W*nG{ zMwoov%%^J1xC^)TuuDTB$W8<_=+8vD6tkGxx;thqs5d(?c?7CE6)s+@(AAiGm?TF;%u-xvyHKTVZ@Uq0dPf5m@fTL4mR{Tb(aLp5 z4XhY@a=UsSiOMeVok#(qq7Qop&|HsYBr(~2Uh_W5MKqil_`hHay{g z<=af95oxHC=HW-T-8Q@=DYaN-njK?3ax2YtQm1;yQYnIEMJ=d)6C5EzHDKzzv^I9d z?qC#97NA^pG%xHJ?gPIz6MtU~tmYYT-=k_=_u^`c06K_xAmJBdI2^gAf-b$(TQrbVgxgp< zYh70>=(}kQZ0fK`U>=`xE(`=eq;9gwgo@rFAd}hKW)XE|3rM53$v9 z60d6fM4FvnaSGa&s$@gpan39(*jm{w_n$E6N}Sfz%z^$rNlbfU%JSg&dr#g=SI{MB z$uQMDsO3W}-5fl@A!c|l1u;8PsxBoh{gWQT!uUU{i(g;}nB|W~ZOd}#-@=fTynJ9Q z(y(gb)S4mv8kNEy z-Afi|R8KzPbmRmA>^~=SO^d|`J8ZU2GB;y4%$R#0cH?=99-N>AXFeRow{2wXwS4a7 zYrRnRL$mvq-AWg00EBc-W~R9I6O`D;Sh1jKv+Q-`Oj95>GnvJ!m|c}#e4v}g$*H$s zXl~&fir_dbGbwR)l5}l2Cn6I?|YBn_9 zPWFr-Z-PYzcGP1<-Q36LlfI@;vLwakq5K9m*U&0*3u9b;{d-ieSBKt*M8{xz#<0rl%Ra4OT02h2oJNp1#) zQQ>1!t1QcTkXI)Po&Sa*-$qdWFGzpMX2s#PDhCrRZD?<-RM2?Y{Y$6I>BK;!u;(J3 zed^QB-I<8Jy>PjOeAiSHBO}?0(Sr;Er@1_z+b30?vn>6hZ^j0WzSz6Y{2Z}}|2*bc zTe|Rr%frKV^Xcbf=YE5Vtxn3~dF5v^AIlZFd>$1C>GG1-naukd1(2{Kk!Qp2Rs7xf zP@_c0VJ4?!pyS=vei!D7(L`&2iLUt?I?I(Wtm<|4>y@`FBV(pBp05L=1`i`?xE@MV zmGhU?Ee-up%ee>Di))mW_L+Y}Iza3Ae2pEM$YqURYGY-VQuoe+igpI8>mS8jE5a}~ zA}QC|_F!|%`=Vt*4b4Th2ta~DOnGT!+%#2X2)iq7fFaadY`8d(Q*1RLW8_SvFDZboN~EOB8*5+{Uwux|>73BQxv`!)<-1%ZQ+l6atAf7fwrM^gOaZ zENk^BBrOk%)k~a*r8(1Prnh$=b+`IYP$PB+m0o33ZtiU%X5?p09&K+t~o=+>oI|nRXH|CeTw?} z`g8zlxd0h-2C2IY)htDo92MHK-xL@GmZEX4*k@wqjz7}Vw<-ePB2cl7r0Yemv23xw ze%ze3YiSNW_e6VtO}Njd%Ml&r>Y|6G$4Hm_mE7VVzj-;cpt?*V_vW$kYsx+S*s@s? z=aUsy^k1Q#HX>{*N!VaXQ_3|#_q2qX5R3DpxxpSU6CeE2@9I`VSp*NA9{ut{U_46M ztz68EE%TOTm6X_TW%B}%iVIF&xj_*k?7G`BUgMhacB*M}KvhYm!H-aZ2M%e;1wQ=3 zcuiGDJYTn(!*eN9rQum+goQdTWd|fiSvK8D8W7{WO-jlLY{!zWb;{M%fRc!aI~7hF zsRP+BBve#YJ4I@mS9edh`;|6Nj@II*;`wZ%Qc~`#vM#0I@65of}il z?YJaIA^h*K>p2a@K#(rX*mmn1VYUKl;nO7EZ{9&&%e0zDQBx|UEIG$c<_9Ve91Pbh9 zqH-!h2YKKPGIN`Ba5Pi={29Y(Xaqh==CA&th5jssk2ybI0uY`7qKTXT1oD7uZ1ze~ z#}-0I{&G8*iO$?ajd_^Dwaa)P6kHzd`i&v_UeJRK%?u21Uq* zg@<=~oE3pPx}gn&BYOKbNv)-eLMKHOr-=D;or?-mGma&72xNW|1iff^aC9hvrdn%d zAYlLz3OM{%zvVo_?aO6fxhNLS@AF_y(*Zjx3PcN_Qg#Oi2Nl**DVY8BhMNk;S&%Ol zFBh!9eeFY9Yj5=)1AjWs(%jzq1bwNt1?Aq`rU6dg+bZc62?sQ=^WymGuAtea4T+n= zG|U?LrMuI81W@axkcbGHjg5_B53Gx7VwEL^jVSxd#fvSgV^z4`hQsXz!>8i_SZa2k zg%HGE5c3B%M$)R8G1`m7+NyiQL^KM%Vr=ia_13c9qbNPU1+*)^2NO8<*pn4*2LiN^ z)YMd1$q!gEzg^R?tx`eOxr~w@KO|#?oQrb?2ku_R#i4ot^1d@$Yi6J42Zz!gcL}bw zfb3ufaKm2jF%uRdig6?$9A^W$uTB@l*^h342uQ3)si5QPV|UXr1-7%R^I~S6yY@4w zfFHWz`Gfsu^=Kh1t2BP3Rx{x2Dtn4P3lt9z7qi2CS3t}*C*KM<_zTZ+-IaM5toqVF z<%GfY>uLvbRGWR>WSL(A#lw1-=*|ZbDN&08_4osAl6?Zly^N(qlk}zM6>@mnxQ`a{l4#1l2kK`MWm9 z31g-8%m;7}D?L%a)7f~NI4>pp<-r$J9{UDF!xj^oi5wO!ArEF_kdJzv?alC+_1w3k zOstj_O%k=8md*is#qmM6XIBNXjCj_gs*;_o$)X*!emH$sJAb-cR>xCgXU~G*Tb?*Q zaop0;II^D*-WxSs4epT+C2!R?k%nJ;5Hu zhwS%!m6+wnsX%jsk!g|dA%-L$JoGaTYTw*dukyPXx#xB*Y;niVQ|7Ik()Y!UloS_q z+{t^R0t8*(T4Z%({|(h6s#3IDJY>V)cY62fUW8^=0crGq8FUZ+gBeU&GXxKJ8d4qMO4}nU&&9<(jhU9X zba$*kQE!y>O1CxFzj9=Fdpdqap%>xDo>00t#I3iMGzm?Kf>D^~HTPC$GKTK4Imsp~1y8u+V zs$zwfmbP)Y4RV{v>6|eokrVNci+cIadESD!htrpFP76+p3G~Nl?k`4*UY;-bhv)Ky zZXNjM@nKO6QtZc8E1l|(sxvNL@&zpHZ3PDs-D@-LPj3@}8c}M?BUftGJ2jI*jTin7 zxB!8_efcD*0lUl6%uWt6j1L3tJGDl0l)@2XlLKFWKh~9G`->v_hnh4@R8^@pb zA2U5=W-b$9V@8TZmCWx(5mC2z`znMbM7>P>^t8PrB76P5bE!4!LHDWU!{A05=|;ON zjqC?H=%Qu4>Oc3XEeUWMDCLoVQ2fkLkmxK+@Oa_VS|JXl@gXi{^h&G=z}nDDydW!} zRsQB{AUU5RkdPFf&Ed-WP0`~*9M;nFI55u9F3TU90NMRm5%)^4F)QH$W%(N|kAkH^ z{-o0dCl9ZVR`3%iD~V!Dapg(K5lGXvr`SE*?rPi?!FZdJ(z>fC+*Yfg3exy8wm+}v z7_Mzx!D(2xK!0XUW0mS;e6OvN zFhHm02gZORUY_^T?>su8OLwB6>G|Q6kuo_T7_>vS6x&ysQPt(4B)1l4B zva4;@)*fiCh4#4CHgJe(fv_9voW{72KpErwzoHT$I6=jp5W*u32Z4>DS_-&Y;$!o$ z<g}&IfsZ*|N#hWvsL0XT%xR?K%@PygWS7BE~)|xLqQOEw1ck?~sCf!M_ zB_^G1L@gl2{M%b77X{uzCT9H$-?-VXx&AAx*7h@K4R+8lxD_anFyTuT0#ZIpAQ+{5 z7l>LeEP_zm@{yaaJxm^~(|g;$I#3((8|}J(CsfdOUeN_@D+)tPt$ECYUwBjZXp2Y zp%YUlYZA08O?WdGrTv=oYwa%aDk?8}D68hfnf>rZQPHtqflo_2ze0eXiAfeT8O_9+ zCUHI}L+A?;iuXb45< zYo&^BIyQWEU?0V0Q`6JaHMNIs-@e^Z)BjAH;|bLmYuiS4seXatM+kJaVvb5iL4n8J zHBxan^IR>&!6KvltryzSC^HTf5D1DMs{k93(6>jJVx@PSNt8BN^*km&h zeXh!JYu8LeIZ-96@HysRmIK{{)4{7U2(;~`E2uJ@EV!#_`S8@{^U|rj{EBN2E-QCl z%2R9F{D0O2E0WXYKL?ck(+LO$$z^USNN9Y-qEvgL&!E01R(;%Q>dV6`MK`)0JZQU2 zxKt_t(Z)B6$HyAvK@qw5hDu37=82K+dNXa23W(CnaHvckai6S&B;obuKvKiQb91LL zHM=)nqhYa<&!5ZwJD+&N+JnEk9ZfbI;M^KU%kQ06g}vdjwk={2aFuZX%1tVk_GorK z11C_VI{9%eqHxK_PxOJEx`4X=dv19BVzf18$(Q%}L46sk-S?D9+x0X3{RLR5$CRgA zJ{P1xFwu*3$+W#Rv?sXTX0*K5w}|XkEeLkaR!?A89c?GSojU+sS)iXvkMltagnn_a zsWq!A1O~jc)(jx z_O8pclqxD$SU;d-9z@q&SoPCTAE2GN;kM?YDjC*Sfd|I~>VGI8aiFs`_-~4`z!2BRT=Zt;V$fCkIn7_J;J`KF1vKh zcQ*m0S4+0Ixc_r_isDBUFrX0sXqTngD`~v4v{%Px=jix0cn8t*|p;=W7*HGu_?o!+4r&{_w=!UT*Z2 zp&C<`&0ij_cZChdm|u4f#zwczdeR+&QbSdP;{KRabg5UV{Xfz@+yBgYUJ?FRv+p;b zY|!$c)^vDxwagS`d-P;9nnY9s(In&R(08SXD3ZvcU5WoKHhLwp=&b(YO^Sow;Yq^S zSStMlzAYpa6ZrI2X>ci{QzGvr5ue~UL;rz5c`yAVvGk^t5!b!39k{~^bnVU9 zq$1xUDv5hbg8yqeMf~rm(>e&X{P`s7c;F#JI-ufV+OuR z&~upyJxfFA#2-UJk>eBgTA`((i4l_`5f!aVka7&L6=G8ATrHqpSnW;o4)CvQ`G)~>d==6At&J6W7 zp4&7CmuPXVb6A0D)wm$+9gw(oP)f(q2i&Gz5iK!`Xua;-q5FeSBK}ReVnh@(&Af}H z0u7^aG=h$6g1raYZaH{@MaR{2!d080?Rgs2j;2f2BzH~VpWLEj%y5k~I@B_i-W@wYMkGt2)D?qUdmK!_x?iUO2!+a^U{RYv#GjB5W2wveR1ktJRj| zJ=d*HJ~@Efj28MbOtdF_1U!II9Ny9*O)BhC+L0uz4aUkeNCOU4PjcVYDp8ssB5S%F zR5!KZRtHg6$0_-DFd+1T{~~f-7r)%Xb$=|6*NhC`-YUtjd($I2aFa@i0cdIb289se zU{dbqAZ8>?bk^+VWl3VdBKxe$VP$$Q*)s}6pkfG~67GOb-WZ~eMF$Y-kf${OV575n z9nq&M!7S_mfLF{`YFe7YV7B)p=%4Lq{sx$r*;CMEiwG++>5Q`-u`a{mFb14#f56%x z$6oVpXz7p%>*NOH&KYpac}Adwd2oD+$QXT(gN&Rg`%L0^(3{BV{gs;#0=lcMlk!mL z{|Lepwqvna<3G6syVD1B$08obAhy zkbgC$s(}s!1!ZJH+h2U{IjvnFI$rRbG#pZc(&|zOtdzhQkR}H>e0BC2WZCgNU zDys-m3`!Tz+kJ}COT#@)x z%xFkBRJP#3iJC@-?Xy{X4j`kG8K=t3k15b+$LZjp7{)5UXqL$n-SOC6=x_F|4!Vnn z5vZZ>+IyP!YKMf7?Bh*RE=dr3vE~PPkWmnS$$YvC?p$ZiaP3x?Al&q~cH?D92<~5k zpAvK@SJ;rpt#4AjL5on{{x&0sXgOS(aNGK_bDdLs((Bv>+bGa&J}3|-muJK?|2ZIl zTUdo>8WlWZ$&mRy8q#55yj-w}VYOFO--PeN3N)da#rD4Aq7{1`%;usUQkYtg^i=6QQZPnHk7t zgO3#qB2~4Ai_9f;g&0I0=|));+*|mBaGX23a~E z=!yevbFZGR(<#JhN}cUu%4)tv3~!;a3s)s0A@IfY=VZ&~Gnq)1p5S4@VFEMuL0Y+} zmJ2t9ULHn{+hcSy`7Auht}aTd0oAn<-;I(RWvQeE|8?A-d}74(*8V(G;&xskW``G> zv~6#xuLFOFvWHVSyh(b;a@st_a~~zEmFOjx_W{x9r8uarLLDsn_LuKMRi1h>**C>v2c_r-C*DLQllu4hS-GQ5RtF^Kn3m z`%^S!1meirF{lvQFrV5pIixzJhA`p5zfMq!6v}#z#YJW3la^~6y56_*6#}Bej(r9H z)FIoB-D(($F>HqMxW>O$@14W!O^R=2yN3e;+EGd0DB#{%4#VKBeYvD#_Ig*qgrhy9Tag}(g$avj`V67u@{p=_w4nCI@Y7U)-fM$7qMk$^}N zBB!C7;pG1rx5lrQ{jZem*DcDGv8reeG)bi7e@Jzzqm;_!Y<@y1Wl9X)0^+*8nWHORipES25@NWJ-=m3b)Dlw6X=e35_ zQ~*(e4MaKHOXjimWfR{gCiLlmUIsu|$?l)a1TDQUnAmj6?*l4^L`pJ2U<_yRH1I`I z^pB+e4{u)qRn@xviyTnVgN2BqfMTGuNVh1Wlz?nHM5H^VK@e0_N?O20Yz&#Hjc3;YTvk z$tql2V272&>ne7%M;xD`yq&{;&>4RT)m&<9={JAlO30#7MxY1E!pBqsy}T)i`{gBe zVr#d|9ZN2#vyjzD`FIV5VL%zHLh7)_FuwFVQ);up-6Md7I4pGNn2)!gH@Kd?x{_7o zw*3 zj7d)q2PMJ;2BIwqa=O<*)G#EfC|&WUWFR6UVzHUNsaWigw`AiC<8)F6W6G&yX+MN1 zZaPZKWpm_`RXRFrP+kl{SGd!194KSg*T}QMm8;%sH^y-7`gM+#8M-=01c2&UG5?}$ z-tu~%<%XwW7x5o#4z0}BF9{H@Fp>@E-TVfv=2aOPzqkXfF3qHTfPz{L6@pw&vBaq; z?raZ)rVzROfsdbfp@;6F!Lu{qvT@I+l5`IoMo;0z?{q6t@`$9 z=8H=Z+Ld)$dkrvUFhUZOvzi`B9qQ`?YOqCLkY=vgvke}vS|}pO3|p{QDT#MW=WJV? zQ2h?~Xi9Oymw-9SZph3h&D8cP;T#FP!+7A11SqDy{U4t%?JD7TrCfMSOogtTb*R|s`($dm27|ydp1-b0&j`( zDM->Nl*Ik*2PGE9ufhdrDJ3EJf-JU>USc^5{FNt}3a4Fp*S~$9fj*&)`_}bNrJGyx zY)c4cr7$`wN>L=E>ky}IEuMT5e)mL)5_C#FXdZ&7l~e5Oa@b^50}9+771ZF0)>wp7 z-G6N8St`RsNj%ltHeomJEzZru8f6$A5(&{b72Ml$>qW7) z3f917u>wE&{9mk%Lbr}88Ys3s_#L;}yLNf5DN-a3!lq2#_f~|dh34@l$0&$}tr>H^ zFdoC%bfyxHQ#cFw{XCi5Y?qdqGJc+gzaEQ$rL{Z!fus>s)LA4d#Q(#Bre)??FtG&< zF;6-V8f%ZW!$-R&*<%D1sljy3zMzSp|83NmgAh8paT7bqw7cHEfqc`UN2ASu5GPiHV4Oiyc>LfEJNpPS!918?qIfjWQ_UKWPLh zs<)@*zOtvlu>@ziO4gSXU~&v(J`{E7k2g@6h6qxDet8Z9RFG9vWr5Y0c3By+i~wd| zx2|kbgbB0xM3m7B=KrO)RO>ezR10m=I$}0e92X)G|l@Enj?r>6L65Q^+jINn7X??_vDqwKJL58YI4m!L;h9smaItdqx=2) zAu8_hxr8niBBg_O4c%JcDr7AfV*TZYz|vk@{*XihFZN^jx)kbZ&s;SELcbeGju}AI zl^d&~1af1phX=s>Bqk)}`X?o?LI9JxF~3(|!5x#a#9DMrZ%;adon}3G@@?l+c~jF= zqHGjCl95E34u(T=a(y9%i6mu^%EZaW`GxY@Hit&e6@qeFS-E4E#okQ)HmLO;d#M;v z)SV{onZzC6?`y<})!FJ=-d}&qKYaZ9+jh`kRlnUUK#!6lh;Amx#@$)~dq7Auw>2E| z(GEN%5LN;$^NUlbPhWFzC`?o@>6LYv?98gYR|>gp*q}*Jev1Zs#O~#r^U>qz_wvY6 z(n!Q)j>25=6l7?@wEa6MZA!Z@to6>!&d$kXSV z$Iq%R@caynN_l@qPyj%A6JKUWQDvvL;M*~9K(wlefSHy0SMdjJRS(~Lr=qQiCRi<7 zoB(&9q#V8%o;9%-$_HZsC<?waivYny9hg$v)s@BG#qgEUV4J2TqKa zmtRDLph2yLK5Nxlu!!>}<3h-uiM=c%W6`Ctb?R4oDH<%Wzl|?!OhIUl^vW57(?v1= zW(XlON&2Y&GoxUc$n=H4!O%kY8pUWe$so(Exz`z7D0&eks%)b9Rb$kpgh0$Wi-$oAF5oYTTO_2z= zOO$@ARQs>y4=@Vq-N0Qw9^UVnm0O(cU;i}xC5bVUj zg(mLE2sog~Y!_D22LSkc4gg3EP_D0~BfC2;`$KduQY%93(wV3pGA?QES6XCJNEM70 z526NW0A!Oe#IZHzau#%NJ;A0{Fl>A}#Hex>Dh+DTYM|+=%V&hv8GD3~aK{d1gSIgE zBhg=IPm2^*ajP$&tlMN8^SUSGfu!=89d4`z23ZR=UZ(wmV^F zR!|bSQmtP@YYss`Bh-xmI4HXfd2+j^reXZri~wm}I7Ro-fy?ggA3m|yFSL1{jTS7e-14SE~-0EvPH9ww*Y|j8t zuGNN^6rV#=-!mq4H6($^^P9O%k0RdBIkrr#v-y~J;HMP!P1v?7_G5~Z4!SOd=+!jlaV&iPZ?g7>@rVY7E3lYU> zCpxvX*yn75-wMEGuoQ+yl5U)hVvf7vn5HfCTtSGp>z-di!i!S2sD1_4cO5 z@U{`s7_I;=pqA4{PivGDp_xdMqZilud3SxW`i{3@v%~HEgr^Yso)t6VDES#0RL7Jeh3H?{}@;4;x zdrEU%sj<-Rd!^woO-oBl2P$MMeXTT0Y>d2d2&q$XqQ107zbD7v60UuHeE{C`l>fX31;BfEnv}ESFv%kzLkrLY= zoHsw1Sq?JJ+M?eq^F}1TC;A8{SUuAbkVaiTJch4fsCF-H;q8C*64_O#MP^b%Zkv|x zy7*XQ_WENSP}RQwVK>{AgzZGPq~%5$*(W$QA7~d<-x}OIhUcmPUQA9{ zh%Ud(@YLMtC05Os9HSGsc6aco_QYOa7Z>tRQ|cYuTL9cMqkCypn&l7n&%D31_Zo~T zSv1d4K8>WtwSEY_-ByTfxael2RAE#$OI9**ipz>)4mJ;`pMi`!aEKG?i2vA`l z)4m=iDA#5c4k0bOPM-W+cyD{3A@-7+vESo5Wt7GKQhODdAz z9V6s=>vVq2!G{l(=zaaK3=CSWWH>~0n+l>}GB|gn!Cg`jIOa7VHR;E_O5vQP@BZNn*)McBtG#tD$ zkj0D4$fr?s7&!T`<9B}4_08MfuOxi<5q{_&9kH=zGY+ynu*wZwO}gi9={N9!!7t1< zYGL~kMX~7H+R<7j1y>okpp6E&6d^0!NwgB~NpsW{Ym=rY7D(?Cyg=CK60 zM$vdC5>-;A+moN=u)Z{`jZ2DDDS#5|cM>@h5*0m3%JmiO&M(o;1_rKbEhH!SJj>@A zb8b1!8k-cCNzB+M&KN~erL;#U9Pb&6QOEv6ht2?L=?9=}pr~yWaep#}!RqJP3UhnHUCZFg{AY zvxD$3`uTiICfLz?{M_?wQQheznW!>6E+^|(Tq$;gWP_{k*v!ul7KF~LGZkujjoedN zT8#6FV2_@-=;GX(XfXLKW4P~G=G;@`#>%W=loOZ+Q_N6Ws$?kV34|#H7%Z!`(6+%O ze(m<{*C-0|3Eou%ihLC;=){{RiEO^F2W$)^Zc9tWW(Bl;URi+VJv-G{)qo*I?Q33N zBuD1%-Qct=rki0ZfG@trBh^|WiOsisDqGnAPs%QI*yii&e8r7Eu*^r^ODnFL)S&N| zh&oa+vOEYD*%(QaqjNjyf?8i9}4*q zGL}wi~>Af_NX1EBAcIRafDP*bMmCJ*otFSQHLkAQqOIMq^bJ69? zmjg#TP@Eee0~TkNmC|oNbO0169bEX;Rqu-{!qmm>wG7N0yP0&VGx~^QYCG(3m6s8~ zvk0HeUC@bSN3*>F4d}pDffUu(k4LH6GhZUTYrx2?1=8FDQ-j{6xC49T{q@$-7+bv? z{caFKlRI3^PWX;=zoM|KD{o(P1a@AMPxDURmm^0livTlxmR<{YMxL=0WCQm=JQ64o zf*=0ozsBa2mzP(8i!1hf0zKaK?-81KXq!h-g>Hc>h*1r8pH-gI>1y)BU0JE|PR*3_ zfF49^FyE!GAzur^h8wLN!5xpOC(+FHBiY3Zg|*GwT0fOpdn*Z` z$nnRXDpR=bE(ybC@aN~m_tFUWR}2XsHW2u)={>Pa-yFCUk|_Qojf1NXDE)L>>E}#L z&Kjhg%}`n}Y$#f*ZvM0BY`c%tgSF5cdsqskL*l!#GVW(wOzd;Rs>Y0nG^It++vLr+ z5cua&_PWleN&j~)xWT&CqL%*rzdGOUOfN1zhZu@rSPp}J^-~lyKI&G21~>|#0=^-} z`PH2@hYlQ|hKJD~id)D&%pXF;QW(IPY%Qzm3RmwxGJz!IhrktI)I?bp#n3=hfmW<8 zKgG^~l#?Qeo>{i}L;m4je>ToKW$HM1=cwQ?^5*l^*sT@z;ggVf;=0cSI_8s33P9|Q5QRx)#x*oe9LQPZ{MDV zIC>-wdGS$MrH4spat63LkXrR|PzsX9lhrh>8EBt?T8dEn8P} z+zpzHzK?L=o{9KinhF$lGiX1MCSg}5<5{}y9v&Sq@Jl6J%^7TUz`)MqbCW$?p^^SMoufB@4Q9Z#TT-mgS)haoLuEDY+W#6xCD7vN((-3f%f7v; z1GI?`92hl7k#Us6n6<5Wv~x9ScTN22q9mO=ny(b|%%eI0#!FZQbS>R}(Kutm(<8GJ zd*a+Vv}Xn-07lPPqof6D_N|#}(}2}^LM7MqjU7Ag>GJesK168&)aye}&-CmcwkkS4 zHV4aqz`(Q6#2f$$Oc7aj!V1FtrGr)U9099R`Tq^PTdp;S|dKZ-Op z%b<;Yg2+=SJd!G_ExsN;v4iT*yqeL4-jT)l35rZ%H@TfWx>nPmkElz(U439_Yka~s zl^Gz+drJ}kF$!CMYlLo%Bd72xO*TBu)h&V;Iu@%>G!vTfW8{AN{qYypy~){XWHa>z zZh*;c?(!WqHZ@D%Pt8}Ie!JX3`&c0;W^ z5FOlbSs!K{uEnh|FX7>~cnn>e1doO?cZZ+k``3@QTY2GxY7;9*P%y*GOtTWc+NL(S zOQSe~|KNVbyedrp>6h5~JoVY>d!w7|0jjPRn=&d(p7Ixf7@|XI@ z!38~HqP7i5as5YNYXY2KXt01`;&Wq9Df33n?`y79g?l-wuZU2i3%X3w3)#p!Tb@ zQM_OH@R*w@^&`_ZzkrB6*C` zd4LxXwo2euv^vtYxE{f&|2-3l9?qpFS@O4`;}H&C<7Z`+1rZ?^0Aya>APm=5hw%jk zG$pPCqogWLZ@x2tkYy|!9GL9wao$*CxihH}#H``Us{f3KL=RB&#}qtr2xbF|qRzq< z-1RfTNE5en&z=CF$ZODp|BCg`@gG@6@U~yr7T-MkLQ7j)$soDMymmAH35^QSV+A>) zAVz>*f~?eM!3b0(!O?Tuh!p<%EC`$?XE)RpYJ=%4*gnbs-9L5u`3G>|0 z;Z`0dO*L{!c2<==MF}@zyojP8mF`y*PZ9hI;)g@PXNrzR?>*%0k|FH%1cfLD6_aIG z^ovJ$ngTq+amf@M#-VwYXSXf)PFH{}#Ow|ZP|c;Kkvvtklqdo`A1CoNShOJwCCd|7 zt}yKqr-K{NkHKvCvs~24-e~|s$T5hwW4Zg?2K?Xah2w`&1*5KrZHB-t62%*Qh&_SA z%oZ=bl{4y#XidjCf$>z_x)4%0Ggw=nJCCYH%PZmC5VFo;vm>0wFM$rqvzdm zEN#K@$HEWA#KfM0j4AN=G?HinA1?;7fuzVh)?=1!UV^tDvSyVA8jDkk=K%{*IZt3yoJn z2~b{9q14zAJ*=m^BPsBSxA$|j^`xc?iQxqKjvp7ap6X>ln{T#K*8W;wPD?8P=>Q_$ zGXp#}(-B0=mgdQDmxHC=UuZ;hj>4gcN58$oY&XNpy-wWbYhA3|!5 z^B58(Dt?FdV=Xr^WW-Wg?st2s?#xm0SZw-tt6yJxdK(OG(K}LS%QbM%>si?F^o%p< zUTe@sR(|?Lx9!nnaSNQSf~lZ5h!)L;Av(G*&f`CrbMw(=8NgO0cH`e!l(ZiN(MYej zi*+T%oxowe+8s&8t@h494TjWUfvzP>2v5!y)a@KJxpTu zzV}3N9)BLW#ZQ~ECdp>13Igb$9(6jUKG2P{cnOYF4g#La1n0tqE_W+{S!I^0}HsnGfww-Gdks$mE ziUb{BCN zADbj;yezFbsm-<6*0YNSlwQ}&WTG)IG; zjlM-}S1DF(KUIa4^~K=n6_?4LolGV9+54#5sV&|H}zTV{8jg;v=; zqUKoX$?-r)P4a=D*v+hIB;i!^MI?{j!pi&iQnp~*x(xuYIpmTBJ0gw(?AjTV2EPw1 z&$oZr%l|Ec9+5rQ!%-Mxy4QotA|kc0QA!J`sT{sdhbnICd9noz7P>JXI-Lxeg3YNK zmq_*{gIXRU3~!cQCME(n+W`JsdIeY{d|6U3a8pvSSXC_6<_-oRJ&q*OtATW!WhLKo zgc{nu3uDDYKgJl^v%gr{G!}rxHk^DB9v;pMd!J^&g;O0X9XSAOC);qrQQYK-XVW`n z*L#8@c?@ojpFC{LB6+57lM6~$F38M7`{@3|HUe8vdW)NK0=jnNL;}d3UZbE%lU`nw zI1;yRDfd5xRxl{Ni9eCR11hBFKof^dVM-dIum)NcrTmUk-O=oicry-*BE&h9TeFq; z5;V_msU5zLD9o4~$j85+j>E9T^NHP_F|9koA%b{#xCstw@GyXFaH@M_(&)<@6ha5VvNg8G9 zHVLh+tx2r7fn*DTG#xlF*QIP_zRkkiVHOEw3XxQp;9jR!|J@ywKHz;4v$M;$Vtwx6 zG1>Wjra22UCAj$9RDK<5bmK41D=DdhN9m&CtGgfszl7+IGsjx>ZxSc6={l}V_571-|>PwQvsmIfic)SRKQscgpfD+zTaFx zBM-!!2$$32|5{EmGn$YRe0&~&RCAbiNDgs-S%&^Bkzp}Y>#j#q!`$aZH0r#3oz1L} zp)GM2OQ_|$J%*gqAlD9^@hdUg;u*=G>5JhP?VYSBrPwrft-@=GB3)3vXIzz4nB`V{ zk?8og?4!MiT4qx>U`I^jTpqA2yfoi4u^cm0)g<$#wPDhgH~;n zwvoJ*2+^V+0kIV3a3?}MF>7H*ah@$UOk#fWn?$)(FoAW>)mw3F7miBW6U<@4xM|i+ z^`gR^^P5@>A-7A!2eSN2(dGqyVikGFs?=U<^TdKhx;FEq)Uw$U8e+jtTW76-vN{n# zlQG$yFQ|O%+b-e=$ePeTQ`5|hX=9VMe!r^~xwWKZX_lI2dmR^5Ns^O240yaaBD?;VTMd^{t8eR`SWUk%)tcH7SBc>aS)fhf+m`Neq&S1VCjiH$Ref<P-2U=UPU){NxN2j%i^`6iT#35iF-95Eo}fP`|VQhgEV z6-+UjKscycf{pRooQBkXnTFO+{)=fSv8C_gXY-9co(XS%L`i=M75MusguAQ1XqJEb zEm0xZyTtnmpIIU40Ex}uI|8+ZxBoS>;IgoAHN@%=X*x<@yZ6^*vi8EAniKc`YXHId z^XJXKyg#wIQx#pL3(dc%HNRA#S05~=dR0+AFrcm%a^GhCHlhqfEtX%N^tqfg5@4$`udDOug2bZDg>Z*Qx{Sx)kNF^UXhlgRCquN5556&32G9h{Gg| zXwTZ}>T8uea~9i~+j($;-3E=a3>1J7Y*Gl(;rT*JKD)qSgiTk&8*UMQ1))s~1vs-0 zEw=3a)I5s8K_vbJ@Jo8%i3w$Jg#J@v61FLk!wFmT0=z&(hV#9nYt5e_@qnYX4u-Px z!+I_}Kb=4*(&0zs4+V^y-#tF;cO$mDk&l81;XF-X>jbyeXcY`lo4L%I-@vxzb6&F| zP?MwE)QyXYn_CI(i^6{c?9^2_xR*GKJv_({d)&V-1}M%T#JE011pRs2 zC8z9y*%A=BS}^tNXS2ypK}^fG-sGA5u(fdp0w%;jG!g|5sX6EtLO;`jRDj^Y^Md4*=3NTJ zsT2$lY&OnRvPun=fxT-|7q+DJ2HBZ_o1O+%4j!PQVj62p;qcEy!2X*oucvDAQ##@N zj>wm24jZ_}I&d%gSz4%Wx#$#%ZxdMm>K(1+vOR^oLH}mH>Gkz1~U zBG+YAGez{Bluo1dJ^;Hu1vx~=y+WlU@!MX?-PM1WC|3bavI20_X@}hi99#AS{$+qj zEvs39?%q7VftCSGEhu6P6{Z&2vWsn5>FpH6?_P{#Z_g#SydKPY4I;wzl4~#D#9iAv z5maBceFR$K1&Is)%6LXJpCG6`Bp@q84a8g$Y|+hf+wD|V3iO4e<4Cm zDul73#fBdfcR}6|Y5KC+3W^?so36D$wvca)c_~B~#AOk^w0=w2{H-r!yI_J=6Nc|_h0c&nh^*d@K=hYub+h4bKKvB>HD^x$0nn+M0}QpA`N z-*`XoNMQD#XT=lnj5Je`8kSA}8DNBbS`@8i!k`u>YPlm%BiN!p1jwS&4|5Pw9n^GP zJ&+yqlwqWD)~YF@;p-P;X)lo|jW9_EjGXSbgw_;t&e6&-E7t_jL3W9M(%PXl;nej4 zv~Si&gkQdy^{=?rXRxtSvOx#!GfN4hvK5fupC6E!|P?<&|>h|oq^-sa%l z3@P1xR&Ut?A-zbUw4~TlJ&;N0u|RE|`RFjWWh}~YV>jtFbRt6RZ?LFYlFT}al%U&u ztz8VNr<@jf6gfZgmnnQJ2A;hZ5S_uVl~f!T+W|u#gl-Ef9WkI=(hGfaEo_BCB3#*) z(fkZ_xax#Eo&hDzg`y`}eyVR3)+V}uOsGG(`H<&k7a{=S+c)R;(WPWf(WN=2C4332 zT?bZKsuldCAZl3PSq|4+6F{aBs<2LrlePH+(phN7)^8)ShT$)}6wtCj|FmLvn2n

!J)_mr7VcwUhvdSup0txZRUfL0P!iFFm!S(Gt=9_C?iVi0 z)w3FjJN8#B?^%DP|F?YCvR~I{e0kju)0q}N6W-ZG)N|^$p?MdqJ_CSO)f3&q02v!r zGYh-MQU3M-Shd-TZX?p@tJ>rZoPLD&`lIyBosVnaE7D3d0pwUtPp%^1*?Uh?u>BvW zh0l!n<8S_f2$tWHo!ALw|AQW;%pjAufO(hwmfYl~4yta@miVGBNx8!^dTBH0=f#&V zun-^sWO^ABk{XS(Rxr9VKNW%^EQWRgOw^D4MU`1BTJzK5b)gHfXfSmTD$aOsDNlGG zA7bhL*0a1rQw!jVvoIyZ^Zrrw5{HVwxu3s`1G9;G$;}5Xvbf645 zMAU(lz6R|jM$(Q??Dt`5A0UO3_r#$nr*Vlq2k@l^xV$>?bzH%jri5a+i+C3tA0Z^A zfP<+)@1c$Lr&KSrF>6>x1MlGQ&*-QZ7W5mopB)ESE!E@u_Xoufcg-4XIEAAT&wWX{79c-SM^(4ZsJTG&3_h^Q<@DD&ZKHIZ}k^ zc~L2;Z|-@>^>v`!%Rt*P07nfrAdnV| z)9PD>%T;=Kcm(g0`=cFD?O>@aXWTBrYiJO*PVVo&NpcTT3}UchD&Wcj;+2_U-iH4+y{}i)*5yESOj1Qu(ijK{MG9O19O) zkVc^vW}B&?po>+s{b{o&%>%G|Jt!y$)*L`jD~Vb_S|_9k>L_t8S>n&!`@Jp7Zm1KZ z)6Syq$aBAYNn_GYO@3to%*-ez%cS#q1i_bSJB=nPy2?+ZHen>@3t9}DPXW?-z51Zr z9`o|$c3bi}QaR|iiB6?lZnPM(3ELh-zMagYz`({Y+v345Okz#+qD&VN@AvsTbr~uG z3Yct>5v2LjT!a1YBGY-us{6jwNNTLE$ML=XAPmaY@Q>=Pe#v4)^hYNoii|w8%R5AP zS|WkNN(MqQ^{+ucgI3Bvu__PC++hfIfY$(lpl#_bRB~?{zk|32uG7<|EyjXfd%9sD zAF}QI!x{z|E)pwPp;exFQX$_mjw?A+2dD6V)q>smJ!>&6Ynl6(CKjXQ|KI#%LO&Gu z;3q5j?k9@{9INMEh7xfBf@`NlGvAfAW**`Mr|{!O3Ei|kn23ED=#SsS9abfV9VaDE0Bu5 z+wD?fR$&;gc{wN~1zHpJk_P`DEoBVmJlS+!KlJ!#C6oxP2Y7#~FHZh6jv>hf?l>v^ z9^d5u||E(t8uO;wbEnz5({TR39gZ~$!?{7l;sI!nOa?l6DOI+>)apuXfUcFqy z^=QmlCH$jg@9YGUH-Q4f2rRm2KK#LbPXF%t{YGG9jtg7D`^ExCZ~AX`qCq8hnYj~zub zwk&Wuc}(P%aGoopbQ7XK|D>PW()jmtp<1RCWD6Z)C|))IjOifU$Vi^E$uaH`XB!3& zB#JWD-T<3AEm~!$2$@l7s~`6|GY`X&(>~rZ4`e9HS&?WRM)_{GFzY@Cw;I@fK{ijm zKAJ^=wzkY3!?YXN)O;6xfIFZHCWzCpE2FSt6{LXm^s5bM-${aeN&x&QQn^7&TUny| zNU(d+4)SrY!{>z%MY;*6Zehdk0C|UK}>@Bb9zOYdU(SNJ^atJOpT_t;SP~itf%>PA$&sI6t3+uOK0~l5Hg>DdMQ81#b{l(U0 zwD<3}F4gaaHaeST$LZFH>|B`+4!skQ_a*hj3{!S13{D3I4b zN9p7DBr{}RMl}HTr_f)2gb&9f1A_fWjFeK;_()1=ld3)}5Og-OLSF0Kd4@en{eKb> zomY=RyDCEZRx7w~2`F)W4;c|R*l~ZWIGko>{UBSWU%R770Ibf;i(26R*4{ulbqk=M z={m+^Hy7Jwa0mk`m#FWyt_A&-tv%ug_X1Ai?^LHsr#>sMrk@-0R1}3sqB3{mNJ1vr zC~Pa$


^sH|$(=q!^Mux)w%AB@NXgRhQBzJrO`e{+9^YUR6b(b z`IF!Bb)|-FEOX9QMmEPw4;Q~WtBSTE%w<^_f0jCoDp0lEZfr&>p+WELsXC?F zF*B;5QiBsDHcNm0|KbI4SN+?SZfa{zUbvCIKELWWpJj2Vl0WA3TRpR09K|BKT?2=i z;^EogY0x`(NXlmalHNIue8OfB&5{e|zr<2B1&lp*g$x7LfsUzKzu)lN+ZH(jfCnN6 zFk0vW=TJxx9$TW*^qU=8D4D@=*%GaTw6bQrGyG*82=Smbv?eZE!a}EY3gUTCh7fqh9nfPJjNVZtC-^6{X%bEwXs`MkHS z2KLjcH;-+&M&A#)hkK=LvWg;_O28x<9al%MUR(%YK_x*lLQtp*!q01Z+}73>-Kui; z?8v{2nC$UMg66>d>nCBv3yb4%0~vgSAt96W-egOJJy^4b)Q0XvSt#Wg8@aYYL2Ehl z5luJyec^TmnOR1^i4W*7@JT+A=JgenDH6(Vz*U4;WX@pw`uZ|u6(E1@i2CMtu$gDX zdu7E3I&TaDE6d6uGAP^}Ql}I|Ec7^SfS{P&zYJMbz4z`zJxKEsN|4sPbqfih*(S6Pt_Y86H^OXR&gui?_TC+ zq(VzsOYW9xq{5(v>2hYvl`M|;&=BQS zYQ!*->+6>$orrhycx5*QnI23?_?^YUBtNW6It~iafgiE)C>c^C#VvWP?T$$0hakI!#i@G4|j*^bN0vq>ek!5!kwy+NcF3={a7G#fL zl)e`+&G^|Ak%Hv^Jt1RsyU6 z?mbp9Kqf^c=_Ob{TZQtNZ6~tY#9Lo4?8PjfU)&-F#c#&EM6!HF5D&3kjBI9>qw#@Z z{CNi>-JHED(xmi@x?xn?9%gh;T!*;W#jgmBdHQnXo^mJUO;o9-KM&9CysdTZr4tC9{?`Ka7j&H>h%+8!4ljnZq$ohU#R*?Z><)55dB_ z%nb1zyn8)V(N7ru2SBDOKr~{LMXa_{> z(Gw-W(rC7Us{ID_y@%R<&Pql+LjO8K=`ZeO?hWq--^*IRp6$3J432pShUE1pF!}X2a{LEttH?ik&7gH)91%n4TtafVp zwjqEKwaulaf(F+adtxf*LKD8C2=Sf7{xyR&*L=dmSI?Zf%uPV3hz_-`b7Spm8X8#2 zh(uGqCRshE1`^9W?y&ik7Qcc%3cgRfaXQ1^L*O=oGY@)Kwcz)aAZQ1=X@(#@KY3*? zFzm)*vvcQWHekC!?QWfhK*y{<@L#J+`Gj7({``4Em_LnV*t00`O1D*459Kxwuknv@ z*{QVF*x`~h@%qQ$x-0wE7dXJmBpxP+g>~LwkuzhXSMTLrr#p9TK_&mx!=RuUTWo&9 zC|go6=6-W?9S&i~>#v0$7P)onR+!)ds5h7&S?o&y6Puxwl;lR6va4G?f92lpL4NMU z#uYKFMOcFDwd|Pa8iCzQ^IKme4?r^88txjxG^yxGUQP@9P{)$k309J5+X{q)2J&6- zh-PP>+6!GeU8pkY{fgh@!$^x?K+riS#mc9QEg4&y(rMCrX{Octs{n+=(hC#c^0&|$ zF9QRa2>^yOmqa~fXBVGWLwcWU;@ZYhN=DU7LUm^MTSi7^w9M04&#CvS>^LjNGDr63 zGSBwCOhJ+xUrOB7HZM@{;qk}r%H+lCgG|on#iZTZYnLDbcV_Al*`UJ}kJq=gYvik| zXEdwBAL^0lwLsuC_!V?Gk+<;MbC^_oUdBjP_b0AB zYJBl@AkoolxL))s^RS-PxJ%;a#xQ<{5$!F@V?X%Qe&8Md@wcTdEc5D(8=A$bS-Hz1 zEEZ(L&fV3;erx~K^DdBo27vF#kt5hcQrTaSuk(5I@4X({x{9V%WCcTc>{)GVPMf#F zbm9b9c?bvyuHL#u4Tha#TZSDbiC=c30!%#unW^HyH>x>&6-I3#kx|bC4tZ`ngJgqE zGL4nc;a$x$AFO+Yaly&h`2Pn6+L zv`AHVKwuwLHb~y}l9aw%zPKg-JuUWo8j?3O5hy_hm<^=8a3_9YI*)dDeA9BHd8=Rr z>zt}!6Nw~PNVM?q!2{Dgff1Q_ z+pw5cgP;|v&>{h{KR3!QDJdB!E}kFJ0a?t8(I&VldpBkt0dMGR^d5X*C^2>;y^oO| zirNz6`6eZJw!dvm^z67PtChd;^**`Kl<Y(YhaY~EM&>$`o zmLpEKLtHV>4;VEr4Am{Hr+W%&5?&?ap4b=~fa@NB@fA5p8xRBrNnw_=@ISI4 zU$3J%O04HyKFTXn1}9&A7$4859qwJRgHnrMGi9`E&H=_I13v4YhRliU56gOTekeDkJ1>fZ1$U z<}w?C+Bh!}UvmGXZX+E# zJ7Jz%TYUdbde_rTm2T{WRhVUk1q4cf%x)3;+Mutv2m})~0Q%_xF|dFQCZ^=&RQ?!b zXT}d?^g^>3Q{L{A*P31l_h80Io|Fwk+}R?#cHQBlpo+Hw!)2FwAt=o{z>T2IXE!&E z4TKlCl@j;@vYUGkGavsfj8a>Z&0E6JZMl7jyj+x;Fzvvy%{=d%10LR+D~BJcpk2N^ zV4E$9vfM=kf{RIf5%<_#2T3DD(^6^QM2ciEw(~&5(h{3dn0DYBIhF$KhR2WPS+Sg(acM<)nq9pwU-8C5ezD8mRAu@X* zppGyOMjng13CY|k-kl}WWxI_-7@xf^orbe03*xtrEjF#ecb;!5|M8|TweWZiT}mbp zc9d=T$ECqyQQ`h7{Vo=8Xmv<=uh{>4Ho`z$@FtVE#?2TqIT zTJPhgijO6Zv51+C#Z$R!4E>4W>bI|x>@cXBt)O*$Sy!Z1+yHt~7OnZ}cm-G1eAu*9 zY0sQ`EF31SdpOB_7QoAeH#?|uy$2G40(Pwogt#H>j!Gt%t(37cvKe!|{E~SN zmQG~@B-TP0{*lZt|C1-(IE-0V@y4 zHS&wpE~{4TMgL5+9T8CGym&3uuj(4cFzfZK`7^3^=37tb!(-Tx%XvsrVtLV#QaoSX z*)w(RYA2acq_F&y4 zOqLfw_{t|0zL^D(j^+=-3foGE#<>N+W&drOKwKdI~;Q z#@c794O1iHgJ+Y~iaOnW^iB2=bnx}%e+lZ`gYwOK$t_-5*aR-ao1A}q*>yK#6=%$@ zmzW~p2*dNW`Vi~~x~;k|=Z-pgSxtV>T!5j|qBcGqb2ibMDE@J)0vI7$7r@++|Cu0& z>afnRtLdw$BAZo}t9LUkl<2uE!Ut?(d*<35lLPG?{4LC7Zot9hJgTyJFxA*w1kEcI zU_*9if7J;asXraPCgP=F-r1N)FD{YAk!_OGx?d@1V26qxI~={}?16JQ;0A>^`BM)& zi=dgyX>HU;n4Aw_cXku@f?1}OGab{h5X9?+5FInKZ zexC*NFQaTC`2FD3tObAiKz1B{HXuvMw|RIhk2-!(!-RZH+I_%1UXD~LfgIBq19%J= zH0$)6J&GHjt>JP+7Q&0eC!Yd4E!h#8;p(NbITvwd0Z$00YV>0CxH!->+ zfwHKh6^uy+AXBP;!PADJFj&Pae@;~{8kyNv)m7s^8?gE2J@jBc#3gxm6w=|k-NO&= z(N{YWhu;R|V_w2~!l=yJ^?5V9rpm!q5)9^LhZ&@z7mO+q8L37b+VSucUE<}f7%jyyCGk%#f)++0PU%C5 zPcUuBng#Lksk1(tAkV2Yod-`vT-LkcVhM#3N|0sdxhjGD6t#)^K_-WLbU(h1s>G{L zYUC!f6X@flC2|~YJI9lXORfqWYmPp?BDtUgh2rwrV z(eHNbd5wC^0gv~`Vck4 z?$F#%Z_7pLq(!e`h)<1c9jG5anY1LGJHJQoN{lW)^5MV}h$%YpV1P-l zB=IDSSiH=XZ{8>n?EdTv`@4Iu%`vlw;&%`)u6HPx#eK@6sPc4@oOMn?088e%Yuz;Mf6J+_V=;mI}cTyL_ev)%Tl}7KkX&e zbNPJ*pOdbu_-nr(%&AeRPI%-!DDv~dGtXVU3I9PIkLW#iLXq8vQU`8*b^e;tGp^8| z`|0^XlgYqX4MX>{8it)rzN;?G?3^}{Xx=Mpzzq*oxcub&n`UDpYb^tXpYExeTFGj*j4CbGM_9jK+ht%5?d7x*PjaDb-* zrDdbcf|l;^fuD^X^5hZWIc5fF^TXo9iY=t_D*LVYHeY_cnLQfx=E7;W-X+2sdTR*X z%fEJz60B@JgkHO(?dQIH#FuSaf9a(y{e%X9%VJRN)Kc1ypD$SGH0a5PAWF!jbQ~HY zxdx&TLm-T-!KhOZu^b&@T*Jx4uj(=Zv6kro)>fdkw@CD?yaCXdR_JG>Cq#ke;DPhX zr{wH$C-eQ5!s?6Np#f0VgKJ-6f@kz=8b0H>v>b*DdQY-9&1N3&JOhI+Y1a9f1!CCiab%(F+M(wF@ zl%Fy-+hIQQ&(>XFJSHzZ39f-|1wd>+pb3S{@R86*FsA0~cY}+)osS=Pi$j5}9^fRj zIW79kG-;>oHv4+Ew%mD;G-ZLhqQF+#M+p+8$#QAWeh`^fht=^p46x^c&;x;_3<|0J zx!#iyh1}|1uxrPogXeC^tJy|Lyx^YC#S%Vz1++QiK@8hhz$Tqkd{klfgyTQl-l_BY z#9>*6u^U=uAdeXb@)ug=N7-tYla`dEE_7cez$o$rVyK@e10CJX4+L=*bboo-JHh>t z4SCl&l(;KT6O)k0f^3<671YCMO+BIKu07!(m>z>gAg#a&cG*2HE7zUaY55qORHcrQ z@qg=iM1SddWG4uE9cCrS9O9cfp1FBHWW?8HHgw+06M@ zqgq*3QtlI<-aP`=MK4(&?R{dDVp7RXWdLkk^f{vuI4`q699BR^9Lnw3HfAr-d!y5t zCHg_vCH~{#wvM-hOneUmJMw^}=>CJsgN-Y@W(e?h@{&^0PQelBb>q=pu)gH`n1FJI z@k`6QJm?80g)WB=CNKr8Nu|DkSu#RR_*oZ-UPffXb+M_!P1g12XHvq%tKlZXdOn@MSUpppYZFa#Me*5 z1qcm~y?VrGJ}lNKGHeKrXSsOUbM~-LISEdPWF^cPX>+^`hBW2nnptawV3E#SVP&cW zXfYH4m^TEmIOAFDw{1%ja;+us9X}wRio-k3zIipCnB8~b0u)grCo41tC3@WGsg2Ap;bC83oGhdKl&w+PvhrZxnf5D_mKZX>F zmT3%_SW3{QB12{Y7~xcer(wCU>btb-+l*cx!f(P>ijtAcGQEz@-npKK14soINWneI z|Hs;)1B;=D2p#o5F?lZ?n}Hkhqf<<^ZyJ_7@q!)MeQi1Hdc^t?~_ z<;2S`4zY|C+huV)FBxc6IL`8;$R(WDi-oEl91=b=Wd59yIpGnOdP-|pCvsVNuPtHf2OH1 zPEgjaa`CV|Z*T1O-Jka#dn}yFOc^w2b-31sK*YL&TIq1eRj={uS+3P1Mhh}z$>|d& z4!S$!P0VXAmi~$XKx)tCud3|=5By(WVMDXEtr(sU+m=%2KoDb?|L_fVN)i6of`{;K zT|B%SkJIQ&k~6gg`Z#z| z!w{11eFs~aG|SpQf4#!?vuk|Je5m*-#iIcWavKZ1%D|#>7y(8}C>MrU0L6tVE_Gng z=9>onkNsH3$)cj8^YKB>gjcF}iHeXY{8aODcbJfHXEN0C<7a$(ua?_4JD0V+W7$@! zClqeTB=dUb588SlI#R^D6D;jSn)`W|#olc71>SUc?@3KQ^SK8czuZolA831mB}ds6 zzKi3M)N9lYPp)kcso&w2ToQgge5jlVPjVY7fW#c#H^I=64#L&C2JSI>lO#oOof^UR zaCTTE1+_RU2WvRp|E43qUj}v&Ou!JD`CQGex`Au>SWYq|@ms@co4lpVvawPp?8nn8 z6G6Vz8~j4o|>r@~hfCmt%9vlROH}C=<3e@lvH<31YGZ+n9WFrLcK2B*m z^R-0OSiPqN03_>Hj6Cd>g_Ubq072}5rKE@26=pYzJCR^T9=62Uc?E3{fuX>(s}GKX zJHqySRPGNDiFc>`v(@N_YC<2F^)d@4^J@uxAw4Ajj{}!4}!yA#+G@Rs0Fvv@^uB!;v2Ggn|c)G;?>n?yjCmS*Nb1 z>49yp4MdJQz9LVH@1;D_6DnM~n0SgUUT@I2phg*g-(FN>c3s6#B*@|K(jYxV%L|v0 zX!L(5dk?54vn^~`9T^qIE~0|K=-5EO1_($M8%igDgrf8sdN+V5Hku7kX;P#l6ln<% zLKLM40@6YWMMP={NDCzdzWsvE+1pkFLF0qoYg!$fH==try(E_Pw3uj7yLOrjulsy4O>Hv*JxSi4Cjnn6RSD~G^ zuoAv@r-KRV$B8Hn{15b6&!e$2v6nc$;s|3u3!Z|f4bhVmew}VPH$TDvQ%B_9(2@bX}?W)x9(jM=@SSEa* zVnKJ1M**kN1K4#>iBU!KfbMu-ZAec%oj;hF#!&|Rlwl!>ma?y4qb!%@u9&EimO)qp zo6xl-9nwki0f6*HH^Iq)r;U{0S7PK{2jzu(QF%*JJ&@8N3$YRep!a%qehJP(TFiID zN8L4-Mtimj$`uCat>awaC~>nd*rw6bh;%!Sep9?WF}Rt4g|aX}a%C-z-XV$rPt@BH z)B+$p9dPV@3)Go83QFQ^nYeYAR1J8t5X|IIAcl81(OB_u>=YKn6%a2s(|Mr~hC%&t zcYF$Pjcq)|>S$Y^hK4)QSx3KPK4r1a7BSlZ}aqOH8!##uV z;v%e_5~2$9NXAZ`P0nkxKir@C9;oRaEkNH~V6TRQuK%GaQdp3WxxxYRaDH)}v3JWm zcFbc|Oe}NHf!=k-RaaNr$fvx0n#mU}TJsu~)0~FN-FLLS;#c=_EwWo)dY(V=U^z(B zxn$)YQWE_tE^|`!D=OLNkT4COcSt;=H*)Gj{piTphZ3ppXZ*r9AP0p1TCuXW_0Kon z9EIe(-atj{-6b*q`Cz;*cTnC!TZ!CZHt_0-2Ooe+`>QWgsYg4 zOZ_*=lScIy%i7Lzy`zk%Gp!C5S$qr1dAa_K=0geomLqcCYAr7AiT0h7FX`|XP~m<1t5NKm;mgJ)*UsW5CzTs7<1P*Y|3*tWk^yIjeqB4zRU16i{>d()NasfKLF<-XcOqU^P4H(VBG zPW)5;ZJ1SRDpBKrU9o}Zhsq5ppl#3Zzd1Uw(d-~`rW$Wj0l}C4@X+&{{#v}PWo^$Y z>+c^~bNlNj<7=7v_`DLSvbLD#@gLfpIL#$D?;jW8*qW`4Oq3M&M8Eq9m#93bZo9J~ zQ=h+Nu9a((nSUsFfrxp8L5(eDL6Ed!f|j_X=rbnq6!Skyx1y8$^M^xfe>=jv=D=XA z+aB7fGa*K!wY@sAYBw`7kgA!B|L3+AsAxRBvgWYt-+l#QeLeF!cyw(QdiBlOrZ93O z>hew}=||If+wNXj4$b#t6^5VU$SvNpJ76x+%6=XETB-VE&61L=>|br<(Qk`?YWnF< zHoEVPMb+#(b=EB4z3j}3n;k_@Lph58aSvMcccp8#S9f_t91_P3N1o5tjoqju;Fu`O zwAMYMU*9%oQhHThU^*m1a4|{l5Tjtkyu0u45wH5T*S`xLk>OX=JE&5RhA*ZIyEj4z zl?yn%ChWtBf$q1*c7;qmXZC~~Yh+$a%-qUdtPXoEAWi9x{+0X_r>7?d?h8t6AC`0| zIl`7~TxfDov9FA~GUTfAeA60J6d(aIeIciTli&+J;r7`jlaIJX44Tn*F1y76QW0f? zj0#FJE1!#?sfhCp9rVe>!wi9a99$g;dx;l3y+ik$EQ1iO$2p}?<6+oHP9=ae1T@U# zx2alh142eXry|U)ngCyl2AL}ewm-u+kXsl!NCK&ImmB!*&;VPpXO5Z)Y@eMOZEvcR zoi%hk7OT<;w~!q$mnq214O2XPn8!~NCL@3sIlurc9O8IC0ZE+EwaTqp;|<{=MM{76 z!i0GgpyGUpc`0|j7%81YZy)#JR&#+)fZJ zO!njDG4@J8N(p`tKcMq0LuElXd!1kM0aV16aB=4$#{KA>(Wfk`3l&f;*&r15>0o{R z?06SyJsTiRyY_k=<-ntfm;&fLNWo<@K#)YVXE5>02M^3b?5;>2A)PA3y9VGMk3oUU z<_p7TF)FN=4-4hi>|7)0k)XF;{UiR)eK|{x99qv}6?&cNwtbHV8<6V4^wkcdv3(_Serzf=zjE6rnz*f(>r40gBZG2$dapJkaQZj#-QtHg z&kUSGWBbgy`R?LpHy}s=TnL{;>L&*p!zP-LX_63S$-ddk_W{5WBV)tyVVe(hYa=(n zeYp!+F(B{!em3X^&W9bpJTK6CNPYJ!m6FL@dIMueljR z?%VJ>y9N%5&i0Ie8Rb81y;?FNxxbDB^OWCz(V3?LH>rya_&FGr1-SDq5N5UaMn4EN z-ixQML1tcQKXfLulhz!CizeBdQ#? zaZquiZsSH~0A+UP4n=hGTxRTTx{3Q|K|Z& z0_dT*JOhQJ8M@qb_`6-ws0~P>Z&uk^LobVrifax#7OV9OWHs#8g+`TnPQMU^@~?Hr zO;E#O;$N?h3J%ubDIr>Web22DF~Yn;IU;g$5_X(M@PyZ=B=psY)oscNk!LubJ?p$R zQidY1L$@-%j;LbxPCSVkcSceWpn*#|wOdLZ^%jXUC7Ng1x&z*rrQ`uVF7qt!@^aL@(rs*}ki83D9 zf6{~VD0lP7I%}L|92hO1a*94avCm(9w7TtbVCT4me05FyX3D9^6}ZeYC0GYJr?A?1j`t84 zG>Y71H$){a?BO+exUC{3c}QS0GCJxRX~T-favuwE=&wB%0`u0FY_g9#JZ(KYE8?E& z{y8fp%s8n$=?NX&pR11-ph)ol?tQa5h`ejio>%RvsDr_x+NW%J-uaUus(;NV)G8S- z*?&n{GhYE7c1H2dNTd-GI|>{TWhKLzRdMP^wY7qiTO4+~{4GmZ=g5i{u;4UDr9Ui5 zQrMkC%J)XiMo4gMdLY5^FlcJON&c}1VG@fYa!KZtmzy9L59gTL^zaHj(%Kuv8)j)xFH>a{sU^9Z0T`H*v zJRu=bB+vhFu|CgidEfyLOvDhL&sPmh^S}rXz1%zoN2-R|Yx=m~A1ol3t=t z#cl+a{CR{O2+9#5O9ka1$JeusX08`%94#@_i2Y?5RP;op+vD`bkAP zP-Jm=4{PU5do?LpKZ$S48(u%_UFH|lO3IC(cS#+5yqUa5_rSrB(ETvI{7HO*=_Qz- zNXf$R=16ys@sjk@o4g9 znG*4b2jAe@N3v`Ni^7Np6#1@+SYCLl6&9_&J|Uc_prR;aF!~@`eHTX)|HAg{ZGvuc z+y9z0YH(;e4#{1W2u$>eey1)qn0z7r+A^ z?>Tr11-b#5DRe+wbkHyh@Fn`eA$EN%zGwK>)rUI{b++mQIwjfaFM4ZhOxL>+hifH* zx{=GoLx$UggE$2y2Ts)=3A`3gbo$0-Pg~z~Jgho&>%h;K|1Yl_UjD%+0wkxgsV#$Y z<9|)|$!js+il0jgz%ET37S>9BK$Iwj0iPWZSyEKjy4=U@L=(U_s){hI!p>Bv3ecVsvti!G4WZ@4wXNpbCTG zi|BmoC;P$lGq%+(tEd8IQafi$n#*n?pdeh^cA!0F;rcJL@CMgSWw-it8noiUz5~)% zVSe}ys;I%R?K(cAJHA)9VQhMm9@>8B?_L!XZW|QQ3x;1?6V7jNylUB@F7QF%oYz}=ERw6rEALkl$|uzbwuavr5d|@4 zFREz=oCA8D-uneWysYk~JW2beuQ&`1fFXS?s&coOaVPLy`xfXzH#Tz@`={yG-4U|q z;ubQ8EUmXI+$xnv=y2^RyO!K;!*9omI-^$}eZkvlj`rJN45^nksnv@wI~U}~$;#!N z)Onw-LIoxu38%&`5Zu9-g+if-t)nXK&~XN01tSnyJIA{quJ}TmD{6cPQ->GRgyw)y z=l0$zpXRleh7FDq`h{q;~tDe zUB?M(&q~_2odDCk$LMSc4lqXaBx@sk&G`6n(jW;;8dlhaAm;j>c_lSqA>4m4i4B#; z=dZaJqpifvUNw=A_wZN-J4f&=l{r5zwoJKt(~plb15Az-#+b{SArKBvpCGPS=> zCsUsnzmo~c(OV4F3|D#}3cj7{T5=pgJM zyCfHcUEXLo$l>F$5g^Q{g%_-y>gSxHlIpXdndlEb zFi6_WclHb7;K!4;pwp7^RyQIJsg)3#tSSsn(rH-^oDL|}jNsULdLko!KW;aw1vzjt zq18vmo{p=+hBc;3t+_wv#IT?vnf5b`#FGP zdh#eDa$2aZ{xq%cv}T!m$|g3^JK@|ir_9$n+5^##35Z^MAO}}T=CDpV7BpmDSBKpu ziv5>aJyh`LZ=Kc84jRE`aQ(i}%;OEI)niH?$YDe-IQs<15>^XRNH0$=GD~D20#q`3S-_^=x$+ zf)1BSy=WpJ)F{_bA&764;(HeBx=(FTk7Z9I)PlL`ke{UajLgx6_Egd%&crjo4twhy!9o?ae*naJMqXy!#~l6Cg6>tyO20)_W%E$Imr#ZU3q&{aH4 zo_SHC3!bt0XDtSMusMsL`_IU)1lTGkbbs*qAPpXIzHsp&!>h4cyR?;s%cbVqXw%5P zMAC`Zq7|*+zUEkSQUE*s#P?a!FhaA0>#Pk1&zZ^(cOdhsc7U}p=kNXDynNz|{KZ?X zhOVJ7g26&D_%=Ahk_sirO$@3aZY?GSi^z$8Y=$m~Di0Ar`aABp6;Kl4UonlwftAlw4m>S?n4uF0D! z-<-R!e^fPDjk@anNI?>Nm2II#c`MGpa6uLZPvaXy z${!kfv|C{Q$lYNR4{`(p80vNM^3PmrG`Tk;j(6TsaS%!u$p(6~(3)vu1(bEM{&$d7 z3J^sa_gEBAPVBDKevy1K4W7!B8^<~pG@dY}8unZsqQ`{iT=C2u)ET^*eFd}``3To0 z_{#`BG70ven~7Y-!AX5(mh3jwq1ocP@H)NZSP;6Gq9NhNw8^6z18`aShr_M*K0;%;)d85*tLo5&qL?9`kY;Q`bsnE9Fybk z?CjD592`n92dGoPK}q3^&l4)|rFi2nH4(ytB5zA(a!b zy|q%V?e#33;qk}v={ugv+gEv+b>H;3i;1q;wk2tvM>!A2lzUniVly6zq+cF0rXY(<|JJom~wmdN~!mF>JIxXN_CG@KcCtYiGOe%L3k08P`3mQi0=impwXt(OC8+ zV!Z{Ex9MOV_TXap>mdQ`g7A=Qy{r{N>oW4{4H~O$08-pdBJp7_7@!Pkn5E{4X35ioK$GP`=QQ<#&BHnlJqH+=VM1HtlWi{ zHJKBFA+oB&wMVpX9R~JlYD{wYt094na<%jW>k|?g&$?zB_%^X>Vj^d*8VfjRi1A@F zl(^+!z{6x#QKq8?ksuF5>VqbEWD=QPfK}8?2kTPa&eZ4&@OXoa2`O)>fO;YWPrDy- zn9j4aJZj`CjYT2%a?W0?H{v<+CHg6d14xpG6L^k3wq^~nj2v&A84i6^ao#Z?F;x3Z z$fe@Z7|GD`jO%%w@{gc4aVm5Eqep&U#erF6-a9{9i>&uV_gTzfGyR>?wWjK1#`6Ka zrn0*y{Chqw$Q%`he-G#8flbTE1*0-JsZ5DdI=Q>jd4t1j6}N$wt4m(U{qy2AfvpV}1XI*vZJ~JnK{ZIp z`7tb(II`TQy)1z09OB7bGEH~Q##_h%#=BSb>r9j#w)sMfR=ay~XwcB+kSsnt<#+n# zCktjGCZ*?^yp(B|{4Y>*m9~Z6?>>{Bt)1^&oz>)UE-{w)Bd2gEF}PDdO+BTKvS4(9 zHwmht*5SFl?ZKC`(Y9hd51 z34VA+KTgWXcCgZ{v#r|QrI+lLS*2U|hg+{+&K{pftFz&(6F(FQ7mSZA=km*5_SnM< zGN}#5dP>CZtPh$(CTH15qXGMb|s!J>eQsTYPlSkOU8ThM~f)`Gw$r*7YpW!#% z)M;2l9><|e=cl#Q+>+r72j>gB@{yAvtXbg|DP<;~!_GOOA2#qT;C z*{rk%HrO!VBzC$4%K~W+b6iK{v38$ZacJeky6B1(s=DJg@!h{|*pd%akyi5cP>G8T z%=)Y*i{lW%+G+Q+E<)*0E!|YSfyz1~(STedcPVuidVc)T&ty}V#<#Oe8{|i(^CmzD zVVMy;EiHYo^e>KjM#oW%7SwLNcH>(h$ zgk8#SeQr(1?hD?@aK%vs5e?yOKJFZP$)kS!;h3=Qk<{8Fb}ER4Uh zn4Kv6;@*89DMoIMsHw_jM6bJz*qo%{l$|hTavXE-$(K|7qyep`k|($&&&HQ$M!B@Ir#Z05Nvg|>~+7OgKb$Vg^sfWIHRY^+Ah!b z2hVg4IoqrhUVVmu=|lfeFQd}kX!d+mS4()bo)N#=ia7Z+9-*L|E?XRT2~x_0bl<}8 z_Fy^FWrznZ^aeI`8mPQs(j;pMu22O)-7XSt_lGxG^fIu>o)|%jhXmv{jqk2RLbHK{ zY72`+fR57Oml7&z5u{KP%xVMZq>1UV0fDG^9D22yX_Ee@xA&n7U7mrd+h-0Or<)mf zO_5DUemeKROq|SsItL#C83@;Nex+nx`%-9+czR;<@;-+h%lI@c)O#z8mXjx#C3T$N zDr;_55t0Lwf_7+hJ1hH4d>P_Ibr>cYZp4ST3!C49un$D*=25ZNsx5wL`UuYelG`#i zkFbvCkuMP}owz?!nKQVPKJR7V8cl6Gupgix3(eNYKi`(!A5M?zWD!H1O&-<$=W zcF~kO;vnwE+J93QkZLZ`cGG>Nx*T^F$8^!|W=CD7Bg^$JmG6SXpHqRHL({{S2pI&7 zL>EecP+dC~ZMZgJ?j}rCQAQdI@Q8FE6O{5~u#Xm*otOu>tSDG3-Ql*-sq?>;{dlXv zgVUNx71%8_hbl-J)8@(Fhrk2zs6D)lJ+}DAJJlpYTD&#vI5=J>PDw$d+iLsgD}>I@ zk!`#E&ymw4pBY;g9D$v=V&D^lcTM6svH#L-2KaFjy{$qA~jq@QhY}LtdlKePc ztT88XtZ%<0ot9jGLH6Q<5O>Y6=u9&&*S$3-Wp$CYcz`j!@8`{r1dbG`u#HhK_jV*I z)Sa8U2Ix&}Ed+tXz@j%JjZ>SkDGpq9W>xoPCjL;~RW*G7>a6w~6>ie^r&miv9-dFY zcZfFwk9~xiN%twi?I_Sm*J46AQ1OUP(0$wUuis6>CmzJ9JNgY5n!pcdNTi>y;G*?U zK^1V2`uSH+-8H$05VQaYUdTvo6hhs*U%7Ekz`ZgGhP*^%?)^x=XyBFID1fi7EfhqF zsJDcW*=dpciz&dIZbNK>NPS^4qwu&+6V`{n0J2QF6ovG^ z;1@>LTlvyYKx56Cv#Ng^>nrg6_uo3L&0ha}C(n;{lO*NhfL0z^0TuLFTb||dow~0z z{v=}Pqd^i=4JSPYEEt*2lpd0M!|}N(8IK#Y#_#$zuG#=7-N^F;rX~t7HF0rYo`Y#e)N6TC8&kM@DbKec3NPbuq(laCo|B;9D00+MuSY*eBXGs^ca?Q3LD0iB5dRo z)W3@}SQz6G)+)Y>mV0vE7pN`6Wl^q;i<pN}_aOHE4XrPlD0%$M^liG9->?8HqUJ6xrC1`)@XHls%& z`II`=azArk+mluy=9lj-fTa`O^o(KcUU-lqyBWF{i&otn0@E}A{@|Y(cp|8!t!?zi zE(1WyeuP~HN+ickMWHbAlJ3z;RJK=4aRe<;k%Sst1Npg81v8Oh#BT!2Igc`VX(_|< zXohH18*{$Kp4}{vd5yhKw@^73W!u^u#xG|d+`TA|p6f72Pf{uNHCv}L44W1tJijZ` zr0s4qjK~d|EU*YP`|QlTWyhX?;XL$_ADw4T=Vkr3DBcy`J=lTTRpw?Qi^Dsa z4dl!LxfGLJOO%~da<9@oaXZ%P+sLyCQ=L&NqwA_>SWqeaGw~xb|{h?IJXJ zES7vMFg}|vR!-}CVcK0vYGlejCX6%Vc}Uo;_ZcMLVOtu>l+2}iXwgF^v9pe7*PTuM zi{ag}@F9ZG`)ruNxIa&IX~v4$8_A8u3n>!z%ZucO#@D^AIKQADynR-Go~hcTCXv1} zk`?qsdbTxb;))Sm1|0+AgY5$XgYA2x^4(kBCaAPXHASi~e6;MGieX$Yr82WRzB)X_ zOnFk;-pjHK1={OLU5>%V{`FeKvHUwErjPFnF6(FDG)vYCetsN7*(>>ZggqNUz zsFl(!(3Xjw8q#MCzmLlFnwgFxa|H7*^HiF#N^SE5_+mAUc#Fs1$NJj$x;hp;CU!(n zoasXBlpxd12(%SYJ)jdJp6(`aMUX*p*S+KMvJUxmL?&x;L*BtlnTCBnAVKog zb$BZvaO)HH9Hr$8Ez|ZsHA|Yh3IF{MhpXRAI!DT!!;4j}t45`$<}i~&BgXPHDLu3x ze*p}`1p5&+iBlrp=$+z5h}lNoypD#!hj5unbe^?5;oS;uS{pW+JoOj%}+J3y9`1R%V0aNHx7XMp`0 zB5mLWs2-6TbsOY?cW4JPyaPz4VC`?en;UNWagP`C773aF381A_ta~JyM`gAp>C#qD z6Bugb$6`GeB3N%AjLzeqZbr@R+V_~o85~+`{**4`n?&V~^Yz{0yhd|A7b`s$+{g^W zl17@TZod678pSV|YkPtEkV{d@<_ry&ry@Rm0%Hf+$D#@CV&T@f3uv>l)M>LC9ScS_EX3ryYWZvgyAWH%x^)1`k`Y{B$j+3Hj2oN z+zbIRkm|&j8t?;V1O<>{(tt3k2E-WELM;I)Os<8hLgh)oUpDtW}jlyW$yx$i)EtHM-0K_@&S_>xZ}w zn3y3HbY$Y%2C^_jE!w?15A2FMz*ChDcxL~Ls_PE%Lsss^9+Qz_>CGuQC~J2Dd#=hF zJvTzqWsXY2-!Ja+r~BmjwNKsjyqzW8J*j6?LyFAEk32vA7P)sNiJS8T1o$0jgU6Ye zm$H|(@a5N&I?Z}|ree@b{lk(BI zILj!$(&7D-zdK8p#qh?^7LJ{^@99tl+RfP8R{N2HJDtgq>x#Rm`7~_22Pw#gh>!Ci z;kp@Wk%3(zCbhW&j(aVImnOyV*cx%&41)`fEA`JBDLgWy3-_72yp27WSmc zt@IEng6cC`G~?O;!FPD}XQS@MlQ|`G{34IZQA01nhOwao$zj{6oPq>1b8_ny*{~C- z3uiwNFLlT-dh><$%9%=KnP+U38gQN+#Lirh)@{a?^jTt~r;E}mH$KS`t#;0H|HDz# zOe&>vCAFZTmi|Souc>@O7pOfzfZQCB^B^7(hkE%|t3{`~1vQzVTX;cy1!PHYUHdTMYs%{@Q}1qgTSpXT?3F%;d+x)rW(`*Y3D>p7Ah2&1l6$DtWyb4VLO16bSkjzvZ<6MJY5K@ zYs`6ME62V*2wEj+kinW6BY^rYWT^W>1_ULNrN~3NW=$$@nf}L6+<`$*@<8YBa`W3V zyJxvb<+Bs#ftjdvSi@y7T60mffYcgt=KM3I&j>SPMIx&&bW3*#C&2+vTMQPstsY<$ zRZQi;T|^y~h9;fI7O$lnQ7*suJ8DiA zWQ?@Jc$@|eA)=y16hw&85B`D3KRQ}QnI}iJ-;B`8kNj@q;9_t7d0jgSc{lG>oySG#O&$z>?; zUU`m^2|$qBf8Sx%OSS zISS-RMu%Otd(HfcbT&|BCa9%6e`O@?C6wP>$#UN=hT156H&)h{Kzbcn*C#^kG=VLMFN3drpsAY8Ol4D0`>sSv5t_b!98YM3YYDe$MyZEb&JpH zf`re={|3irFt(i_ti0E2zWY~~PumQwsQeCm+;owBVI~Kw%GtKKs`1Ovo4r)`75b;1 zaNPt~kE18*GMy2Ybc^4BsN?VpedAl zY)mihAVXm3Tv&yZKn|bYDe8T`sY2FN>B52i!Xt9s&UvM(3x>^RcdPVy`U~5o?@31G z@HLqs^+LeVfcuaY#Qk<_-HLUt*h>J&p{z|Al1TMoxX%3%>?{_38*B-d53Bs87X9fi z^|y=`vA;kFvY;?XS$(mH!T^|!Zn%h?Vtp!5Tn)GC3R1qzdI5>XXc>*F)Y==Y|4Zrm zQ;hqM8uh2+mSS|^dJmzS8G-t>QAah)y0%n_xhWRHK)6``6yG_o6Hc;bAA*)n{)Lwh zx$7A0-V@WUp2*;(`|T(ek(`|x{NRZ1zf4!6RHtV=)1M7>kF^Hs%$MvPbm<0{WcLJ< zL$~5Jq~={e{4IR}2HW4_)fFc&u$}-iL0kIW98NsYqpofXzIEj`V)$j~v@8Dvv*DF^ zgtfO_nfZ3N*ipi|BB#%3v~S)fE`gQ3#*DJrO-5mTgxBXHr+e1e` zW!y_NSNO;)_8z~)D!`ZgX}cAdlX@&jXMIjp_B1&b#j#R-uTd~Q5b@I|FaVq{Rtvo~ z|9^jbAxv0bs9lIM6M)+1N#Q;k_dQb_|D!AxgMG4KD_)W@*9B7gNVa z%lD_yeqY%KfgnkE`3F!xFeMWgg$?r4hy!~vn60vQL*{l{xEK)P(t zjwN>Ka`)j({azrsVPe^G3}$cBK)x9^*N@DWb6Eai<7$f|tsXhZy<(08#o&Rk2x2~Q zZ9%;E>Wer?nnmYK?IX=6yVetuSYaF|aB(6sh0!QJ*>^%@iMHx#QXS_m5&Qmo)A>p* zLi$g9tO|uXCe>%^@-s9f(^0<(%a??seB2*L6s6jA=D&}a;ZGy*~PBpY@t>bbc{6G5bhOw($_L~-v=HGgt|=|-nA zl}H{0j~+Q+-}Y0E^amga|P)sY2{Y{FMeuLGL4gJx_VoKN8Taj4l09ejUmrm-pU23j3VQyvK-BV6?|&8vCK zQgNL`*SSuD?6hS{e-dk}{>!CS+LF58Tai1J`cI@$nIC6(ye^3gw`bnr#u-w>?0XoQ zYj@BZjY#&(3Fk%4Ww)b})Ozwiab#WrB~IL(4UDYeCWhm7CF=KltWR^MU4Nrrsk=;2 zDeCDna^}m-w!F&S;1Df4(wviP!xGkUFe)t}&g?;z;fk4czi-Y6Al5~-NN`kA;w5Si zXBt@eFU(t!yet0jm1Ugtf5J2zdnMK6v;5me z4y(MmIZzTI;hLv8a$)T3s%^au4+~^Hb6Dn;%R7}$rzhOUwHvn_H|%$~q9@4ke=|O2 z`o_o2r6#C8W2n^fjdjg}We}%9y0M=Ao0GHp?_J(xXcU+wd?{7;%U4}yhcI5w2{>+k ze0C|%!{H*OO|L}vHxlT)O|J6DmbyO}m#0&U;&OgVQ74*sGIH^D&2QInlpU(EFjXI@)blPcf4IcL?Z1<~nRha@c|AE@fR(<(Kw{t9)J?^W<}|20;T);5E)jKT5x!8kjHkZyiM@b03SQg0d04nn?} zTbf}z+BJmc@io9bO{*Ne^E!!>JL6VEqAtbUCw`~~r(4iyQ=eemk?Yet!j~qb$12IO ze~FD}7B|29i7F@qs_>vD8&ShLs_=tkzB2~Z$7o_<|BOac8lnL@!Nt|-mLdl zVJ+P-=^mEoooA=MO}OCnSz(4;9bV^?UpJu^l(-?egf8DpHHuyOge__{IcirUfmt!6Ze$SyuHbUbgv89+;EXg^E z^nf~=iFR20R$e)y(!x=?t6I%Cr%c4E<+Sd5`dzT^Ye~Ub$L0a6>W64`!4Zbs*ut0$ zi#FMhDY9wL)fJa&U&^tKV94KUvxMGCNz*8~hG^L%M^sOaTzlQ2CJSX#`RbL&GNUGn zxGX(&jpp-JAw^@wMLj@J_)CemYZmtD4a8U;%n#k3E&O!r>&mkVM?L6OQcy_j7(qSO z4g29dp8*-LpHv1KFLm=u*jtB#iHw1@@^eCIeRFyxD!;oa<7F&5{b}^FHOz9%YxmLO zUfD)GduAjPD2&%5(kN^y!xi0ut&*vUqRH!Jn%S#DF=4H4li!i%^Ok?92{Q zE2#9c30|$DJWnM=L68WBt3ta(M#|dc>tbwpl4Czamqc!`n`PulMjg+wu*v?j1$rwq zy_HzJG_NAl{YA>vkYoIVYII8WlnqdhlO3lF#LR!x-534pUgO!*UTZVkBE|4df68nq za%J>Zgl1lUd5Zbz-ew6ohIgn#Qw5dL<2asSU?JSduRZ}c(IXs+(Q&%Ysd6HG(3nv& z|3^ZNbLZ~ol~0#=1yv;+qyD^wet@y*d!o%u4Rv&R&h_cfrPU$_lo*IuV72^NbrcIf z-MGXkmS*%YL7n{I#O?>8ASFptb3xxWJZstPFi|2V^}Y|tHh#P}kE{Lu%f99RQ!#=^ z>o0$n|F_ymwsuI#_)-|(L}b9?g&x{c`N(Sh8hGyn^Ne<((@h%5Q|$Kf$^BS$x-Yt? zZ}tBw>b!kM9 z+_vZGvuET(-^Ks+#s9aWv+iu)A84|V_)Yr*)elcd7$@w6N_=2B=k|%+IjchBf7b7B zkBpz?BWSd3v{!%3HEWy|fLIUusvM`%KH&!uIpAk9J#b3K=gd@%K)rcMK6j`(LasR+TqGr1*y`ks8PMiz>0L0A_rm3K2l|F3K+5X>@J~ zB7upBqc1YwUm-QL0!etqUienxL$^}p!8gDB-HsUoa~knVTk+bE887K$YFgwdlNb-f zeh%hW)WHcVs;GDOY(ZK8Siu(~EEWQ=3E(~Ac{2%7?kZ?O*Jg0P&Yh0HM$@ zgYNP%0og7hekEAc?3RJtP0|l71T}$Q-TV&^HO@Z;rw*&V*II!R5!8tI-@?{yhekdF z2Id%aKF!ko8b=Y5#L87$8M3zz=OBI&p)7#&Svf~{;x_ER3UiBA0Rsi)&O=z^}CL02-*e>KnFT{XRF*>fHU+|@q&`g!2Zkx z2@e9yMuUs4OeD_k0~&sq*gJq%b#Z(~upsx*wgynIG8|NAKWvq6E5))5>Gu(41Q?PT z_1mrdTwTTteu(XG{ACYoJAkkwPY}V32N4Si8x`k6&(MNswjsi60<(E#w*^=u3kb4g zBCzzmuMYgOL5t+zM4ulvn@4a5{O5o1K~t;u9r*lGbe9p?r-19?2QinFuVC;tRx+jL z3vi;5H5p8S(vZ!Zknexg|2TYR0oO+%u#c-|;z1n7t5)Zq!Yrt#jM>3nW>Q}`bV*do z%`YB~CjJm70^jCQtfW zS1i)BvPXb~$MH}Ng-1yCEos1CbS0x(tcbq``_%O#5ilr|@zl%;7AtM*QWFtK?2_mJ zZkpZQpTs^R@!SgT)UmJcWeM&yP`x`Gm&A_OzLD{=&UWZp3TCvEI?$_q?~YC2%oMY( z1z?sUu5$$t=UyxP7wW6DP2*pPU4YAnw@bL{aHWksJoGM@fO z<>Ll~N48XOy};yw@>*9+=rf_mbm|^kO@qH=Y_&BCCW{C+`~Dz(FUuidD-W=Sqj)l| z4f{kIW(11cG`DWquX2Spu4|`HNkz%`?U3wAdtXcNVeGo9g1a$zX1{&BeS$q+v9F&r3M=Jp#`yV*s zryt1An+9CQ&Vp5fVa;VBv$cYPI(xwuGMB!Ic| zY?k^WllGX9b$y|@xtSqhSLY;*$|V@Nb;oC_ykRn@I(h78<087dSGJp=F~k)J9u4C! zz(vq#DI$6HQ`GnjgBC7E)e|$G=||r~^0AMO=;Z`*gwI@|7MnYE4P>5Xd`ZCS;ww

3JZ=HwB@2)Ev{zP}~4?NLi{q0w5r!Tc@Qkl|R_q+o6h3# zh4#k&VS4_eYSpQg*KpR`@SX75{*N3b0gIyB#lE7lnKSU?MP$|EeJXbg>3p_aJo`~4 zB7=Wso$;$y2e;7=_q9v7yk_!WwPa6+*%QnLP0=N8fw1E%o<0*Mae1LKzEpE$p7y)- z4yU^mDxT(=L~G5Aep#C3*C8$9pW3f7Tq7s?HK_9);ywGnFtlEicfla~?N{DY)57k% z52S%50p1-VArac`aBW*Ar^c<^+i>??S#iYsUl+FDo~wVeQXc&F)9VhnLI3l&>%&AZ z!w@wayzy2(6qcX~GCT=K4X-4|u;Tmw@RHY~h#8j@%KmhdZ7yc^90+$_GTs^y zDI#8G2*itiHuj$bNuosFW!BLzV~{C<5bpo+SbO--HIMK-{hFgM1-2ErrgG8FaKjDJ z_jl&pE_PAs3jfZ3tV=v1H;Np#7f^ zmo?5KNl1hRLcfD}G8@k>_b6GQB2yu|I$GYp6hZZA!h1}mdZc+kSdbs@xA2MMO(VEQ zlSXj(P8aZ-$b^)qv2**STs}HPS+Dq@gMiNrT=XeM+weD-rL3{w;TI3p2GO}Y$AU{| zU08!bv;#(- zdGo_PaDR};n@~#eDX5@~qkKAr(qQhY*M1$8O~h`!1SJlh5=?t;Iihw|$_Jy2-$BgqH`2p+y1WK<9_c%WRyoTngHs4p0Xno~uEfN2F7j-Y{0k zfT~1vo8C-Dg*8a4m|#ItxXn-}=o}2zctLh23n6V|ovW9CpdIW~XU-9o=SO>SW-S-L zzOu`5rq5_D{+qcg2E1dDQ~_TY^jG!ailFBH$ z2eHt|fGNq?!HM2x=|4++sHtYRAPz4pSGy&j)8)Ksyj&NO&J8-L9E3?~mR_ujinqP) z1kytv-@{q|VUM;_f0R3??PMgqM#fM1kZ@8dQt5I{#2=ArxcP&r29V&+=+%~(f7-#5 zk1ziSjFn8c;YMnKogRp{JTKaYqe*N z*@vGQwmRviRp2%UwodTjvO(5tr^bQlkj?H8p;g?S#iEa9 zs%}J-U%ftdkqlQXMm{L(Rlev`@Cj*Onh5tVB>c{~rN#fe;@b}1^@=U)+>(g#jhb$e%WW^+F*>y^Vu6#${4!RHTIFX*QKd-j6NL5ulDnZ``MuU zZGrh~=uof!eIKd}Y*xBanmAK+#peqjgxnC)F1|v$KThZeVkD1A;%oz@VKH-kdjf6f z;0&(r4@aZNL%aSU*tOOLB>+hv2c7yQAL^j71J<_U+Z+U9e=ERh(82p$qGPH3jqV0u z!RCNLbg`{@=<3jWh1AHG%6SEqI})gadke|4iF9uKJNL%qD~M`d&q%6%!(Wi!AX<4v zSyAU^*?K(@VMqc1g;)kiOEFTqz|1JZXGMIWa@aaQKDtW*iTN|yc9m@;o~?0R=NSGg zs=g-sD09B0?g_I&8gVyPgQlWlHB4@ep!AB)(oWl{ z7%>Zjys#tw;Ip7$eDnWSC;W0c2m5V+j90b}*uEby+DL|?c-3bL{_t^hb(Gk+sHHmi zqpp4wSwmlkL|}-1jd}QOWLZ#z`YX^+yDLk+i0eK%cSyZ570(}O0>pw|W>K0k~ z=L*}P1#)sz%!MAc7QIeQ?EaSh&L+>Ea-kCr42w0)hRgNvFVha~)t=D0knxtd+ME2f zWl22T&>1waZ9tB-hnas~UY&FeSObAQ0m2VLB<1RAnYI>OmZaz%%yft^d4@$s(4}lCCmvY_vs*rA#+^e@A9cVd+%Bdnf42*WR zjkoYC0%OWkPrthKJi%vJltyr-Ai!de8*7?jfst>g3~F=~@7+spjG|mc8;?b8RWallU7oW-e~m)UE#mYAQ8R)_)C^#%dOZzEm&ip`b@I!%5EoJ8 zruTpYyZHZfcjoa>?{EJvr8Fukla^Cdr(%>=NwP#DEw)*NA)#d7WlbWHQi)bu2s75g zXlx}RWvPs9EQzv=rHpmPa$j%M_niAazu)=ye%$A;>R4w!@Aq|Guj{&=FK5hJ1#a&? z=_obiu?<@X*~rHO>K#*PU5dgZw7R~8;L&_w=D-P_{JqP8pchIx zzn~X;m8zSnp=?7uEF?!9ux+t|O>Gw=%bLWt{!sv&)1ctfhbM!~H=f%%S@~%H#jiy* zfa7imUb`v4?9t&Y( zXcTB`)?gdt0+!RGo=67Zc7$p)u?u^jzi;x~4*@9)4T%Vj0t~c2k7b#;r=IRiMQbGJ zzCb2AXz*raxXviIR19N9xTz112n-@b$yUN{OLJbyaV{iWALK+*STG-exL~Vy=$-{R z-m$dIa>`Nb$CC|^gH&Wi-GFpPk9{N7$bfpZdvh)4>%QKvEKBA%sWzRGrS7i(Nx808 znDk@AP-u%W zOo9Y)0eHtQo8l-3|AVo=3b8SbpM!tBRz2n^&oO@k&L;X`xr8+&k)>IZYWv|ua5C-$ z3C&=4qN3JL-k~Q#pRPgn_lW<Tbu7HkY1%g>o z(iZ*xHnG2!Z7*4dcCY!M2AlqwD8)6z9*^w-i72%LNt<;B|@t?`wPLB z-d6k&A@ExE89}eOiFezhoWdT`eHg&o>n-{K3;(&4YS1MiXBBKNu!ZVFPBKc^-_btS~yocdR z?8DZ4J}`97YBoZao#YLCgnmHv@EzUmH+#P7%w`$~7GkEr$*qGT%V1@~28!f|RfJby z_x(9*d)e~lVX87omAnG&aTW==``B66w&~vPYarT+?vrTx#(VEkC(+Jd5DlLB=1?SCaaz z$#4$l;(IA6`nGrdagv$voC*ka?e(YEu(GB?2o_8;lZjv zqW?=$I!i^jo*B=BpI>zOZ!5>Y{jm$+Os8x15#N8^#bwJECTzPM^>N?7_Xn;5Cq5lk z^vwKH?-X!C{VroA#jr;3`$NK2Ci!oji5#C<_{iUKyW;uAR=)h!3Ue>EXbztVdx_GS z*}sa`!w8K~+gy@+TNfT*%l`)3#H`+74t8+qdrV5|fAcfb*wUcdpG5X4ZP{Ue0dVTy zdIn>fGZe>mE<^=Wgjz%#BH(89*Q$8T9^`ptGBcc8Mh2`~XCqJ(=EMnQbB{2LX`t?Rz}HDI^!(1A&kUu8U^q0>?hWJR+pP z3av&Xgs%bI0R)3Uwj(Ir=u6Q2=n6SSBV=Mvw2mACybMkLll@gt7i2~N+nigCH?{=3 zL682Jp$TNg%uF}Fs$c6!Fta574L)wtFAqoQ0^E`>K)A?p28M)un@RP=zN*6}(B^P8 ziDAR1Dz67KY^pagDKl!HLH^6CR6Eo zUwruuUg0n+6@U}v`UKumdh{@0p`QnAbT2EvCvF0}i!^lJtnNb*o~yW<+P>%x?lQ8fg@?3~(1k@5t0Batc%t5N*)v*jNWDL2Ky6N$#Y5ScE zgz|7t2xsYMU-OW}sV=;&UZWAN{En|9TSW+=ocL=xVO9VmvxX2dQoKqd=Qar&jhTQ{ zt`~QnuojS^19XgL;J2J3(FG}#7B5)!v;yr4mLs48AS;Q7@f=aAgKno0HW_9=oxILs zdvSh@gL$wfiqb*p19urFe8PrOR5Pz#xHTk&M$iCWa&=nI&c6I{0I*X$vh=L7IN!tw z({>+EwZde#Myi}5*bW-Zqx#I?$m!ZO<%D*+o$F^9)J2cG25(5L9+JO*G-ss9UY zWZt%k8dAmL!J~P5eG!&D`;q+AG-BQZ&Ymn0U`ILMoMRtL6C(fOA$2Oh&b&N@Mwsiq zenI3axeHi7iAw}tOZfe)A^L6iT$-W%qb2V6s>iafsr(gC4|U9ydGJ#6@LPyc)lVo0 zVhJh8iI3NrQjCAk@4P?^;JVu0)Aoubf zgNgB~%PwKj^yqt%94!32jWbl$Kp4lR2lL;aTiDO$j}aNIlc|>PiKVKODyWV1_Zg%9 zJlceY9v%aVB=N?4aysj1$R;9KAv$=uJHOuVU+|YX)F%Zs+qB>M&8)jeUf*Oa_CV*$ zvyd*9B{6iDlOe6nKW&iTzmVe1cuN_wcO&=qfM>m{@K22IgiFEIN`X|YQs-8uNcD=buOL7G>gr!3l+(^Qh`B&6EGt?-nth<=y$*%? z2D4+yFLz{7Vc^X#1c|30m{PA6>Q{N9D{p#_&Af*npM4nu0^|R;3$BV_!?3U6mk}to zKbTOc`}6+fJEKnih=hQPp7sUp@25?a;Y#MNlkhU|**)^FUvFk=R`iH}Kx&NfT$J?r zvI&mBN8h^MIb+#*f(m{a<3jW2)D*!T5Zly_Sa!_{bMr)D@Qi)s=rU8-Cg`s)+gUp~ z`-}UCqN8-Dh;*5(>Dzz|N>X#aVr<2m5x@{kW}VppaLI=AT!r-1@Yq1-!lR^Yt24Q9 zQd2-wYI&3?Qt8W^0xd)2;)?ve`oPm*B-`}BZylcnf;UnG01jq`6ewD4GJIv5i0mvI zaNV2Lc6^!uKOZebU-WXbOI4&C+Of5Lm;68Ag7q*umoIhaE#2rTtKmgL1eDD!+H`2c zHTE~>HnXGlU%Wfq-^^s*J%j4+?3sZ5^alq})z2aSZo0N~?K$wxvb$pI;PBiTOx2DB zuW93P^v{Ru#hEr6pB4Mjwrb2KtGIDdzf4L`6KER8=Aj-X66hZbTVIA~yZ3t(>@nxI zU0dH9Ml!S~AYagm`qc$dMMnuYmV+hCIvDeT9=S73XKSL^7@@u^7uBiOf7Y55Ga(%I zQV7auFS_OOv={R9YIt!$9|h@&w&f!7R`6mDhkxomfU3{VX;|pAi5NjlK~!l&?hcjy z&KGFky>sBk@PybaR7n3a0HFfF2!X}nKc6lL#Q03b17bq8K*4_F^{3&G{g{#W_}dEA zr@MiGYjHds-R|$!297YHRtBTqdNj=B#Tm*#YZM_(t{`ADB`js^gD>fXWfXmeH0qM& zr|!d43^9ZpmWoJS`j>&W#{Ka|pxF^$#zoaN4o0_az(%BR3y>HxU{A#DME5xv1xn*i zeBy1;Z~j3DKXo?4FWn7WLn#85ppvY=2WI#=rWc)VtX;F!Mr2HB7v z++qgNkH`qqIoFCu;lBUZG|W1PW`hgrqIuKqbhY6E-4A4IH>cvS%C9D^_xxG8tmUPQHAzJ7d2Aq6c$wR4J4MTZ!6 z?nO9J#>+b19H%M9IYr_5pZ)WZLxTOwBA$+OjlNzEDgUPq( z--?@|?hasx0zx}+vww2N!Wp{?IBJ87;I+UkJF{f2*7Ye?1mgxiv)Omfrx z+j^#czJ-cWXWV3gac_6vzkT`)xX;Hk365mLqnR6Gp2K?hxn&45Mf!jR3$(AtZ+g|pX=qKyo6 zwV;cq7Xt{`!#yd=zBjUPBfr+t#a;8@{g&*|18s#?(hC&|4PD=$e&kDKXxWXxRlAA0nAbq~(3O^@q$k1QgTX1>;c9p^4c5ZrB$czkm2yyKQxZ?aw#0pgA+a z*Xv8=8OV~+xj+^IRIS)+lQ7M-)>;92mXCq0DW5-27;Skg8Jhe{Lsfaac4i5@VwZ1i zUNNFDy)_s6CktZ6vd2Ja0j>&h^oh%=yBT3u#xgCy2Ox2t>+619*Op?`24j~_T!)mHx^C}?;5I-wf;s~ft;t>|iPc`L?VdLnwS`vJ`T}EERVfxFlEE%0IvU8nY z>iJr)PANH;Q;@D^zv$?FyGLYnEX0PRNK$Qyc@oy z;G~JH)(oN(cBh%XyNhh=>7vL3dS>oHgXz6)$#k6im@g9dDqYMg{7*D0> zcpVbPC=jH$FmRj8ex^>$+V%sf0#HJA?%c4dI~!LzjiMr(_%>Nj0eS;W|GQ}BXvwN= zrhYHl&dFS`P>vxkwz#?*?I)HTiSA73OcNY=Q6=xlp1W2=ThzAPmJo6h_aq!l5Y%WR zo9|B8Z3VY+e>+=1Zs<1ua`0Lms4WDnXzC%N9Fi$n&0p8}`(tXL{u_|Dkw*>Qt#s7j zwSpdgJ#!x&0fDs?Dd|JVjt-TM!}LgOsI$s{5UU%YBbvruE&0isu!4#+ zZLXRNl`;Ij3=d+yiL%GUwHx1_bdU60`2@{k;Xw*nMYGF8J49>M48$8oKDK9t;_s{v zJj#Qykg_`H?~1`=KZxh>6JR`m1*TEl`Z0jQ8cPt9%lOJNPqjw(#JX>$&m)NKx)pqhmcFFhS-5-f^x2iHcGfE~2gIYy#c z1Gc(*@1BtS34|EfW_7G~Oe3Yi>)Nw`IxI|fx}$Fd;AomP6a$iqg!PLFRcKHSn2+Cz zsz3p1fSEUQ*l!JSXTsbqT1hsbE+C}$tO2_OyS~j6Hv*E>Q8UFAoAo#AaQ|2CPiD{Z zj1UnYl0qGn%Dby=Ook*PV3 z-p6uNc(iW@O1_G>*Sc{9(pL}KVQ0D~nmuh+X%OO2R4olZn!Kx=EkABEQ81nuJ0Ch1 zXng*N(XeH4j{k>>_QyZ7YyKDft0a_~Flk5+o3*2Y-@ri!xa-N_(EGbpjqLR1el@|x z|Cj$bHyRaU0l_T%(IX4hkVv~aD+$eLpRHTBo&lXHU#hf&F*%n!E1s4C&a^e$S=*PJ zV6UT25QMp|g729q)WRNjQ(g7h{b~Kb6k~_GA|;XOr>@|193=#?3U-W1zj7+~*Cu0= zUD0mhP96sd!Qt+)wiLd>9`D(Uf1arHlhwjM4mou2#r&#|565V94geSb!W&(Mp&h#E z(U141IlHNhrX<0%^zV0Q@F*OZFfpyookhC-5uEyIsOB!B4%y@VQTJ2k3Go4t^eq}0 z|NR`6=_Cl1;Jz%42om1aPtk-U+q130l04?UU75y~um!TM721afSR6e~Z-jdG?X)`} zw(DV2dLTQXWy(}!c1mg9yeG*z7!!gaL_{577j?MSl0C-=7fE0nIuB4lR3W(?#Or!<#oK-OIgYudh1SZUx)Z19zH!<9~2T?Ka34RTW zbRU+?%i$h7k7CASp?kQAA-J>3Q`od=1e%8X=mH*qg*I%X>0u*OcT*uCpeR++ATdQx z8hrnE72KC##J;!k&NJy&&(AOs$g)0_z#dCbn0!}qYXeJuc!M|<2hXg81@dvRf{lCy zNrhYQ$d%I&~wF7n~$<;KTrX*n6e!88IVL?C3u2c0hkO)}M_h2QiY1?tYhTyY* z1MYB5LfMxWi8H>Z#I-8 zd$o;HYk4^HNh&t`4^4pt)V(p}bRz6$S0jcS&e@p>HD?1%qd)kldSGJiP}?2Ox{lYS z6v>($pqq1GB5;N+@#qKQfbaaX3O0PX$R!}$5l|{;;E6^I^3Kd0r!b^b(DJI@bQtuSTqrO!V2d8$Ne zPLZK|_g2y}DzgvrMVJ@t-56R@nqWs9Z+7Kq#^~GdQ@m+p{MF zoY31Pi)#*g4Zhpn&lQy|z3F^7R<5NtIP~bD)^ds1h<5Y#*gte=ARSM&52|=oEcddN zD3(hq*DYL0Y^`=zd{ie8>8+S~>PW)O<9bn~4+g>zIp{=T z(a%;Ga9t9m+|{Sze;N8aPHfDekSFeeP*x3IGpW2jS**8)tv%mZEFujpSKQXbA5FB| zvr_JIbJf=S245><1cHWB&=sMhEGJ;W*T1}Y;?VeOF9kyt4VFsev`%Yzo zZ#jT)P4V**;^IuXIgJY8KZBi`dT;3@RU&FospU(BLZcbtEqxhQrdN(h>mjV_N>mBWg9?E@87=^D!3aZQV*c z{8JzmPo?h1^HGS4F}e5J@D%(zM13aB??x|KThy%>YZ1>MS8{L4Po;Ssz0w4}GX`6G zfc9gcmheTBM#6qa9dxhp(a*3*Lt2ugsqx%>c?@loe`@I9goYmVln>5yIM7;4B_TZP z@_x=(u2LBaD*g5W+EVAu!{bZW>dCBEuQS(P(E;e0KBY4N(hLbo&FJ{du4a3)QfN&w zpyr(^QT^d;#rxHL0;547uj}&+!g*s&)m=i?vo0NzwQ(tZC+uCI;dJZGD2ef+GAlA< zN~|raJ=9*?I63lmWL&{Ze0Zu`m=i0o>#-hF>Y^bl^Ay+IsQDPXG5oQUXPD4s0Ef;L zW!%}cQE{YuOD*P`kf%8NW9fv-!w{w2;b=@Q!ubjgaugLuYQ7|AjC@h!kj zeuFaOnYUrbz4k|IGK=ftj4^?+AN zAf6qgNHp$6@?y#Ex zY~OeKPNgfc^Wdi`wEW$tqb>>&jGr%O+beF>uEG3YwOJm#-g*Kce`rLLy?o?^W%IKc)c;NYcza)dF9OTcsz~9Lg??NCh7)9%EU{h@efE<=WMmpAg5s-V zz0Na+n}{LE5wWXB!FeI;YV}X7FV1Oce8{M!2yYZ+HA8g>F=I#%DXatILwUnZPD{Op z3`K@X3@YU`V;bLi3Y&1qw#OeHD~k`<+tAT`@6)?cy7p(V3p#(9>s_@cNvb?g$4s9dE$4IghNM@^!6Seg;}dmZhfJz!mtzJ%V>>`#!JT}%!g{P zqh6P=mnb9(POTOC0yIN85+P=-I!ri2TZ5nG+%ojX&?V3^%@dePY0Nm|6B>u#SR zXua5>#Za9LI~g!|l@NBA6=Z$rqWc&3-Rf_p0&6WlDJD&jT7`NRf4y%Ja4k;#$7D(A zO7Fr$m9W&%QLy6a$G~c%`A!CM@=@W#%M!2P7kmTh=)&QWAHOno-($l2bw0ME$)}Vf zn?Kek4Hq5v&ybI=!eNO5OZ+o%vc&qNLc{Cocc&pVH3ER%U=4N9bkh0NuIA-otBjNN zR+)NhrMu0Cf0%4xn$@v1<&w(RA8+dCEV_j6D4kR= zSeD>}-`X%GeWl!wQB8wZQtDp9q;QRZQX4fqD^1-;S>l`*#e!hh!(V9BtU5Dx03w22 ze7Qw3RU#_s6?9!)Owh)crEG@>w)-l*%QE@P%PdjQy%|jy$zG;ilWQ z{q-GPrxk~@%nrtv*K2alYraYG0zz?`PA!lk(FF-(p}PMn|4;?*_Yy~jY58GWs*V{-lr!ZbGyUS-L(Sb zNx=jAn;3L8=9|$a-)y)1BEH;yJh6MESp4DvF(*P Cx|s?9 literal 123556 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 W^YItB1|0(b$Vn+m=H7bv^#1{To)|*_ From 1d7fe3d93a4eedbad1ecd52e1d0a54e7524eb2ef Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 24 Jun 2024 01:30:59 +0200 Subject: [PATCH 16/37] Separate query --- benches/visual/query.bench.lua | 2 +- lib/init.lua | 119 +++++++++++++++++---------------- 2 files changed, 64 insertions(+), 57 deletions(-) diff --git a/benches/visual/query.bench.lua b/benches/visual/query.bench.lua index 57eddaa..3d00783 100644 --- a/benches/visual/query.bench.lua +++ b/benches/visual/query.bench.lua @@ -7,7 +7,7 @@ local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matte local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr) local newWorld = Matter.World.new() -local jecs = require(ReplicatedStorage.Shim) +local jecs = require(ReplicatedStorage.Lib) local mirror = require(ReplicatedStorage.mirror) local mcs = mirror.World.new() local ecs = jecs.World.new() diff --git a/lib/init.lua b/lib/init.lua index cfde17f..9cf8926 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -654,63 +654,13 @@ export type Query = typeof(EmptyQuery) type CompatibleArchetype = { archetype: Archetype, indices: { number } } -function World.query(world: World, ...): Query - -- breaking? - if (...) == nil then - error("Missing components") - end +local function PreparedQuery( + compatibleArchetypes: { CompatibleArchetype } , components: { i53 }) - local compatibleArchetypes: { CompatibleArchetype } = {} - local length = 0 - - local components = { ... } - local archetypes = world.archetypes local queryLength = #components - - local firstArchetypeMap: ArchetypeMap - 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 = 1 - local compatibleArchetype: CompatibleArchetype = compatibleArchetypes[lastArchetype] + local compatibleArchetype: CompatibleArchetype = compatibleArchetypes[lastArchetype] if not compatibleArchetype then return EmptyQuery @@ -792,7 +742,7 @@ function World.query(world: World, ...): Query queryOutput[i] = columns[tr[i]][row] end - return entityId, unpack(queryOutput :: any, 1, queryLength) + return entityId, unpack(queryOutput, 1, queryLength) end function preparedQuery:__iter() @@ -822,7 +772,7 @@ function World.query(world: World, ...): Query end end - lastArchetype, compatibleArchetype = next(compatibleArchetypes :: any) + lastArchetype, compatibleArchetype = next(compatibleArchetypes) if not lastArchetype then return EmptyQuery end @@ -833,6 +783,63 @@ function World.query(world: World, ...): Query return setmetatable({}, preparedQuery) :: any end +function World.query(world: World, ...): Query + -- breaking? + if (...) == nil then + error("Missing components") + end + + local compatibleArchetypes: { CompatibleArchetype } = {} + local length = 0 + + local components = { ... } :: any + local archetypes = world.archetypes + + local firstArchetypeMap: ArchetypeMap + 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 + + return PreparedQuery(compatibleArchetypes, components) +end + function World.__iter(world: World): () -> any local dense = world.entityIndex.dense local sparse = world.entityIndex.sparse From f852ca9a883067705692058b4725d00ec7b8cc71 Mon Sep 17 00:00:00 2001 From: Kalrnlo <62822174+kalrnlo@users.noreply.github.com> Date: Sun, 23 Jun 2024 21:01:55 -0400 Subject: [PATCH 17/37] Cleanup functions and add missing types (#53) * update * Cleanup functions and add missing types * Replace 0x10 with ECS_ID_FLAGS_MASK * Fixes --- lib/init.lua | 113 ++++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 59 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index 9cf8926..76e9e9e 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -11,14 +11,14 @@ type ArchetypeId = number type Column = { any } +type ArchetypeEdge = { + add: Archetype, + remove: Archetype, +} + type Archetype = { id: number, - edges: { - [i53]: { - add: Archetype, - remove: Archetype, - }, - }, + edges: { [i53]: ArchetypeEdge }, types: Ty, type: string | number, entities: { number }, @@ -75,7 +75,7 @@ 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 function addFlags(isPair: boolean): number local typeFlags = 0x0 if isPair then @@ -95,29 +95,21 @@ local function addFlags(isPair: boolean) end local function ECS_COMBINE(source: number, target: number): i53 - local e = source * 268435456 + target * ECS_ID_FLAGS_MASK - return e + return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) end -local function ECS_IS_PAIR(e: number) - if e > ECS_ENTITY_MASK then - return (e % 2 ^ 4) // FLAGS_PAIR ~= 0 - end - return false +local function ECS_IS_PAIR(e: number): boolean + return if e > ECS_ENTITY_MASK then (e % ECS_ID_FLAGS_MASK) // FLAGS_PAIR ~= 0 else false end -- HIGH 24 bits LOW 24 bits -local function ECS_GENERATION(e: i53) - if e > ECS_ENTITY_MASK then - e = e // 0x10 - return e % ECS_GENERATION_MASK - end - return 0 +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 // 0x10 + local flags = e // ECS_ID_FLAGS_MASK local id = flags // ECS_ENTITY_MASK local generation = flags % ECS_GENERATION_MASK @@ -128,29 +120,20 @@ end -- FIRST gets the high ID local function ECS_ENTITY_T_HI(e: i53): i24 - if e > ECS_ENTITY_MASK then - e = e // 0x10 - return e % ECS_ENTITY_MASK - end - return e + 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 - if e > ECS_ENTITY_MASK then - e = e // 0x10 - return e // ECS_ENTITY_MASK - end - return e + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else 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 function getAlive(entityIndex: EntityIndex, id: i24) - local entityId = entityIndex.dense[id] - return entityId +local function getAlive(entityIndex: EntityIndex, id: i24): i53 + return entityIndex.dense[id] end -- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits @@ -236,7 +219,7 @@ local function archetypeAppend(entity: number, archetype: Archetype): number return length end -local function newEntity(entityId: i53, record: Record, archetype: Archetype) +local function newEntity(entityId: i53, record: Record, archetype: Archetype): Record local row = archetypeAppend(entityId, archetype) record.archetype = archetype record.row = row @@ -252,7 +235,7 @@ local function moveEntity(entityIndex: EntityIndex, entityId: i53, record: Recor record.row = destinationRow end -local function hash(arr): string +local function hash(arr: { number }): string return table.concat(arr, "_") end @@ -262,10 +245,10 @@ local function ensureComponentRecord( componentId: number, i: number ): ArchetypeMap - local archetypesMap = componentIndex[componentId] + local archetypesMap = componentIndex[componentId] if not archetypesMap then - archetypesMap = ({ size = 0, cache = {} } :: any) :: ArchetypeMap + archetypesMap = ({ size = 0, cache = {} } :: any) :: ArchetypeMap componentIndex[componentId] = archetypesMap end @@ -275,7 +258,7 @@ local function ensureComponentRecord( return archetypesMap end -local function ECS_ID_IS_WILDCARD(e) +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) @@ -329,7 +312,8 @@ end local World = {} World.__index = World -function World.new() + +function World.new(): World local self = setmetatable({ archetypeIndex = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, @@ -347,12 +331,13 @@ function World.new() ROOT_ARCHETYPE = (nil :: any) :: Archetype, }, World) self.ROOT_ARCHETYPE = archetypeOf(self, {}) + return self end export type World = typeof(World.new()) -function World.component(world: World) +function World.component(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, @@ -363,7 +348,7 @@ function World.component(world: World) return nextEntityId(world.entityIndex, componentId) end -function World.entity(world: World) +function World.entity(world: World): i53 local entityId = world.nextEntityId + 1 world.nextEntityId = entityId return nextEntityId(world.entityIndex, entityId + REST) @@ -411,6 +396,7 @@ 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 @@ -472,7 +458,7 @@ local function ensureArchetype(world: World, types, prev): Archetype return archetypeOf(world, types, prev) end -local function findInsert(types: { i53 }, toAdd: i53) +local function findInsert(types: { i53 }, toAdd: i53): number for i, id in types do if id == toAdd then return -1 @@ -484,7 +470,7 @@ local function findInsert(types: { i53 }, toAdd: i53) return #types + 1 end -local function findArchetypeWith(world: World, node: Archetype, componentId: i53) +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 @@ -502,7 +488,7 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53 return ensureArchetype(world, destinationType, node) end -local function ensureEdge(archetype: Archetype, componentId: i53) +local function ensureEdge(archetype: Archetype, componentId: i53): ArchetypeEdge local edges = archetype.edges local edge = edges[componentId] if not edge then @@ -600,7 +586,7 @@ function World.remove(world: World, entityId: i53, componentId: i53) end -- Keeping the function as small as possible to enable inlining -local function get(record: Record, componentId: i24) +local function get(record: Record, componentId: i24): any local archetype = record.archetype if not archetype then return nil @@ -615,7 +601,7 @@ local function get(record: Record, componentId: i24) return archetype.columns[archetypeRecord][record.row] end -function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): any +function World.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 @@ -654,9 +640,7 @@ export type Query = typeof(EmptyQuery) type CompatibleArchetype = { archetype: Archetype, indices: { number } } -local function PreparedQuery( - compatibleArchetypes: { CompatibleArchetype } , components: { i53 }) - +local function PreparedQuery(compatibleArchetypes: { CompatibleArchetype } , components: { i53 }) local queryLength = #components local lastArchetype = 1 @@ -673,7 +657,7 @@ local function PreparedQuery( local i = 1 - local function queryNext() + local function queryNext(): ...any local archetype = compatibleArchetype.archetype local entityId = archetype.entities[i] @@ -745,15 +729,15 @@ local function PreparedQuery( return entityId, unpack(queryOutput, 1, queryLength) end - function preparedQuery:__iter() + function preparedQuery:__iter(): () -> ...any return queryNext end - function preparedQuery:next() + function preparedQuery:next(): ...any return queryNext() end - function preparedQuery:without(...) + function preparedQuery:without(...: any): Query local withoutComponents = { ... } for i = #compatibleArchetypes, 1, -1 do local archetype = compatibleArchetypes[i].archetype @@ -783,7 +767,7 @@ local function PreparedQuery( return setmetatable({}, preparedQuery) :: any end -function World.query(world: World, ...): Query +function World.query(world: World, ...: any): Query -- breaking? if (...) == nil then error("Missing components") @@ -840,15 +824,20 @@ function World.query(world: World, ...): Query return PreparedQuery(compatibleArchetypes, components) end -function World.__iter(world: World): () -> any +type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) + +function World.__iter(world: World): WorldIterator local dense = world.entityIndex.dense local sparse = world.entityIndex.sparse local last - return function() + -- new solver doesnt like the world iterator type even tho its correct + -- so any cast here i come + local function iterator() local lastEntity: number?, entityId: number = next(dense, last) - if not lastEntity then - return + if not lastEntity then + -- ignore type error + return end last = lastEntity @@ -872,8 +861,14 @@ function World.__iter(world: World): () -> any return entityId, entityData end + + return iterator :: any end +-- freezing it incase somebody tries doing something stupid and modifying it +-- (unlikely but its easy to add extra safety so) +table.freeze(World) + -- __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 } From d4be467c6ccf2fbf0430435a01915f3b8f1f3a63 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 24 Jun 2024 03:20:43 +0200 Subject: [PATCH 18/37] Rename files to luau (#54) * Rename files to luau * Rename remaining files --- benches/{query.lua => query.luau} | 0 benches/visual/{insertion.bench.lua => insertion.bench.luau} | 0 benches/visual/{query.bench.lua => query.bench.luau} | 0 benches/visual/{spawn.bench.lua => spawn.bench.luau} | 0 lib/{init.lua => init.luau} | 0 mirror/{init.lua => init.luau} | 0 rgb.lua => rgb.luau | 0 testez.d.lua => testez.d.luau | 0 testkit.lua => testkit.luau | 0 tests/{world.lua => world.luau} | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename benches/{query.lua => query.luau} (100%) rename benches/visual/{insertion.bench.lua => insertion.bench.luau} (100%) rename benches/visual/{query.bench.lua => query.bench.luau} (100%) rename benches/visual/{spawn.bench.lua => spawn.bench.luau} (100%) rename lib/{init.lua => init.luau} (100%) rename mirror/{init.lua => init.luau} (100%) rename rgb.lua => rgb.luau (100%) rename testez.d.lua => testez.d.luau (100%) rename testkit.lua => testkit.luau (100%) rename tests/{world.lua => world.luau} (100%) 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 100% rename from benches/visual/insertion.bench.lua rename to benches/visual/insertion.bench.luau diff --git a/benches/visual/query.bench.lua b/benches/visual/query.bench.luau similarity index 100% rename from benches/visual/query.bench.lua rename to benches/visual/query.bench.luau 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/lib/init.lua b/lib/init.luau similarity index 100% rename from lib/init.lua rename to lib/init.luau 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/rgb.lua b/rgb.luau similarity index 100% rename from rgb.lua rename to rgb.luau 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.luau similarity index 100% rename from tests/world.lua rename to tests/world.luau From 6a507d35ea6e8a8609a38d725666297f9c317d96 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 24 Jun 2024 04:08:57 +0200 Subject: [PATCH 19/37] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5ece64..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 @@ -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 From c95bc2a582f825d89f9316e2e19b813019692576 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 24 Jun 2024 19:02:27 +0200 Subject: [PATCH 20/37] Update benchmarks (#55) * Update bench.project.json for luau files * Stress test insertion --- bench.project.json | 2 +- benches/visual/insertion.bench.luau | 30 +++++------------------------ 2 files changed, 6 insertions(+), 26 deletions(-) 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/visual/insertion.bench.luau b/benches/visual/insertion.bench.luau index 8e24f29..40bea46 100644 --- a/benches/visual/insertion.bench.luau +++ 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 - }, } From bc43ee336b337284179d2dd82ed6e3e4649eed49 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 25 Jun 2024 01:44:27 +0200 Subject: [PATCH 21/37] Freeze the world inside export declaration --- lib/init.luau | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 76e9e9e..54b642f 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -865,10 +865,6 @@ function World.__iter(world: World): WorldIterator return iterator :: any end --- freezing it incase somebody tries doing something stupid and modifying it --- (unlikely but its easy to add extra safety so) -table.freeze(World) - -- __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 } @@ -999,7 +995,7 @@ export type WorldShim = typeof(setmetatable( )) return table.freeze({ - World = (World :: any) :: { new: () -> WorldShim }, + World = (table.freeze(World) :: any) :: { new: () -> WorldShim }, OnAdd = (ON_ADD :: any) :: Entity, OnRemove = (ON_REMOVE :: any) :: Entity, From 6b4597ab96ed0093879eccf1a88e0831da6024e0 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 26 Jun 2024 11:51:49 +0200 Subject: [PATCH 22/37] Remove allocations per compatible archetype to optimize fragmented iterations --- lib/init.luau | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 54b642f..479cce6 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -640,13 +640,15 @@ export type Query = typeof(EmptyQuery) type CompatibleArchetype = { archetype: Archetype, indices: { number } } -local function PreparedQuery(compatibleArchetypes: { CompatibleArchetype } , components: { i53 }) +local function PreparedQuery( + compatibleArchetypes: { Archetype } , components: { i53 }, indices: { [number]: number }) + local queryLength = #components local lastArchetype = 1 - local compatibleArchetype: CompatibleArchetype = compatibleArchetypes[lastArchetype] + local archetype: Archetype = compatibleArchetypes[lastArchetype] - if not compatibleArchetype then + if not archetype then return EmptyQuery end @@ -657,17 +659,17 @@ local function PreparedQuery(compatibleArchetypes: { CompatibleArchetype } , com local i = 1 + local length = #compatibleArchetypes + local function queryNext(): ...any - local archetype = compatibleArchetype.archetype local entityId = archetype.entities[i] while entityId == nil do lastArchetype += 1 - if lastArchetype > #compatibleArchetypes then + archetype = compatibleArchetypes[lastArchetype] + if lastArchetype > length then return end - compatibleArchetype = compatibleArchetypes[lastArchetype] - archetype = compatibleArchetype.archetype i = 1 entityId = archetype.entities[i] end @@ -676,7 +678,7 @@ local function PreparedQuery(compatibleArchetypes: { CompatibleArchetype } , com i+=1 local columns = archetype.columns - local tr = compatibleArchetype.indices + local tr = indices[lastArchetype] if queryLength == 1 then return entityId, columns[tr[1]][row] @@ -740,7 +742,7 @@ local function PreparedQuery(compatibleArchetypes: { CompatibleArchetype } , com function preparedQuery:without(...: any): Query local withoutComponents = { ... } for i = #compatibleArchetypes, 1, -1 do - local archetype = compatibleArchetypes[i].archetype + local archetype = compatibleArchetypes[i] local records = archetype.records local shouldRemove = false @@ -756,7 +758,7 @@ local function PreparedQuery(compatibleArchetypes: { CompatibleArchetype } , com end end - lastArchetype, compatibleArchetype = next(compatibleArchetypes) + lastArchetype, archetype = next(compatibleArchetypes) if not lastArchetype then return EmptyQuery end @@ -773,6 +775,7 @@ function World.query(world: World, ...: any): Query error("Missing components") end + local columns = {} local compatibleArchetypes: { CompatibleArchetype } = {} local length = 0 @@ -797,7 +800,7 @@ function World.query(world: World, ...: any): Query local archetype = archetypes[id] local archetypeRecords = archetype.records - local indices = {} + local records = {} local skip = false for i, componentId in components do @@ -807,7 +810,7 @@ function World.query(world: World, ...: any): Query break end -- index should be index.offset - indices[i] = index + records[i] = index end if skip then @@ -815,13 +818,11 @@ function World.query(world: World, ...: any): Query end length += 1 - compatibleArchetypes[length] = { - archetype = archetype, - indices = indices, - } + compatibleArchetypes[length] = archetype + columns[length] = records end - return PreparedQuery(compatibleArchetypes, components) + return PreparedQuery(compatibleArchetypes, components, columns) end type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) From 0ff2348a6ed528a9f745dfbbb2f36b53cff7c543 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 26 Jun 2024 15:50:00 +0200 Subject: [PATCH 23/37] Uniform function declarations --- lib/init.luau | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 479cce6..f49a6d6 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -652,22 +652,18 @@ local function PreparedQuery( return EmptyQuery end - local preparedQuery = {} - preparedQuery.__index = preparedQuery local queryOutput = {} local i = 1 - local length = #compatibleArchetypes - local function queryNext(): ...any local entityId = archetype.entities[i] while entityId == nil do lastArchetype += 1 archetype = compatibleArchetypes[lastArchetype] - if lastArchetype > length then + if not archetype then return end i = 1 @@ -730,16 +726,8 @@ local function PreparedQuery( return entityId, unpack(queryOutput, 1, queryLength) end - - function preparedQuery:__iter(): () -> ...any - return queryNext - end - function preparedQuery:next(): ...any - return queryNext() - end - - function preparedQuery:without(...: any): Query + local function without(self, ...): Query local withoutComponents = { ... } for i = #compatibleArchetypes, 1, -1 do local archetype = compatibleArchetypes[i] @@ -765,8 +753,16 @@ local function PreparedQuery( return self end + + local preparedQuery = { + __iter = function() + return queryNext + end, + next = queryNext, + without = without + } - return setmetatable({}, preparedQuery) :: any + return (setmetatable(preparedQuery, preparedQuery) :: any):: Query end function World.query(world: World, ...: any): Query From 0256c765a0666e262c2164a28de32ae9b34de6b4 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 2 Jul 2024 12:53:33 +0200 Subject: [PATCH 24/37] Drainless iterators --- lib/init.luau | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index f49a6d6..110cc54 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -640,8 +640,8 @@ export type Query = typeof(EmptyQuery) type CompatibleArchetype = { archetype: Archetype, indices: { number } } -local function PreparedQuery( - compatibleArchetypes: { Archetype } , components: { i53 }, indices: { [number]: number }) +local function preparedQuery(compatibleArchetypes: { Archetype }, + components: { i53? }, indices: { { number } }) local queryLength = #components @@ -652,7 +652,6 @@ local function PreparedQuery( return EmptyQuery end - local queryOutput = {} local i = 1 @@ -746,48 +745,47 @@ local function PreparedQuery( end end - lastArchetype, archetype = next(compatibleArchetypes) - if not lastArchetype then - return EmptyQuery - end - return self end - local preparedQuery = { + local query = { __iter = function() + i = 1 + lastArchetype = 1 + archetype = compatibleArchetypes[1] + return queryNext end, next = queryNext, without = without } - return (setmetatable(preparedQuery, preparedQuery) :: any):: Query + return setmetatable(query, query) :: any end -function World.query(world: World, ...: any): Query +function World.query(world: World, ...: number?): Query -- breaking? if (...) == nil then error("Missing components") end - local columns = {} - local compatibleArchetypes: { CompatibleArchetype } = {} + local indices: { { number } } = {} + local compatibleArchetypes: { Archetype } = {} local length = 0 - local components = { ... } :: any - local archetypes = world.archetypes + local components: { number? } = { ... } + local archetypes: { Archetype } = world.archetypes :: any local firstArchetypeMap: ArchetypeMap local componentIndex = world.componentIndex for _, componentId in components do - local map = componentIndex[componentId] + local map: ArchetypeMap = componentIndex[componentId] :: any if not map then return EmptyQuery end - if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then + if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size < map.size then firstArchetypeMap = map end end @@ -796,7 +794,7 @@ function World.query(world: World, ...: any): Query local archetype = archetypes[id] local archetypeRecords = archetype.records - local records = {} + local records: { number } = {} local skip = false for i, componentId in components do @@ -815,25 +813,27 @@ function World.query(world: World, ...: any): Query length += 1 compatibleArchetypes[length] = archetype - columns[length] = records + indices[length] = records end - return PreparedQuery(compatibleArchetypes, components, columns) + return preparedQuery(compatibleArchetypes, components, indices) end type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) function World.__iter(world: World): WorldIterator - local dense = world.entityIndex.dense - local sparse = world.entityIndex.sparse + local entityIndex = world.entityIndex + local dense = entityIndex.dense + local sparse = entityIndex.sparse local last -- new solver doesnt like the world iterator type even tho its correct -- so any cast here i come + local i = 0 local function iterator() - local lastEntity: number?, entityId: number = next(dense, last) - if not lastEntity then - -- ignore type error + i+=1 + local entityId = dense[i] + if not entityId then return end From 0f67cb1c863c74684d0a82603836d96db9fbd2a3 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 2 Jul 2024 14:20:35 +0200 Subject: [PATCH 25/37] tuple isnt optional --- lib/init.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 110cc54..1242cba 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -763,7 +763,7 @@ local function preparedQuery(compatibleArchetypes: { Archetype }, return setmetatable(query, query) :: any end -function World.query(world: World, ...: number?): Query +function World.query(world: World, ...: number): Query -- breaking? if (...) == nil then error("Missing components") @@ -773,7 +773,7 @@ function World.query(world: World, ...: number?): Query local compatibleArchetypes: { Archetype } = {} local length = 0 - local components: { number? } = { ... } + local components: { number } = { ... } local archetypes: { Archetype } = world.archetypes :: any local firstArchetypeMap: ArchetypeMap From 5ff6a43750efd5cb9edba995af9fb997094b4975 Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 3 Jul 2024 01:24:17 +0200 Subject: [PATCH 26/37] Add clear (#59) * Rename files to luau * Rename remaining files * Update bench.project.json for luau files * Stress test insertion * Add clear * Add a few guards * Use next() in World.__iter * Add tests --- lib/init.luau | 28 +++++++++-- tests/world.luau | 126 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 112 insertions(+), 42 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 1242cba..0aa3a9f 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -128,6 +128,10 @@ 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 @@ -442,6 +446,24 @@ function World.delete(world: World, entityId: i53) sparse[entityId] = nil :: any dense[#dense] = nil :: any + +end + +function World.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 local function ensureArchetype(world: World, types, prev): Archetype @@ -823,7 +845,6 @@ type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() - function World.__iter(world: World): WorldIterator local entityIndex = world.entityIndex - local dense = entityIndex.dense local sparse = entityIndex.sparse local last @@ -832,14 +853,13 @@ function World.__iter(world: World): WorldIterator local i = 0 local function iterator() i+=1 - local entityId = dense[i] + local entityId, record = next(sparse, last) if not entityId then return end - last = lastEntity + last = entityId - 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 diff --git a/tests/world.luau b/tests/world.luau index 7ca883c..43a5f2f 100644 --- a/tests/world.luau +++ b/tests/world.luau @@ -22,10 +22,11 @@ local function CHECK_NO_ERR(s: string, fn: (T...) -> (), ...: T...) end local N = 10 +type World = jecs.WorldShim + TEST("world", function() - do - CASE("should be iterable") - local world = jecs.World.new() + do CASE("should be iterable") + local world = jecs.World.new() :: World local A = world:component() local B = world:component() local eA = world:entity() @@ -36,9 +37,7 @@ TEST("world", function() 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) @@ -50,14 +49,79 @@ TEST("world", function() 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") + 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() @@ -80,8 +144,7 @@ TEST("world", function() CHECK(#entities == 0) end - do - CASE("should query all matching entities when irrelevant component is removed") + 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() @@ -110,8 +173,7 @@ TEST("world", function() CHECK(added == N) end - do - CASE("should query all entities without B") + do CASE("should query all entities without B") local world = jecs.World.new() local A = world:component() local B = world:component() @@ -135,8 +197,7 @@ TEST("world", function() CHECK(#entities == 0) end - do - CASE("should allow setting components in arbitrary order") + do CASE("should allow setting components in arbitrary order") local world = jecs.World.new() local Health = world:entity() @@ -149,8 +210,7 @@ TEST("world", function() CHECK(world:get(id, Poison) == 5) end - do - CASE("should allow deleting components") + do CASE("should allow deleting components") local world = jecs.World.new() local Health = world:entity() @@ -171,8 +231,7 @@ TEST("world", function() CHECK(world:get(id1, Health) == 50) end - do - CASE("should allow remove that doesn't exist on entity") + do CASE("should allow remove that doesn't exist on entity") local world = jecs.World.new() local Health = world:entity() @@ -186,8 +245,7 @@ TEST("world", function() CHECK(world:get(id, Health) == 50) end - do - CASE("should increment generation") + do CASE("should increment generation") local world = jecs.World.new() local e = world:entity() CHECK(ECS_ID(e) == 1 + jecs.Rest) @@ -197,8 +255,7 @@ TEST("world", function() CHECK(ECS_GENERATION(e) == 1) -- 1 end - do - CASE("should get alive from index in the dense array") + do CASE("should get alive from index in the dense array") local world = jecs.World.new() local _e = world:entity() local e2 = world:entity() @@ -213,8 +270,7 @@ TEST("world", function() CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3) end - do - CASE("should allow querying for relations") + do CASE("should allow querying for relations") local world = jecs.World.new() local Eats = world:entity() local Apples = world:entity() @@ -227,8 +283,7 @@ TEST("world", function() end end - do - CASE("should allow wildcards in queries") + do CASE("should allow wildcards in queries") local world = jecs.World.new() local Eats = world:entity() local Apples = world:entity() @@ -247,8 +302,7 @@ TEST("world", function() end end - do - CASE("should match against multiple pairs") + do CASE("should match against multiple pairs") local world = jecs.World.new() local Eats = world:entity() local Apples = world:entity() @@ -280,8 +334,7 @@ TEST("world", function() CHECK(count == 1) end - do - CASE("should only relate alive entities") + do CASE("should only relate alive entities") local world = jecs.World.new() local Eats = world:entity() @@ -308,8 +361,7 @@ TEST("world", function() CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) end - do - CASE("should error when setting invalid pair") + do CASE("should error when setting invalid pair") local world = jecs.World.new() local Eats = world:entity() local Apples = world:entity() @@ -320,8 +372,7 @@ TEST("world", function() world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") end - do - CASE("should find target for ChildOf") + do CASE("should find target for ChildOf") local world = jecs.World.new() local ChildOf = world:component() @@ -343,7 +394,6 @@ TEST("world", function() local count = 0 for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do - print(name) count += 1 end CHECK(count == 2) From b8f35ccb85f2f8e960b3ebebb77cb5fb11ec5cd8 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 3 Jul 2024 02:10:11 +0200 Subject: [PATCH 27/37] Change variable names and style --- lib/init.luau | 60 ++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 0aa3a9f..98d30ed 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -63,23 +63,25 @@ type ArchetypeDiff = { 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 HI_COMPONENT_ID = 256 -local ECS_ID_FLAGS_MASK = 0x10 -local ECS_ENTITY_MASK = bit32.lshift(1, 24) -local ECS_GENERATION_MASK = bit32.lshift(1, 16) +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 EcsRest = HI_COMPONENT_ID + 6 + +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, FLAGS_PAIR) -- HIGHEST bit in the ID. + 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 @@ -99,7 +101,7 @@ local function ECS_COMBINE(source: number, target: number): i53 end local function ECS_IS_PAIR(e: number): boolean - return if e > ECS_ENTITY_MASK then (e % ECS_ID_FLAGS_MASK) // FLAGS_PAIR ~= 0 else false + 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 @@ -266,7 +268,7 @@ 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 == WILDCARD or second == WILDCARD + return first == EcsWildcard or second == EcsWildcard end local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archetype @@ -287,11 +289,11 @@ local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archet local relation = ECS_PAIR_RELATION(world.entityIndex, componentId) local object = ECS_PAIR_OBJECT(world.entityIndex, componentId) - local idr_r = ECS_PAIR(relation, WILDCARD) + local idr_r = ECS_PAIR(relation, EcsWildcard) ensureComponentRecord(componentIndex, id, idr_r, i) records[idr_r] = i - local idr_t = ECS_PAIR(WILDCARD, object) + local idr_t = ECS_PAIR(EcsWildcard, object) ensureComponentRecord(componentIndex, id, idr_t, i) records[idr_t] = i end @@ -327,7 +329,7 @@ function World.new(): World sparse = {} :: { [i53]: Record }, } :: EntityIndex, hooks = { - [ON_ADD] = {}, + [EcsOnAdd] = {}, }, nextArchetypeId = 0, nextComponentId = 0, @@ -355,7 +357,7 @@ end function World.entity(world: World): i53 local entityId = world.nextEntityId + 1 world.nextEntityId = entityId - return nextEntityId(world.entityIndex, entityId + REST) + return nextEntityId(world.entityIndex, entityId + EcsRest) end -- TODO: @@ -369,7 +371,7 @@ function World.target(world: World, entity: i53, relation: i24): i24? return nil end - local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)] + local componentRecord = world.componentIndex[ECS_PAIR(relation, EcsWildcard)] if not componentRecord then return nil end @@ -424,8 +426,8 @@ function World.delete(world: World, entityId: i53) 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)) + archetypeDelete(world, ECS_PAIR(entityId, EcsWildcard)) + archetypeDelete(world, ECS_PAIR(EcsWildcard, entityId)) if archetype then local entities = archetype.entities @@ -1011,15 +1013,15 @@ export type WorldShim = typeof(setmetatable( } )) -return table.freeze({ - World = (table.freeze(World) :: any) :: { new: () -> WorldShim }, +return { + World = (World :: any) :: { new: () -> WorldShim }, - OnAdd = (ON_ADD :: any) :: Entity, - OnRemove = (ON_REMOVE :: any) :: Entity, - OnSet = (ON_SET :: any) :: Entity, - Wildcard = (WILDCARD :: any) :: Entity, - w = (WILDCARD :: any) :: Entity, - Rest = REST, + OnAdd = (EcsOnAdd :: any) :: Entity, + OnRemove = (EcsOnRemove :: any) :: Entity, + OnSet = (EcsOnSet :: any) :: Entity, + Wildcard = (EcsWildcard :: any) :: Entity, + w = (EcsWildcard :: any) :: Entity, + Rest = EcsRest, IS_PAIR = ECS_IS_PAIR, ECS_ID = ECS_ENTITY_T_LO, @@ -1031,4 +1033,4 @@ return table.freeze({ pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> number, getAlive = getAlive, -}) +} From ec833464a5440e70b8c2cc5843f7e933ee14f6d3 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 3 Jul 2024 02:10:16 +0200 Subject: [PATCH 28/37] Add parent method --- lib/init.luau | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 98d30ed..5b56e67 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -361,9 +361,10 @@ function World.entity(world: World): i53 end -- TODO: --- should have an additional `index` parameter which selects the nth target +-- 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 -function World.target(world: World, entity: i53, relation: i24): i24? +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 @@ -384,6 +385,13 @@ function World.target(world: World, entity: i53, relation: i24): i24? return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord]) end +local function parent(world: World, entity: i53) + return target(world, entity, EcsChildOf) +end + +World.target = target +World.parent = parent + -- 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 From ebc916c7ef36329c179d624fd1179445d71866e6 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 3 Jul 2024 02:13:35 +0200 Subject: [PATCH 29/37] Remove redundant TODO --- lib/init.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/init.luau b/lib/init.luau index 5b56e67..1f9aa18 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -360,7 +360,6 @@ function World.entity(world: World): i53 return nextEntityId(world.entityIndex, entityId + EcsRest) end --- TODO: -- 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 From 0324d3a33f0bfe0ed8887e6fd6ba9f15bd6b5b84 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 3 Jul 2024 02:23:16 +0200 Subject: [PATCH 30/37] ChildOf as built-in component --- lib/init.luau | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/init.luau b/lib/init.luau index 1f9aa18..8d78e16 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -338,6 +338,9 @@ function World.new(): World }, World) self.ROOT_ARCHETYPE = archetypeOf(self, {}) + -- Initialize built-in components + nextEntityId(self.entityIndex, EcsChildOf) + return self end @@ -1026,8 +1029,11 @@ return { OnAdd = (EcsOnAdd :: any) :: Entity, OnRemove = (EcsOnRemove :: any) :: Entity, OnSet = (EcsOnSet :: any) :: Entity, + Wildcard = (EcsWildcard :: any) :: Entity, w = (EcsWildcard :: any) :: Entity, + ChildOf = EcsChildOf, + Rest = EcsRest, IS_PAIR = ECS_IS_PAIR, From b4a1db871e79f8e63feae6e2a7d36d5edaa8a095 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 3 Jul 2024 02:46:54 +0200 Subject: [PATCH 31/37] Change test to use built in component --- tests/world.luau | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/world.luau b/tests/world.luau index 43a5f2f..04873f6 100644 --- a/tests/world.luau +++ b/tests/world.luau @@ -374,23 +374,20 @@ TEST("world", function() do CASE("should find target for ChildOf") local world = jecs.World.new() + local ChildOf = jecs.ChildOf + local pair = ECS_PAIR - 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:add(bob, pair(ChildOf, alice)) world:set(bob, Name, "bob") - world:add(sara, ECS_PAIR(ChildOf, alice)) + world:add(sara, pair(ChildOf, alice)) world:set(sara, Name, "sara") - CHECK(parent(bob) == alice) -- O(1) + CHECK(world:parent(bob) == alice) -- O(1) local count = 0 for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do From bd2ed21fc3db3185b29bc86fc64cc7555b3764f4 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 3 Jul 2024 02:47:29 +0200 Subject: [PATCH 32/37] Bump wally version --- wally.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wally.toml b/wally.toml index a19b86f..58e1835 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ukendio/jecs" -version = "0.1.0" +version = "0.2.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] From f041d6fa746c6117af4ffe6ba612a46ab2e2fcf1 Mon Sep 17 00:00:00 2001 From: EncodedVenom <32179912+EncodedVenom@users.noreply.github.com> Date: Tue, 2 Jul 2024 21:42:57 -0400 Subject: [PATCH 33/37] Rename Matter -> Jecs in release workflow (#60) * Rename Matter -> Jecs in release workflow * Delete body from release message This should theoretically prevent the overriding description bug that Ukendio mentioned on discord. Unsure since I haven't tested in another repo workflow yet. --- .github/workflows/release.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 From de34636bb3486b3dc0a3958eabe041c055fbd364 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 3 Jul 2024 17:48:32 +0200 Subject: [PATCH 34/37] Add EcsComponent built-in component --- lib/init.luau | 386 +++++++++++++++++++++++++------------------------- 1 file changed, 196 insertions(+), 190 deletions(-) diff --git a/lib/init.luau b/lib/init.luau index 8d78e16..797172e 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -70,7 +70,8 @@ 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 EcsRest = HI_COMPONENT_ID + 6 +local EcsComponent = HI_COMPONENT_ID + 6 +local EcsRest = HI_COMPONENT_ID + 7 local ECS_PAIR_FLAG = 0x8 local ECS_ID_FLAGS_MASK = 0x10 @@ -138,8 +139,28 @@ 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 function getAlive(entityIndex: EntityIndex, id: i24): i53 - return entityIndex.dense[id] +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 @@ -316,48 +337,18 @@ local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archet return archetype end -local World = {} -World.__index = World +export type World = { + archetypeIndex: { [string]: Archetype }, + archetypes: Archetypes, + componentIndex: ComponentIndex, + entityIndex: EntityIndex, + nextArchetypeId: number, + nextComponentId: number, + nextEntityId: number, + ROOT_ARCHETYPE: Archetype +} -function World.new(): World - 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 - -export type World = typeof(World.new()) - -function World.component(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 - return nextEntityId(world.entityIndex, componentId) -end - -function World.entity(world: World): i53 +local function entity(world: World): i53 local entityId = world.nextEntityId + 1 world.nextEntityId = entityId return nextEntityId(world.entityIndex, entityId + EcsRest) @@ -391,93 +382,6 @@ local function parent(world: World, entity: i53) return target(world, entity, EcsChildOf) end -World.target = target -World.parent = parent - --- 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 - world:remove(entity, id) - end - end - - componentIndex[id] = nil :: any - 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, 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 - -function World.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 - local function ensureArchetype(world: World, types, prev): Archetype if #types < 1 then return world.ROOT_ARCHETYPE @@ -547,7 +451,7 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet return add end -function World.add(world: World, entityId: i53, componentId: i53) +local function add(world: World, entityId: i53, componentId: i53) local entityIndex = world.entityIndex local record = entityIndex.sparse[entityId] local from = record.archetype @@ -562,7 +466,7 @@ function World.add(world: World, entityId: i53, componentId: i53) end -- Symmetric like `World.add` but idempotent -function World.set(world: World, entityId: i53, componentId: i53, data: unknown) +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) @@ -590,6 +494,20 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown) 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) @@ -608,7 +526,7 @@ local function archetypeTraverseRemove(world: World, componentId: i53, from: Arc return remove end -function World.remove(world: World, entityId: i53, componentId: i53) +local function remove(world: World, entityId: i53, componentId: i53) local entityIndex = world.entityIndex local record = entityIndex.sparse[entityId] local sourceArchetype = record.archetype @@ -619,8 +537,92 @@ function World.remove(world: World, entityId: i53, componentId: i53) 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 get(record: Record, componentId: i24): any +local function fetch(record: Record, componentId: i24): any local archetype = record.archetype if not archetype then return nil @@ -635,23 +637,23 @@ local function get(record: Record, componentId: i24): any return archetype.columns[archetypeRecord][record.row] end -function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any +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 = get(record, a) + local va = fetch(record, a) if b == nil then return va elseif c == nil then - return va, get(record, b) + return va, fetch(record, b) elseif d == nil then - return va, get(record, b), get(record, c) + return va, fetch(record, b), fetch(record, c) elseif e == nil then - return va, get(record, b), get(record, c), get(record, d) + return va, fetch(record, b), fetch(record, c), fetch(record, d) else error("args exceeded") end @@ -696,11 +698,13 @@ local function preparedQuery(compatibleArchetypes: { Archetype }, while entityId == nil do lastArchetype += 1 archetype = compatibleArchetypes[lastArchetype] + if not archetype then return end + i = 1 - entityId = archetype.entities[i] + entityId = archetype.entities[1] end local row = i @@ -782,7 +786,7 @@ local function preparedQuery(compatibleArchetypes: { Archetype }, return self end - local query = { + local it = { __iter = function() i = 1 lastArchetype = 1 @@ -794,10 +798,10 @@ local function preparedQuery(compatibleArchetypes: { Archetype }, without = without } - return setmetatable(query, query) :: any + return setmetatable(it, it) :: any end -function World.query(world: World, ...: number): Query +local function query(world: World, ...: number): Query -- breaking? if (...) == nil then error("Missing components") @@ -854,46 +858,6 @@ function World.query(world: World, ...: number): Query end type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) - -function World.__iter(world: World): WorldIterator - local entityIndex = world.entityIndex - local sparse = entityIndex.sparse - local last - - -- new solver doesnt like the world iterator type even tho its correct - -- so any cast here i come - local i = 0 - local function iterator() - i+=1 - local entityId, record = next(sparse, last) - if not entityId then - return - end - - last = 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 - - return iterator :: any -end - -- __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 } @@ -908,6 +872,7 @@ export type QueryShim = typeof(setmetatable({ return nil :: any end, })) + export type WorldShim = typeof(setmetatable( {} :: { @@ -1023,16 +988,57 @@ export type WorldShim = typeof(setmetatable( } )) +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 :: any) :: { new: () -> WorldShim }, + World = World :: { new: () -> WorldShim }, - OnAdd = (EcsOnAdd :: any) :: Entity, - OnRemove = (EcsOnRemove :: any) :: Entity, - OnSet = (EcsOnSet :: any) :: Entity, + OnAdd = EcsOnAdd :: Entity, + OnRemove = EcsOnRemove :: Entity, + OnSet = EcsOnSet :: Entity, - Wildcard = (EcsWildcard :: any) :: Entity, - w = (EcsWildcard :: any) :: Entity, + Wildcard = EcsWildcard :: Entity, + w = EcsWildcard :: Entity, ChildOf = EcsChildOf, + Component = EcsComponent, Rest = EcsRest, From afc0c949f0d2ef78f225a879af1cfdc7662b032e Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 3 Jul 2024 17:48:55 +0200 Subject: [PATCH 35/37] Bump wally version --- wally.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wally.toml b/wally.toml index 58e1835..4a82285 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ukendio/jecs" -version = "0.2.0" +version = "0.2.1" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] From 6559c56d476141f83b7d0cf979baa91d5834bb86 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sat, 6 Jul 2024 16:21:44 +0200 Subject: [PATCH 36/37] Initial commit (#63) --- CHANGELOG.md | 116 ++++++++++++++++++++++++++++++++++++++++ {lib => src}/index.d.ts | 0 {lib => src}/init.luau | 0 wally.toml | 3 +- 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md rename {lib => src}/index.d.ts (100%) rename {lib => src}/init.luau (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2e878b5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,116 @@ +# 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] + +## [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/lib/index.d.ts b/src/index.d.ts similarity index 100% rename from lib/index.d.ts rename to src/index.d.ts diff --git a/lib/init.luau b/src/init.luau similarity index 100% rename from lib/init.luau rename to src/init.luau diff --git a/wally.toml b/wally.toml index 4a82285..c81bb8a 100644 --- a/wally.toml +++ b/wally.toml @@ -3,5 +3,6 @@ name = "ukendio/jecs" 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 From c0e73273d1064aaa83947bf0ef82903efd8034d3 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sat, 6 Jul 2024 16:36:00 +0200 Subject: [PATCH 37/37] Add backwards iteration (#61) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Iterate backwards * Should test for removing as well * Add test * Add faĆ­ling test * Add to changelog --- CHANGELOG.md | 8 +++-- src/init.luau | 26 +++++++------- tests/world.luau | 93 ++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 93 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e878b5..6fd615a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,16 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ## [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) +- 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 diff --git a/src/init.luau b/src/init.luau index 797172e..d89e406 100644 --- a/src/init.luau +++ b/src/init.luau @@ -184,13 +184,9 @@ local function nextEntityId(entityIndex: EntityIndex, index: i24): i53 return id end -local function transitionArchetype( - entityIndex: EntityIndex, - to: Archetype, - destinationRow: i24, - from: Archetype, - sourceRow: i24 -) +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 @@ -690,11 +686,11 @@ local function preparedQuery(compatibleArchetypes: { Archetype }, local queryOutput = {} - local i = 1 + local entities = archetype.entities + local i = #entities local function queryNext(): ...any - local entityId = archetype.entities[i] - + local entityId = entities[i] while entityId == nil do lastArchetype += 1 archetype = compatibleArchetypes[lastArchetype] @@ -703,12 +699,13 @@ local function preparedQuery(compatibleArchetypes: { Archetype }, return end - i = 1 - entityId = archetype.entities[1] + entities = archetype.entities + i = #entities + entityId = entities[i] end local row = i - i+=1 + i-=1 local columns = archetype.columns local tr = indices[lastArchetype] @@ -788,9 +785,10 @@ local function preparedQuery(compatibleArchetypes: { Archetype }, local it = { __iter = function() - i = 1 lastArchetype = 1 archetype = compatibleArchetypes[1] + entities = archetype.entities + i = #entities return queryNext end, diff --git a/tests/world.luau b/tests/world.luau index 04873f6..1aaa04c 100644 --- a/tests/world.luau +++ b/tests/world.luau @@ -25,30 +25,23 @@ local N = 10 type World = jecs.WorldShim TEST("world", function() - do CASE("should be iterable") + do CASE("should find every component id") 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) + world:entity() + world:entity() + world:entity() - for id, data in world do - 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) + 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") @@ -395,6 +388,70 @@ TEST("world", function() 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()