commit bc32ed08eb506a2a091f81cdae9bc6d68f936a38 Author: Ukendio Date: Fri Mar 28 03:09:30 2025 +0100 Orphaned diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..05cde82 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.luau text eol=lf +*.html linguist-vendored diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.md b/.github/ISSUE_TEMPLATE/BUG-REPORT.md new file mode 100644 index 0000000..92bf500 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: File a bug report for any behavior that you believe is unintentional or problematic +title: "" +labels: bug +assignees: "" +--- + +## Describe the bug + +Put a clear and concise description of what the bug is. This should be short and to the point, not to exceed more than a paragraph. Put the details inside your reproduction steps. + +## Reproduction + +Make an easy-to-follow guide on how to reproduce it. Does it happen all the time? Will specific features affect reproduction? All these questions should be answered for a good issue. + +This is a good place to put rbxl files or scripts that help explain your reproduction steps. + +## Expected Behavior + +What you expect to happen + +## Actual Behavior + +What actually happens diff --git a/.github/ISSUE_TEMPLATE/DOCUMENTATION.md b/.github/ISSUE_TEMPLATE/DOCUMENTATION.md new file mode 100644 index 0000000..6c80bd2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/DOCUMENTATION.md @@ -0,0 +1,15 @@ +--- +name: Documentation +about: Open an issue to add, change, or otherwise modify any part of the documentation. +title: "" +labels: documentation +assignees: "" +--- + +## Which Sections Does This Issue Cover? + +[Put sections (e.g. Query Concepts), page links, etc as necessary] + +## What Needs To Change? + +What specifically needs to change and what suggestions do you have to change it? diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md new file mode 100644 index 0000000..3a511c2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md @@ -0,0 +1,27 @@ +--- +name: Feature Request +about: File a feature request for something you believe should be added to Jecs +title: "" +labels: enhancement +assignees: "" +--- + +## Describe your Feature + +You should explain your feature here, and the motivation for why you want it. + +## Implementation + +Explain how you would implement your feature here. Provide relevant API examples and such here (if applicable). + +## Alternatives + +What other alternative implementations or otherwise relevant information is important to why you decided to go with this specific implementation? + +## Considerations + +Some questions that need to be answered include the following: + +- Will old code break in response to this feature? +- What are the performance impacts with this feature (if any)? +- How is it useful to include? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..2e52705 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Brief Description of your Changes. + +Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here. + +## Impact of your Changes + +What implications will this have on the project? Will there be altered behavior or performance with this change? + +## Tests Performed + +What have you done to ensure this change has the least possible impact on the project? + +## Additional Comments + +Anything else you feel is relevant. \ No newline at end of file diff --git a/.github/workflows/analysis.yaml b/.github/workflows/analysis.yaml new file mode 100644 index 0000000..9535921 --- /dev/null +++ b/.github/workflows/analysis.yaml @@ -0,0 +1,19 @@ +name: analysis + +on: [push, pull_request, workflow_dispatch] + +jobs: + run: + name: Run Luau Analyze + runs-on: ubuntu-latest + + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install Luau + uses: encodedvenom/install-luau@v4.3 + + - name: Analyze + run: | + output=$(luau-analyze src || true) # Suppress errors for now. diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml new file mode 100644 index 0000000..17f20b6 --- /dev/null +++ b/.github/workflows/dependabot.yml @@ -0,0 +1,11 @@ +--- +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: npm + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml new file mode 100644 index 0000000..41c68dc --- /dev/null +++ b/.github/workflows/deploy-docs.yaml @@ -0,0 +1,64 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# +name: deploy-docs + +on: + # Runs on pushes targeting the `main` branch. Change this to `master` if you're + # using the `master` branch as the default branch. + push: + branches: [main] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + # - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm + # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm # or pnpm / yarn + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install dependencies + run: npm ci # or pnpm install / yarn install / bun install + - name: Build with VitePress + run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..bce339d --- /dev/null +++ b/.github/workflows/publish-npm.yml @@ -0,0 +1,17 @@ +name: publish-npm + +on: + push: + branches: [main] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: "20" + - uses: JS-DevTools/npm-publish@v3 + with: + token: ${{ secrets.NPM_AUTH_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..a423627 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,71 @@ +name: release + +on: + push: + tags: ["v*"] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install Rokit + uses: CompeyDev/setup-rokit@v0.1.2 + + - name: Install Dependencies + run: wally install + + - name: Build + run: rojo build --output build.rbxm default.project.json + + - name: Upload Build Artifact + uses: actions/upload-artifact@v3 + with: + name: build + path: build.rbxm + + release: + name: Release + needs: [build] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Download Jecs Build + uses: actions/download-artifact@v3 + with: + name: build + path: build + + - name: Rename Build + run: mv build/build.rbxm jecs.rbxm + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + name: Jecs ${{ github.ref_name }} + files: | + jecs.rbxm + + publish: + name: Publish + needs: [release] + runs-on: ubuntu-latest + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install Rokit + uses: CompeyDev/setup-rokit@v0.1.2 + + - name: Wally Login + run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }} + + - name: Publish + run: wally publish diff --git a/.github/workflows/unit-testing.yaml b/.github/workflows/unit-testing.yaml new file mode 100644 index 0000000..1dbf6f7 --- /dev/null +++ b/.github/workflows/unit-testing.yaml @@ -0,0 +1,31 @@ +name: unit-testing + +on: [push, pull_request, workflow_dispatch] + +jobs: + run: + name: Run Luau Tests + runs-on: ubuntu-latest + timeout-minutes: 2 + + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install Luau + uses: encodedvenom/install-luau@v4.3 + with: + version: "latest" + verbose: "true" + + - name: Run Unit Tests + id: run_tests + run: | + output=$(luau test/tests.luau) + echo "$output" + if [[ "$output" == *"0 fails"* ]]; then + echo "Unit Tests Passed" + else + echo "Error: One or More Unit Tests Failed." + exit 1 + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6861d7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,73 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz +*.rbxm + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Wally files +DevPackages +Packages +wally.lock +WallyPatches + +# Typescript +/node_modules +/include + +# Misc +roblox.toml +sourcemap.json +drafts/ + +# Cached Vitepress (docs) + +/docs/.vitepress/cache +/docs/.vitepress/dist + +.vitepress/cache +.vitepress/dist + +# Luau tools +profile.* + +# Patch files + +*.patch + +genhtml.perl diff --git a/.luaurc b/.luaurc new file mode 100644 index 0000000..d1ae244 --- /dev/null +++ b/.luaurc @@ -0,0 +1,9 @@ +{ + "aliases": { + "jecs": "jecs", + "testkit": "tools/testkit", + "mirror": "mirror", + "tools": "tools", + }, + "languageMode": "strict" +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..537ac33 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "trailingComma": "all", + "useTabs": true +} diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..0c02788 --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,9 @@ +syntax = "All" +column_width = 120 +line_endings = "Unix" +indent_type = "Tabs" +indent_width = 4 +quote_style = "AutoPreferDouble" +call_parentheses = "Always" +space_after_function_names = "Never" +collapse_simple_statement = "Never" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..52f87cc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,205 @@ +# 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] + +- `[world]`: + - 16% faster `world:get` + - `world:has` no longer typechecks components after the 8th one. +- `[typescript]` + + - Fixed Entity type to default to `undefined | unknown` instead of just `undefined` + +- `[query]`: + - Fixed bug where `world:clear` did not invoke `jecs.OnRemove` hooks + - Changed `query.__iter` to drain on iteration + - It will initialize once wherever you left iteration off at last time + - Changed `query:iter` to restart the iterator + - Removed `query:drain` and `query:next` + - If you want to get individual results outside of a for-loop, you need to call `query:iter` to initialize the iterator and then call the iterator function manually + ```lua + local it = world:query(A, B, C):iter() + local entity, a, b, c = it() + entity, a, b, c = it() -- get next results + ``` +- `[world` + - Fixed a bug with `world:clear` not invoking `jecs.OnRemove` hooks +- `[typescript]`: + - Changed pair to accept generics + - Improved handling of Tags + +## [0.3.2] - 2024-10-01 + +- `[world]`: + - Changed `world:cleanup` to traverse a header type for graph edges. (Edit) + - Fixed a regression that occurred when you call `world:set` following a `world:remove` using the same component + - Remove explicit error in JECS_DEBUG for `world:target` when not applying an index parameter +- `[typescript]` : + - Fixed `world.set` with NoInfer + +## [0.3.1] - 2024-10-01 + +- `[world]`: + - Added an index parameter to `world:target` + - Added a way to change the components limit via `_G.JECS_HI_COMPONENT_ID` + - Set it to whatever number you want but try to make it as close to the number of components you will use as possible + - Make sure to set this before importing jecs or else it will not work + - Added debug mode, enable via setting `_G.JECS_DEBUG` to true + - Make sure to set this before importing jecs or else it will not work + - Added `world:cleanup` which is called to cleanup empty archetypes manually + - Changed `world:delete` to delete archetypes that are dependent on the passed entity + - Changed `world:delete` to delete entity's children before the entity to prevent cycles +- `[query]`: + - Fixed the iterator to not drain by default +- `[typescript]` + - Fixed entry point of the package.json file to be `src` rather than `src/init` + - Fixed `query.next` returning a query object whereas it would be expected to return a tuple containing the entity and the corresponding component values + - Exported `query.archetypes` + - Changed `pair` to return a number instead of an entity + - Preventing direct usage of a pair as an entity while still allowing it to be used as a component + - Exported built-in components `ChildOf` and `Name` + - Exported `world.parent` + +## [0.2.10] - 2024-09-07 + +- `[world]`: + - Improved performance for hooks + - Changed `world:set` to be idempotent when setting tags +- `[traits]`: + - Added cleanup condition `jecs.OnDelete` for when the entity or component is deleted + - Added cleanup action `jecs.Remove` which removes instances of the specified (component) id from all entities + - This is the default cleanup action + - Added component trait `jecs.Tag` which allows for zero-cost components used as tags + - Setting data to a component with this trait will do nothing +- `[luau]`: + - Exported `world:contains()` + - Exported `query:drain()` + - Exported `Query` + - Improved types for the hook `OnAdd`, `OnSet`, `OnRemove` + - Changed functions to accept any ID including pairs in type parameters + - Applies to `world:add()`, `world:set()`, `world:remove()`, `world:get()`, `world:has()` and `world:query()` + - New exported type `Id = Entity | Pair` + - Changed `world:contains()` to return a `boolean` instead of an entity which may or may not exist + - Fixed `world:has()` to take the correct parameters + +## [0.2.2] - 2024-07-07 + +### Added + +- Added `query:replace(function(...T) return ...U end)` for replacing components in place + - Method is fast pathed to replace the data to the components for each corresponding entity + +### Changed + +- Iterator now goes backwards instead to prevent common cases of iterator invalidation + +## [0.2.1] - 2024-07-06 + +### Added + +- Added `jecs.Component` built-in component which will be added to ids created with `world:component()`. + - Used to find every component id with `query(jecs.Component) + +## [0.2.0] - 2024-07-03 + +### Added + +- Added `world:parent(entity)` and `jecs.ChildOf` respectively as first class citizen for building parent-child relationships. + - Give a parent to an entity with `world:add($source, pair(ChildOf, $target))` + - Use `world:parent(entity)` to find the target of the relationship +- Added user-facing Luau types + +### Changed + +- Improved iteration speeds 20-40% by manually indexing rather than using `next()` :scream: + +## [0.1.1] - 2024-05-19 + +### Added + +- Added `world:clear(entity)` for removing the components to the corresponding entity +- Added Typescript Types + +## [0.1.0] - 2024-05-13 + +### Changed + +- Optimized iterator + +## [0.1.0-rc.6] - 2024-05-13 + +### Added + +- Added a `jecs.Wildcard` term + - it lets you query any partially matched pairs + +## [0.1.0-rc.5] - 2024-05-10 + +### Added + +- Added Entity relationships for creating logical connections between entities +- Added `world:__iter method` which allows for iteration over the whole world to get every entity + - used for reconciling whole worlds such as via replication, saving/loading, etc +- Added `world:add(entity, component)` which adds a component to the entity + - it is an idempotent function, so calling it twice and in any order should be fine + +### Fixed + +- Fixed component overriding when in disorder + - Previously setting the components in different order results in it overriding component data because it incorrectly mapped the index of the column. So it took the index from the source archetype rather than the destination archetype + +## [0.0.0-prototype.rc.3] - 2024-05-01 + +### Added + +- Added observers +- Added an arm to query `query:without()` for chaining invariants. + +### Changed + +- Separates ranges for components and entity IDs. + + - IDs created with `world:component()` will promote array lookups rather than map lookups in the `component_index` 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.2]: https://github.com/ukendio/jecs/releases/tag/v0.2.2 +[0.2.1]: https://github.com/ukendio/jecs/releases/tag/v0.2.1 +[0.2.0]: https://github.com/ukendio/jecs/releases/tag/v0.2.0 +[0.1.1]: https://github.com/ukendio/jecs/releases/tag/v0.1.1 +[0.1.0]: https://github.com/ukendio/jecs/releases/tag/v0.1.0 +[0.1.0-rc.6]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.6 +[0.1.0-rc.5]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.5 +[0.0.0-prototype-rc.3]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3 +[0.0.0-prototype.rc.2]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.2 +[0.0.0-prototype-rc.1]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..605eef8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 jecs authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d44797c --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +

+ +

+ +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE) [![Wally](https://img.shields.io/github/v/tag/ukendio/jecs?&style=for-the-badge)](https://wally.run/package/ukendio/jecs) + +Just a stupidly fast Entity Component System + +- [Entity Relationships](https://ajmmertens.medium.com/building-games-in-ecs-with-entity-relationships-657275ba2c6c) as first class citizens +- Iterate 800,000 entities at 60 frames per second +- Type-safe [Luau](https://luau-lang.org/) API +- Zero-dependency package +- Optimized for column-major operations +- Cache friendly [archetype/SoA](https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorization-fe21690805f9) storage +- Rigorously [unit tested](https://github.com/Ukendio/jecs/actions/workflows/ci.yaml) for stability + +### Example + +```lua +local world = jecs.World.new() +local pair = jecs.pair + +-- These components and functions are actually already builtin +-- but have been illustrated for demonstration purposes +local ChildOf = world:component() +local Name = world:component() + +local function parent(entity) + return world:target(entity, ChildOf) +end +local function getName(entity) + return world:get(entity, Name) +end + +local alice = world:entity() +world:set(alice, Name, "alice") + +local bob = world:entity() +world:add(bob, pair(ChildOf, alice)) +world:set(bob, Name, "bob") + +local sara = world:entity() +world:add(sara, pair(ChildOf, alice)) +world:set(sara, Name, "sara") + +print(getName(parent(sara))) + +for e in world:query(pair(ChildOf, alice)) do + print(getName(e), "is the child of alice") +end + +-- Output +-- "alice" +-- bob is the child of alice +-- sara is the child of alice +``` + +21,000 entities 125 archetypes 4 random components queried. +![Queries](assets/image-3.png) +Can be found under /benches/visual/query.luau + +Inserting 8 components to an entity and updating them over 50 times. +![Insertions](assets/image-4.png) +Can be found under /benches/visual/insertions.luau diff --git a/assets/.github/ISSUE_TEMPLATE/BUG-REPORT.md b/assets/.github/ISSUE_TEMPLATE/BUG-REPORT.md new file mode 100644 index 0000000..5094c17 --- /dev/null +++ b/assets/.github/ISSUE_TEMPLATE/BUG-REPORT.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: File a bug report for any behavior that you believe is unintentional or problematic +title: "[BUG]" +labels: bug +assignees: '' + +--- + +## Describe the bug +Put a clear and concise description of what the bug is. This should be short and to the point, not to exceed more than a paragraph. Put the details inside your reproduction steps. + +## Reproduction +Make an easy-to-follow guide on how to reproduce it. Does it happen all the time? Will specific features affect reproduction? All these questions should be answered for a good issue. + +This is a good place to put rbxl files or scripts that help explain your reproduction steps. + +## Expected Behavior +What you expect to happen + +## Actual Behavior +What actually happens \ No newline at end of file diff --git a/assets/.github/ISSUE_TEMPLATE/DOCUMENTATION.md b/assets/.github/ISSUE_TEMPLATE/DOCUMENTATION.md new file mode 100644 index 0000000..0fbfcf4 --- /dev/null +++ b/assets/.github/ISSUE_TEMPLATE/DOCUMENTATION.md @@ -0,0 +1,14 @@ +--- +name: Documentation +about: Open an issue to add, change, or otherwise modify any part of the documentation. +title: "[DOCS]" +labels: documentation +assignees: '' + +--- + +## Which Sections Does This Issue Cover? +[Put sections (e.g. Query Concepts), page links, etc as necessary] + +## What Needs To Change? +What specifically needs to change and what suggestions do you have to change it? \ No newline at end of file diff --git a/assets/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md b/assets/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md new file mode 100644 index 0000000..7559dc9 --- /dev/null +++ b/assets/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md @@ -0,0 +1,27 @@ +--- +name: Feature Request +about: File a feature request for something you believe should be added to Jecs +title: "[FEATURE]" +labels: enhancement +assignees: '' + +--- + +## Describe your Feature + +You should explain your feature here, and the motivation for why you want it. + +## Implementation + +Explain how you would implement your feature here. Provide relevant API examples and such here (if applicable). + +## Alternatives + +What other alternative implementations or otherwise relevant information is important to why you decided to go with this specific implementation? + +## Considerations + +Some questions that need to be answered include the following: +- Will old code break in response to this feature? +- What are the performance impacts with this feature (if any)? +- How is it useful to include? diff --git a/assets/.github/PULL_REQUEST_TEMPLATE.md b/assets/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..349788c --- /dev/null +++ b/assets/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Brief Description of your Changes. + +Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here. + +## Impact of your Changes + +What implications will this have on the project? Will there be altered behavior or performance with this change? + +## Tests Performed + +What have you done to ensure this change has the least possible impact on the project? + +## Additional Comments + +Anything else you feel is relevant. \ No newline at end of file diff --git a/assets/.github/workflows/analysis.yaml b/assets/.github/workflows/analysis.yaml new file mode 100644 index 0000000..18da7bc --- /dev/null +++ b/assets/.github/workflows/analysis.yaml @@ -0,0 +1,19 @@ +name: analysis + +on: [push, pull_request, workflow_dispatch] + +jobs: + run: + name: Run Luau Analyze + runs-on: ubuntu-latest + + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install Luau + uses: encodedvenom/install-luau@v2.1 + + - name: Analyze + run: | + output=$(luau-analyze src || true) # Suppress errors for now. diff --git a/assets/.github/workflows/dependabot.yml b/assets/.github/workflows/dependabot.yml new file mode 100644 index 0000000..17f20b6 --- /dev/null +++ b/assets/.github/workflows/dependabot.yml @@ -0,0 +1,11 @@ +--- +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: npm + directory: "/" + schedule: + interval: "daily" diff --git a/assets/.github/workflows/deploy-docs.yaml b/assets/.github/workflows/deploy-docs.yaml new file mode 100644 index 0000000..41c68dc --- /dev/null +++ b/assets/.github/workflows/deploy-docs.yaml @@ -0,0 +1,64 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# +name: deploy-docs + +on: + # Runs on pushes targeting the `main` branch. Change this to `master` if you're + # using the `master` branch as the default branch. + push: + branches: [main] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + # - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm + # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm # or pnpm / yarn + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install dependencies + run: npm ci # or pnpm install / yarn install / bun install + - name: Build with VitePress + run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/assets/.github/workflows/publish-npm.yml b/assets/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..bce339d --- /dev/null +++ b/assets/.github/workflows/publish-npm.yml @@ -0,0 +1,17 @@ +name: publish-npm + +on: + push: + branches: [main] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: "20" + - uses: JS-DevTools/npm-publish@v3 + with: + token: ${{ secrets.NPM_AUTH_TOKEN }} diff --git a/assets/.github/workflows/release.yaml b/assets/.github/workflows/release.yaml new file mode 100644 index 0000000..a423627 --- /dev/null +++ b/assets/.github/workflows/release.yaml @@ -0,0 +1,71 @@ +name: release + +on: + push: + tags: ["v*"] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install Rokit + uses: CompeyDev/setup-rokit@v0.1.2 + + - name: Install Dependencies + run: wally install + + - name: Build + run: rojo build --output build.rbxm default.project.json + + - name: Upload Build Artifact + uses: actions/upload-artifact@v3 + with: + name: build + path: build.rbxm + + release: + name: Release + needs: [build] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Download Jecs Build + uses: actions/download-artifact@v3 + with: + name: build + path: build + + - name: Rename Build + run: mv build/build.rbxm jecs.rbxm + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + name: Jecs ${{ github.ref_name }} + files: | + jecs.rbxm + + publish: + name: Publish + needs: [release] + runs-on: ubuntu-latest + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install Rokit + uses: CompeyDev/setup-rokit@v0.1.2 + + - name: Wally Login + run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }} + + - name: Publish + run: wally publish diff --git a/assets/.github/workflows/unit-testing.yaml b/assets/.github/workflows/unit-testing.yaml new file mode 100644 index 0000000..1dbf6f7 --- /dev/null +++ b/assets/.github/workflows/unit-testing.yaml @@ -0,0 +1,31 @@ +name: unit-testing + +on: [push, pull_request, workflow_dispatch] + +jobs: + run: + name: Run Luau Tests + runs-on: ubuntu-latest + timeout-minutes: 2 + + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install Luau + uses: encodedvenom/install-luau@v4.3 + with: + version: "latest" + verbose: "true" + + - name: Run Unit Tests + id: run_tests + run: | + output=$(luau test/tests.luau) + echo "$output" + if [[ "$output" == *"0 fails"* ]]; then + echo "Unit Tests Passed" + else + echo "Error: One or More Unit Tests Failed." + exit 1 + fi diff --git a/assets/image-1.png b/assets/image-1.png new file mode 100644 index 0000000..57acaab Binary files /dev/null and b/assets/image-1.png differ diff --git a/assets/image-2.png b/assets/image-2.png new file mode 100644 index 0000000..7da53a1 Binary files /dev/null and b/assets/image-2.png differ diff --git a/assets/image-3.png b/assets/image-3.png new file mode 100644 index 0000000..06c02fd Binary files /dev/null and b/assets/image-3.png differ diff --git a/assets/image-4.png b/assets/image-4.png new file mode 100644 index 0000000..2c739cf Binary files /dev/null and b/assets/image-4.png differ diff --git a/assets/image-5.png b/assets/image-5.png new file mode 100644 index 0000000..e841a37 Binary files /dev/null and b/assets/image-5.png differ diff --git a/assets/jecs_darkmode.svg b/assets/jecs_darkmode.svg new file mode 100644 index 0000000..23a466e --- /dev/null +++ b/assets/jecs_darkmode.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/jecs_lightmode.svg b/assets/jecs_lightmode.svg new file mode 100644 index 0000000..cc93360 --- /dev/null +++ b/assets/jecs_lightmode.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/logo_old.png b/assets/logo_old.png new file mode 100644 index 0000000..2f62e5f Binary files /dev/null and b/assets/logo_old.png differ diff --git a/bench.project.json b/bench.project.json new file mode 100644 index 0000000..04596de --- /dev/null +++ b/bench.project.json @@ -0,0 +1,28 @@ +{ + "name": "jecs-test", + "tree": { + "$className": "DataModel", + "StarterPlayer": { + "$className": "StarterPlayer", + "StarterPlayerScripts": { + "$className": "StarterPlayerScripts", + "$path": "tests" + } + }, + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + "Lib": { + "$path": "jecs.luau" + }, + "benches": { + "$path": "benches" + }, + "mirror": { + "$path": "mirror.luau" + }, + "DevPackages": { + "$path": "benches/visual/DevPackages" + } + } + } +} diff --git a/benches/cached.luau b/benches/cached.luau new file mode 100644 index 0000000..df8ff38 --- /dev/null +++ b/benches/cached.luau @@ -0,0 +1,160 @@ +local jecs = require("@jecs") +local mirror = require("@mirror") + +type i53 = number + +do + TITLE(testkit.color.white_underline("Jecs query")) + local ecs = jecs.World.new() + do + TITLE("one component in common") + + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("4 component", function() + for _ in world:query(D, C, B, A) do + end + end) + end + + local D1 = ecs:component() + local D2 = ecs:component() + local D3 = ecs:component() + local D4 = ecs:component() + local D5 = ecs:component() + local D6 = ecs:component() + local D7 = ecs:component() + local D8 = ecs:component() + + local function flip() + return math.random() >= 0.15 + end + + local added = 0 + local archetypes = {} + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:entity() + + local combination = "" + + if flip() then + combination ..= "B" + ecs:set(entity, D2, { value = true }) + end + if flip() then + combination ..= "C" + ecs:set(entity, D3, { value = true }) + end + if flip() then + combination ..= "D" + ecs:set(entity, D4, { value = true }) + end + if flip() then + combination ..= "E" + ecs:set(entity, D5, { value = true }) + end + if flip() then + combination ..= "F" + ecs:set(entity, D6, { value = true }) + end + if flip() then + combination ..= "G" + ecs:set(entity, D7, { value = true }) + end + if flip() then + combination ..= "H" + ecs:set(entity, D8, { value = true }) + end + + if #combination == 7 then + added += 1 + ecs:set(entity, D1, { value = true }) + end + archetypes[combination] = true + end + + local a = 0 + for _ in archetypes do + a += 1 + end + + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end +end + +do + TITLE(testkit.color.white_underline("Mirror query")) + local ecs = mirror.World.new() + do + TITLE("one component in common") + + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("4 component", function() + for _ in world:query(D, C, B, A) do + end + end) + end + + local D1 = ecs:component() + local D2 = ecs:component() + local D3 = ecs:component() + local D4 = ecs:component() + local D5 = ecs:component() + local D6 = ecs:component() + local D7 = ecs:component() + local D8 = ecs:component() + + local function flip() + return math.random() >= 0.15 + end + + local added = 0 + local archetypes = {} + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:entity() + + local combination = "" + + if flip() then + combination ..= "B" + ecs:set(entity, D2, { value = true }) + end + if flip() then + combination ..= "C" + ecs:set(entity, D3, { value = true }) + end + if flip() then + combination ..= "D" + ecs:set(entity, D4, { value = true }) + end + if flip() then + combination ..= "E" + ecs:set(entity, D5, { value = true }) + end + if flip() then + combination ..= "F" + ecs:set(entity, D6, { value = true }) + end + if flip() then + combination ..= "G" + ecs:set(entity, D7, { value = true }) + end + if flip() then + combination ..= "H" + ecs:set(entity, D8, { value = true }) + end + + if #combination == 7 then + added += 1 + ecs:set(entity, D1, { value = true }) + end + archetypes[combination] = true + end + + local a = 0 + for _ in archetypes do + a += 1 + end + + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end +end diff --git a/benches/general.luau b/benches/general.luau new file mode 100644 index 0000000..3df5538 --- /dev/null +++ b/benches/general.luau @@ -0,0 +1,244 @@ +local jecs = require("@jecs") +local testkit = require("@testkit") + +local BENCH, START = testkit.benchmark() + +local function TITLE(s: string) + print() + print(testkit.color.white(s)) +end + +local N = 2 ^ 17 + +local pair = jecs.pair + +do + TITLE("create") + local world = jecs.World.new() + + BENCH("entity", function() + for i = 1, START(N) do + world:entity() + end + end) + + local A = world:component() + local B = world:component() + + BENCH("pair", function() + for i = 1, START(N) do + jecs.pair(A, B) + end + end) +end + +do + TITLE("set") + + local world = jecs.World.new() + local A = world:component() + + local entities = table.create(N) + + for i = 1, N do + entities[i] = world:entity() + end + + BENCH("add", function() + for i = 1, START(N) do + world:set(entities[i], A, 1) + end + end) + + BENCH("set", function() + for i = 1, START(N) do + world:set(entities[i], A, 2) + end + end) + + BENCH("remove", function() + for i = 1, START(N) do + world:remove(entities[i], A) + end + end) +end + +-- we have a separate benchmark for relationships. +-- this is due to that relationships have a very high id compared to normal +-- components, which cause them to get added into the hashmap portion. +do + TITLE("set relationship") + + local world = jecs.World.new() + local A = world:component() + + local entities = table.create(N) + + for i = 1, N do + entities[i] = world:entity() + world:set(entities[i], A, 1) + end + + local pair = jecs.pair(A, world:entity()) + + BENCH("add", function() + for i = 1, START(N) do + world:set(entities[i], pair, 1) + end + end) + + BENCH("set", function() + for i = 1, START(N) do + world:set(entities[i], pair, 2) + end + end) + + BENCH("remove", function() + for i = 1, START(N) do + world:remove(entities[i], pair) + end + end) +end + +do + TITLE("get") + + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + local entities = table.create(N) + + for i = 1, N do + entities[i] = world:entity() + world:set(entities[i], A, 1) + world:set(entities[i], B, 1) + world:set(entities[i], C, 1) + world:set(entities[i], D, 1) + end + + BENCH("1", function() + for i = 1, START(N) do + world:get(entities[i], A) + end + end) + + BENCH("2", function() + for i = 1, START(N) do + world:get(entities[i], A, B) + end + end) + + BENCH("3", function() + for i = 1, START(N) do + world:get(entities[i], A, B, C) + end + end) + + BENCH("4", function() + for i = 1, START(N) do + world:get(entities[i], A, B, C, D) + end + end) +end + +do + TITLE("target") + + BENCH("1st target", function() + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + local entities = table.create(N) + + for i = 1, N do + local ent = world:entity() + entities[i] = ent + + world:set(ent, pair(A, A)) + world:set(ent, pair(A, B)) + world:set(ent, pair(A, C)) + world:set(ent, pair(A, D)) + end + + for i = 1, START(N) do + world:target(entities[i], A, 0) + end + end) +end + +--- this benchmark is used to view how fragmentation affects query performance +--- we use this by determining how many entities should fit per arcehtype, instead +--- of creating x amount of archetypes. this would scale better with any amount of +--- entities. +do + TITLE(`query {N} entities`) + + local function view_bench(n: number) + BENCH(`{n} entities per archetype`, function() + local world = jecs.World.new() + + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + + for i = 1, N, n do + local ct = world:entity() + for j = 1, n do + local id = world:entity() + world:set(id, A, true) + world:set(id, B, true) + world:set(id, C, true) + world:set(id, D, true) + world:add(id, ct) + end + end + + local q = world:query(A, B, C, D) + START() + for id in q do + end + end) + + BENCH(`inlined query`, function() + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + + for i = 1, N, n do + local ct = world:entity() + for j = 1, n do + local id = world:entity() + world:set(id, A, true) + world:set(id, B, true) + world:set(id, C, true) + world:set(id, D, true) + world:add(id, ct) + end + end + + local archetypes = world:query(A, B, C, D):archetypes() + START() + for _, archetype in archetypes do + local columns, records = archetype.columns, archetype.records + local a = columns[records[A]] + local b = columns[records[B]] + local c = columns[records[C]] + local d = columns[records[D]] + for row in archetype.entities do + local _1, _2, _3, _4 = a[row], b[row], c[row], d[row] + end + end + end) + end + + for i = 13, 0, -1 do + view_bench(2 ^ i) + end +end diff --git a/benches/query.luau b/benches/query.luau new file mode 100644 index 0000000..ecd7fd5 --- /dev/null +++ b/benches/query.luau @@ -0,0 +1,246 @@ +--!optimize 2 +--!native + +local testkit = require("@testkit") +local BENCH, START = testkit.benchmark() +local function TITLE(title: string) + print() + print(testkit.color.white(title)) +end + +local jecs = require("@jecs") +local mirror = require("@mirror") + +type i53 = number + +do + TITLE(testkit.color.white_underline("Jecs query")) + local ecs = jecs.World.new() + do + TITLE("one component in common") + + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("1 component", function() + for _ in world:query(A) do + end + end) + + BENCH("2 component", function() + for _ in world:query(B, A) do + end + end) + + BENCH("4 component", function() + for _ in world:query(D, C, B, A) do + end + end) + + BENCH("8 component", function() + for _ in world:query(H, G, F, E, D, C, B, A) do + end + end) + + local e = world:entity() + world:set(e, A, true) + world:set(e, B, true) + world:set(e, C, true) + world:set(e, D, true) + world:set(e, E, true) + world:set(e, F, true) + world:set(e, G, true) + world:set(e, H, true) + + BENCH("Update Data", function() + for _ = 1, 100 do + world:set(e, A, false) + world:set(e, B, false) + world:set(e, C, false) + world:set(e, D, false) + world:set(e, E, false) + world:set(e, F, false) + world:set(e, G, false) + world:set(e, H, false) + end + end) + end + + local D1 = ecs:component() + local D2 = ecs:component() + local D3 = ecs:component() + local D4 = ecs:component() + local D5 = ecs:component() + local D6 = ecs:component() + local D7 = ecs:component() + local D8 = ecs:component() + + local function flip() + return math.random() >= 0.15 + end + + local added = 0 + local archetypes = {} + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:entity() + + local combination = "" + + if flip() then + combination ..= "B" + ecs:set(entity, D2, { value = true }) + end + if flip() then + combination ..= "C" + ecs:set(entity, D3, { value = true }) + end + if flip() then + combination ..= "D" + ecs:set(entity, D4, { value = true }) + end + if flip() then + combination ..= "E" + ecs:set(entity, D5, { value = true }) + end + if flip() then + combination ..= "F" + ecs:set(entity, D6, { value = true }) + end + if flip() then + combination ..= "G" + ecs:set(entity, D7, { value = true }) + end + if flip() then + combination ..= "H" + ecs:set(entity, D8, { value = true }) + end + + if #combination == 7 then + added += 1 + ecs:set(entity, D1, { value = true }) + end + archetypes[combination] = true + end + + local a = 0 + for _ in archetypes do + a += 1 + end + + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end +end + +do + TITLE(testkit.color.white_underline("Mirror query")) + local ecs = mirror.World.new() + do + TITLE("one component in common") + + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("1 component", function() + for _ in world:query(A) do + end + end) + + BENCH("2 component", function() + for _ in world:query(B, A) do + end + end) + + BENCH("4 component", function() + for _ in world:query(D, C, B, A) do + end + end) + + BENCH("8 component", function() + for _ in world:query(H, G, F, E, D, C, B, A) do + end + end) + + local e = world:entity() + world:set(e, A, true) + world:set(e, B, true) + world:set(e, C, true) + world:set(e, D, true) + world:set(e, E, true) + world:set(e, F, true) + world:set(e, G, true) + world:set(e, H, true) + + BENCH("Update Data", function() + for _ = 1, 100 do + world:set(e, A, false) + world:set(e, B, false) + world:set(e, C, false) + world:set(e, D, false) + world:set(e, E, false) + world:set(e, F, false) + world:set(e, G, false) + world:set(e, H, false) + end + end) + end + + local D1 = ecs:component() + local D2 = ecs:component() + local D3 = ecs:component() + local D4 = ecs:component() + local D5 = ecs:component() + local D6 = ecs:component() + local D7 = ecs:component() + local D8 = ecs:component() + + local function flip() + return math.random() >= 0.15 + end + + local added = 0 + local archetypes = {} + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:entity() + + local combination = "" + + if flip() then + combination ..= "B" + ecs:set(entity, D2, { value = true }) + end + if flip() then + combination ..= "C" + ecs:set(entity, D3, { value = true }) + end + if flip() then + combination ..= "D" + ecs:set(entity, D4, { value = true }) + end + if flip() then + combination ..= "E" + ecs:set(entity, D5, { value = true }) + end + if flip() then + combination ..= "F" + ecs:set(entity, D6, { value = true }) + end + if flip() then + combination ..= "G" + ecs:set(entity, D7, { value = true }) + end + if flip() then + combination ..= "H" + ecs:set(entity, D8, { value = true }) + end + + if #combination == 7 then + added += 1 + ecs:set(entity, D1, { value = true }) + end + archetypes[combination] = true + end + + local a = 0 + for _ in archetypes do + a += 1 + end + + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end +end diff --git a/benches/visual/despawn.bench.luau b/benches/visual/despawn.bench.luau new file mode 100644 index 0000000..5c424d9 --- /dev/null +++ b/benches/visual/despawn.bench.luau @@ -0,0 +1,64 @@ +--!optimize 2 +--!native + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Matter = require(ReplicatedStorage.DevPackages.Matter) +local ecr = require(ReplicatedStorage.DevPackages.ecr) +local jecs = require(ReplicatedStorage.Lib) +local pair = jecs.pair +local newWorld = Matter.World.new() +local ecs = jecs.World.new() +local mirror = require(ReplicatedStorage.mirror) +local mcs = mirror.World.new() + +local C1 = ecs:component() +local C2 = ecs:entity() +ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete)) +local C3 = ecs:entity() +ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete)) +local C4 = ecs:entity() +ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E1 = mcs:component() +local E2 = mcs:entity() +mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E3 = mcs:entity() +mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E4 = mcs:entity() +mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete)) + +local registry2 = ecr.registry() + +return { + ParameterGenerator = function() + local j = ecs:entity() + ecs:set(j, C1, true) + local m = mcs:entity() + mcs:set(m, E1, true) + for i = 1, 1000 do + local friend1 = ecs:entity() + local friend2 = mcs:entity() + + ecs:add(friend1, pair(C2, j)) + ecs:add(friend1, pair(C3, j)) + ecs:add(friend1, pair(C4, j)) + + mcs:add(friend2, pair(E2, m)) + mcs:add(friend2, pair(E3, m)) + mcs:add(friend2, pair(E4, m)) + end + return { + m = m, + j = j, + } + end, + + Functions = { + Mirror = function(_, a) + mcs:delete(a.m) + end, + + Jecs = function(_, a) + ecs:delete(a.j) + end, + }, +} diff --git a/benches/visual/insertion.bench.luau b/benches/visual/insertion.bench.luau new file mode 100644 index 0000000..f33821d --- /dev/null +++ b/benches/visual/insertion.bench.luau @@ -0,0 +1,82 @@ +--!optimize 2 +--!native + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Matter = require(ReplicatedStorage.DevPackages.Matter) +local ecr = require(ReplicatedStorage.DevPackages.ecr) +local jecs = require(ReplicatedStorage.Lib) +local newWorld = Matter.World.new() +local ecs = jecs.World.new() +local mirror = require(ReplicatedStorage.mirror) +local mcs = mirror.World.new() + +local A1 = Matter.component() +local A2 = Matter.component() +local A3 = Matter.component() +local A4 = Matter.component() +local A5 = Matter.component() +local A6 = Matter.component() +local A7 = Matter.component() +local A8 = Matter.component() + +local B1 = ecr.component() +local B2 = ecr.component() +local B3 = ecr.component() +local B4 = ecr.component() +local B5 = ecr.component() +local B6 = ecr.component() +local B7 = ecr.component() +local B8 = ecr.component() + +local C1 = ecs:component() +local C2 = ecs:component() +local C3 = ecs:component() +local C4 = ecs:component() +local C5 = ecs:component() +local C6 = ecs:component() +local C7 = ecs:component() +local C8 = ecs:component() +local E1 = mcs:component() +local E2 = mcs:component() +local E3 = mcs:component() +local E4 = mcs:component() +local E5 = mcs:component() +local E6 = mcs:component() +local E7 = mcs:component() +local E8 = mcs:component() + +local registry2 = ecr.registry() +return { + ParameterGenerator = function() + return + end, + + Functions = { + Mirror = function() + local e = mcs:entity() + for i = 1, 5000 do + mcs:set(e, E1, false) + mcs:set(e, E2, false) + mcs:set(e, E3, false) + mcs:set(e, E4, false) + mcs:set(e, E5, false) + mcs:set(e, E6, false) + mcs:set(e, E7, false) + mcs:set(e, E8, false) + end + end, + Jecs = function() + local e = ecs:entity() + for i = 1, 5000 do + ecs:set(e, C1, false) + ecs:set(e, C2, false) + ecs:set(e, C3, false) + ecs:set(e, C4, false) + ecs:set(e, C5, false) + ecs:set(e, C6, false) + ecs:set(e, C7, false) + ecs:set(e, C8, false) + end + end, + }, +} diff --git a/benches/visual/query.bench.luau b/benches/visual/query.bench.luau new file mode 100644 index 0000000..16602f1 --- /dev/null +++ b/benches/visual/query.bench.luau @@ -0,0 +1,164 @@ +--!optimize 2 +--!native + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +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 mirror = require(ReplicatedStorage.mirror) +local mcs = mirror.World.new() +local ecs = jecs.World.new() + +local A1 = Matter.component() +local A2 = Matter.component() +local A3 = Matter.component() +local A4 = Matter.component() +local A5 = Matter.component() +local A6 = Matter.component() +local A7 = Matter.component() +local A8 = Matter.component() + +local B1 = ecr.component() +local B2 = ecr.component() +local B3 = ecr.component() +local B4 = ecr.component() +local B5 = ecr.component() +local B6 = ecr.component() +local B7 = ecr.component() +local B8 = ecr.component() + +local D1 = ecs:component() +local D2 = ecs:component() +local D3 = ecs:component() +local D4 = ecs:component() +local D5 = ecs:component() +local D6 = ecs:component() +local D7 = ecs:component() +local D8 = ecs:component() + +local E1 = mcs:entity() +local E2 = mcs:entity() +local E3 = mcs:entity() +local E4 = mcs:entity() +local E5 = mcs:entity() +local E6 = mcs:entity() +local E7 = mcs:entity() +local E8 = mcs:entity() + +local registry2 = ecr.registry() + +local function flip() + return math.random() >= 0.25 +end + +local N = 2 ^ 16 - 2 +local archetypes = {} + +local hm = 0 +for i = 1, N do + local id = registry2.create() + local combination = "" + local n = newWorld:spawn() + local entity = ecs:entity() + local m = mcs:entity() + + if flip() then + registry2:set(id, B1, { value = true }) + ecs:set(entity, D1, { value = true }) + newWorld:insert(n, A1({ value = true })) + mcs:set(m, E1, { value = 2 }) + end + if flip() then + combination ..= "B" + registry2:set(id, B2, { value = true }) + ecs:set(entity, D2, { value = true }) + mcs:set(m, E2, { value = 2 }) + newWorld:insert(n, A2({ value = true })) + end + if flip() then + combination ..= "C" + registry2:set(id, B3, { value = true }) + ecs:set(entity, D3, { value = true }) + mcs:set(m, E3, { value = 2 }) + newWorld:insert(n, A3({ value = true })) + end + if flip() then + combination ..= "D" + registry2:set(id, B4, { value = true }) + ecs:set(entity, D4, { value = true }) + mcs:set(m, E4, { value = 2 }) + + newWorld:insert(n, A4({ value = true })) + end + if flip() then + combination ..= "E" + registry2:set(id, B5, { value = true }) + ecs:set(entity, D5, { value = true }) + mcs:set(m, E5, { value = 2 }) + + newWorld:insert(n, A5({ value = true })) + end + if flip() then + combination ..= "F" + registry2:set(id, B6, { value = true }) + ecs:set(entity, D6, { value = true }) + mcs:set(m, E6, { value = 2 }) + newWorld:insert(n, A6({ value = true })) + end + if flip() then + combination ..= "G" + registry2:set(id, B7, { value = true }) + ecs:set(entity, D7, { value = true }) + mcs:set(m, E7, { value = 2 }) + newWorld:insert(n, A7({ value = true })) + end + if flip() then + combination ..= "H" + registry2:set(id, B8, { value = true }) + newWorld:insert(n, A8({ value = true })) + ecs:set(entity, D8, { value = true }) + mcs:set(m, E8, { value = 2 }) + end + + if combination:find("BCDF") then + if not archetypes[combination] then + print(combination) + end + hm += 1 + end + archetypes[combination] = true +end +print("TEST", hm) + +local count = 0 + +for _, archetype in ecs:query(D2, D4, D6, D8):archetypes() do + count += #archetype.entities +end + +print(count) + +return { + ParameterGenerator = function() + return + end, + + Functions = { + Matter = function() + for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do + end + end, + + ECR = function() + for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do + end + end, + + Jecs = function() + for entityId, firstComponent in ecs:query(D2, D4, D6, D8) do + end + end, + }, +} diff --git a/benches/visual/remove.bench.luau b/benches/visual/remove.bench.luau new file mode 100644 index 0000000..5af2a17 --- /dev/null +++ b/benches/visual/remove.bench.luau @@ -0,0 +1,49 @@ +--!optimize 2 +--!native + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Matter = require(ReplicatedStorage.DevPackages.Matter) +local ecr = require(ReplicatedStorage.DevPackages.ecr) +local jecs = require(ReplicatedStorage.Lib) +local pair = jecs.pair +local ecs = jecs.World.new() +local mirror = require(ReplicatedStorage.mirror) +local mcs = mirror.World.new() + +local C1 = ecs:component() +local C2 = ecs:entity() +ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete)) +local C3 = ecs:entity() +ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete)) +local C4 = ecs:entity() +ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E1 = mcs:component() +local E2 = mcs:entity() +mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E3 = mcs:entity() +mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E4 = mcs:entity() +mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete)) + +return { + ParameterGenerator = function() + end, + + Functions = { + Mirror = function() + local m = mcs:entity() + for i = 1, 100 do + mcs:add(m, E3) + mcs:remove(m, E3) + end + end, + + Jecs = function() + local j = ecs:entity() + for i = 1, 100 do + ecs:add(j, C3) + ecs:remove(j, C3) + end + end, + }, +} diff --git a/benches/visual/spawn.bench.luau b/benches/visual/spawn.bench.luau new file mode 100644 index 0000000..5dc2b84 --- /dev/null +++ b/benches/visual/spawn.bench.luau @@ -0,0 +1,37 @@ +--!optimize 2 +--!native + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Matter = require(ReplicatedStorage.DevPackages.Matter) +local ecr = require(ReplicatedStorage.DevPackages.ecr) +local jecs = require(ReplicatedStorage.Lib) +local newWorld = Matter.World.new() +local ecs = jecs.World.new() + +return { + ParameterGenerator = function() + local registry2 = ecr.registry() + + return registry2 + end, + + Functions = { + Matter = function() + for i = 1, 1000 do + newWorld:spawn() + end + end, + + ECR = function(_, registry2) + for i = 1, 1000 do + registry2.create() + end + end, + + Jecs = function() + for i = 1, 1000 do + ecs:entity() + end + end, + }, +} diff --git a/benches/visual/wally.toml b/benches/visual/wally.toml new file mode 100644 index 0000000..cb0f731 --- /dev/null +++ b/benches/visual/wally.toml @@ -0,0 +1,11 @@ +[package] +name = "private/private" +version = "0.1.0-rc.6" +registry = "https://github.com/UpliftGames/wally-index" +realm = "shared" +include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] +exclude = ["**"] + +[dev-dependencies] +Matter = "matter-ecs/matter@0.8.0" +ecr = "centau/ecr@0.8.0" \ No newline at end of file diff --git a/coverage/ansi.luau.html b/coverage/ansi.luau.html new file mode 100644 index 0000000..3246e47 --- /dev/null +++ b/coverage/ansi.luau.html @@ -0,0 +1,69 @@ + + + + +

ansi.luau Coverage

+

Total Execution Hits: 1

+

Function Coverage Overview: 11.11%

+ +
+

Function Coverage:

+ + + + + + + + + +
FunctionHits
1
white_underline:20
white:60
green:100
red:140
yellow:180
red_highlight:220
green_highlight:260
gray:300
+

Source Code:

+ + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> + +
LineHitsCode
11return {
21white_underline = function(s: any)
30return `\27[1;4m{s}\27[0m`
4N/Aend,
5N/A
61white = function(s: any)
70return `\27[37;1m{s}\27[0m`
8N/Aend,
9N/A
101green = function(s: any)
110return `\27[32;1m{s}\27[0m`
12N/Aend,
13N/A
141red = function(s: any)
150return `\27[31;1m{s}\27[0m`
16N/Aend,
17N/A
181yellow = function(s: any)
190return `\27[33;1m{s}\27[0m`
20N/Aend,
21N/A
221red_highlight = function(s: any)
230return `\27[41;1;30m{s}\27[0m`
24N/Aend,
25N/A
261green_highlight = function(s: any)
270return `\27[42;1;30m{s}\27[0m`
28N/Aend,
29N/A
301gray = function(s: any)
310return `\27[30;1m{s}\27[0m`
32N/Aend,
330}
\ No newline at end of file diff --git a/coverage/entity_visualiser.luau.html b/coverage/entity_visualiser.luau.html new file mode 100644 index 0000000..fbc1917 --- /dev/null +++ b/coverage/entity_visualiser.luau.html @@ -0,0 +1,74 @@ + + + + +

entity_visualiser.luau Coverage

+

Total Execution Hits: 1

+

Function Coverage Overview: 25.00%

+ +
+

Function Coverage:

+ + + + +
FunctionHits
1
pe:60
name:110
components:150
+

Source Code:

+ + + + +> + + + +> +> + + +> +> + + + + +> +> + + + + + + + + + + + +> + + +> + + +> +> + + + + +
LineHitsCode
11local jecs = require("@jecs")
21local ECS_GENERATION = jecs.ECS_GENERATION
31local ECS_ID = jecs.ECS_ID
41local ansi = require("@tools/ansi")
5N/A
61local function pe(e: any)
70local gen = ECS_GENERATION(e)
80return ansi.green(`e{ECS_ID(e)}`) .. ansi.yellow(`v{gen}`)
9N/Aend
10N/A
111local function name(world: jecs.World, id: any)
120return world:get(id, jecs.Name) or `${id}`
13N/Aend
14N/A
151local function components(world: jecs.World, entity: any)
160local r = jecs.entity_index_try_get(world.entity_index, entity)
170if not r then
180return false
19N/Aend
20N/A
210local archetype = r.archetype
220local row = r.row
230print(`Entity {pe(entity)}`)
240print("-----------------------------------------------------")
250for i, column in archetype.columns do
260local component = archetype.types[i]
270local n
280if jecs.IS_PAIR(component) then
290n = `({name(world, jecs.pair_first(world, component))}, {name(world, jecs.pair_second(world, component))})`
300else
310n = name(world, component)
32N/Aend
330local data = column[row] or "TAG"
340print(`| {n} | {data} |`)
35N/Aend
360print("-----------------------------------------------------")
370return true
38N/Aend
39N/A
401return {
411components = components,
421prettify = pe,
430}
\ No newline at end of file diff --git a/coverage/index.html b/coverage/index.html new file mode 100644 index 0000000..92518d4 --- /dev/null +++ b/coverage/index.html @@ -0,0 +1,12 @@ + + + +

Coverage Report

+ + + + + + + +
FileTotal HitsFunctions
tests.luau10067
jecs.luau100644797
testkit.luau182631
lifetime_tracker.luau111
entity_visualiser.luau14
ansi.luau19
\ No newline at end of file diff --git a/coverage/jecs.luau.html b/coverage/jecs.luau.html new file mode 100644 index 0000000..9a0664c --- /dev/null +++ b/coverage/jecs.luau.html @@ -0,0 +1,2798 @@ + + + + +

jecs.luau Coverage

+

Total Execution Hits: 1006447

+

Function Coverage Overview: 84.54%

+ +
+

Function Coverage:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionHits
1
ECS_COMBINE:14265683
ECS_IS_PAIR:1471729
ECS_GENERATION_INC:15165684
ECS_ENTITY_T_LO:166162390
ECS_GENERATION:1705
ECS_ENTITY_T_HI:1740
ECS_PAIR:178133381
ECS_PAIR_FIRST:185648
ECS_PAIR_SECOND:189722
entity_index_try_get_any:193133207
entity_index_try_get:20666607
entity_index_try_get_fast:22025535
entity_index_is_alive:230922
entity_index_get_alive:234842
ecs_get_alive:2429
entity_index_new_id:26586562
ecs_pair_first:2864
ecs_pair_second:2915
query_match:29610
find_observers:3191518
archetype_move:3281822
archetype_append:38822380
new_entity:39820558
entity_move:4091822
hash:423569
fetch:4271809
world_get:438734
world_has_one_inline:469892
world_has:48580
world_target:5071440
ECS_ID_IS_WILDCARD:5390
id_record_ensure:5451509
archetype_append_to_records:6081507
archetype_create:633635
world_entity:69866852
world_parent:7021
archetype_ensure:706580
find_insert:720551
find_archetype_with:732551
find_archetype_without:75033
archetype_init_edge:767582
archetype_ensure_edge:77822971
init_edge_for_add:792549
init_edge_for_remove:80933
create_edge_for_add:832551
create_edge_for_remove:84333
archetype_traverse_add:85422092
archetype_traverse_remove:870297
world_add:88719316
world_set:9192775
world_component:971121
world_remove:983298
archetype_fast_delete_last:100876
archetype_fast_delete:101658
archetype_delete:1025134
world_clear:10646
archetype_disconnect_edge:116450
archetype_remove_edge:117523
archetype_clear_edges:118036
archetype_destroy:121337
world_cleanup:12491
world_delete:127065684
world_contains:1446145
NOOP:14500
query_iter_init:146126
world_query_iter_next:1527556
world_query_iter_next:15537
world_query_iter_next:15800
world_query_iter_next:16082
world_query_iter_next:16370
world_query_iter_next:16670
world_query_iter_next:16980
world_query_iter_next:17300
world_query_iter_next:17642
query_iter:179820
query_without:18066
query_with:18361
query_archetypes:18705
query_cached:18746
on_create_callback:19244
on_delete_callback:19281
cached_query_iter:194111
world_query_iter_next:200111
world_query_iter_next:20278
world_query_iter_next:20540
world_query_iter_next:20820
world_query_iter_next:21110
world_query_iter_next:21410
world_query_iter_next:21720
world_query_iter_next:22040
world_query_iter_next:22380
world_query:229533
world_each:23554
:237212
world_children:23892
world_new:243973
+

Source Code:

+> +> +> +> +> + + +> + + +> + +> + +> + + + + + + + +> + +> + + + + + +> + + + + + + + + + +> + + + + + + + + + +> + + + + + +> + + + + + + + + + + + +> + +> + +> + +> + + + + + + +> + + + + + + + + +> + + + + +> + +> + + + + + + + + + + +> + +> + + + + + + + + + + + + + + +> + + + + + + +> + + +> + + + +> + + +> + + +> + +> + + +> +> + + + + +> + + + +> +> + +> + +> +> + + +> +> + + +> +> + + +> +> + + + +> + +> +> + + +> +> + + +> +> + + + + + +> + + +> +> + +> +> + + + + + + +> + + +> +> + +> +> + + + + + +> +> + +> +> + + +> +> + + + + +> + +> +> + + + +> +> + +> + + +> +> + + +> +> + + + +> +> + +> +> + + + + + + + + + +> +> + + + + + + +> + +> +> + + + +> +> + + + +> +> + + + + +> + + + +> +> +> + + + + + +> +> +> +> + +> +> + + + + + +> + +> +> + + + + + + + + + + + +> + + + +> + + + +> +> +> + +> +> + + +> +> +> + +> + +> + +> +> + +> +> +> +> + + +> + + +> +> + + +> + +> + + + + +> +> + + + + + + + + +> +> + + + + + + + + + +> +> + + + + + + + + + + + + +> +> + + +> +> + + + +> + + +> +> + +> +> + + + + + +> +> + + + +> +> + + + +> + +> + + + + + + + + + + +> +> +> + + + + +> +> + + + +> +> + +> + +> +> + + + + +> +> + + + +> +> + +> + + + +> +> +> + +> +> + + + + + +> +> + + + +> +> + +> + + + +> +> + + +> +> + + + +> +> + + +> +> + + + + +> +> + + + + +> + + + + + + + + + + + + +> +> + + +> + +> + + +> +> + +> + +> + + +> +> + + + + + + + + +> + + + + + + + + + + + +> + +> +> + +> +> + + + + + + + + + + + + + + + +> + + + + + + +> +> +> + + + +> + + +> + + +> + + + + + + + + +> + + + + +> + + + +> + + + + + + +> + + + +> +> + + + + +> +> +> + + + + +> + + + +> +> +> +> + + +> + +> +> + + +> +> + + +> +> + + + +> +> + + + + +> +> + +> +> + + + + +> + + +> +> + +> +> + + +> +> +> +> + + + +> +> + +> + +> + +> +> + + + + + + + + + +> +> + + +> + +> +> + + + + + + + + + +> +> + + + + + + + + + +> +> + +> +> + + + + + + +> + + + +> + + +> +> +> +> + + + + + + + + + + + + +> + + + +> + + +> +> +> +> + + + + + + + + + +> +> + + + + + + + + + +> +> + + + + + + + +> + + + +> +> + +> +> + + + + + + +> + +> + + + +> +> + +> +> + + + + + + + + + +> +> + + + + +> + + + + + +> +> +> + + +> + + +> +> +> + + + + + +> +> + + + + +> + +> +> + + + + + + +> +> + +> +> + +> + + + +> + +> +> +> + + +> + +> + + + +> +> + + + +> +> +> + + + +> +> + +> + +> + +> +> + + + + + +> + +> + + +> +> + + + + + +> +> + +> + +> +> +> + + + + +> +> +> +> + + + + + +> +> +> +> + + + + + + + + + +> + +> + + + + +> +> + + +> +> + + + + + +> +> +> + +> + + + + +> +> +> + + + + + + + + + +> + + + + + + + + + +> + + +> +> +> + + + +> + + + + + + + +> + + + +> + + + + +> + + +> + + +> +> + + +> +> + + +> +> + + + +> +> + + + +> +> +> +> + + + + + + + + + + + + + +> + + + +> +> + + + +> +> +> +> +> + + + + + +> + + +> +> +> + + + +> +> + + + + + + + +> + + + +> +> + + + + + + +> +> + + + + + + +> +> + + +> +> + + + +> +> + + + + + + +> + + + + +> + + + +> +> +> +> + + + + + + + + +> +> +> +> + + +> + + + +> +> +> + + +> + + + +> +> + + +> +> + + + + + +> +> + + +> + +> +> + +> +> + + + + + +> + + + +> + + + + + +> + + + + +> +> + +> + + + + + + + +> +> + +> +> +> +> + + + +> + + + + + + + +> + + + +> + + + + +> + + + + + + + +> + + + + +> + + +> +> +> + + +> + + +> + + + +> +> + + + + +> +> +> +> + + +> +> +> + + + + + + + + + + +> + +> + + + + + + + + + + + + +> + + + +> +> + + + +> +> +> + + +> +> +> +> + + + + +> + + + + + + +> + + +> +> + + +> +> + +> + + + + + + + + +> + + +> + + + + + +> + + + + +> + + + + +> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +> +> + + + + + + + + +> +> + + + + +> + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + + + + + +> +> + + +> + +> + + + + + + + + + +> +> + + + + +> + + + +> +> + + +> + + +> +> + +> +> +> + + +> +> + + + + +> + +> +> + + + + + + + + +> + + + + +> +> +> + + +> +> + + + +> + +> +> + +> +> + + + + +> + + + + +> + + + + +> +> +> + + +> +> + + + +> + +> +> + +> +> +> +> +> + + +> +> + + + + + + + +> +> + + +> + + + +> + + + + + + + +> + +> +> + + + + + +> + + + + +> +> + + + + +> + + + + +> +> + + +> +> + + + + + +> +> + + +> + + +> + + + + + +> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +> +> + +> +> + + + + + + + + +> +> + + + + +> + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + + + + +> +> + + +> + +> + + + + + + + + +> +> + + + + +> + + + + + + + + + + + +> +> + + +> + +> + + + + + + + + + +> +> + + + + +> + + + +> +> + + +> + + + + + + + + +> +> + + +> +> + +> +> +> + + + + + + +> +> + + + + + + + + +> + + + +> + +> + +> + + +> + + + + + +> + + + + +> +> + + +> +> +> + + +> +> + + + + +> + +> + +> + + + + + +> +> +> + + +> +> + + +> +> + +> +> + + + + +> +> + + + + + + +> +> + + +> + + + + + + +> + + + + +> + + +> +> +> + + +> +> + + + + + + + + + + + + + + + + + + +> + + + + + + +> + + +> + + + + + + + + + + + + + + + + +> + + + + + + + + + + + + + +> + + +> + + +> + +> + + + +> +> + +> + +> +> + + + + + + +> + + + + + + + + + + + + +> + +> + +> +> + +> + + + + + + +> + + + + + + + + + +> + + + + +> + + + + + + + +> + + + + + + +> + + +> + +> +> + +> +> + +> +> + +> + +> +> + +> + +> + +> + +> + +> + + + + +> +> + +> +> + +> +> + +> + +> + +> +> + + + + + + + + + +> +> +> +> +> +> +> +> +> +> +> +> +> +> +> +> +> + + + +> + + + + + + + + + + + + + +> + +> +> + + + + +> + +> + + + + +> + + + + + + + + + + + + + + + +> + +> + + + + + +> + + + + + + +> + + +
LineHitsCode
1N/A--!optimize 2
2N/A--!native
3N/A--!strict
4N/A--draft 4
5N/A
60type i53 = number
70type i24 = number
8N/A
90type Ty = { i53 }
100type ArchetypeId = number
11N/A
120type Column = { any }
13N/A
140type Map = { [K]: V }
15N/A
160type ecs_graph_edge_t = {
170from: ecs_archetype_t,
180to: ecs_archetype_t?,
190id: number,
200prev: ecs_graph_edge_t?,
210next: ecs_graph_edge_t?,
220}
23N/A
240type ecs_graph_edges_t = Map
25N/A
260type ecs_graph_node_t = {
270add: ecs_graph_edges_t,
280remove: ecs_graph_edges_t,
290refs: ecs_graph_edge_t,
300}
31N/A
320type ecs_archetype_t = {
330id: number,
340types: Ty,
350type: string,
360entities: { number },
370columns: { Column },
380records: { [i53]: number },
390counts: { [i53]: number },
400} & ecs_graph_node_t
41N/A
420export type Archetype = {
430id: number,
440types: Ty,
450type: string,
460entities: { number },
470columns: { Column },
480records: { [Id]: number },
490counts: { [Id]: number },
500}
51N/A
520type ecs_record_t = {
530archetype: ecs_archetype_t,
540row: number,
550dense: i24,
560}
57N/A
580type ecs_id_record_t = {
590cache: { number },
600counts: { number },
610flags: number,
620size: number,
630hooks: {
640on_add: ((entity: i53) -> ())?,
650on_set: ((entity: i53, data: any) -> ())?,
660on_remove: ((entity: i53) -> ())?,
670},
680}
69N/A
700type ecs_id_index_t = Map
71N/A
720type ecs_archetypes_map_t = { [string]: ecs_archetype_t }
73N/A
740type ecs_archetypes_t = { ecs_archetype_t }
75N/A
760type ecs_entity_index_t = {
770dense_array: Map,
780sparse_array: Map,
790alive_count: number,
800max_id: number,
810}
82N/A
830type ecs_query_data_t = {
840compatible_archetypes: { ecs_archetype_t },
850ids: { i53 },
860filter_with: { i53 },
870filter_without: { i53 },
880next: () -> (number, ...any),
890world: ecs_world_t,
900}
91N/A
920type ecs_observer_t = {
930callback: (archetype: ecs_archetype_t) -> (),
940query: ecs_query_data_t,
950}
96N/A
970type ecs_observable_t = Map>
98N/A
990type ecs_world_t = {
1000entity_index: ecs_entity_index_t,
1010component_index: ecs_id_index_t,
1020archetypes: ecs_archetypes_t,
1030archetype_index: ecs_archetypes_map_t,
1040max_archetype_id: number,
1050max_component_id: number,
1060ROOT_ARCHETYPE: ecs_archetype_t,
1070observable: Map>,
1081}
109N/A
1101local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256
111N/A-- stylua: ignore start
1121local EcsOnAdd = HI_COMPONENT_ID + 1
1131local EcsOnRemove = HI_COMPONENT_ID + 2
1141local EcsOnSet = HI_COMPONENT_ID + 3
1151local EcsWildcard = HI_COMPONENT_ID + 4
1161local EcsChildOf = HI_COMPONENT_ID + 5
1171local EcsComponent = HI_COMPONENT_ID + 6
1181local EcsOnDelete = HI_COMPONENT_ID + 7
1191local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
1201local EcsDelete = HI_COMPONENT_ID + 9
1211local EcsRemove = HI_COMPONENT_ID + 10
1221local EcsName = HI_COMPONENT_ID + 11
1231local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12
1240local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13
1251local EcsRest = HI_COMPONENT_ID + 14
126N/A
1271local ECS_ID_DELETE = 0b0000_0001
1281local ECS_ID_IS_TAG = 0b0000_0010
1291local ECS_ID_HAS_ON_ADD = 0b0000_0100
1301local ECS_ID_HAS_ON_SET = 0b0000_1000
1310local ECS_ID_HAS_ON_REMOVE = 0b0001_0000
1321local ECS_ID_MASK = 0b0000_0000
133N/A
1340local ECS_ENTITY_MASK = bit32.lshift(1, 24)
1351local ECS_GENERATION_MASK = bit32.lshift(1, 16)
136N/A
1370local NULL_ARRAY = table.freeze({})
1380local ECS_INTERNAL_ERROR = [[
1390This is an internal error, please file a bug report via the following link:
140N/A
1410https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md
1421]]
143N/A
1440local function ECS_COMBINE(id: number, generation: number): i53
1451return id + (generation * ECS_ENTITY_MASK)
146N/Aend
1471local ECS_PAIR_OFFSET = 2^48
148N/A
1490local function ECS_IS_PAIR(e: number): boolean
1500return e > ECS_PAIR_OFFSET
151N/Aend
152N/A
15365540local function ECS_GENERATION_INC(e: i53): i53
15465540if e > ECS_ENTITY_MASK then
1550local id = e % ECS_ENTITY_MASK
15665540local generation = e // ECS_ENTITY_MASK
157N/A
1581local next_gen = generation + 1
1590if next_gen >= ECS_GENERATION_MASK then
1600return id
161N/Aend
162N/A
163144return ECS_COMBINE(id, next_gen)
164N/Aend
1650return ECS_COMBINE(e, 1)
166N/Aend
167N/A
1680local function ECS_ENTITY_T_LO(e: i53): i24
1690return e % ECS_ENTITY_MASK
170N/Aend
171N/A
1720local function ECS_GENERATION(e: i53)
1730return e // ECS_ENTITY_MASK
174N/Aend
175N/A
1760local function ECS_ENTITY_T_HI(e: i53): i24
1770return e // ECS_ENTITY_MASK
178N/Aend
179N/A
180133381local function ECS_PAIR(pred: i53, obj: i53): i53
1810pred %= ECS_ENTITY_MASK
182133381obj %= ECS_ENTITY_MASK
183N/A
1840return obj + (pred * ECS_ENTITY_MASK) + ECS_PAIR_OFFSET
185N/Aend
186N/A
1870local function ECS_PAIR_FIRST(e: i53): i24
1880return (e - ECS_PAIR_OFFSET) // ECS_ENTITY_MASK
189N/Aend
190N/A
1910local function ECS_PAIR_SECOND(e: i53): i24
1920return (e - ECS_PAIR_OFFSET) % ECS_ENTITY_MASK
193N/Aend
194N/A
1950local function entity_index_try_get_any(
1960entity_index: ecs_entity_index_t,
197133207entity: number
1980): ecs_record_t?
199133207local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)]
200N/A
2010if not r or r.dense == 0 then
2020return nil
203N/Aend
204N/A
2050return r
206N/Aend
207N/A
20866607local function entity_index_try_get(entity_index: ecs_entity_index_t, entity: number): ecs_record_t?
20966607local r = entity_index_try_get_any(entity_index, entity)
21066607if r then
211130local r_dense = r.dense
2120if r_dense > entity_index.alive_count then
21366477return nil
214N/Aend
2150if entity_index.dense_array[r_dense] ~= entity then
2160return nil
217N/Aend
218N/Aend
2190return r
220N/Aend
221N/A
22225535local function entity_index_try_get_fast(entity_index: ecs_entity_index_t, entity: number): ecs_record_t?
22325243local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)]
22424if r then
2250if entity_index.dense_array[r.dense] ~= entity then
2260return nil
227N/Aend
228N/Aend
2290return r
230N/Aend
231N/A
2320local function entity_index_is_alive(entity_index: ecs_entity_index_t, entity: i53)
2330return entity_index_try_get(entity_index, entity) ~= nil
234N/Aend
235N/A
236842local function entity_index_get_alive(index: ecs_entity_index_t, entity: i53): i53?
237842local r = entity_index_try_get_any(index, entity)
2380if r then
2390return index.dense_array[r.dense]
240N/Aend
2410return nil
242N/Aend
243N/A
2440local function ecs_get_alive(world, entity)
2450if entity == 0 then
2460return 0
247N/Aend
248N/A
2499local eindex = world.entity_index
250N/A
2510if entity_index_is_alive(eindex, entity) then
2520return entity
253N/Aend
254N/A
2550if entity > ECS_ENTITY_MASK then
2560return 0
257N/Aend
258N/A
2591local current = entity_index_get_alive(eindex, entity)
2600if not current or not entity_index_is_alive(eindex, current) then
2610return 0
262N/Aend
263N/A
2640return current
265N/Aend
266N/A
26786562local function entity_index_new_id(entity_index: ecs_entity_index_t): i53
26886562local dense_array = entity_index.dense_array
26986562local alive_count = entity_index.alive_count
27065559local max_id = entity_index.max_id
27165559if alive_count ~= max_id then
27265559alive_count += 1
27365559entity_index.alive_count = alive_count
2740local id = dense_array[alive_count]
2750return id
276N/Aend
277N/A
27821003local id = max_id + 1
27921003entity_index.max_id = id
28021003alive_count += 1
28121003entity_index.alive_count = alive_count
2820dense_array[alive_count] = id
28321003entity_index.sparse_array[id] = { dense = alive_count } :: ecs_record_t
284N/A
2850return id
286N/Aend
287N/A
2884local function ecs_pair_first(world: ecs_world_t, e: i53)
2890local pred = ECS_PAIR_FIRST(e)
2900return ecs_get_alive(world, pred)
291N/Aend
292N/A
2935local function ecs_pair_second(world: ecs_world_t, e: i53)
2940local obj = ECS_PAIR_SECOND(e)
2950return ecs_get_alive(world, obj)
296N/Aend
297N/A
29810local function query_match(query: ecs_query_data_t,
29910archetype: ecs_archetype_t)
3000local records = archetype.records
30110local with = query.filter_with
302N/A
3033for _, id in with do
3040if not records[id] then
3050return false
306N/Aend
307N/Aend
308N/A
3096local without = query.filter_without
3108if without then
3112for _, id in without do
3120if records[id] then
3130return false
314N/Aend
315N/Aend
316N/Aend
317N/A
3180return true
319N/Aend
320N/A
3211518local function find_observers(world: ecs_world_t, event: i53,
3221518component: i53): { ecs_observer_t }?
3231499local cache = world.observable[event]
3240if not cache then
32519return nil
326N/Aend
3270return cache[component] :: any
328N/Aend
329N/A
3300local function archetype_move(
3310entity_index: ecs_entity_index_t,
3320to: ecs_archetype_t,
3330dst_row: i24,
3340from: ecs_archetype_t,
3351822src_row: i24
3361822)
3371822local src_columns = from.columns
3381822local dst_columns = to.columns
3390local dst_entities = to.entities
3401822local src_entities = from.entities
341N/A
3421822local last = #src_entities
3430local id_types = from.types
3441822local records = to.records
345N/A
3460for i, column in src_columns do
3470if column == NULL_ARRAY then
3480continue
349N/Aend
350N/A-- Retrieves the new column index from the source archetype's record from each component
351N/A-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
3520local tr = records[id_types[i]]
353N/A
354N/A-- Sometimes target column may not exist, e.g. when you remove a component.
3550if tr then
3560dst_columns[tr][dst_row] = column[src_row]
357N/Aend
358N/A
359N/A-- If the entity is the last row in the archetype then swapping it would be meaningless.
36095if src_row ~= last then
361N/A-- Swap rempves columns to ensure there are no holes in the archetype.
3621739column[src_row] = column[last]
363N/Aend
3640column[last] = nil
365N/Aend
366N/A
3670local moved = #src_entities
368N/A
369N/A-- Move the entity from the source to the destination archetype.
370N/A-- Because we have swapped columns we now have to update the records
371N/A-- corresponding to the entities' rows that were swapped.
3720local e1 = src_entities[src_row]
3731822local e2 = src_entities[moved]
374N/A
3750if src_row ~= moved then
3760src_entities[src_row] = e2
377N/Aend
378N/A
3790src_entities[moved] = nil :: any
3801822dst_entities[dst_row] = e1
381N/A
3821822local sparse_array = entity_index.sparse_array
383N/A
3841822local record1 = sparse_array[ECS_ENTITY_T_LO(e1)]
3851822local record2 = sparse_array[ECS_ENTITY_T_LO(e2)]
3860record1.row = dst_row
3870record2.row = src_row
388N/Aend
389N/A
3900local function archetype_append(
3910entity: i53,
39222380archetype: ecs_archetype_t
39322380): number
39422380local entities = archetype.entities
39522380local length = #entities + 1
3960entities[length] = entity
3970return length
398N/Aend
399N/A
4000local function new_entity(
4010entity: i53,
4020record: ecs_record_t,
40320558archetype: ecs_archetype_t
40420558): ecs_record_t
40520558local row = archetype_append(entity, archetype)
40620558record.archetype = archetype
4070record.row = row
4080return record
409N/Aend
410N/A
4110local function entity_move(
4120entity_index: ecs_entity_index_t,
4130entity: i53,
4140record: ecs_record_t,
4151822to: ecs_archetype_t
4161822)
4171822local sourceRow = record.row
4181822local from = record.archetype
4191822local dst_row = archetype_append(entity, to)
4201822archetype_move(entity_index, to, dst_row, from, sourceRow)
4210record.archetype = to
4220record.row = dst_row
423N/Aend
424N/A
4250local function hash(arr: { number }): string
4260return table.concat(arr, "_")
427N/Aend
428N/A
4291809local function fetch(id: i53, records: { number },
4300columns: { Column }, row: number): any
4311809local tr = records[id]
432N/A
4330if not tr then
4340return nil
435N/Aend
436N/A
4370return columns[tr][row]
438N/Aend
439N/A
440734local function world_get(world: ecs_world_t, entity: i53,
441734a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any
44275local record = entity_index_try_get_fast(world.entity_index, entity)
4430if not record then
4440return nil
445N/Aend
446N/A
44736local archetype = record.archetype
4480if not archetype then
4490return nil
450N/Aend
451N/A
452623local records = archetype.records
4530local columns = archetype.columns
454623local row = record.row
455N/A
456623local va = fetch(a, records, columns, row)
457N/A
458593if not b then
4590return va
460593elseif not c then
461593return va, fetch(b, records, columns, row)
4620elseif not d then
4630return va, fetch(b, records, columns, row), fetch(c, records, columns, row)
4640elseif not e then
4650return va, fetch(b, records, columns, row), fetch(c, records, columns, row), fetch(d, records, columns, row)
4660else
4670error("args exceeded")
468N/Aend
469N/Aend
470N/A
471892local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): boolean
47273local record = entity_index_try_get_fast(world.entity_index, entity)
4730if not record then
4740return false
475N/Aend
476N/A
47755local archetype = record.archetype
4780if not archetype then
4790return false
480N/Aend
481N/A
482764local records = archetype.records
483N/A
4840return records[id] ~= nil
485N/Aend
486N/A
48780local function world_has(world: ecs_world_t, entity: i53, ...: i53): boolean
48822local record = entity_index_try_get_fast(world.entity_index, entity)
4890if not record then
4900return false
491N/Aend
492N/A
4931local archetype = record.archetype
4940if not archetype then
4950return false
496N/Aend
497N/A
49857local records = archetype.records
499N/A
50025for i = 1, select("#", ...) do
5010if not records[select(i, ...)] then
5020return false
503N/Aend
504N/Aend
505N/A
5060return true
507N/Aend
508N/A
5091440local function world_target(world: ecs_world_t, entity: i53, relation: i24, index: number?): i24?
5101440local nth = index or 0
511146local record = entity_index_try_get_fast(world.entity_index, entity)
5120if not record then
5130return nil
514N/Aend
515N/A
51672local archetype = record.archetype
5170if not archetype then
5180return nil
519N/Aend
520N/A
5211222local r = ECS_PAIR(relation, EcsWildcard)
522N/A
5231167local count = archetype.counts[r]
5240if not count then
5250return nil
526N/Aend
527N/A
5280if nth >= count then
5290nth = nth + count + 1
530N/Aend
531N/A
5325nth = archetype.types[nth + archetype.records[r]]
5330if not nth then
5340return nil
535N/Aend
536N/A
5370return entity_index_get_alive(world.entity_index,
5380ECS_PAIR_SECOND(nth))
539N/Aend
540N/A
5410local function ECS_ID_IS_WILDCARD(e: i53): boolean
5420local first = ECS_ENTITY_T_HI(e)
5430local second = ECS_ENTITY_T_LO(e)
5440return first == EcsWildcard or second == EcsWildcard
545N/Aend
546N/A
5471509local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t
5481509local component_index = world.component_index
5490local entity_index = world.entity_index
5501509local idr: ecs_id_record_t = component_index[id]
551N/A
552704if not idr then
553704local flags = ECS_ID_MASK
554704local relation = id
555704local target = 0
556384local is_pair = ECS_IS_PAIR(id)
557384if is_pair then
558384relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53
559383assert(relation and entity_index_is_alive(
560383entity_index, relation), ECS_INTERNAL_ERROR)
561383target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53
5620assert(target and entity_index_is_alive(
5630entity_index, target), ECS_INTERNAL_ERROR)
564N/Aend
565N/A
5660local cleanup_policy = world_target(world, relation, EcsOnDelete, 0)
567702local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0)
568N/A
569702local has_delete = false
570N/A
5710if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then
5720has_delete = true
573N/Aend
574N/A
575702local on_add, on_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, EcsOnRemove)
576N/A
577702local is_tag = not world_has_one_inline(world, relation, EcsComponent)
578N/A
5790if is_tag and is_pair then
5800is_tag = not world_has_one_inline(world, target, EcsComponent)
581N/Aend
582N/A
583702flags = bit32.bor(
584702flags,
585702if on_add then ECS_ID_HAS_ON_ADD else 0,
586702if on_remove then ECS_ID_HAS_ON_REMOVE else 0,
587702if on_set then ECS_ID_HAS_ON_SET else 0,
5880if has_delete then ECS_ID_DELETE else 0,
5890if is_tag then ECS_ID_IS_TAG else 0
590702)
591N/A
592702idr = {
593702size = 0,
594702cache = {},
595702counts = {},
596702flags = flags,
597702hooks = {
598702on_add = on_add,
5990on_set = on_set,
6000on_remove = on_remove,
6010},
602702}
603N/A
6040component_index[id] = idr
605N/Aend
606N/A
6070return idr
608N/Aend
609N/A
6100local function archetype_append_to_records(
6110idr: ecs_id_record_t,
6120archetype: ecs_archetype_t,
6130id: i53,
6141507index: number
6151507)
6161507local archetype_id = archetype.id
6171507local archetype_records = archetype.records
6181507local archetype_counts = archetype.counts
6191507local idr_columns = idr.cache
6201507local idr_counts = idr.counts
6211405local tr = idr_columns[archetype_id]
6221405if not tr then
6230idr_columns[archetype_id] = index
6241405idr_counts[archetype_id] = 1
625N/A
6260archetype_records[id] = index
627102archetype_counts[id] = 1
628102else
629102local max_count = idr_counts[archetype_id] + 1
6300idr_counts[archetype_id] = max_count
6310archetype_counts[id] = max_count
632N/Aend
633N/Aend
634N/A
635635local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: i53?): ecs_archetype_t
6360local archetype_id = (world.max_archetype_id :: number) + 1
637635world.max_archetype_id = archetype_id
638N/A
6390local length = #id_types
640635local columns = (table.create(length) :: any) :: { Column }
641N/A
6420local records: { number } = {}
643635local counts: {number} = {}
644N/A
645635local archetype: ecs_archetype_t = {
646635columns = columns,
647635entities = {},
648635id = archetype_id,
649635records = records,
650635counts = counts,
6510type = ty,
652635types = id_types,
653N/A
654635add = {},
6550remove = {},
6560refs = {} :: ecs_graph_edge_t,
657635}
658N/A
659987for i, component_id in id_types do
6600local idr = id_record_ensure(world, component_id)
661987archetype_append_to_records(idr, archetype, component_id, i)
662N/A
663260if ECS_IS_PAIR(component_id) then
664260local relation = ECS_PAIR_FIRST(component_id)
665260local object = ECS_PAIR_SECOND(component_id)
666260local r = ECS_PAIR(relation, EcsWildcard)
6670local idr_r = id_record_ensure(world, r)
668260archetype_append_to_records(idr_r, archetype, r, i)
669N/A
670260local t = ECS_PAIR(EcsWildcard, object)
6710local idr_t = id_record_ensure(world, t)
6720archetype_append_to_records(idr_t, archetype, t, i)
673N/Aend
674N/A
6750if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then
676393columns[i] = {}
6770else
6780columns[i] = NULL_ARRAY
679N/Aend
680N/Aend
681N/A
6821405for id in records do
6830local observer_list = find_observers(world, EcsOnArchetypeCreate, id)
6840if not observer_list then
6858continue
686N/Aend
6874for _, observer in observer_list do
6880if query_match(observer.query, archetype) then
6890observer.callback(archetype)
690N/Aend
691N/Aend
692N/Aend
693N/A
6940world.archetype_index[ty] = archetype
695633world.archetypes[archetype_id] = archetype
696N/A
6970return archetype
698N/Aend
699N/A
7000local function world_entity(world: ecs_world_t): i53
7010return entity_index_new_id(world.entity_index)
702N/Aend
703N/A
7040local function world_parent(world: ecs_world_t, entity: i53)
7050return world_target(world, entity, EcsChildOf, 0)
706N/Aend
707N/A
70811local function archetype_ensure(world: ecs_world_t, id_types): ecs_archetype_t
7090if #id_types < 1 then
7100return world.ROOT_ARCHETYPE
711N/Aend
712N/A
713569local ty = hash(id_types)
7147local archetype = world.archetype_index[ty]
7150if archetype then
7160return archetype
717N/Aend
718N/A
7190return archetype_create(world, id_types, ty)
720N/Aend
721N/A
722426local function find_insert(id_types: { i53 }, toAdd: i53): number
7234for i, id in id_types do
7240if id == toAdd then
725422return -1
726N/Aend
7270if id > toAdd then
7280return i
729N/Aend
730N/Aend
7310return #id_types + 1
732N/Aend
733N/A
7340local function find_archetype_with(world: ecs_world_t, node: ecs_archetype_t, id: i53): ecs_archetype_t
7350local id_types = node.types
736N/A-- Component IDs are added incrementally, so inserting and sorting
737N/A-- them each time would be expensive. Instead this insertion sort can find the insertion
738N/A-- point in the types array.
739N/A
740551local dst = table.clone(node.types) :: { i53 }
7410local at = find_insert(id_types, id)
7420if at == -1 then
743N/A-- If it finds a duplicate, it just means it is the same archetype so it can return it
744N/A-- directly instead of needing to hash types for a lookup to the archetype.
745547return node
746N/Aend
747547table.insert(dst, at, id)
748N/A
7490return archetype_ensure(world, dst)
750N/Aend
751N/A
7520local function find_archetype_without(
7530world: ecs_world_t,
7540node: ecs_archetype_t,
75533id: i53
75633): ecs_archetype_t
75733local id_types = node.types
7580local at = table.find(id_types, id)
7590if at == nil then
7600return node
761N/Aend
762N/A
7630local dst = table.clone(id_types)
76433table.remove(dst, at)
765N/A
7660return archetype_ensure(world, dst)
767N/Aend
768N/A
7690local function archetype_init_edge(
7700archetype: ecs_archetype_t,
7710edge: ecs_graph_edge_t,
7720id: i53,
773582to: ecs_archetype_t
774582)
775582edge.from = archetype
7760edge.to = to
7770edge.id = id
778N/Aend
779N/A
7800local function archetype_ensure_edge(
7810world: ecs_world_t,
7820edges: ecs_graph_edges_t,
78322971id: i53
78422971): ecs_graph_edge_t
785584local edge = edges[id]
786584if not edge then
7870edge = {} :: ecs_graph_edge_t
7880edges[id] = edge
789N/Aend
790N/A
7910return edge
792N/Aend
793N/A
794549local function init_edge_for_add(world, archetype: ecs_archetype_t, edge: ecs_graph_edge_t, id, to: ecs_archetype_t)
795549archetype_init_edge(archetype, edge, id, to)
796545archetype_ensure_edge(world, archetype.add, id)
797545if archetype ~= to then
7980local to_refs = to.refs
799545local next_edge = to_refs.next
800N/A
801545to_refs.next = edge
8020edge.prev = to_refs
803545edge.next = next_edge
804N/A
8050if next_edge then
8060next_edge.prev = edge
807N/Aend
808N/Aend
809N/Aend
810N/A
8110local function init_edge_for_remove(
8120world: ecs_world_t,
8130archetype: ecs_archetype_t,
8140edge: ecs_graph_edge_t,
8150id: number,
81633to: ecs_archetype_t
81733)
81833archetype_init_edge(archetype, edge, id, to)
81933archetype_ensure_edge(world, archetype.remove, id)
82033if archetype ~= to then
8210local to_refs = to.refs
82233local prev_edge = to_refs.prev
823N/A
82433to_refs.prev = edge
8250edge.next = to_refs
82633edge.prev = prev_edge
827N/A
8280if prev_edge then
8290prev_edge.next = edge
830N/Aend
831N/Aend
832N/Aend
833N/A
8340local function create_edge_for_add(
8350world: ecs_world_t,
8360node: ecs_archetype_t,
8370edge: ecs_graph_edge_t,
838551id: i53
839549): ecs_archetype_t
840549local to = find_archetype_with(world, node, id)
8410init_edge_for_add(world, node, edge, id, to)
8420return to
843N/Aend
844N/A
8450local function create_edge_for_remove(
8460world: ecs_world_t,
8470node: ecs_archetype_t,
8480edge: ecs_graph_edge_t,
84933id: i53
85033): ecs_archetype_t
85133local to = find_archetype_without(world, node, id)
8520init_edge_for_remove(world, node, edge, id, to)
8530return to
854N/Aend
855N/A
8560local function archetype_traverse_add(
8570world: ecs_world_t,
8580id: i53,
85922092from: ecs_archetype_t
86022092): ecs_archetype_t
8610from = from or world.ROOT_ARCHETYPE
86222092local edge = archetype_ensure_edge(world, from.add, id)
863N/A
864551local to = edge.to
8650if not to then
8660to = create_edge_for_add(world, from, edge, id)
867N/Aend
868N/A
8690return to :: ecs_archetype_t
870N/Aend
871N/A
8720local function archetype_traverse_remove(
8730world: ecs_world_t,
8740id: i53,
875297from: ecs_archetype_t
8760): ecs_archetype_t
877297from = from or world.ROOT_ARCHETYPE
878N/A
879297local edge = archetype_ensure_edge(world, from.remove, id)
880N/A
88133local to = edge.to
8820if not to then
8830to = create_edge_for_remove(world, from, edge, id)
884N/Aend
885N/A
8860return to :: ecs_archetype_t
887N/Aend
888N/A
8890local function world_add(
8900world: ecs_world_t,
8910entity: i53,
89219316id: i53
89319316): ()
89419316local entity_index = world.entity_index
8950local record = entity_index_try_get_fast(entity_index, entity)
8960if not record then
8970return
898N/Aend
899N/A
90019315local from = record.archetype
9013local to = archetype_traverse_add(world, id, from)
9020if from == to then
90319312return
904N/Aend
9050if from then
90619170entity_move(entity_index, entity, record, to)
90719170else
9080if #to.types > 0 then
9090new_entity(entity, record, to)
910N/Aend
911N/Aend
912N/A
9130local idr = world.component_index[id]
91419312local on_add = idr.hooks.on_add
915N/A
9160if on_add then
9170on_add(entity)
918N/Aend
919N/Aend
920N/A
9212775local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown): ()
9222775local entity_index = world.entity_index
9230local record = entity_index_try_get_fast(entity_index, entity)
9240if not record then
9250return
926N/Aend
927N/A
9282774local from: ecs_archetype_t = record.archetype
9292774local to: ecs_archetype_t = archetype_traverse_add(world, id, from)
9300local idr = world.component_index[id]
9312774local idr_hooks = idr.hooks
932N/A
9330if from == to then
934N/A-- If the archetypes are the same it can avoid moving the entity
935N/A-- and just set the data directly.
9362local tr = to.records[id]
9372local column = from.columns[tr]
9382column[record.row] = data
9390local on_set = idr_hooks.on_set
9400if on_set then
9410on_set(entity, data)
942N/Aend
943N/A
9440return
945N/Aend
946N/A
9471384if from then
948N/A-- If there was a previous archetype, then the entity needs to move the archetype
9491388entity_move(entity_index, entity, record, to)
9500else
9511388if #to.types > 0 then
952N/A-- When there is no previous archetype it should create the archetype
9530new_entity(entity, record, to)
954N/Aend
955N/Aend
956N/A
9570local tr = to.records[id]
9582772local column = to.columns[tr]
959N/A
9602765column[record.row] = data
961N/A
9620local on_add = idr_hooks.on_add
9630if on_add then
9640on_add(entity)
965N/Aend
966N/A
9671local on_set = idr_hooks.on_set
9680if on_set then
9690on_set(entity, data)
970N/Aend
971N/Aend
972N/A
973121local function world_component(world: World): i53
9740local id = (world.max_component_id :: number) + 1
9750if id > HI_COMPONENT_ID then
976N/A-- IDs are partitioned into ranges because component IDs are not nominal,
977N/A-- so it needs to error when IDs intersect into the entity range.
978121error("Too many components, consider using world:entity() instead to create components.")
979N/Aend
980121world.max_component_id = id
981N/A
9820return id
983N/Aend
984N/A
985298local function world_remove(world: ecs_world_t, entity: i53, id: i53)
986298local entity_index = world.entity_index
9870local record = entity_index_try_get_fast(entity_index, entity)
9880if not record then
989298return
990N/Aend
991298local from = record.archetype
992N/A
9930if not from then
9940return
995N/Aend
996N/A
997296if from.records[id] then
998296local idr = world.component_index[id]
9993local on_remove = idr.hooks.on_remove
10000if on_remove then
10010on_remove(entity)
1002N/Aend
1003N/A
1004296local to = archetype_traverse_remove(world, id, record.archetype)
1005N/A
10060entity_move(entity_index, entity, record, to)
1007N/Aend
1008N/Aend
1009N/A
1010153local function archetype_fast_delete_last(columns: { Column }, column_count: number, types: { i53 }, entity: i53)
1011135for i, column in columns do
10120if column ~= NULL_ARRAY then
10130column[column_count] = nil
1014N/Aend
1015N/Aend
1016N/Aend
1017N/A
1018109local function archetype_fast_delete(columns: { Column }, column_count: number, row, types, entity)
1019103for i, column in columns do
1020103if column ~= NULL_ARRAY then
10210column[row] = column[column_count]
10220column[column_count] = nil
1023N/Aend
1024N/Aend
1025N/Aend
1026N/A
1027134local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, row: number)
1028134local entity_index = world.entity_index
1029134local component_index = world.component_index
1030134local columns = archetype.columns
1031134local id_types = archetype.types
1032134local entities = archetype.entities
1033134local column_count = #entities
10340local last = #entities
1035134local move = entities[last]
1036N/A-- We assume first that the entity is the last in the archetype
1037134local delete = move
1038N/A
103958if row ~= last then
104058local record_to_move = entity_index_try_get_any(entity_index, move)
10410if record_to_move then
10420record_to_move.row = row
1043N/Aend
1044N/A
10450delete = entities[row]
10460entities[row] = move
1047N/Aend
1048N/A
1049262for _, id in id_types do
1050262local idr = component_index[id]
10513local on_remove = idr.hooks.on_remove
10520if on_remove then
10530on_remove(delete)
1054N/Aend
1055N/Aend
1056N/A
1057134entities[last] = nil :: any
1058N/A
10590if row == last then
106058archetype_fast_delete_last(columns, column_count, id_types, delete)
10610else
10620archetype_fast_delete(columns, column_count, row, id_types, delete)
1063N/Aend
1064N/Aend
1065N/A
10666local function world_clear(world: ecs_world_t, entity: i53)
10676local entity_index = world.entity_index
10686local component_index = world.component_index
10696local archetypes = world.archetypes
10706local tgt = ECS_PAIR(EcsWildcard, entity)
10716local idr_t = component_index[tgt]
10726local idr = component_index[entity]
10730local rel = ECS_PAIR(entity, EcsWildcard)
10746local idr_r = component_index[rel]
1075N/A
10764if idr then
10774local count = 0
107811local queue = {}
107911for archetype_id in idr.cache do
108011local idr_archetype = archetypes[archetype_id]
108111local entities = idr_archetype.entities
108211local n = #entities
10830count += n
10844table.move(entities, 1, n, #queue + 1, queue)
1085N/Aend
10860for _, e in queue do
10870world_remove(world, e, entity)
1088N/Aend
1089N/Aend
1090N/A
10910if idr_t then
10920local queue
10930local ids
1094N/A
10950local count = 0
10960local archetype_ids = idr_t.cache
10970for archetype_id in archetype_ids do
10980local idr_t_archetype = archetypes[archetype_id]
10990local idr_t_types = idr_t_archetype.types
11000local entities = idr_t_archetype.entities
11010local removal_queued = false
1102N/A
11030for _, id in idr_t_types do
11040if not ECS_IS_PAIR(id) then
11050continue
1106N/Aend
11070local object = entity_index_get_alive(
11080entity_index, ECS_PAIR_SECOND(id))
11090if object ~= entity then
11100continue
1111N/Aend
11120if not ids then
11130ids = {}
1114N/Aend
11150ids[id] = true
11160removal_queued = true
1117N/Aend
1118N/A
11190if not removal_queued then
11200continue
1121N/Aend
1122N/A
11230if not queue then
11240queue = {}
1125N/Aend
1126N/A
11270local n = #entities
11280table.move(entities, 1, n, count + 1, queue)
11290count += n
1130N/Aend
1131N/A
11320for id in ids do
11330for _, child in queue do
11340world_remove(world, child, id)
1135N/Aend
1136N/Aend
1137N/Aend
1138N/A
11391if idr_r then
11401local count = 0
11411local archetype_ids = idr_r.cache
11421local ids = {}
11432local queue = {}
11442for archetype_id in archetype_ids do
11452local idr_r_archetype = archetypes[archetype_id]
11462local entities = idr_r_archetype.entities
11472local tr = idr_r_archetype.records[rel]
11482local tr_count = idr_r_archetype.counts[rel]
11492local types = idr_r_archetype.types
11500for i = tr, tr + tr_count - 1 do
11512ids[types[i]] = true
1152N/Aend
11532local n = #entities
11540table.move(entities, 1, n, count + 1, queue)
11550count += n
1156N/Aend
1157N/A
11583for _, e in queue do
11590for id in ids do
11600world_remove(world, e, id)
1161N/Aend
1162N/Aend
1163N/Aend
1164N/Aend
1165N/A
116650local function archetype_disconnect_edge(edge: ecs_graph_edge_t)
116750local edge_next = edge.next
116819local edge_prev = edge.prev
11690if edge_next then
117050edge_next.prev = edge_prev
1171N/Aend
11720if edge_prev then
11730edge_prev.next = edge_next
1174N/Aend
1175N/Aend
1176N/A
117723local function archetype_remove_edge(edges: ecs_graph_edges_t, id: i53, edge: ecs_graph_edge_t)
11780archetype_disconnect_edge(edge)
11790edges[id] = nil :: any
1180N/Aend
1181N/A
118236local function archetype_clear_edges(archetype: ecs_archetype_t)
118336local add: ecs_graph_edges_t = archetype.add
118436local remove: ecs_graph_edges_t = archetype.remove
11859local node_refs = archetype.refs
11869for id, edge in add do
11870archetype_disconnect_edge(edge)
118836add[id] = nil :: any
1189N/Aend
119018for id, edge in remove do
11910archetype_disconnect_edge(edge)
11920remove[id] = nil :: any
1193N/Aend
1194N/A
119522local cur = node_refs.next
119622while cur do
119722local edge = cur :: ecs_graph_edge_t
119822local next_edge = edge.next
11990archetype_remove_edge(edge.from.add, edge.id, edge)
12000cur = next_edge
1201N/Aend
1202N/A
12031cur = node_refs.prev
12041while cur do
12051local edge: ecs_graph_edge_t = cur
12061local next_edge = edge.prev
12070archetype_remove_edge(edge.from.remove, edge.id, edge)
12080cur = next_edge
1209N/Aend
1210N/A
12110node_refs.next = nil
12120node_refs.prev = nil
1213N/Aend
1214N/A
12151local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t)
12160if archetype == world.ROOT_ARCHETYPE then
12170return
1218N/Aend
1219N/A
122036local component_index = world.component_index
122136archetype_clear_edges(archetype)
122236local archetype_id = archetype.id
122336world.archetypes[archetype_id] = nil :: any
12240world.archetype_index[archetype.type] = nil :: any
122536local records = archetype.records
1226N/A
1227113for id in records do
12280local observer_list = find_observers(world, EcsOnArchetypeDelete, id)
12290if not observer_list then
12302continue
1231N/Aend
12321for _, observer in observer_list do
12330if query_match(observer.query, archetype) then
12340observer.callback(archetype)
1235N/Aend
1236N/Aend
1237N/Aend
1238N/A
1239113for id in records do
1240113local idr = component_index[id]
1241113idr.cache[archetype_id] = nil :: any
1242113idr.counts[archetype_id] = nil
1243113idr.size -= 1
12440records[id] = nil :: any
12450if idr.size == 0 then
12460component_index[id] = nil :: any
1247N/Aend
1248N/Aend
1249N/Aend
1250N/A
12510local function world_cleanup(world: ecs_world_t)
12521local archetypes = world.archetypes
1253N/A
12544for _, archetype in archetypes do
12550if #archetype.entities == 0 then
12560archetype_destroy(world, archetype)
1257N/Aend
1258N/Aend
1259N/A
12600local new_archetypes = table.create(#archetypes) :: { ecs_archetype_t }
12611local new_archetype_map = {}
1262N/A
12636for index, archetype in archetypes do
12640new_archetypes[index] = archetype
12650new_archetype_map[archetype.type] = archetype
1266N/Aend
1267N/A
12680world.archetypes = new_archetypes
12690world.archetype_index = new_archetype_map
1270N/Aend
1271N/A
127265684local function world_delete(world: ecs_world_t, entity: i53)
127365684local entity_index = world.entity_index
12741local record = entity_index_try_get(entity_index, entity)
12750if not record then
12760return
1277N/Aend
1278N/A
12790local archetype = record.archetype
128065683local row = record.row
1281N/A
12820if archetype then
1283N/A-- In the future should have a destruct mode for
1284N/A-- deleting archetypes themselves. Maybe requires recycling
12850archetype_delete(world, archetype, row)
1286N/Aend
1287N/A
128865683local delete = entity
128965683local component_index = world.component_index
129065683local archetypes = world.archetypes
12910local tgt = ECS_PAIR(EcsWildcard, delete)
129265683local rel = ECS_PAIR(delete, EcsWildcard)
1293N/A
129465683local idr_t = component_index[tgt]
12950local idr = component_index[delete]
129665683local idr_r = component_index[rel]
1297N/A
12988if idr then
12991local flags = idr.flags
13001if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
13010for archetype_id in idr.cache do
13021local idr_archetype = archetypes[archetype_id]
1303N/A
13041local entities = idr_archetype.entities
13052local n = #entities
13060for i = n, 1, -1 do
13070world_delete(world, entities[i])
1308N/Aend
1309N/A
13100archetype_destroy(world, idr_archetype)
1311N/Aend
131212else
131312for archetype_id in idr.cache do
131412local idr_archetype = archetypes[archetype_id]
131512local entities = idr_archetype.entities
131610local n = #entities
13170for i = n, 1, -1 do
13180world_remove(world, entities[i], delete)
1319N/Aend
1320N/A
13210archetype_destroy(world, idr_archetype)
1322N/Aend
1323N/Aend
1324N/Aend
1325N/A
132613if idr_t then
13270local children
132813local ids
1329N/A
133013local count = 0
133118local archetype_ids = idr_t.cache
133218for archetype_id in archetype_ids do
133318local idr_t_archetype = archetypes[archetype_id]
133418local idr_t_types = idr_t_archetype.types
13350local entities = idr_t_archetype.entities
133618local removal_queued = false
1337N/A
13380for _, id in idr_t_types do
13390if not ECS_IS_PAIR(id) then
134024continue
1341N/Aend
134224local object = entity_index_get_alive(
13430entity_index, ECS_PAIR_SECOND(id))
13440if object ~= delete then
134520continue
1346N/Aend
134720local id_record = component_index[id]
134820local flags = id_record.flags
13498local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE)
135015if flags_delete_mask ~= 0 then
135115for i = #entities, 1, -1 do
13520local child = entities[i]
13538world_delete(world, child)
1354N/Aend
135512break
13566else
13570if not ids then
135812ids = {}
1359N/Aend
13600ids[id] = true
13610removal_queued = true
1362N/Aend
1363N/Aend
1364N/A
13650if not removal_queued then
136610continue
1367N/Aend
13680if not children then
136910children = {}
1370N/Aend
137110local n = #entities
13720table.move(entities, 1, n, count + 1, children)
13730count += n
1374N/Aend
1375N/A
137617if ids then
137719for _, child in children do
13780for id in ids do
13790world_remove(world, child, id)
1380N/Aend
1381N/Aend
1382N/Aend
1383N/A
13840for archetype_id in archetype_ids do
13850archetype_destroy(world, archetypes[archetype_id])
1386N/Aend
1387N/Aend
1388N/A
13890if idr_r then
13900local archetype_ids = idr_r.cache
13910local flags = idr_r.flags
13920if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
13930for archetype_id in archetype_ids do
13940local idr_r_archetype = archetypes[archetype_id]
13950local entities = idr_r_archetype.entities
13960local n = #entities
13970for i = n, 1, -1 do
13980world_delete(world, entities[i])
1399N/Aend
14000archetype_destroy(world, idr_r_archetype)
1401N/Aend
14020else
14030local children = {}
14040local count = 0
14050local ids = {}
14060for archetype_id in archetype_ids do
14070local idr_r_archetype = archetypes[archetype_id]
14080local entities = idr_r_archetype.entities
14090local tr = idr_r_archetype.records[rel]
14100local tr_count = idr_r_archetype.counts[rel]
14110local types = idr_r_archetype.types
14120for i = tr, tr_count - 1 do
14130ids[types[tr]] = true
1414N/Aend
14150local n = #entities
14160table.move(entities, 1, n, count + 1, children)
14170count += n
1418N/Aend
1419N/A
14200for _, child in children do
14210for id in ids do
14220world_remove(world, child, id)
1423N/Aend
1424N/Aend
1425N/A
14260for archetype_id in archetype_ids do
14270archetype_destroy(world, archetypes[archetype_id])
1428N/Aend
1429N/Aend
1430N/Aend
1431N/A
143265683local dense_array = entity_index.dense_array
143365683local index_of_deleted_entity = record.dense
14340local index_of_last_alive_entity = entity_index.alive_count
143565683entity_index.alive_count = index_of_last_alive_entity - 1
1436N/A
143765683local last_alive_entity = dense_array[index_of_last_alive_entity]
143865683local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: ecs_record_t
143965683r_swap.dense = index_of_deleted_entity
144065683record.archetype = nil :: any
14410record.row = nil :: any
144265683record.dense = index_of_last_alive_entity
1443N/A
14440dense_array[index_of_deleted_entity] = last_alive_entity
14450dense_array[index_of_last_alive_entity] = ECS_GENERATION_INC(entity)
1446N/Aend
1447N/A
14480local function world_contains(world: ecs_world_t, entity): boolean
14490return entity_index_is_alive(world.entity_index, entity)
1450N/Aend
1451N/A
14520local function NOOP() end
1453N/A
14540export type QueryInner = {
14550compatible_archetypes: { Archetype },
14560ids: { i53 },
14570filter_with: { i53 },
14580filter_without: { i53 },
14590next: () -> (number, ...any),
14600world: World,
14611}
1462N/A
14630local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any)
146426local world_query_iter_next
1465N/A
146626local compatible_archetypes = query.compatible_archetypes
146726local lastArchetype = 1
14684local archetype = compatible_archetypes[1]
14690if not archetype then
147022return NOOP :: () -> (number, ...any)
1471N/Aend
147222local columns = archetype.columns
147322local entities = archetype.entities
14740local i = #entities
147522local records = archetype.records
1476N/A
147722local ids = query.ids
147822local A, B, C, D, E, F, G, H, I = unpack(ids)
14790local a: Column, b: Column, c: Column, d: Column
148022local e: Column, f: Column, g: Column, h: Column
1481N/A
14825if not B then
14833a = columns[records[A]]
14843elseif not C then
14852a = columns[records[A]]
14860b = columns[records[B]]
14870elseif not D then
14880a = columns[records[A]]
14892b = columns[records[B]]
14901c = columns[records[C]]
14911elseif not E then
14921a = columns[records[A]]
14931b = columns[records[B]]
14941c = columns[records[C]]
14950d = columns[records[D]]
14960elseif not F then
14970a = columns[records[A]]
14980b = columns[records[B]]
14990c = columns[records[C]]
15001d = columns[records[D]]
15010e = columns[records[E]]
15020elseif not G then
15030a = columns[records[A]]
15040b = columns[records[B]]
15050c = columns[records[C]]
15060d = columns[records[D]]
15071e = columns[records[E]]
15080f = columns[records[F]]
15090elseif not H then
15100a = columns[records[A]]
15110b = columns[records[B]]
15120c = columns[records[C]]
15130d = columns[records[D]]
15140e = columns[records[E]]
15151f = columns[records[F]]
15160g = columns[records[G]]
15170elseif not I then
15180a = columns[records[A]]
15190b = columns[records[B]]
15200c = columns[records[C]]
15210d = columns[records[D]]
15220e = columns[records[E]]
15230f = columns[records[F]]
15240g = columns[records[G]]
15250h = columns[records[H]]
1526N/Aend
1527N/A
1528556if not B then
1529556function world_query_iter_next(): any
153026local entity = entities[i]
153126while entity == nil do
153226lastArchetype += 1
153318archetype = compatible_archetypes[lastArchetype]
15340if not archetype then
15350return nil
1536N/Aend
1537N/A
15388entities = archetype.entities
15390i = #entities
15400if i == 0 then
15418continue
1542N/Aend
15438entity = entities[i]
15448columns = archetype.columns
15450records = archetype.records
15460a = columns[records[A]]
1547N/Aend
1548N/A
15490local row = i
1550538i -= 1
1551N/A
15525return entity, a[row]
1553N/Aend
15547elseif not C then
15557function world_query_iter_next(): any
15563local entity = entities[i]
15573while entity == nil do
15583lastArchetype += 1
15593archetype = compatible_archetypes[lastArchetype]
15600if not archetype then
15610return nil
1562N/Aend
1563N/A
15640entities = archetype.entities
15650i = #entities
15660if i == 0 then
15670continue
1568N/Aend
15690entity = entities[i]
15700columns = archetype.columns
15710records = archetype.records
15720a = columns[records[A]]
15730b = columns[records[B]]
1574N/Aend
1575N/A
15760local row = i
15774i -= 1
1578N/A
15792return entity, a[row], b[row]
1580N/Aend
15810elseif not D then
15820function world_query_iter_next(): any
15830local entity = entities[i]
15840while entity == nil do
15850lastArchetype += 1
15860archetype = compatible_archetypes[lastArchetype]
15870if not archetype then
15880return nil
1589N/Aend
1590N/A
15910entities = archetype.entities
15920i = #entities
15930if i == 0 then
15940continue
1595N/Aend
15960entity = entities[i]
15970columns = archetype.columns
15980records = archetype.records
15990a = columns[records[A]]
16000b = columns[records[B]]
16010c = columns[records[C]]
1602N/Aend
1603N/A
16040local row = i
16050i -= 1
1606N/A
16072return entity, a[row], b[row], c[row]
1608N/Aend
16092elseif not E then
16102function world_query_iter_next(): any
16111local entity = entities[i]
16121while entity == nil do
16131lastArchetype += 1
16141archetype = compatible_archetypes[lastArchetype]
16150if not archetype then
16160return nil
1617N/Aend
1618N/A
16190entities = archetype.entities
16200i = #entities
16210if i == 0 then
16220continue
1623N/Aend
16240entity = entities[i]
16250columns = archetype.columns
16260records = archetype.records
16270a = columns[records[A]]
16280b = columns[records[B]]
16290c = columns[records[C]]
16300d = columns[records[D]]
1631N/Aend
1632N/A
16330local row = i
16341i -= 1
1635N/A
16361return entity, a[row], b[row], c[row], d[row]
1637N/Aend
16380elseif not F then
16390function world_query_iter_next(): any
16400local entity = entities[i]
16410while entity == nil do
16420lastArchetype += 1
16430archetype = compatible_archetypes[lastArchetype]
16440if not archetype then
16450return nil
1646N/Aend
1647N/A
16480entities = archetype.entities
16490i = #entities
16500if i == 0 then
16510continue
1652N/Aend
16530entity = entities[i]
16540columns = archetype.columns
16550records = archetype.records
16560a = columns[records[A]]
16570b = columns[records[B]]
16580c = columns[records[C]]
16590d = columns[records[D]]
16600e = columns[records[E]]
1661N/Aend
1662N/A
16630local row = i
16640i -= 1
1665N/A
16661return entity, a[row], b[row], c[row], d[row], e[row]
1667N/Aend
16680elseif not G then
16690function world_query_iter_next(): any
16700local entity = entities[i]
16710while entity == nil do
16720lastArchetype += 1
16730archetype = compatible_archetypes[lastArchetype]
16740if not archetype then
16750return nil
1676N/Aend
1677N/A
16780entities = archetype.entities
16790i = #entities
16800if i == 0 then
16810continue
1682N/Aend
16830entity = entities[i]
16840columns = archetype.columns
16850records = archetype.records
16860a = columns[records[A]]
16870b = columns[records[B]]
16880c = columns[records[C]]
16890d = columns[records[D]]
16900e = columns[records[E]]
16910f = columns[records[F]]
1692N/Aend
1693N/A
16940local row = i
16950i -= 1
1696N/A
16971return entity, a[row], b[row], c[row], d[row], e[row], f[row]
1698N/Aend
16990elseif not H then
17000function world_query_iter_next(): any
17010local entity = entities[i]
17020while entity == nil do
17030lastArchetype += 1
17040archetype = compatible_archetypes[lastArchetype]
17050if not archetype then
17060return nil
1707N/Aend
1708N/A
17090entities = archetype.entities
17100i = #entities
17110if i == 0 then
17120continue
1713N/Aend
17140entity = entities[i]
17150columns = archetype.columns
17160records = archetype.records
17170a = columns[records[A]]
17180b = columns[records[B]]
17190c = columns[records[C]]
17200d = columns[records[D]]
17210e = columns[records[E]]
17220f = columns[records[F]]
17230g = columns[records[G]]
1724N/Aend
1725N/A
17260local row = i
17270i -= 1
1728N/A
17291return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row]
1730N/Aend
17310elseif not I then
17320function world_query_iter_next(): any
17330local entity = entities[i]
17340while entity == nil do
17350lastArchetype += 1
17360archetype = compatible_archetypes[lastArchetype]
17370if not archetype then
17380return nil
1739N/Aend
1740N/A
17410entities = archetype.entities
17420i = #entities
17430if i == 0 then
17440continue
1745N/Aend
17460entity = entities[i]
17470columns = archetype.columns
17480records = archetype.records
17490a = columns[records[A]]
17500b = columns[records[B]]
17510c = columns[records[C]]
17520d = columns[records[D]]
17530e = columns[records[E]]
17540f = columns[records[F]]
17550g = columns[records[G]]
17560h = columns[records[H]]
1757N/Aend
1758N/A
17590local row = i
17600i -= 1
1761N/A
17620return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row]
1763N/Aend
17641else
17652local output = {}
17662function world_query_iter_next(): any
17671local entity = entities[i]
17681while entity == nil do
17691lastArchetype += 1
17701archetype = compatible_archetypes[lastArchetype]
17710if not archetype then
17720return nil
1773N/Aend
1774N/A
17750entities = archetype.entities
17760i = #entities
17770if i == 0 then
17780continue
1779N/Aend
17800entity = entities[i]
17810columns = archetype.columns
17820records = archetype.records
1783N/Aend
1784N/A
17850local row = i
17861i -= 1
1787N/A
17880for j, id in ids do
17890output[j] = columns[records[id]][row]
1790N/Aend
1791N/A
17920return entity, unpack(output)
1793N/Aend
1794N/Aend
1795N/A
17960query.next = world_query_iter_next
17970return world_query_iter_next
1798N/Aend
1799N/A
180020local function query_iter(query): () -> (number, ...any)
180119local query_next = query.next
18020if not query_next then
180320query_next = query_iter_init(query)
1804N/Aend
18050return query_next
1806N/Aend
1807N/A
18086local function query_without(query: ecs_query_data_t, ...: i53)
18096local without = { ... }
18106query.filter_without = without
18113local compatible_archetypes = query.compatible_archetypes
18123for i = #compatible_archetypes, 1, -1 do
18133local archetype = compatible_archetypes[i]
18140local records = archetype.records
18153local matches = true
1816N/A
18172for _, id in without do
18182if records[id] then
18190matches = false
18200break
1821N/Aend
1822N/Aend
1823N/A
18240if matches then
18250continue
1826N/Aend
1827N/A
18280local last = #compatible_archetypes
18290if last ~= i then
18302compatible_archetypes[i] = compatible_archetypes[last]
1831N/Aend
18320compatible_archetypes[last] = nil :: any
1833N/Aend
1834N/A
18350return query :: any
1836N/Aend
1837N/A
18381local function query_with(query: ecs_query_data_t, ...: i53)
18391local compatible_archetypes = query.compatible_archetypes
18400local with = { ... }
18411query.filter_with = with
1842N/A
18430for i = #compatible_archetypes, 1, -1 do
18440local archetype = compatible_archetypes[i]
18450local records = archetype.records
18460local matches = true
1847N/A
18480for _, id in with do
18490if not records[id] then
18500matches = false
18510break
1852N/Aend
1853N/Aend
1854N/A
18550if matches then
18560continue
1857N/Aend
1858N/A
18590local last = #compatible_archetypes
18600if last ~= i then
18610compatible_archetypes[i] = compatible_archetypes[last]
1862N/Aend
18630compatible_archetypes[last] = nil :: any
1864N/Aend
1865N/A
18660return query :: any
1867N/Aend
1868N/A
1869N/A-- Meant for directly iterating over archetypes to minimize
1870N/A-- function call overhead. Should not be used unless iterating over
1871N/A-- hundreds of thousands of entities in bulk.
18720local function query_archetypes(query)
18730return query.compatible_archetypes
1874N/Aend
1875N/A
18766local function query_cached(query: ecs_query_data_t)
18776local with = query.filter_with
18781local ids = query.ids
18790if with then
18805table.move(ids, 1, #ids, #with + 1, with)
18810else
18820query.filter_with = ids
1883N/Aend
1884N/A
18850local compatible_archetypes = query.compatible_archetypes
18866local lastArchetype = 1
1887N/A
18886local A, B, C, D, E, F, G, H, I = unpack(ids)
18890local a: Column, b: Column, c: Column, d: Column
18906local e: Column, f: Column, g: Column, h: Column
1891N/A
18926local world_query_iter_next
18936local columns: { Column }
18946local entities: { number }
18956local i: number
18966local archetype: ecs_archetype_t
18970local records: { number }
18986local archetypes = query.compatible_archetypes
1899N/A
19000local world = query.world :: { observable: ecs_observable_t }
1901N/A-- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively
1902N/A-- because the event will be emitted for all components of that Archetype.
19036local observable = world.observable :: ecs_observable_t
19046local on_create_action = observable[EcsOnArchetypeCreate]
19056if not on_create_action then
19060on_create_action = {}
19076observable[EcsOnArchetypeCreate] = on_create_action
1908N/Aend
19096local query_cache_on_create = on_create_action[A]
19106if not query_cache_on_create then
19110query_cache_on_create = {}
19120on_create_action[A] = query_cache_on_create
1913N/Aend
1914N/A
19156local on_delete_action = observable[EcsOnArchetypeDelete]
19166if not on_delete_action then
19170on_delete_action = {}
19186observable[EcsOnArchetypeDelete] = on_delete_action
1919N/Aend
19206local query_cache_on_delete = on_delete_action[A]
19216if not query_cache_on_delete then
19220query_cache_on_delete = {}
19230on_delete_action[A] = query_cache_on_delete
1924N/Aend
1925N/A
19260local function on_create_callback(archetype)
19270table.insert(archetypes, archetype)
1928N/Aend
1929N/A
19301local function on_delete_callback(archetype)
19311local i = table.find(archetypes, archetype) :: number
19321local n = #archetypes
19330archetypes[i] = archetypes[n]
19340archetypes[n] = nil
1935N/Aend
1936N/A
19370local observer_for_create = { query = query, callback = on_create_callback }
19386local observer_for_delete = { query = query, callback = on_delete_callback }
1939N/A
19400table.insert(query_cache_on_create, observer_for_create)
19416table.insert(query_cache_on_delete, observer_for_delete)
1942N/A
194311local function cached_query_iter()
194411lastArchetype = 1
19451archetype = compatible_archetypes[lastArchetype]
19460if not archetype then
194710return NOOP
1948N/Aend
194910entities = archetype.entities
195010i = #entities
195110records = archetype.records
19526columns = archetype.columns
19534if not B then
19544a = columns[records[A]]
19554elseif not C then
19560a = columns[records[A]]
19570b = columns[records[B]]
19580elseif not D then
19590a = columns[records[A]]
19600b = columns[records[B]]
19610c = columns[records[C]]
19620elseif not E then
19630a = columns[records[A]]
19640b = columns[records[B]]
19650c = columns[records[C]]
19660d = columns[records[D]]
19670elseif not F then
19680a = columns[records[A]]
19690b = columns[records[B]]
19700c = columns[records[C]]
19710d = columns[records[D]]
19720e = columns[records[E]]
19730elseif not G then
19740a = columns[records[A]]
19750b = columns[records[B]]
19760c = columns[records[C]]
19770d = columns[records[D]]
19780e = columns[records[E]]
19790f = columns[records[F]]
19800elseif not H then
19810a = columns[records[A]]
19820b = columns[records[B]]
19830c = columns[records[C]]
19840d = columns[records[D]]
19850e = columns[records[E]]
19860f = columns[records[F]]
19870g = columns[records[G]]
19880elseif not I then
19890a = columns[records[A]]
19900b = columns[records[B]]
19910c = columns[records[C]]
19920d = columns[records[D]]
19930e = columns[records[E]]
19940f = columns[records[F]]
19950g = columns[records[G]]
19960h = columns[records[H]]
1997N/Aend
1998N/A
19990return world_query_iter_next
2000N/Aend
2001N/A
200211if not B then
200311function world_query_iter_next(): any
20046local entity = entities[i]
20056while entity == nil do
20066lastArchetype += 1
20076archetype = compatible_archetypes[lastArchetype]
20080if not archetype then
20090return nil
2010N/Aend
2011N/A
20120entities = archetype.entities
20130i = #entities
20140if i == 0 then
20150continue
2016N/Aend
20170entity = entities[i]
20180columns = archetype.columns
20190records = archetype.records
20200a = columns[records[A]]
2021N/Aend
2022N/A
20230local row = i
20245i -= 1
2025N/A
20261return entity, a[row]
2027N/Aend
20288elseif not C then
20298function world_query_iter_next(): any
20304local entity = entities[i]
20314while entity == nil do
20324lastArchetype += 1
20334archetype = compatible_archetypes[lastArchetype]
20340if not archetype then
20350return nil
2036N/Aend
2037N/A
20380entities = archetype.entities
20390i = #entities
20400if i == 0 then
20410continue
2042N/Aend
20430entity = entities[i]
20440columns = archetype.columns
20450records = archetype.records
20460a = columns[records[A]]
20470b = columns[records[B]]
2048N/Aend
2049N/A
20500local row = i
20514i -= 1
2052N/A
20530return entity, a[row], b[row]
2054N/Aend
20550elseif not D then
20560function world_query_iter_next(): any
20570local entity = entities[i]
20580while entity == nil do
20590lastArchetype += 1
20600archetype = compatible_archetypes[lastArchetype]
20610if not archetype then
20620return nil
2063N/Aend
2064N/A
20650entities = archetype.entities
20660i = #entities
20670if i == 0 then
20680continue
2069N/Aend
20700entity = entities[i]
20710columns = archetype.columns
20720records = archetype.records
20730a = columns[records[A]]
20740b = columns[records[B]]
20750c = columns[records[C]]
2076N/Aend
2077N/A
20780local row = i
20790i -= 1
2080N/A
20810return entity, a[row], b[row], c[row]
2082N/Aend
20830elseif not E then
20840function world_query_iter_next(): any
20850local entity = entities[i]
20860while entity == nil do
20870lastArchetype += 1
20880archetype = compatible_archetypes[lastArchetype]
20890if not archetype then
20900return nil
2091N/Aend
2092N/A
20930entities = archetype.entities
20940i = #entities
20950if i == 0 then
20960continue
2097N/Aend
20980entity = entities[i]
20990columns = archetype.columns
21000records = archetype.records
21010a = columns[records[A]]
21020b = columns[records[B]]
21030c = columns[records[C]]
21040d = columns[records[D]]
2105N/Aend
2106N/A
21070local row = i
21080i -= 1
2109N/A
21100return entity, a[row], b[row], c[row], d[row]
2111N/Aend
21120elseif not F then
21130function world_query_iter_next(): any
21140local entity = entities[i]
21150while entity == nil do
21160lastArchetype += 1
21170archetype = compatible_archetypes[lastArchetype]
21180if not archetype then
21190return nil
2120N/Aend
2121N/A
21220entities = archetype.entities
21230i = #entities
21240if i == 0 then
21250continue
2126N/Aend
21270entity = entities[i]
21280columns = archetype.columns
21290records = archetype.records
21300a = columns[records[A]]
21310b = columns[records[B]]
21320c = columns[records[C]]
21330d = columns[records[D]]
21340e = columns[records[E]]
2135N/Aend
2136N/A
21370local row = i
21380i -= 1
2139N/A
21400return entity, a[row], b[row], c[row], d[row], e[row]
2141N/Aend
21420elseif not G then
21430function world_query_iter_next(): any
21440local entity = entities[i]
21450while entity == nil do
21460lastArchetype += 1
21470archetype = compatible_archetypes[lastArchetype]
21480if not archetype then
21490return nil
2150N/Aend
2151N/A
21520entities = archetype.entities
21530i = #entities
21540if i == 0 then
21550continue
2156N/Aend
21570entity = entities[i]
21580columns = archetype.columns
21590records = archetype.records
21600a = columns[records[A]]
21610b = columns[records[B]]
21620c = columns[records[C]]
21630d = columns[records[D]]
21640e = columns[records[E]]
21650f = columns[records[F]]
2166N/Aend
2167N/A
21680local row = i
21690i -= 1
2170N/A
21710return entity, a[row], b[row], c[row], d[row], e[row], f[row]
2172N/Aend
21730elseif not H then
21740function world_query_iter_next(): any
21750local entity = entities[i]
21760while entity == nil do
21770lastArchetype += 1
21780archetype = compatible_archetypes[lastArchetype]
21790if not archetype then
21800return nil
2181N/Aend
2182N/A
21830entities = archetype.entities
21840i = #entities
21850if i == 0 then
21860continue
2187N/Aend
21880entity = entities[i]
21890columns = archetype.columns
21900records = archetype.records
21910a = columns[records[A]]
21920b = columns[records[B]]
21930c = columns[records[C]]
21940d = columns[records[D]]
21950e = columns[records[E]]
21960f = columns[records[F]]
21970g = columns[records[G]]
2198N/Aend
2199N/A
22000local row = i
22010i -= 1
2202N/A
22030return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row]
2204N/Aend
22050elseif not I then
22060function world_query_iter_next(): any
22070local entity = entities[i]
22080while entity == nil do
22090lastArchetype += 1
22100archetype = compatible_archetypes[lastArchetype]
22110if not archetype then
22120return nil
2213N/Aend
2214N/A
22150entities = archetype.entities
22160i = #entities
22170if i == 0 then
22180continue
2219N/Aend
22200entity = entities[i]
22210columns = archetype.columns
22220records = archetype.records
22230a = columns[records[A]]
22240b = columns[records[B]]
22250c = columns[records[C]]
22260d = columns[records[D]]
22270e = columns[records[E]]
22280f = columns[records[F]]
22290g = columns[records[G]]
22300h = columns[records[H]]
2231N/Aend
2232N/A
22330local row = i
22340i -= 1
2235N/A
22360return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row]
2237N/Aend
22380else
22390local queryOutput = {}
22400function world_query_iter_next(): any
22410local entity = entities[i]
22420while entity == nil do
22430lastArchetype += 1
22440archetype = compatible_archetypes[lastArchetype]
22450if not archetype then
22460return nil
2247N/Aend
2248N/A
22490entities = archetype.entities
22500i = #entities
22510if i == 0 then
22520continue
2253N/Aend
22540entity = entities[i]
22550columns = archetype.columns
22560records = archetype.records
2257N/Aend
2258N/A
22590local row = i
22600i -= 1
2261N/A
22620if not F then
22630return entity, a[row], b[row], c[row], d[row], e[row]
22640elseif not G then
22650return entity, a[row], b[row], c[row], d[row], e[row], f[row]
22660elseif not H then
22670return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row]
22680elseif not I then
22690return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row]
2270N/Aend
2271N/A
22720for j, id in ids do
22730queryOutput[j] = columns[records[id]][row]
2274N/Aend
2275N/A
22760return entity, unpack(queryOutput)
2277N/Aend
2278N/Aend
2279N/A
22806local cached_query = query :: any
22816cached_query.archetypes = query_archetypes
22826cached_query.__iter = cached_query_iter
22836cached_query.iter = cached_query_iter
22840setmetatable(cached_query, cached_query)
22850return cached_query
2286N/Aend
2287N/A
22881local Query = {}
22891Query.__index = Query
22901Query.__iter = query_iter
22911Query.iter = query_iter_init
22921Query.without = query_without
22931Query.with = query_with
22940Query.archetypes = query_archetypes
22951Query.cached = query_cached
2296N/A
229733local function world_query(world: ecs_world_t, ...)
22980local compatible_archetypes = {}
229933local length = 0
2300N/A
230133local ids = { ... }
2302N/A
230333local archetypes = world.archetypes
2304N/A
23050local idr: ecs_id_record_t?
230633local component_index = world.component_index
2307N/A
230833local q = setmetatable({
230933ids = ids,
231033compatible_archetypes = compatible_archetypes,
23110world = world,
231233}, Query)
2313N/A
231447for _, id in ids do
23157local map = component_index[id]
23160if not map then
23170return q
2318N/Aend
2319N/A
23200if idr == nil or map.size < idr.size then
23210idr = map
2322N/Aend
2323N/Aend
2324N/A
23250if not idr then
23260return q
2327N/Aend
2328N/A
232948for archetype_id in idr.cache do
23300local compatibleArchetype = archetypes[archetype_id]
23310if #compatibleArchetype.entities == 0 then
233234continue
2333N/Aend
233434local records = compatibleArchetype.records
2335N/A
233634local skip = false
2337N/A
233847for i, id in ids do
23390local tr = records[id]
23400if not tr then
23410skip = true
23420break
2343N/Aend
2344N/Aend
2345N/A
23460if skip then
23470continue
2348N/Aend
2349N/A
23500length += 1
23510compatible_archetypes[length] = compatibleArchetype
2352N/Aend
2353N/A
23540return q
2355N/Aend
2356N/A
23574local function world_each(world: ecs_world_t, id: i53): () -> ()
23580local idr = world.component_index[id]
23590if not idr then
23600return NOOP
2361N/Aend
2362N/A
23634local idr_cache = idr.cache
23644local archetypes = world.archetypes
23654local archetype_id = next(idr_cache, nil) :: number
23660local archetype = archetypes[archetype_id]
23670if not archetype then
23680return NOOP
2369N/Aend
2370N/A
23710local entities = archetype.entities
23724local row = #entities
2373N/A
237412return function(): any
237511local entity = entities[row]
237611while not entity do
23774archetype_id = next(idr_cache, archetype_id) :: number
23780if not archetype_id then
23797return
2380N/Aend
23817archetype = archetypes[archetype_id]
23827entities = archetype.entities
23830row = #entities
23848entity = entities[row]
2385N/Aend
23860row -= 1
23870return entity
2388N/Aend
2389N/Aend
2390N/A
23910local function world_children(world: ecs_world_t, parent: i53)
23920return world_each(world, ECS_PAIR(EcsChildOf, parent))
2393N/Aend
2394N/A
23950export type Record = {
23960archetype: Archetype,
23970row: number,
23980dense: i24,
23990}
24000export type ComponentRecord = {
24010cache: { [Id]: number },
24020counts: { [Id]: number },
24030flags: number,
24040size: number,
24050hooks: {
24060on_add: ((entity: Entity) -> ())?,
24070on_set: ((entity: Entity, data: any) -> ())?,
24080on_remove: ((entity: Entity) -> ())?,
24090},
24100}
24110export type ComponentIndex = Map
24120export type Archetypes = { [Id]: Archetype }
2413N/A
24140export type EntityIndex = {
24150dense_array: Map,
24160sparse_array: Map,
24170alive_count: number,
24180max_id: number,
24191}
2420N/A
24210local World = {}
24221World.__index = World
2423N/A
24241World.entity = world_entity
24251World.query = world_query
24261World.remove = world_remove
24271World.clear = world_clear
24281World.delete = world_delete
24291World.component = world_component
24301World.add = world_add
24311World.set = world_set
24321World.get = world_get
24331World.has = world_has
24341World.target = world_target
24351World.parent = world_parent
24361World.contains = world_contains
24371World.cleanup = world_cleanup
24380World.each = world_each
24391World.children = world_children
2440N/A
244173local function world_new()
244273local entity_index = {
244373dense_array = {},
244473sparse_array = {},
24450alive_count = 0,
244673max_id = 0,
244773} :: ecs_entity_index_t
244873local self = setmetatable({
244973archetype_index = {} :: { [string]: Archetype },
245073archetypes = {} :: Archetypes,
245173component_index = {} :: ComponentIndex,
24520entity_index = entity_index,
245373ROOT_ARCHETYPE = (nil :: any) :: Archetype,
2454N/A
24550max_archetype_id = 0,
245673max_component_id = 0,
2457N/A
24580observable = {} :: Observable,
245973}, World) :: any
2460N/A
246173self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
2462N/A
246318688for i = 1, HI_COMPONENT_ID do
24640local e = entity_index_new_id(entity_index)
24650world_add(self, e, EcsComponent)
2466N/Aend
2467N/A
24681022for i = HI_COMPONENT_ID + 1, EcsRest do
2469N/A-- Initialize built-in components
24700entity_index_new_id(entity_index)
2471N/Aend
2472N/A
247373world_add(self, EcsName, EcsComponent)
247473world_add(self, EcsOnSet, EcsComponent)
247573world_add(self, EcsOnAdd, EcsComponent)
247673world_add(self, EcsOnRemove, EcsComponent)
24770world_add(self, EcsWildcard, EcsComponent)
247873world_add(self, EcsRest, EcsComponent)
2479N/A
248073world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd")
248173world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove")
248273world_set(self, EcsOnSet, EcsName, "jecs.OnSet")
248373world_set(self, EcsWildcard, EcsName, "jecs.Wildcard")
248473world_set(self, EcsChildOf, EcsName, "jecs.ChildOf")
248573world_set(self, EcsComponent, EcsName, "jecs.Component")
248673world_set(self, EcsOnDelete, EcsName, "jecs.OnDelete")
248773world_set(self, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget")
248873world_set(self, EcsDelete, EcsName, "jecs.Delete")
248973world_set(self, EcsRemove, EcsName, "jecs.Remove")
24900world_set(self, EcsName, EcsName, "jecs.Name")
249173world_set(self, EcsRest, EcsRest, "jecs.Rest")
2492N/A
249373world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete))
2494N/A
24950return self
2496N/Aend
2497N/A
24980World.new = world_new
2499N/A
25000export type Entity = { __T: T }
25010export type Id = { __T: T }
25020export type Pair = Id

25030type ecs_id_t = Id | Pair | Pair<"Tag", T>
25040export type Item = (self: Query) -> (Entity, T...)
25050export type Iter = (query: Query) -> () -> (Entity, T...)
2506N/A
25070export type Query = typeof(setmetatable({}, {
25080__iter = (nil :: any) :: Iter,
25090})) & {
25100iter: Iter,
25110with: (self: Query, ...Id) -> Query,
25120without: (self: Query, ...Id) -> Query,
25130archetypes: (self: Query) -> { Archetype },
25140cached: (self: Query) -> Query,
25150}
2516N/A
25170export type Observer = {
25180callback: (archetype: Archetype) -> (),
25190query: QueryInner,
25200}
2521N/A
25220export type Observable = {
25230[Id]: {
25240[Id]: {
25250{ Observer }
25260}
25270}
25280}
2529N/A
25300export type World = {
25310archetype_index: { [string]: Archetype },
25320archetypes: Archetypes,
25330component_index: ComponentIndex,
25340entity_index: EntityIndex,
25350ROOT_ARCHETYPE: Archetype,
2536N/A
25370max_component_id: number,
25380max_archetype_id: number,
2539N/A
25400observable: any,
2541N/A
2542N/A--- Creates a new entity
25430entity: (self: World, id: Entity?) -> Entity,
2544N/A--- Creates a new entity located in the first 256 ids.
2545N/A--- These should be used for static components for fast access.
25460component: (self: World) -> Entity,
2547N/A--- Gets the target of an relationship. For example, when a user calls
2548N/A--- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity.
25490target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?,
2550N/A--- Deletes an entity and all it's related components and relationships.
25510delete: (self: World, id: Entity) -> (),
2552N/A
2553N/A--- Adds a component to the entity with no value
25540add: (self: World, id: Entity, component: Id) -> (),
2555N/A--- Assigns a value to a component on the given entity
25560set: (self: World, id: Entity, component: Id, data: T) -> (),
2557N/A
25580cleanup: (self: World) -> (),
2559N/A-- Clears an entity from the world
25600clear: (self: World, id: Entity) -> (),
2561N/A--- Removes a component from the given entity
25620remove: (self: World, id: Entity, component: Id) -> (),
2563N/A--- Retrieves the value of up to 4 components. These values may be nil.
25640get: ((self: World, id: Entity, Id) -> A?)
25650& ((self: World, id: Entity, Id, Id) -> (A?, B?))
25660& ((self: World, id: Entity, Id, Id, Id) -> (A?, B?, C?))
25670& (self: World, id: Entity, Id, Id, Id, Id) -> (A?, B?, C?, D?),
2568N/A
2569N/A--- Returns whether the entity has the ID.
25700has: (self: World, entity: Entity, ...Id) -> boolean,
2571N/A
2572N/A--- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil.
25730parent:(self: World, entity: Entity) -> Entity,
2574N/A
2575N/A--- Checks if the world contains the given entity
25760contains:(self: World, entity: Entity) -> boolean,
2577N/A
25780each: (self: World, id: Id) -> () -> Entity,
2579N/A
25800children: (self: World, id: Id) -> () -> Entity,
2581N/A
2582N/A--- Searches the world for entities that match a given query
25830query: ((World, Id) -> Query)
25840& ((World, Id, Id) -> Query)
25850& ((World, Id, Id, Id) -> Query)
25860& ((World, Id, Id, Id, Id) -> Query)
25870& ((World, Id, Id, Id, Id, Id) -> Query)
25880& ((World, Id, Id, Id, Id, Id, Id) -> Query)
25890& ((World, Id, Id, Id, Id, Id, Id, Id) -> Query)
25900& ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query)
25910}
2592N/A-- type function ecs_id_t(entity)
2593N/A-- local ty = entity:components()[2]
2594N/A-- local __T = ty:readproperty(types.singleton("__T"))
2595N/A-- if not __T then
2596N/A-- return ty:readproperty(types.singleton("__jecs_pair_value"))
2597N/A-- end
2598N/A-- return __T
2599N/A-- end
2600N/A
2601N/A-- type function ecs_pair_t(first, second)
2602N/A-- if ecs_id_t(first):is("nil") then
2603N/A-- return second
2604N/A-- else
2605N/A-- return first
2606N/A-- end
2607N/A-- end
2608N/A
26091return {
26100World = World :: { new: () -> World },
26111world = World.new :: () -> World,
2612N/A
26131OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>,
26141OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>,
26151OnSet = EcsOnSet :: Entity<(entity: Entity, data: any) -> ()>,
26161ChildOf = EcsChildOf :: Entity,
26171Component = EcsComponent :: Entity,
26181Wildcard = EcsWildcard :: Entity,
26191w = EcsWildcard :: Entity,
26201OnDelete = EcsOnDelete :: Entity,
26211OnDeleteTarget = EcsOnDeleteTarget :: Entity,
26221Delete = EcsDelete :: Entity,
26231Remove = EcsRemove :: Entity,
26240Name = EcsName :: Entity,
26251Rest = EcsRest :: Entity,
2626N/A
26270pair = (ECS_PAIR :: any) :: (first: Id

, second: Id) -> Pair,

2628N/A
2629N/A-- Inwards facing API for testing
26301ECS_ID = ECS_ENTITY_T_LO,
26311ECS_GENERATION_INC = ECS_GENERATION_INC,
26320ECS_GENERATION = ECS_GENERATION,
26331ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD,
2634N/A
26351ECS_ID_DELETE = ECS_ID_DELETE,
2636N/A
26371IS_PAIR = ECS_IS_PAIR,
26381pair_first = ecs_pair_first,
26390pair_second = ecs_pair_second,
26401entity_index_get_alive = entity_index_get_alive,
2641N/A
26421archetype_append_to_records = archetype_append_to_records,
26431id_record_ensure = id_record_ensure,
26441archetype_create = archetype_create,
26451archetype_ensure = archetype_ensure,
26461find_insert = find_insert,
26471find_archetype_with = find_archetype_with,
26481find_archetype_without = find_archetype_without,
26491archetype_init_edge = archetype_init_edge,
26501archetype_ensure_edge = archetype_ensure_edge,
26511init_edge_for_add = init_edge_for_add,
26521init_edge_for_remove = init_edge_for_remove,
26531create_edge_for_add = create_edge_for_add,
26541create_edge_for_remove = create_edge_for_remove,
26550archetype_traverse_add = archetype_traverse_add,
26561archetype_traverse_remove = archetype_traverse_remove,
2657N/A
26581entity_move = entity_move,
2659N/A
26601entity_index_try_get = entity_index_try_get,
26611entity_index_try_get_any = entity_index_try_get_any,
26621entity_index_try_get_fast = entity_index_try_get_fast,
26630entity_index_is_alive = entity_index_is_alive,
26641entity_index_new_id = entity_index_new_id,
2665N/A
26661query_iter = query_iter,
26671query_iter_init = query_iter_init,
26681query_with = query_with,
26691query_without = query_without,
26700query_archetypes = query_archetypes,
26711query_match = query_match,
2672N/A
26730find_observers = find_observers,
26740}
\ No newline at end of file diff --git a/coverage/lifetime_tracker.luau.html b/coverage/lifetime_tracker.luau.html new file mode 100644 index 0000000..2c66b17 --- /dev/null +++ b/coverage/lifetime_tracker.luau.html @@ -0,0 +1,254 @@ + + + + +

lifetime_tracker.luau Coverage

+

Total Execution Hits: 1

+

Function Coverage Overview: 9.09%

+ +
+

Function Coverage:

+ + + + + + + + + + + +
FunctionHits
1
print_centered_entity:120
name:260
pad:300
lifetime_tracker_add:360
:480
:620
:890
:1350
:1640
:1750
+

Source Code:

+ + + + + +> + +> + + +> + + + +> + +> + + +> + +> + +> +> + + +> + + + + +> +> +> + + + + +> + +> + +> + +> + + + + +> + + + + + + +> + + +> + + + + + +> + + + + + + + +> + +> +> + + + + + +> +> + +> + + + + + +> + + + +> +> + + + + +> + + + +> +> + + + + + + + +> + +> +> +> + +> +> + + + + + +> + +> +> +> +> + + +> +> +> + + + + + + +> + + + + + + + + +> +> + +> + + +> + + + + + + + + + +> +> +> + + + +> + + + + + + + +> + + + +> + + + + + + + +> + + + + + + +> +> +> +> +> +> + + +> + +> +> + +
LineHitsCode
11local jecs = require("@jecs")
21local ECS_GENERATION = jecs.ECS_GENERATION
31local ECS_ID = jecs.ECS_ID
41local __ = jecs.Wildcard
51local pair = jecs.pair
6N/A
71local prettify = require("@tools/entity_visualiser").prettify
8N/A
91local pe = prettify
101local ansi = require("@tools/ansi")
11N/A
121function print_centered_entity(entity, width: number)
130local entity_str = tostring(entity)
140local entity_length = #entity_str
15N/A
160local padding_total = width - 2 - entity_length
17N/A
180local padding_left = math.floor(padding_total / 2)
190local padding_right = padding_total - padding_left
20N/A
210local centered_str = string.rep(" ", padding_left) .. entity_str .. string.rep(" ", padding_right)
22N/A
230print("|" .. centered_str .. "|")
24N/Aend
25N/A
261local function name(world, e)
270return world:get(world, e, jecs.Name) or pe(e)
28N/Aend
291local padding_enabled = false
301local function pad()
310if padding_enabled then
320print("")
33N/Aend
34N/Aend
35N/A
361local function lifetime_tracker_add(world: jecs.World, opt)
370local entity_index = world.entity_index
380local dense_array = entity_index.dense_array
390local component_index = world.component_index
40N/A
410local ENTITY_RANGE = (jecs.Rest :: any) + 1
42N/A
430local w = setmetatable({}, { __index = world })
44N/A
450padding_enabled = opt.padding_enabled
46N/A
470local world_entity = world.entity
480w.entity = function(self, entity)
490if entity then
500return world_entity(world, entity)
51N/Aend
520local will_recycle = entity_index.max_id ~= entity_index.alive_count
530local e = world_entity(world)
540if will_recycle then
550print(`*recycled {pe(e)}`)
560else
570print(`*created {pe(e)}`)
58N/Aend
590pad()
600return e
61N/Aend
620w.print_entity_index = function(self)
630local max_id = entity_index.max_id
640local alive_count = entity_index.alive_count
650local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {})
660local dead = table.move(dense_array, alive_count + 1, max_id, 1, {})
67N/A
680local sep = "|--------|"
690if #alive > 0 then
700print("|-alive--|")
710for i = 1, #alive do
720local e = pe(alive[i])
730print_centered_entity(e, 32)
740print(sep)
75N/Aend
760print("\n")
77N/Aend
78N/A
790if #dead > 0 then
800print("|--dead--|")
810for i = 1, #dead do
820print_centered_entity(pe(dead[i]), 32)
830print(sep)
84N/Aend
85N/Aend
860pad()
87N/Aend
880local timelines = {}
890w.print_snapshot = function(self)
900local timeline = #timelines + 1
910local entity_column_width = 10
920local status_column_width = 8
93N/A
940local header = string.format("| %-" .. entity_column_width .. "s |", "Entity")
950for i = 1, timeline do
960header = header .. string.format(" %-" .. status_column_width .. "s |", string.format("T%d", i))
97N/Aend
98N/A
990local max_id = entity_index.max_id
1000local alive_count = entity_index.alive_count
1010local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {})
1020local dead = table.move(dense_array, alive_count + 1, max_id, 1, {})
103N/A
1040local data = {}
1050print("-------------------------------------------------------------------")
1060print(header)
107N/A
108N/A-- Store the snapshot data for this timeline
1090for i = ENTITY_RANGE, max_id do
1100if dense_array[i] then
1110local entity = dense_array[i]
1120local id = ECS_ID(entity)
1130local status = "alive"
1140if not world:contains(entity) then
1150status = "dead"
116N/Aend
1170data[id] = status
118N/Aend
119N/Aend
120N/A
1210table.insert(timelines, data)
122N/A
123N/A-- Create a table to hold entity data for sorting
1240local entities = {}
1250for i = ENTITY_RANGE, max_id do
1260if dense_array[i] then
1270local entity = dense_array[i]
1280local id = ECS_ID(entity)
129N/A-- Push entity and id into the new `entities` table
1300table.insert(entities, { entity = entity, id = id })
131N/Aend
132N/Aend
133N/A
134N/A-- Sort the entities by ECS_ID
1350table.sort(entities, function(a, b)
1360return a.id < b.id
137N/Aend)
138N/A
139N/A-- Print the sorted rows
1400for _, entity_data in ipairs(entities) do
1410local entity = entity_data.entity
1420local id = entity_data.id
1430local status = "alive"
1440if id > alive_count then
1450status = "dead"
146N/Aend
1470local row = string.format("| %-" .. entity_column_width .. "s |", pe(entity))
1480for j = 1, timeline do
1490local timeline_data = timelines[j]
1500local entity_data = timeline_data[id]
1510if entity_data then
1520row = row .. string.format(" %-" .. status_column_width .. "s |", entity_data)
1530else
1540row = row .. string.format(" %-" .. status_column_width .. "s |", "-")
155N/Aend
156N/Aend
1570print(row)
158N/Aend
1590print("-------------------------------------------------------------------")
1600pad()
161N/Aend
1620local world_add = world.add
1630local relations = {}
1640w.add = function(self, entity: any, component: any)
1650world_add(world, entity, component)
1660if jecs.IS_PAIR(component) then
1670local relation = jecs.pair_first(world, component)
1680local target = jecs.pair_second(world, component)
1690print(`*added ({pe(relation)}, {pe(target)}) to {pe(entity)}`)
1700pad()
171N/Aend
172N/Aend
173N/A
1740local world_delete = world.delete
1750w.delete = function(self, e)
1760world_delete(world, e)
177N/A
1780local idr_t = component_index[pair(__, e)]
1790if idr_t then
1800for archetype_id in idr_t.cache do
1810local archetype = world.archetypes[archetype_id]
1820for _, id in archetype.types do
1830if not jecs.IS_PAIR(id) then
1840continue
185N/Aend
1860local object = jecs.pair_second(world, id)
1870if object ~= e then
1880continue
189N/Aend
1900local id_record = component_index[id]
1910local flags = id_record.flags
1920local flags_delete_mask: number = bit32.band(flags, jecs.ECS_ID_DELETE)
1930if flags_delete_mask ~= 0 then
1940for _, entity in archetype.entities do
1950print(`*deleted dependant {pe(entity)} of {pe(e)}`)
1960pad()
197N/Aend
1980break
1990else
2000for _, entity in archetype.entities do
2010print(
2020`*removed dependency ({pe(jecs.pair_first(world, id))}, {pe(object)}) from {pe(entity)}`
2030)
204N/Aend
205N/Aend
206N/Aend
207N/Aend
208N/Aend
209N/A
2100print(`*deleted {pe(e)}`)
2110pad()
212N/Aend
2130return w
214N/Aend
215N/A
2161return lifetime_tracker_add
\ No newline at end of file diff --git a/coverage/testkit.luau.html b/coverage/testkit.luau.html new file mode 100644 index 0000000..bb57907 --- /dev/null +++ b/coverage/testkit.luau.html @@ -0,0 +1,617 @@ + + + + +

testkit.luau Coverage

+

Total Execution Hits: 1826

+

Function Coverage Overview: 64.52%

+ +
+

Function Coverage:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionHits
1
white_underline:110
white:1524
green:1977
red:23146
yellow:2776
red_highlight:310
green_highlight:350
gray:3984
orange:4373
convert_units:486
output_test_result:13124
CASE:16973
CHECK_EXPECT_ERR:1839
CHECK:2011195
TEST:22424
FOCUS:2370
FINISH:2481
:2640
SKIP:3141
START:3301
BENCH:3423
:3540
round:3726
print2:3960
tos:4010
shallow_eq:4800
deep_eq:5000
test:5331
benchmark:5451
disable_formatting:5490
+

Source Code:

+> +> +> +> +> +> +> + +> + + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> + +> + + + +> + + + + + + + + + + + +> + + + + + + + + + + + +> + +> + + + +> +> + + + +> +> + + + + + + +> +> + +> +> + +> +> +> +> +> + + + + + + + + + + + +> + + + + + + +> + +> + + + + +> + + + + + +> +> + + +> +> +> + +> + + + + + + + + +> + + + +> + +> +> + + + + + +> +> +> + + + +> + + + + + +> + + +> +> + + + + + + +> + + + + + + +> + +> +> +> + + +> + +> + + + +> +> + +> + + + + +> + +> +> + +> +> + +> + + + + + + + +> + +> +> + + +> + + + + + +> +> +> + + + + + + + +> + + + +> + + + + + + +> + +> + + +> + +> + + + +> + +> +> + + + + + + +> + + + +> + + + +> +> +> + +> +> + + + +> +> + +> + +> + + +> +> + + +> +> +> +> +> +> + + + + + +> + +> + + + + + +> + + + + +> +> + + + +> + + + +> + + + +> + + +> +> + + +> + + + + + + +> + + + +> + + + +> +> + + + + + + + + + + +> +> + +> +> +> +> +> +> + + + +> +> + + + +> + + + + + + + + + + + + + + + + +> + + + + + + + +> +> + + + + +> + + + + +> + + + + + + + + +> +> + + +> + +> + +> + + + +> +> + + + + +> +> +> + + + + +> +> +> +> +> +> + + + +> +> + + + +> +> +> + + + +> +> +> + +> +> + + + +> +> + + + + +> + + +> +> +> + + + + +> + + +> +> +> + +> +> +> +> +> +> + + + + + + + + + + + +> +> + + +> +> + + +> +> + +> + + +> + + +
LineHitsCode
1N/A--------------------------------------------------------------------------------
2N/A-- testkit.luau
3N/A-- v0.7.3
4N/A-- MIT License
5N/A-- Copyright (c) 2022 centau
6N/A--------------------------------------------------------------------------------
7N/A
81local disable_ansi = false
9N/A
101local color = {
111white_underline = function(s: string): string
120return if disable_ansi then s else `\27[1;4m{s}\27[0m`
13N/Aend,
14N/A
151white = function(s: string): string
1624return if disable_ansi then s else `\27[37;1m{s}\27[0m`
17N/Aend,
18N/A
191green = function(s: string): string
2077return if disable_ansi then s else `\27[32;1m{s}\27[0m`
21N/Aend,
22N/A
231red = function(s: string): string
24146return if disable_ansi then s else `\27[31;1m{s}\27[0m`
25N/Aend,
26N/A
271yellow = function(s: string): string
2876return if disable_ansi then s else `\27[33;1m{s}\27[0m`
29N/Aend,
30N/A
311red_highlight = function(s: string): string
320return if disable_ansi then s else `\27[41;1;30m{s}\27[0m`
33N/Aend,
34N/A
351green_highlight = function(s: string): string
360return if disable_ansi then s else `\27[42;1;30m{s}\27[0m`
37N/Aend,
38N/A
391gray = function(s: string): string
4084return if disable_ansi then s else `\27[38;1m{s}\27[0m`
41N/Aend,
42N/A
431orange = function(s: string): string
4473return if disable_ansi then s else `\27[38;5;208m{s}\27[0m`
45N/Aend,
460}
47N/A
481local function convert_units(unit: string, value: number): (number, string)
496local sign = math.sign(value)
506value = math.abs(value)
51N/A
526local prefix_colors = {
536[4] = color.red,
546[3] = color.red,
556[2] = color.yellow,
566[1] = color.yellow,
576[0] = color.green,
586[-1] = color.red,
596[-2] = color.yellow,
606[-3] = color.green,
616[-4] = color.red,
620}
63N/A
646local prefixes = {
656[4] = "T",
666[3] = "G",
676[2] = "M",
686[1] = "k",
696[0] = " ",
706[-1] = "m",
716[-2] = "u",
726[-3] = "n",
736[-4] = "p",
740}
75N/A
766local order = 0
77N/A
787while value >= 1000 do
791order += 1
801value /= 1000
81N/Aend
82N/A
8311while value ~= 0 and value < 1 do
847order -= 1
857value *= 1000
86N/Aend
87N/A
886if value >= 100 then
891value = math.floor(value)
905elseif value >= 10 then
912value = math.floor(value * 1e1) / 1e1
923elseif value >= 1 then
931value = math.floor(value * 1e2) / 1e2
94N/Aend
95N/A
966return value * sign, prefix_colors[order](prefixes[order] .. unit)
97N/Aend
98N/A
991local WALL = color.gray("│")
100N/A
101N/A--------------------------------------------------------------------------------
102N/A-- Testing
103N/A--------------------------------------------------------------------------------
104N/A
1050type Test = {
1060name: string,
1070case: Case?,
1080cases: { Case },
1090duration: number,
1100error: {
1110message: string,
1120trace: string,
1130}?,
1140focus: boolean,
1150}
116N/A
1170type Case = {
1180name: string,
1190result: number,
1200line: number?,
1210focus: boolean,
1220}
123N/A
1241local PASS, FAIL, NONE, ERROR, SKIPPED = 1, 2, 3, 4, 5
125N/A
1261local check_for_focused = false
1271local skip = false
1281local test: Test?
1291local tests: { Test } = {}
130N/A
1311local function output_test_result(test: Test)
13224if check_for_focused then
1330local any_focused = test.focus
1340for _, case in test.cases do
1350any_focused = any_focused or case.focus
136N/Aend
137N/A
1380if not any_focused then
1390return
140N/Aend
141N/Aend
142N/A
14324print(color.white(test.name))
144N/A
14524for _, case in test.cases do
14673local status = ({
14773[PASS] = color.green("PASS"),
14873[FAIL] = color.red("FAIL"),
14973[NONE] = color.orange("NONE"),
15073[ERROR] = color.red("FAIL"),
15173[SKIPPED] = color.yellow("SKIP"),
15273})[case.result]
153N/A
15473local line = case.result == FAIL and color.red(`{case.line}:`) or ""
15573if check_for_focused and case.focus == false and test.focus == false then
1560continue
157N/Aend
15873print(`{status}{WALL} {line}{color.gray(case.name)}`)
159N/Aend
160N/A
16124if test.error then
1620print(color.gray("error: ") .. color.red(test.error.message))
1630print(color.gray("trace: ") .. color.red(test.error.trace))
1640else
16524print()
166N/Aend
167N/Aend
168N/A
1691local function CASE(name: string)
17073skip = false
17173assert(test, "no active test")
172N/A
17373local case = {
17473name = name,
17573result = NONE,
17673focus = false,
1770}
178N/A
17973test.case = case
18073table.insert(test.cases, case)
181N/Aend
182N/A
1831local function CHECK_EXPECT_ERR(fn, ...)
1849assert(test, "no active test")
1859local case = test.case
1869if not case then
1870CASE("")
1880case = test.case
189N/Aend
1909assert(case, "no active case")
1919if case.result ~= FAIL then
1929local ok, err = pcall(fn, ...)
1939case.result = if ok then FAIL else PASS
1949if skip then
1950case.result = SKIPPED
196N/Aend
1979case.line = debug.info(stack and stack + 1 or 2, "l")
198N/Aend
199N/Aend
200N/A
2011local function CHECK(value: T, stack: number?): T?
2021195assert(test, "no active test")
203N/A
2041195local case = test.case
205N/A
2061195if not case then
2079CASE("")
2089case = test.case
209N/Aend
210N/A
2111195assert(case, "no active case")
212N/A
2131195if case.result ~= FAIL then
2141195case.result = value and PASS or FAIL
2151195if skip then
2161case.result = SKIPPED
217N/Aend
2181195case.line = debug.info(stack and stack + 1 or 2, "l")
219N/Aend
220N/A
2211195return value
222N/Aend
223N/A
2241local function TEST(name: string, fn: () -> ())
225N/A
22624test = {
22724name = name,
22824cases = {},
22924duration = 0,
23024focus = false,
23124fn = fn
2320}
233N/A
23424table.insert(tests, test)
235N/Aend
236N/A
2371local function FOCUS()
2380assert(test, "no active test")
239N/A
2400check_for_focused = true
2410if test.case then
2420test.case.focus = true
2430else
2440test.focus = true
245N/Aend
246N/Aend
247N/A
2481local function FINISH(): boolean
2491local success = true
2501local total_cases = 0
2511local passed_cases = 0
2521local passed_focus_cases = 0
2531local total_focus_cases = 0
2541local duration = 0
255N/A
2561for _, t in tests do
25724if check_for_focused and not t.focus then
2580continue
259N/Aend
26024test = t
26124fn = t.fn
26224local start = os.clock()
26324local err
26424local success = xpcall(fn, function(m: string)
2650err = { message = m, trace = debug.traceback(nil, 2) }
266N/Aend)
26724test.duration = os.clock() - start
268N/A
26924if not test.case then
2700CASE("")
271N/Aend
27224assert(test.case, "no active case")
273N/A
27424if not success then
2750test.case.result = ERROR
2760test.error = err
277N/Aend
27824collectgarbage()
279N/Aend
280N/A
2811for _, test in tests do
28224duration += test.duration
28324for _, case in test.cases do
28473total_cases += 1
28573if case.focus or test.focus then
2860total_focus_cases += 1
287N/Aend
28873if case.result == PASS or case.result == NONE or case.result == SKIPPED then
28973if case.focus or test.focus then
2900passed_focus_cases += 1
291N/Aend
29273passed_cases += 1
2930else
2940success = false
295N/Aend
296N/Aend
297N/A
29824output_test_result(test)
299N/Aend
300N/A
3011print(color.gray(string.format(`{passed_cases}/{total_cases} test cases passed in %.3f ms.`, duration * 1e3)))
3021if check_for_focused then
3030print(color.gray(`{passed_focus_cases}/{total_focus_cases} focused test cases passed`))
304N/Aend
305N/A
3061local fails = total_cases - passed_cases
307N/A
3081print((fails > 0 and color.red or color.green)(`{fails} {fails == 1 and "fail" or "fails"}`))
309N/A
3101check_for_focused = false
3111return success, table.clear(tests)
312N/Aend
313N/A
3141local function SKIP()
3151skip = true
316N/Aend
317N/A
318N/A--------------------------------------------------------------------------------
319N/A-- Benchmarking
320N/A--------------------------------------------------------------------------------
321N/A
3220type Bench = {
3230time_start: number?,
3240memory_start: number?,
3250iterations: number?,
3260}
327N/A
3281local bench: Bench?
329N/A
3301function START(iter: number?): number
3311local n = iter or 1
3321assert(n > 0, "iterations must be greater than 0")
3331assert(bench, "no active benchmark")
3341assert(not bench.time_start, "clock was already started")
335N/A
3361bench.iterations = n
3371bench.memory_start = gcinfo()
3381bench.time_start = os.clock()
3391return n
340N/Aend
341N/A
3421local function BENCH(name: string, fn: () -> ())
3433local active = bench
3443assert(not active, "a benchmark is already in progress")
345N/A
3463bench = {}
3473assert(bench);
3483(collectgarbage :: any)("collect")
349N/A
3503local mem_start = gcinfo()
3513local time_start = os.clock()
3523local err_msg: string?
353N/A
3543local success = xpcall(fn, function(m: string)
3550err_msg = m .. debug.traceback(nil, 2)
356N/Aend)
357N/A
3583local time_stop = os.clock()
3593local mem_stop = gcinfo()
360N/A
3613if not success then
3620print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
3630print(color.gray(err_msg :: string))
3640else
3653time_start = bench.time_start or time_start
3663mem_start = bench.memory_start or mem_start
367N/A
3683local n = bench.iterations or 1
3693local d, d_unit = convert_units("s", (time_stop - time_start) / n)
3703local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3))
371N/A
3723local function round(x: number): string
3736return x > 0 and x < 10 and (x - math.floor(x)) > 0 and string.format("%2.1f", x)
3746or string.format("%3.f", x)
375N/Aend
376N/A
3773print(
3783string.format(
3793`%s %s %s %s{WALL} %s`,
3803color.gray(round(d)),
3813d_unit,
3823color.gray(round(a)),
3833a_unit,
3843color.gray(name)
3850)
3860)
387N/Aend
388N/A
3893bench = nil
390N/Aend
391N/A
392N/A--------------------------------------------------------------------------------
393N/A-- Printing
394N/A--------------------------------------------------------------------------------
395N/A
3961local function print2(v: unknown)
3970type Buffer = { n: number, [number]: string }
3980type Cyclic = { n: number, [{}]: number }
399N/A
400N/A-- overkill concatenationless string buffer
4010local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
4020local TAB = " "
4030local indent = table.concat(table.create(stack, TAB))
404N/A
4050if type(value) == "string" then
4060local n = str.n
4070str[n + 1] = "\""
4080str[n + 2] = value
4090str[n + 3] = "\""
4100str.n = n + 3
4110elseif type(value) ~= "table" then
4120local n = str.n
4130str[n + 1] = value == nil and "nil" or tostring(value)
4140str.n = n + 1
4150elseif next(value) == nil then
4160local n = str.n
4170str[n + 1] = "{}"
4180str.n = n + 1
4190else -- is table
4200local tabbed_indent = indent .. TAB
421N/A
4220if cyclic[value] then
4230str.n += 1
4240str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
4250return
4260else
4270cyclic.n += 1
4280cyclic[value] = cyclic.n
429N/Aend
430N/A
4310str.n += 3
4320str[str.n - 2] = "{ "
4330str[str.n - 1] = color.gray(tostring(cyclic[value]))
4340str[str.n - 0] = "\n"
435N/A
4360local i, v = next(value, nil)
4370while v ~= nil do
4380local n = str.n
4390str[n + 1] = tabbed_indent
440N/A
4410if type(i) ~= "string" then
4420str[n + 2] = "["
4430str[n + 3] = tostring(i)
4440str[n + 4] = "]"
4450n += 4
4460else
4470str[n + 2] = tostring(i)
4480n += 2
449N/Aend
450N/A
4510str[n + 1] = " = "
4520str.n = n + 1
453N/A
4540tos(v, stack + 1, str, cyclic)
455N/A
4560i, v = next(value, i)
457N/A
4580n = str.n
4590str[n + 1] = v ~= nil and ",\n" or "\n"
4600str.n = n + 1
461N/Aend
462N/A
4630local n = str.n
4640str[n + 1] = indent
4650str[n + 2] = "}"
4660str.n = n + 2
467N/Aend
468N/Aend
469N/A
4700local str = { n = 0 }
4710local cyclic = { n = 0 }
4720tos(v, 0, str, cyclic)
4730print(table.concat(str))
474N/Aend
475N/A
476N/A--------------------------------------------------------------------------------
477N/A-- Equality
478N/A--------------------------------------------------------------------------------
479N/A
4801local function shallow_eq(a: {}, b: {}): boolean
4810if #a ~= #b then
4820return false
483N/Aend
484N/A
4850for i, v in next, a do
4860if b[i] ~= v then
4870return false
488N/Aend
489N/Aend
490N/A
4910for i, v in next, b do
4920if a[i] ~= v then
4930return false
494N/Aend
495N/Aend
496N/A
4970return true
498N/Aend
499N/A
5001local function deep_eq(a: {}, b: {}): boolean
5010if #a ~= #b then
5020return false
503N/Aend
504N/A
5050for i, v in next, a do
5060if type(b[i]) == "table" and type(v) == "table" then
5070if deep_eq(b[i], v) == false then
5080return false
509N/Aend
5100elseif b[i] ~= v then
5110return false
512N/Aend
513N/Aend
514N/A
5150for i, v in next, b do
5160if type(a[i]) == "table" and type(v) == "table" then
5170if deep_eq(a[i], v) == false then
5180return false
519N/Aend
5200elseif a[i] ~= v then
5210return false
522N/Aend
523N/Aend
524N/A
5250return true
526N/Aend
527N/A
528N/A--------------------------------------------------------------------------------
529N/A-- Return
530N/A--------------------------------------------------------------------------------
531N/A
5321return {
5331test = function()
5341return {
5351TEST = TEST,
5361CASE = CASE,
5371CHECK = CHECK,
5381FINISH = FINISH,
5391SKIP = SKIP,
5401FOCUS = FOCUS,
5411CHECK_EXPECT_ERR = CHECK_EXPECT_ERR,
5420}
543N/Aend,
544N/A
5451benchmark = function()
5461return BENCH, START
547N/Aend,
548N/A
5491disable_formatting = function()
5500disable_ansi = true
551N/Aend,
552N/A
5531print = print2,
554N/A
5551seq = shallow_eq,
5561deq = deep_eq,
557N/A
5581color = color,
5590}
\ No newline at end of file diff --git a/coverage/tests.luau.html b/coverage/tests.luau.html new file mode 100644 index 0000000..e2818e0 --- /dev/null +++ b/coverage/tests.luau.html @@ -0,0 +1,2044 @@ + + + + +

tests.luau Coverage

+

Total Execution Hits: 100

+

Function Coverage Overview: 83.58%

+ +
+

Function Coverage:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionHits
1
white_underline:300
white:340
green:380
red:420
yellow:460
red_highlight:500
green_highlight:540
gray:580
pe:630
pp:680
debug_world_inspect:737
record:7417
tbl:7712
archetype:804
records:831
columns:861
row:895
tuple:941
name:1160
:1201
:1361
:1741
:1841
:1891
getTargets:1922
setAttacksAndEats:2123
:2401
:2561
:3121
:3861
:4351
:4461
:4531
:4751
:5141
:5721
:5961
:8961
:10201
:10481
:10851
:11841
:12131
:12311
:12421
:12501
:13251
:13301
:13701
:13911
:15121
:15411
:16391
:16511
:16561
:16671
:16811
:16971
:17111
:17701
updateCooldowns:17772
:18201
:18891
:18971
:19191
:19391
+

Source Code:

+ +> + + + + + + + + + + + + + + +> + + + + + +> + +> + + +> + + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> +> + + +> + +> + + + +> +> + + + +> +> + + + +> + + +> + + +> + + +> + + +> + + +> +> +> + + + + +> +> + +> +> + + + + + + + + + +> +> + +> + + +> +> + + + +> + + + + + + + + + +> +> +> + + + + + +> + + + + +> + +> +> + + + + +> +> + + + + + + +> + + +> +> +> + + +> +> + +> + + +> + + + + + + + + + +> +> +> + + +> + + + + + + + + + + + +> +> +> + +> +> + + +> + + + +> +> + + + + + + + + + + +> +> + + +> + + +> + + +> +> +> + + + +> + + + + + +> + + + + +> +> + + + + + +> + + + +> + +> + + +> +> + + + +> + +> + + + +> + + + +> + +> + +> + + + +> + + + + + + + + + + + + + +> +> + + +> + + + + + + + + + +> +> + + + + + + + + +> +> + + + + + +> +> + +> + + +> + + +> + + + +> + +> +> + + + + + + + + + + + + +> +> + + + + + + +> + + + + + + + +> +> +> + + + + +> + +> + + + +> +> + + +> + +> +> + + + + + + + +> + + +> + +> + +> +> +> + + +> + + + + +> + +> + + + +> + + +> +> + + + + +> + +> + + +> + +> +> +> + + + + +> + + +> + + + + +> +> + + +> + + +> +> +> + + + + + + +> + + + + + +> +> + + + + +> + +> + + +> +> + + +> + +> + + +> + + +> +> +> +> + + + + + + + + + + + +> + + + + +> + + + + +> + + + +> + + + +> + +> + + + + +> + + + + + + + + + + + +> + + +> + +> + + + + + + +> + + + + +> + +> + + +> + + + + +> + +> + + + + + + +> +> + + + + + +> +> + + + + + + +> + + + +> + + + +> + +> +> + + +> +> + +> +> + + + + + + + + + + + +> +> + +> + + + + +> + + +> + + +> +> +> + + + + + + +> + + +> + + + + +> + + + +> + +> +> + + + + +> + + + +> + + + +> +> + + + + + + + + + + +> +> +> + + + + + + + + + + + + + +> + +> + + + + + + + + +> +> + +> + +> +> + + + + + +> + + + +> +> +> + + + + +> + +> +> + + + + +> +> + +> +> + + + + + +> + + + +> + + + + + +> +> +> + + +> +> + +> +> + + + + + + +> + + + + +> +> +> + + + + + + +> + +> + + + + +> + + + +> +> +> + + + + + + + + +> + + +> + + + + + + + + +> +> +> + + +> + + + +> + +> +> + + + + + + + +> + + + +> + + +> + + + +> +> + +> + + +> +> +> + + + + + + +> + + + +> +> +> + + + + +> + +> + + + +> + + + + + +> + + + +> + +> +> + + + + + +> + + + + + +> + + + + +> + +> +> + + + + + + +> + + + + + +> + + + +> + +> +> + +> +> + + + + +> + + + + + +> + + + + +> +> + +> +> +> + + +> + + +> + + + +> + + + +> +> + +> +> + +> + + +> + + + +> + +> +> +> + + + + + +> + + + +> + +> + + +> + + + +> + + + + +> + +> +> +> + + + + +> + + +> + +> + + +> + + +> + + + + + + +> + +> + +> + +> + + + +> +> + +> +> + + + + + + + +> + + + +> + + +> + + +> + + +> + + + + + +> +> + + + + + +> + + + + +> + +> + + +> + + + +> + +> + + + +> + + +> + +> + + +> + + + + +> +> + + + + +> + +> + + + +> + + + +> + + + +> + + + + + + +> +> +> +> +> +> + + + +> + +> + + +> + +> +> + + +> + + + + +> + + + + +> + +> +> +> + + + + + +> + + +> +> + + + + + + + + + + +> +> + + + + +> +> +> + + + +> + + +> + + + +> +> + + +> + +> + + + + +> + + + + + + + + + +> + + + + + +> + + + +> + + +> + + + + + + +> + + +> + + +> + + + + + + +> + + + + +> + + +> +> + + +> + + + +> + + + + +> + + + + +> +> + + + +> + + + +> + + + + + +> +> +> + + +> + + + +> + + + +> + + + + + + + +> +> + + +> +> + + + + +> +> + +> + + + + + + + +> +> + + +> +> + + + + + +> +> +> + + + + + + + + + +> + +> + + +> + + + +> + +> + + + +> + + +> + +> + + +> + + +> +> + + + + + + +> + + + +> + +> + + +> + + +> + +> + + + +> +> +> + + + + + + + +> +> + + +> + + + +> + +> + + + +> + + +> + + +> +> +> + + + +> + + + +> + + + + + +> +> + + + + +> +> +> + + +> +> +> + + + + + + + +> + + +> + + + +> +> +> + + + + + + + + + + + + + + +> + + + + + + + +> + + + +> + + + + + + + + + + + + + + + +> + + + + +> + + +> +> + + + + + + + + +> + + + + + +> + + + +> +> + + + +> + + + +> + + + + +> +> + + + + + + +> +> + +> +> +> + + + + +> + + + + +> +> +> + + + + + + + +> + +> +> + + + + +> + + + + +> + +> +> + + +> + + + + + + +> + +> + + +> + +> + + + + +> + + + + +> +> + + + + +> +> +> +> + + + + + +> + + + + +> + + + + +> + + + + +> +> + + + + + +> +> + +> +> + + + + + +> + +> + +> + +> + + + + +> +> + +> + + +> +> +> +> +> + + + + + + +> + + +> + + + +> + + +> + + +> +> +> + + + +> +> +> + + +> +> + + + + +> + +> + + +> + + + +> + +> +> +> + + + + +> + + + +> + + +> + +> + + + + + +> + + + +> + +> + + + +> + + + +> + +> + +> + +> + + + +> + +> + + + +> + + + +> + +> + +> + + +> + + + +> + +> +> +> + + + +> + + +> + + + +> +> + + +> + +> + + + +> + + + +> + +> + +> + + + +> +> + + +> + +> + +> + + + +> + + + +> + + + +> +> + +> + +> + +> +> + +
LineHitsCode
11local jecs = require("@jecs")
2N/A
31local testkit = require("@testkit")
41local BENCH, START = testkit.benchmark()
51local __ = jecs.Wildcard
61local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
71local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
81local IS_PAIR = jecs.IS_PAIR
91local pair = jecs.pair
101local ecs_pair_first = jecs.pair_first
111local ecs_pair_second = jecs.pair_second
121local entity_index_try_get_any = jecs.entity_index_try_get_any
131local entity_index_get_alive = jecs.entity_index_get_alive
141local entity_index_is_alive = jecs.entity_index_is_alive
151local ChildOf = jecs.ChildOf
161local world_new = jecs.World.new
17N/A
181local it = testkit.test()
191local TEST, CASE = it.TEST, it.CASE
201local CHECK, FINISH = it.CHECK, it.FINISH
211local SKIP, FOCUS = it.SKIP, it.FOCUS
221local CHECK_EXPECT_ERR = it.CHECK_EXPECT_ERR
23N/A
241local N = 2 ^ 8
25N/A
260type World = jecs.World
270type Entity = jecs.Entity
28N/A
291local c = {
301white_underline = function(s: any)
310return `\27[1;4m{s}\27[0m`
32N/Aend,
33N/A
341white = function(s: any)
350return `\27[37;1m{s}\27[0m`
36N/Aend,
37N/A
381green = function(s: any)
390return `\27[32;1m{s}\27[0m`
40N/Aend,
41N/A
421red = function(s: any)
430return `\27[31;1m{s}\27[0m`
44N/Aend,
45N/A
461yellow = function(s: any)
470return `\27[33;1m{s}\27[0m`
48N/Aend,
49N/A
501red_highlight = function(s: any)
510return `\27[41;1;30m{s}\27[0m`
52N/Aend,
53N/A
541green_highlight = function(s: any)
550return `\27[42;1;30m{s}\27[0m`
56N/Aend,
57N/A
581gray = function(s: any)
590return `\27[30;1m{s}\27[0m`
60N/Aend,
610}
62N/A
631local function pe(e)
640local gen = ECS_GENERATION(e)
650return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`)
66N/Aend
67N/A
681local function pp(e)
690local gen = ECS_GENERATION(e)
700return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{jecs.ECS_ENTITY_T_HI(e)}`)
71N/Aend
72N/A
731local function debug_world_inspect(world: World)
747local function record(e): jecs.Record
7517return entity_index_try_get_any(world.entity_index, e) :: any
76N/Aend
777local function tbl(e)
7812return record(e).archetype
79N/Aend
807local function archetype(e)
814return tbl(e).type
82N/Aend
837local function records(e)
841return tbl(e).records
85N/Aend
867local function columns(e)
871return tbl(e).columns
88N/Aend
897local function row(e)
905return record(e).row
91N/Aend
92N/A
93N/A-- Important to order them in the order of their columns
947local function tuple(e, ...)
951for i, column in columns(e) do
962if select(i, ...) ~= column[row(e)] then
970return false
98N/Aend
99N/Aend
1001return true
101N/Aend
102N/A
1037return {
1047record = record,
1057tbl = tbl,
1067archetype = archetype,
1077records = records,
1087row = row,
1097tuple = tuple,
1107columns = columns
1110}
112N/Aend
113N/A
1141local dwi = debug_world_inspect
115N/A
1161local function name(world, e)
1170return world:get(e, jecs.Name)
118N/Aend
119N/A
1201TEST("#adding a recycled target", function()
1211local world = world_new()
1221local R = world:component()
123N/A
1241local e = world:entity()
1251local T = world:entity()
1261world:add(e, pair(R, T))
1271world:delete(T)
1281CHECK(not world:has(e, pair(R, T)))
1291local T2 = world:entity()
1301world:add(e, pair(R, T2))
1311CHECK(world:target(e, R) ~= T)
1321CHECK(world:target(e, R) ~= 0)
133N/A
134N/Aend)
135N/A
1361TEST("#repro2", function()
1371local world = world_new()
1381local Lifetime = world:component() :: jecs.Id
1391local Particle = world:entity()
1401local Beam = world:entity()
141N/A
1421local entity = world:entity()
1431world:set(entity, pair(Lifetime, Particle), 1)
1441world:set(entity, pair(Lifetime, Beam), 2)
1451world:set(entity, pair(4, 5), 6) -- noise
146N/A
1471local entity_visualizer = require("@tools/entity_visualiser")
148N/A-- entity_visualizer.components(world, entity)
149N/A
1501for e in world:each(pair(Lifetime, __)) do
1512local i = 0
1522local nth = world:target(e, Lifetime, i)
1532while nth do
154N/A-- entity_visualizer.components(world, e)
155N/A
1562local data = world:get(e, pair(Lifetime, nth))
1572data -= 1
1582if data <= 0 then
1591world:remove(e, pair(Lifetime, nth))
1600else
1611world:set(e, pair(Lifetime, nth), data)
162N/Aend
1632i += 1
1642nth = world:target(e, Lifetime, i)
165N/Aend
166N/Aend
167N/A
1681CHECK(not world:has(entity, pair(Lifetime, Particle)))
1691CHECK(world:get(entity, pair(Lifetime, Beam)) == 1)
170N/Aend)
171N/A
1721local lifetime_tracker_add = require("@tools/lifetime_tracker")
173N/A
1741TEST("another", function()
1751local world = world_new()
176N/A-- world = lifetime_tracker_add(world, {padding_enabled=false})
1771local e1 = world:entity()
1781local e2 = world:entity()
1791local e3 = world:entity()
1801world:delete(e2)
1811local e2_e3 = pair(e2, e3)
1821CHECK(jecs.pair_first(world, e2_e3) == 0)
1831CHECK(jecs.pair_second(world, e2_e3) == e3)
1841CHECK_EXPECT_ERR(function()
1851world:add(e1, pair(e2, e3))
186N/Aend)
187N/Aend)
188N/A
1891TEST("#repro", function()
1901local world = world_new()
191N/A
1921local function getTargets(relation)
1932local tgts = {}
1942local pairwildcard = pair(relation, jecs.Wildcard)
1952for _, archetype in world:query(pairwildcard):archetypes() do
1962local tr = archetype.records[pairwildcard]
1972local count = archetype.counts[pairwildcard]
1982local types = archetype.types
1992for _, entity in archetype.entities do
2002for i = 0, count - 1 do
2012local tgt = jecs.pair_second(world, types[i + tr])
2022table.insert(tgts, tgt)
203N/Aend
204N/Aend
205N/Aend
2062return tgts
207N/Aend
208N/A
2091local Attacks = world:component()
2101local Eats = world:component()
211N/A
2121local function setAttacksAndEats(entity1, entity2)
2133world:add(entity1, pair(Attacks, entity2))
2143world:add(entity1, pair(Eats, entity2))
215N/Aend
216N/A
2171local e1 = world:entity()
2181local e2 = world:entity()
2191local e3 = world:entity()
2201setAttacksAndEats(e3, e1)
2211setAttacksAndEats(e3, e2)
2221setAttacksAndEats(e1, e2)
2231local d = dwi(world)
2241world:delete(e2)
2251local types1 = { pair(Attacks, e1), pair(Eats, e1) }
2261table.sort(types1)
227N/A
228N/A
2291CHECK(d.tbl(e1).type == "")
2301CHECK(d.tbl(e3).type == table.concat(types1, "_"))
231N/A
2321for _, entity in getTargets(Attacks) do
2331CHECK(entity == e1)
234N/Aend
2351for _, entity in getTargets(Eats) do
2361CHECK(entity == e1)
237N/Aend
238N/Aend)
239N/A
2401TEST("archetype", function()
2411local archetype_traverse_add = jecs.archetype_traverse_add
2421local archetype_traverse_remove = jecs.archetype_traverse_remove
243N/A
2441local world = world_new()
2451local root = world.ROOT_ARCHETYPE
2461local c1 = world:component()
2471local c2 = world:component()
2481local c3 = world:component()
249N/A
2501local a1 = archetype_traverse_add(world, c1, nil :: any)
2511local a2 = archetype_traverse_remove(world, c1, a1)
2521CHECK(root.add[c1].to == a1)
2531CHECK(root == a2)
254N/Aend)
255N/A
2561TEST("world:cleanup()", function()
2571local world = world_new()
2581local A = world:component() :: jecs.Id
2591local B = world:component() :: jecs.Id
2601local C = world:component() :: jecs.Id
261N/A
2621local e1 = world:entity()
2631local e2 = world:entity()
2641local e3 = world:entity()
265N/A
2661world:set(e1, A, true)
267N/A
2681world:set(e2, A, true)
2691world:set(e2, B, true)
270N/A
271N/A
2721world:set(e3, A, true)
2731world:set(e3, B, true)
2741world:set(e3, C, true)
275N/A
2761local archetype_index = world.archetype_index
277N/A
2781CHECK(#archetype_index["1"].entities == 1)
2791CHECK(#archetype_index["1_2"].entities == 1)
2801CHECK(#archetype_index["1_2_3"].entities == 1)
281N/A
2821world:delete(e1)
2831world:delete(e2)
2841world:delete(e3)
285N/A
2861world:cleanup()
287N/A
2881archetype_index = world.archetype_index
289N/A
2901CHECK((archetype_index["1"] :: jecs.Archetype?) == nil)
2911CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil)
2921CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
293N/A
2941local e4 = world:entity()
2951world:set(e4, A, true)
2961CHECK(#archetype_index["1"].entities == 1)
2971CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil)
2981CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
2991world:set(e4, B, true)
3001CHECK(#archetype_index["1"].entities == 0)
3011CHECK(#archetype_index["1_2"].entities == 1)
3021CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
3031world:set(e4, C, true)
3041CHECK(#archetype_index["1"].entities == 0)
3051CHECK(#archetype_index["1_2"].entities == 0)
3061CHECK(#archetype_index["1_2_3"].entities == 1)
307N/Aend)
308N/A
3091local pe = require("@tools/entity_visualiser").prettify
3101local lifetime_tracker_add = require("@tools/lifetime_tracker")
311N/A
3121TEST("world:entity()", function()
3130do
3141CASE("unique IDs")
3151local world = jecs.World.new()
3161local set = {}
3171for i = 1, N do
318256local e = world:entity()
319256CHECK(not set[e])
320256set[e] = true
321N/Aend
322N/Aend
3230do
3241CASE("generations")
3251local world = jecs.World.new()
3261local e = world:entity() :: number
3271CHECK(ECS_ID(e) == 1 + jecs.Rest :: number)
3281CHECK(ECS_GENERATION(e) == 0) -- 0
3291e = ECS_GENERATION_INC(e)
3301CHECK(ECS_GENERATION(e) == 1) -- 1
331N/Aend
332N/A
3331do CASE "pairs"
3341local world = jecs.World.new()
3351local _e = world:entity()
3361local e2 = world:entity()
3371local e3 = world:entity()
338N/A
339N/A-- Incomplete pair, must have a bit flag that notes it is a pair
3401CHECK(IS_PAIR(world:entity()) == false)
341N/A
3421local p = pair(e2, e3)
3431CHECK(IS_PAIR(p) == true)
344N/A
3451CHECK(ecs_pair_first(world, p) == e2 :: number)
3461CHECK(ecs_pair_second(world, p) == e3 :: number)
347N/A
3481world:delete(e2)
3491local e2v2 = world:entity()
3501CHECK(IS_PAIR(e2v2) == false)
351N/A
3521CHECK(IS_PAIR(pair(e2v2, e3)) == true)
353N/Aend
354N/A
3551do CASE "Recycling"
3561local world = world_new()
3571local e = world:entity()
3581world:delete(e)
3591local e1 = world:entity()
3601world:delete(e1)
3611local e2 = world:entity()
3621CHECK(ECS_ID(e2) == e :: number)
3631CHECK(ECS_GENERATION(e2) == 2)
3641CHECK(world:contains(e2))
3651CHECK(not world:contains(e1))
3661CHECK(not world:contains(e))
367N/Aend
368N/A
3691do CASE "Recycling max generation"
3701local world = world_new()
3711local pin = (jecs.Rest :: any) :: number + 1
3721for i = 1, 2^16-1 do
37365535local e = world:entity()
37465535world:delete(e)
375N/Aend
3761local e = world:entity()
3771CHECK(ECS_ID(e) == pin)
3781CHECK(ECS_GENERATION(e) == 2^16-1)
3791world:delete(e)
3801e = world:entity()
3811CHECK(ECS_ID(e) == pin)
3821CHECK(ECS_GENERATION(e) == 0)
383N/Aend
384N/Aend)
385N/A
3861TEST("world:set()", function()
3871do CASE "archetype move"
3880do
3891local world = jecs.World.new()
390N/A
3911local d = debug_world_inspect(world)
392N/A
3931local _1 = world:component()
3941local _2 = world:component()
3951local e = world:entity()
396N/A-- An entity starts without an archetype or row
397N/A-- should therefore not need to copy over data
3981CHECK(d.tbl(e) == nil)
3991CHECK(d.row(e) == nil)
400N/A
4011local archetypes = #world.archetypes
402N/A-- This should create a new archetype since it is the first
403N/A-- entity to have moved there
4041world:set(e, _1, 1)
4051local oldRow = d.row(e)
4061local oldArchetype = d.archetype(e)
4071CHECK(#world.archetypes == archetypes + 1)
4081CHECK(oldArchetype == "1")
4091CHECK(d.tbl(e))
4101CHECK(oldRow == 1)
411N/A
4121world:set(e, _2, 2)
4131CHECK(d.archetype(e) == "1_2")
414N/A-- Should have tuple of fields to the next archetype and set the component data
4151CHECK(d.tuple(e, 1, 2))
416N/A-- Should have moved the data from the old archetype
4171CHECK(world.archetype_index[oldArchetype].columns[_1][oldRow] == nil)
418N/Aend
419N/Aend
420N/A
4211do CASE "pairs"
4221local world = jecs.World.new()
423N/A
4241local C1 = world:component()
4251local C2 = world:component()
4261local T1 = world:entity()
4271local T2 = world:entity()
428N/A
4291local e = world:entity()
430N/A
4311world:set(e, pair(C1, C2), true)
4321world:set(e, pair(C1, T1), true)
4331world:set(e, pair(T1, C1), true)
434N/A
4351CHECK_EXPECT_ERR(function()
4361world:set(e, pair(T1, T2), true :: any)
437N/Aend)
438N/A
4391CHECK(world:get(e, pair(C1, C2)))
4401CHECK(world:get(e, pair(C1, T1)))
4411CHECK(world:get(e, pair(T1, C1)))
4421CHECK(not world:get(e, pair(T1, T2)))
443N/A
4441local e2 = world:entity()
445N/A
4461CHECK_EXPECT_ERR(function()
4471world:set(e2, pair(jecs.ChildOf, e), true :: any)
448N/Aend)
4491CHECK(not world:get(e2, pair(jecs.ChildOf, e)))
450N/Aend
451N/Aend)
452N/A
4531TEST("world:remove()", function()
4540do
4551CASE("should allow remove a component that doesn't exist on entity")
4561local world = jecs.World.new()
457N/A
4581local Health = world:component()
4591local Poison = world:component()
460N/A
4611local id = world:entity()
4620do
4631world:remove(id, Poison)
4641CHECK(true) -- Didn't error
465N/Aend
466N/A
4671world:set(id, Health, 50)
4681world:remove(id, Poison)
469N/A
4701CHECK(world:get(id, Poison) == nil)
4711CHECK(world:get(id, Health) == 50)
472N/Aend
473N/Aend)
474N/A
4751TEST("world:add()", function()
4760do
4771CASE("idempotent")
4781local world = jecs.World.new()
4791local d = debug_world_inspect(world)
4801local _1, _2 = world:component(), world:component()
481N/A
4821local e = world:entity()
4831world:add(e, _1)
4841world:add(e, _2)
4851world:add(e, _2) -- should have 0 effects
4861CHECK(d.archetype(e) == "1_2")
487N/Aend
488N/A
4890do
4901CASE("archetype move")
4910do
4921local world = jecs.World.new()
493N/A
4941local d = debug_world_inspect(world)
495N/A
4961local _1 = world:component()
4971local e = world:entity()
498N/A-- An entity starts without an archetype or row
499N/A-- should therefore not need to copy over data
5001CHECK(d.tbl(e) == nil)
5011CHECK(d.row(e) == nil)
502N/A
5031local archetypes = #world.archetypes
504N/A-- This should create a new archetype
5051world:add(e, _1)
5061CHECK(#world.archetypes == archetypes + 1)
507N/A
5081CHECK(d.archetype(e) == "1")
5091CHECK(d.tbl(e))
510N/Aend
511N/Aend
512N/Aend)
513N/A
5141TEST("world:query()", function()
5151do CASE "cached"
5161local world = world_new()
5171local Foo = world:component()
5181local Bar = world:component()
5191local Baz = world:component()
5201local e = world:entity()
5211local q = world:query(Foo, Bar):without(Baz):cached()
5221world:set(e, Foo, true)
5231world:set(e, Bar, false)
5241local i = 0
525N/A
5261local iter = 0
5271for _, e in q:iter() do
5281iter += 1
5291i=1
530N/Aend
5311CHECK (iter == 1)
5321CHECK(i == 1)
5331for _, e in q:iter() do
5341i=2
535N/Aend
5361CHECK(i == 2)
5371for _, e in q :: any do
5381i=3
539N/Aend
5401CHECK(i == 3)
5411for _, e in q :: any do
5421i=4
543N/Aend
5441CHECK(i == 4)
545N/A
5461CHECK(#q:archetypes() == 1)
5471CHECK(not table.find(q:archetypes(), world.archetype_index[table.concat({Foo, Bar, Baz}, "_")]))
5481world:delete(Foo)
5491CHECK(#q:archetypes() == 0)
550N/Aend
5511do CASE "multiple iter"
5521local world = jecs.World.new()
5531local A = world:component()
5541local B = world:component()
5551local e = world:entity()
5561world:add(e, A)
5571world:add(e, B)
5581local q = world:query(A, B)
5591local counter = 0
5601for x in q:iter() do
5611counter += 1
562N/Aend
5631for x in q:iter() do
5641counter += 1
565N/Aend
5661CHECK(counter == 2)
567N/Aend
5681do CASE "tag"
5691local world = jecs.World.new()
5701local A = world:entity()
5711local e = world:entity()
5721CHECK_EXPECT_ERR(function()
5731world:set(e, A, "test" :: any)
574N/Aend)
5751local count = 0
5761for id, a in world:query(A) :: any do
5771count += 1
5781CHECK(a == nil)
579N/Aend
5801CHECK(count == 1)
581N/Aend
5821do CASE "pairs"
5831local world = jecs.World.new()
584N/A
5851local C1 = world:component() :: jecs.Id
5861local C2 = world:component() :: jecs.Id
5871local T1 = world:entity()
5881local T2 = world:entity()
589N/A
5901local e = world:entity()
591N/A
5921local C1_C2 = pair(C1, C2)
5931world:set(e, C1_C2, true)
5941world:set(e, pair(C1, T1), true)
5951world:set(e, pair(T1, C1), true)
5961CHECK_EXPECT_ERR(function()
5971world:set(e, pair(T1, T2), true :: any)
598N/Aend)
599N/A
6001for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)):iter() do
6011CHECK(a == true)
6021CHECK(b == true)
6031CHECK(c == true)
6041CHECK(d == nil)
605N/Aend
606N/Aend
6070do
6081CASE("query single component")
6090do
6101local world = jecs.World.new()
6111local A = world:component()
6121local B = world:component()
613N/A
6141local entities = {}
6151for i = 1, N do
616256local id = world:entity()
617N/A
618256world:set(id, A, true)
619256if i > 5 then
620251world:set(id, B, true)
621N/Aend
622256entities[i] = id
623N/Aend
624N/A
6251for id in world:query(A) :: any do
626256table.remove(entities, CHECK(table.find(entities, id)))
627N/Aend
628N/A
6291CHECK(#entities == 0)
630N/Aend
631N/A
6320do
6331local world = jecs.World.new() :: World
6341local A = world:component()
6351local B = world:component()
6361local eA = world:entity()
6371world:set(eA, A, true)
6381local eB = world:entity()
6391world:set(eB, B, true)
6401local eAB = world:entity()
6411world:set(eAB, A, true)
6421world:set(eAB, B, true)
643N/A
644N/A-- Should drain the iterator
6451local q = world:query(A)
646N/A
6471local i = 0
6481local j = 0
6491for _ in q :: any do
6502i += 1
651N/Aend
6521for _ in q :: any do
6530j += 1
654N/Aend
6551CHECK(i == 2)
6561CHECK(j == 0)
657N/Aend
658N/Aend
659N/A
6600do
6611CASE("query missing component")
6621local world = jecs.World.new()
6631local A = world:component()
6641local B = world:component()
6651local C = world:component()
666N/A
6671local e1 = world:entity()
6681local e2 = world:entity()
669N/A
6701world:set(e1, A, "abc")
6711world:set(e2, A, "def")
6721world:set(e1, B, 123)
6731world:set(e2, B, 457)
674N/A
6751local counter = 0
6761for _ in world:query(B, C) :: any do
6770counter += 1
678N/Aend
6791CHECK(counter == 0)
680N/Aend
681N/A
6820do
6831CASE("query more than 8 components")
6841local world = jecs.World.new()
6851local components = {}
686N/A
6871for i = 1, 9 do
6889local id = world:component()
6899components[i] = id
690N/Aend
6911local e = world:entity()
6921for i, id in components do
6939world:set(e, id, 13 ^ i)
694N/Aend
695N/A
6961for entity, a, b, c, d, e, f, g, h, i in world:query(unpack(components)) :: any do
6971CHECK(a == 13 ^ 1)
6981CHECK(b == 13 ^ 2)
6991CHECK(c == 13 ^ 3)
7001CHECK(d == 13 ^ 4)
7011CHECK(e == 13 ^ 5)
7021CHECK(f == 13 ^ 6)
7031CHECK(g == 13 ^ 7)
7041CHECK(h == 13 ^ 8)
7051CHECK(i == 13 ^ 9)
706N/Aend
707N/Aend
708N/A
7090do
7101CASE("should be able to get next results")
7111local world = jecs.World.new() :: World
7121world:component()
7131local A = world:component()
7141local B = world:component()
7151local eA = world:entity()
7161world:set(eA, A, true)
7171local eB = world:entity()
7181world:set(eB, B, true)
7191local eAB = world:entity()
7201world:set(eAB, A, true)
7211world:set(eAB, B, true)
722N/A
7231local it = world:query(A):iter()
724N/A
7251local e: number, data = it()
7261while e do
7272if e == eA :: number then
7281CHECK(data)
7291elseif e == eAB :: number then
7301CHECK(data)
7310else
7320CHECK(false)
733N/Aend
734N/A
7352e, data = it()
736N/Aend
7371CHECK(true)
738N/Aend
739N/A
7401do CASE "should query all matching entities when irrelevant component is removed"
7411local world = jecs.World.new()
7421local A = world:component()
7431local B = world:component()
7441local C = world:component()
745N/A
7461local entities = {}
7471for i = 1, N do
748256local id = world:entity()
749N/A
750N/A-- specifically put them in disorder to track regression
751N/A-- https://github.com/Ukendio/jecs/pull/15
752256world:set(id, B, true)
753256world:set(id, A, true)
754256if i > 5 then
755251world:remove(id, B)
756N/Aend
757256entities[i] = id
758N/Aend
759N/A
7601local added = 0
7611for id in world:query(A) :: any do
762256added += 1
763256table.remove(entities, CHECK(table.find(entities, id)))
764N/Aend
765N/A
7661CHECK(added == N)
767N/Aend
768N/A
7690do
7701CASE("should query all entities without B")
7711local world = jecs.World.new()
7721local A = world:component()
7731local B = world:component()
774N/A
7751local entities = {}
7761for i = 1, N do
777256local id = world:entity()
778N/A
779256world:set(id, A, true)
780256if i < 5 then
7814entities[i] = id
7820else
783252world:set(id, B, true)
784N/Aend
785N/Aend
786N/A
7871for id in world:query(A):without(B) :: any do
7884table.remove(entities, CHECK(table.find(entities, id)))
789N/Aend
790N/A
7911CHECK(#entities == 0)
792N/Aend
793N/A
7940do
7951CASE("should allow querying for relations")
7961local world = jecs.World.new()
7971local Eats = world:component()
7981local Apples = world:component()
7991local bob = world:entity()
800N/A
8011world:set(bob, pair(Eats, Apples), true)
8021for e, bool in world:query(pair(Eats, Apples)) :: any do
8031CHECK(e == bob)
8041CHECK(bool)
805N/Aend
806N/Aend
807N/A
8080do
8091CASE("should allow wildcards in queries")
8101local world = jecs.World.new()
8111local Eats = world:component()
8121local Apples = world:entity()
8131local bob = world:entity()
814N/A
8151world:set(bob, pair(Eats, Apples), "bob eats apples")
816N/A
8171local w = jecs.Wildcard
8181for e, data in world:query(pair(Eats, w)) :: any do
8191CHECK(e == bob)
8201CHECK(data == "bob eats apples")
821N/Aend
8221for e, data in world:query(pair(w, Apples)) :: any do
8231CHECK(e == bob)
8241CHECK(data == "bob eats apples")
825N/Aend
826N/Aend
827N/A
8280do
8291CASE("should match against multiple pairs")
8301local world = jecs.World.new()
8311local Eats = world:component()
8321local Apples = world:entity()
8331local Oranges = world:entity()
8341local bob = world:entity()
8351local alice = world:entity()
836N/A
8371world:set(bob, pair(Eats, Apples), "bob eats apples")
8381world:set(alice, pair(Eats, Oranges), "alice eats oranges")
839N/A
8401local w = jecs.Wildcard
8411local count = 0
8421for e, data in world:query(pair(Eats, w)) :: any do
8432count += 1
8442if e == bob then
8451CHECK(data == "bob eats apples")
8460else
8471CHECK(data == "alice eats oranges")
848N/Aend
849N/Aend
850N/A
8511CHECK(count == 2)
8521count = 0
853N/A
8541for e, data in world:query(pair(w, Apples)) :: any do
8551count += 1
8561CHECK(data == "bob eats apples")
857N/Aend
8581CHECK(count == 1)
859N/Aend
860N/A
8611do CASE "should only relate alive entities"
8621local world = jecs.World.new()
8631local Eats = world:entity()
8641local Apples = world:component()
8651local Oranges = world:component()
8661local bob = world:entity()
8671local alice = world:entity()
868N/A
8691world:set(bob, Apples, "apples")
8701world:set(bob, pair(Eats, Apples), "bob eats apples")
8711world:set(alice, pair(Eats, Oranges) :: Entity, "alice eats oranges")
872N/A
8731world:delete(Apples)
8741local Wildcard = jecs.Wildcard
875N/A
8761local count = 0
8771for _, data in world:query(pair(Wildcard, Apples)) :: any do
8780count += 1
879N/Aend
880N/A
8811world:delete(pair(Eats, Apples))
882N/A
8831CHECK(count == 0)
8841CHECK(world:get(bob, pair(Eats, Apples)) == nil)
885N/A
886N/Aend
887N/A
8880do
8891CASE("should error when setting invalid pair")
8901local world = jecs.World.new()
8911local Eats = world:component()
8921local Apples = world:component()
8931local bob = world:entity()
894N/A
8951world:delete(Apples)
8961CHECK_EXPECT_ERR(function()
8971world:set(bob, pair(Eats, Apples), "bob eats apples")
898N/Aend)
899N/Aend
900N/A
9010do
9021CASE("should find target for ChildOf")
9031local world = jecs.World.new()
9041local ChildOf = jecs.ChildOf
905N/A
9061local Name = world:component()
907N/A
9081local bob = world:entity()
9091local alice = world:entity()
9101local sara = world:entity()
911N/A
9121world:add(bob, pair(ChildOf, alice))
9131world:set(bob, Name, "bob")
9141world:add(sara, pair(ChildOf, alice))
9151world:set(sara, Name, "sara")
9161CHECK(world:parent(bob) :: number == alice :: number) -- O(1)
917N/A
9181local count = 0
9191for _, name in world:query(Name, pair(ChildOf, alice)) :: any do
9202count += 1
921N/Aend
9221CHECK(count == 2)
923N/Aend
924N/A
9250do
9261CASE("despawning while iterating")
9271local world = jecs.World.new()
9281local A = world:component()
9291local B = world:component()
930N/A
9311local e1 = world:entity()
9321local e2 = world:entity()
9331world:add(e1, A)
9341world:add(e2, A)
9351world:add(e2, B)
936N/A
9371local count = 0
9381for id in world:query(A) :: any do
9392world:clear(id)
9402count += 1
941N/Aend
9421CHECK(count == 2)
943N/Aend
944N/A
9451do CASE("iterator invalidation")
9461do CASE("adding")
9471SKIP()
9481local world = jecs.World.new()
9491local A = world:component()
9501local B = world:component()
951N/A
9521local e1 = world:entity()
9531local e2 = world:entity()
9541world:add(e1, A)
9551world:add(e2, A)
9561world:add(e2, B)
957N/A
9581local count = 0
9591for id in world:query(A) :: any do
9603world:add(id, B)
961N/A
9623count += 1
963N/Aend
964N/A
9651CHECK(count == 2)
966N/Aend
967N/A
9681do CASE("spawning")
9691local world = jecs.World.new()
9701local A = world:component()
9711local B = world:component()
972N/A
9731local e1 = world:entity()
9741local e2 = world:entity()
9751world:add(e1, A)
9761world:add(e2, A)
9771world:add(e2, B)
978N/A
9791for id in world:query(A) :: any do
9803local e = world:entity()
9813world:add(e, A)
9823world:add(e, B)
983N/Aend
984N/A
9851CHECK(true)
986N/Aend
987N/Aend
988N/A
9891do CASE("should not find any entities")
9901local world = jecs.World.new()
991N/A
9921local Hello = world:component()
9931local Bob = world:component()
994N/A
9951local helloBob = world:entity()
9961world:add(helloBob, pair(Hello, Bob))
9971world:add(helloBob, Bob)
998N/A
9991local withoutCount = 0
10001for _ in world:query(pair(Hello, Bob)):without(Bob) :: any do
10010withoutCount += 1
1002N/Aend
1003N/A
10041CHECK(withoutCount == 0)
1005N/Aend
1006N/A
10071do CASE("without")
1008N/A-- REGRESSION TEST
10091local world = jecs.World.new()
10101local _1, _2, _3 = world:component(), world:component(), world:component()
1011N/A
10121local counter = 0
10131for e, a, b in world:query(_1, _2):without(_3) :: any do
10140counter += 1
1015N/Aend
10161CHECK(counter == 0)
1017N/Aend
1018N/Aend)
1019N/A
10201TEST("world:each", function()
10211local world = world_new()
10221local A = world:component()
10231local B = world:component()
10241local C = world:component()
1025N/A
10261local e3 = world:entity()
10271local e1 = world:entity()
10281local e2 = world:entity()
1029N/A
10301world:set(e1, A, true)
1031N/A
10321world:set(e2, A, true)
10331world:set(e2, B, true)
1034N/A
10351world:set(e3, A, true)
10361world:set(e3, B, true)
10371world:set(e3, C, true)
1038N/A
10391for entity: number in world:each(A) do
10403if entity == e1 :: number or entity == e2 :: number or entity == e3 :: number then
10413CHECK(true)
10423continue
1043N/Aend
10440CHECK(false)
1045N/Aend
1046N/Aend)
1047N/A
10481TEST("world:children", function()
10491local world = world_new()
10501local C = world:component()
10511local T = world:entity()
1052N/A
10531local e1 = world:entity()
10541world:set(e1, C, true)
1055N/A
10561local e2 = world:entity() :: number
1057N/A
10581world:add(e2, T)
10591world:add(e2, pair(ChildOf, e1))
1060N/A
10611local e3 = world:entity() :: number
10621world:add(e3, pair(ChildOf, e1))
1063N/A
10641local count = 0
10651for entity: number in world:children(e1) do
10662count += 1
10672if entity == e2 or entity == e3 then
10682CHECK(true)
10692continue
1070N/Aend
10710CHECK(false)
1072N/Aend
10731CHECK(count == 2)
1074N/A
10751world:remove(e2, pair(ChildOf, e1))
1076N/A
10771count = 0
10781for entity in world:children(e1) do
10791count += 1
1080N/Aend
1081N/A
10821CHECK(count == 1)
1083N/Aend)
1084N/A
10851TEST("world:clear()", function()
10861do CASE("should remove its components")
10871local world = jecs.World.new() :: World
10881local A = world:component()
10891local B = world:component()
10901local C = world:component()
10911local D = world:component()
1092N/A
10931local e = world:entity()
10941local e1 = world:entity()
10951local e2 = world:entity()
1096N/A
10971world:set(e, A, true)
10981world:set(e, B, true)
1099N/A
11001world:set(e1, A, true)
11011world:set(e1, B, true)
1102N/A
11031CHECK(world:get(e, A))
11041CHECK(world:get(e, B))
1105N/A
11061world:clear(A)
11071CHECK(world:get(e, A) == nil)
11081CHECK(world:get(e, B))
11091CHECK(world:get(e1, A) == nil)
11101CHECK(world:get(e1, B))
1111N/Aend
1112N/A
11131do CASE("remove cleared ID from entities")
11141local world = world_new()
11151local A = world:component()
11161local B = world:component()
11171local C = world:component()
1118N/A
11190do
11201local id1 = world:entity()
11211local id2 = world:entity()
11221local id3 = world:entity()
1123N/A
11241world:set(id1, A, true)
1125N/A
11261world:set(id2, A, true)
11271world:set(id2, B, true)
1128N/A
11291world:set(id3, A, true)
11301world:set(id3, B, true)
11311world:set(id3, C, true)
1132N/A
11331world:clear(A)
1134N/A
11351CHECK(not world:has(id1, A))
11361CHECK(not world:has(id2, A))
11371CHECK(not world:has(id3, A))
1138N/A
11391CHECK(world:has(id2, B))
11401CHECK(world:has(id3, B, C))
1141N/A
11421world:clear(C)
1143N/A
11441CHECK(world:has(id2, B))
11451CHECK(world:has(id3, B))
1146N/A
11471CHECK(world:contains(A))
11481CHECK(world:contains(C))
11491CHECK(world:has(A, jecs.Component))
11501CHECK(world:has(B, jecs.Component))
1151N/Aend
1152N/A
11530do
11541local id1 = world:entity()
11551local id2 = world:entity()
11561local id3 = world:entity()
1157N/A
11581local tgt = world:entity()
1159N/A
11601world:add(id1, pair(A, tgt))
11611world:add(id1, pair(B, tgt))
11621world:add(id1, pair(C, tgt))
1163N/A
11641world:add(id2, pair(A, tgt))
11651world:add(id2, pair(B, tgt))
11661world:add(id2, pair(C, tgt))
1167N/A
11681world:add(id3, pair(A, tgt))
11691world:add(id3, pair(B, tgt))
11701world:add(id3, pair(C, tgt))
1171N/A
11721world:clear(B)
11731CHECK(world:has(id1, pair(A, tgt), pair(C, tgt)))
11741CHECK(not world:has(id1, pair(B, tgt)))
11751CHECK(world:has(id2, pair(A, tgt), pair(C, tgt)))
11761CHECK(not world:has(id1, pair(B, tgt)))
11771CHECK(world:has(id3, pair(A, tgt), pair(C, tgt)))
1178N/A
1179N/Aend
1180N/A
1181N/Aend
1182N/Aend)
1183N/A
11841TEST("world:has()", function()
11851do CASE("should find Tag on entity")
11861local world = jecs.World.new()
1187N/A
11881local Tag = world:entity()
1189N/A
11901local e = world:entity()
11911world:add(e, Tag)
1192N/A
11931CHECK(world:has(e, Tag))
1194N/Aend
1195N/A
11961do CASE("should return false when missing one tag")
11971local world = jecs.World.new()
1198N/A
11991local A = world:entity()
12001local B = world:entity()
12011local C = world:entity()
12021local D = world:entity()
1203N/A
12041local e = world:entity()
12051world:add(e, A)
12061world:add(e, C)
12071world:add(e, D)
1208N/A
12091CHECK(world:has(e, A, B, C, D) == false)
1210N/Aend
1211N/Aend)
1212N/A
12131TEST("world:component()", function()
12141do CASE("only components should have EcsComponent trait")
12151local world = jecs.World.new() :: World
12161local A = world:component()
12171local e = world:entity()
1218N/A
12191CHECK(world:has(A, jecs.Component))
12201CHECK(not world:has(e, jecs.Component))
1221N/Aend
1222N/A
12231do CASE("tag")
12241local world = jecs.World.new() :: World
12251local A = world:component()
12261local B = world:entity()
12271local C = world:entity()
12281local e = world:entity()
12291world:set(e, A, "test")
12301world:add(e, B)
12311CHECK_EXPECT_ERR(function()
12321world:set(e, C, 11 :: any)
1233N/Aend)
1234N/A
12351CHECK(world:has(e, A))
12361CHECK(world:get(e, A) == "test")
12371CHECK(world:get(e, B) == nil)
12381CHECK(world:get(e, C) == nil)
1239N/Aend
1240N/Aend)
1241N/A
12421TEST("world:delete", function()
12431do CASE "invoke OnRemove hooks"
12441local world = world_new()
1245N/A
12461local e1 = world:entity()
12471local e2 = world:entity()
1248N/A
12491local Stable = world:component()
12501world:set(Stable, jecs.OnRemove, function(e)
12511CHECK(e == e1)
1252N/Aend)
1253N/A
12541world:set(e1, Stable, true)
12551world:set(e2, Stable, true)
1256N/A
12571world:delete(e1)
1258N/Aend
12591do CASE "delete recycled entity id used as component"
12601local world = world_new()
12611local id = world:entity()
12621world:add(id, jecs.Component)
1263N/A
12641local e = world:entity()
12651world:set(e, id, 1)
12661CHECK(world:get(e, id) == 1)
12671world:delete(id)
12681local recycled = world:entity()
12691world:add(recycled, jecs.Component)
12701world:set(e, recycled, 1)
12711CHECK(world:has(recycled, jecs.Component))
12721CHECK(world:get(e, recycled) == 1)
1273N/Aend
12740do
12751CASE("bug: Empty entity does not respect cleanup policy")
12761local world = world_new()
12771local parent = world:entity()
12781local tag = world:entity()
1279N/A
12801local child = world:entity()
12811world:add(child, jecs.pair(jecs.ChildOf, parent))
12821world:delete(parent)
1283N/A
12841CHECK(not world:contains(parent))
12851CHECK(not world:contains(child))
1286N/A
12871local entity = world:entity()
12881world:add(entity, tag)
12891world:delete(tag)
12901CHECK(world:contains(entity))
12911CHECK(not world:contains(tag))
12921CHECK(not world:has(entity, tag)) -- => true
1293N/Aend
12941do CASE("should allow deleting components")
12951local world = jecs.World.new()
1296N/A
12971local Health = world:component()
12981local Poison = world:component()
1299N/A
13001local id = world:entity()
13011world:set(id, Poison, 5)
13021world:set(id, Health, 50)
13031local id1 = world:entity()
13041world:set(id1, Poison, 500)
13051world:set(id1, Health, 50)
1306N/A
13071world:delete(id)
13081CHECK(not world:contains(id))
13091CHECK(world:get(id, Poison) == nil)
13101CHECK(world:get(id, Health) == nil)
1311N/A
13121CHECK(world:get(id1, Poison) == 500)
13131CHECK(world:get(id1, Health) == 50)
1314N/Aend
1315N/A
13161do CASE("delete entities using another Entity as component with Delete cleanup action")
13171local world = jecs.World.new()
1318N/A
13191local Health = world:entity()
13201world:add(Health, pair(jecs.OnDelete, jecs.Delete))
13211local Poison = world:component()
1322N/A
13231local id = world:entity()
13241world:set(id, Poison, 5)
13251CHECK_EXPECT_ERR(function()
13261world:set(id, Health, 50 :: any)
1327N/Aend)
13281local id1 = world:entity()
13291world:set(id1, Poison, 500)
13301CHECK_EXPECT_ERR(function()
13311world:set(id1, Health, 50 :: any)
1332N/Aend)
1333N/A
13341CHECK(world:has(id, Poison, Health))
13351CHECK(world:has(id1, Poison, Health))
13361world:delete(Poison)
1337N/A
13381CHECK(world:contains(id))
13391CHECK(not world:has(id, Poison))
13401CHECK(not world:has(id1, Poison))
1341N/A
13421world:delete(Health)
13431CHECK(not world:contains(id))
13441CHECK(not world:contains(id1))
13451CHECK(not world:has(id, Health))
13461CHECK(not world:has(id1, Health))
1347N/Aend
1348N/A
1349N/A
13501do CASE("delete children")
13511local world = jecs.World.new()
1352N/A
13531local Health = world:component()
13541local Poison = world:component()
13551local FriendsWith = world:component()
1356N/A
13571local e = world:entity()
13581world:set(e, Poison, 5)
13591world:set(e, Health, 50)
1360N/A
13611local children = {}
13621for i = 1, 10 do
136310local child = world:entity()
136410world:set(child, Poison, 9999)
136510world:set(child, Health, 100)
136610world:add(child, pair(jecs.ChildOf, e))
136710table.insert(children, child)
1368N/Aend
1369N/A
13701BENCH("delete children of entity", function()
13711world:delete(e)
1372N/Aend)
1373N/A
13741for i, child in children do
137510CHECK(not world:contains(child))
137610CHECK(not world:has(child, pair(jecs.ChildOf, e)))
137710CHECK(not world:has(child, Health))
1378N/Aend
1379N/A
13801e = world:entity()
1381N/A
13821local friends = {}
13831for i = 1, 10 do
138410local friend = world:entity()
138510world:set(friend, Poison, 9999)
138610world:set(friend, Health, 100)
138710world:add(friend, pair(FriendsWith, e))
138810table.insert(friends, friend)
1389N/Aend
1390N/A
13911BENCH("remove friends of entity", function()
13921world:delete(e)
1393N/Aend)
1394N/A
13951local d = debug_world_inspect(world)
13961for i, friend in friends do
139710CHECK(not world:has(friend, pair(FriendsWith, e)))
139810CHECK(world:has(friend, Health))
139910CHECK(world:contains(friend))
1400N/Aend
1401N/Aend
1402N/A
14031do CASE("remove deleted ID from entities")
14041local world = world_new()
14050do
14061local A = world:component()
14071local B = world:component()
14081local C = world:component()
14091local id1 = world:entity()
14101local id2 = world:entity()
14111local id3 = world:entity()
1412N/A
14131world:set(id1, A, true)
1414N/A
14151world:set(id2, A, true)
14161world:set(id2, B, true)
1417N/A
14181world:set(id3, A, true)
14191world:set(id3, B, true)
14201world:set(id3, C, true)
1421N/A
14221world:delete(A)
1423N/A
14241CHECK(not world:has(id1, A))
14251CHECK(not world:has(id2, A))
14261CHECK(not world:has(id3, A))
1427N/A
14281CHECK(world:has(id2, B))
14291CHECK(world:has(id3, B, C))
1430N/A
14311world:delete(C)
1432N/A
14331CHECK(world:has(id2, B))
14341CHECK(world:has(id3, B))
1435N/A
14361CHECK(not world:contains(A))
14371CHECK(not world:contains(C))
1438N/Aend
1439N/A
14400do
14411local A = world:component()
14421world:add(A, pair(jecs.OnDeleteTarget, jecs.Delete))
14431local B = world:component()
14441local C = world:component()
14451world:add(C, pair(jecs.OnDeleteTarget, jecs.Delete))
1446N/A
14471local id1 = world:entity()
14481local id2 = world:entity()
14491local id3 = world:entity()
1450N/A
14511world:set(id1, C, true)
1452N/A
14531world:set(id2, pair(A, id1), true)
14541world:set(id2, B, true)
1455N/A
14561world:set(id3, B, true)
14571world:set(id3, pair(C, id2), true)
1458N/A
14591world:delete(id1)
1460N/A
14611CHECK(not world:contains(id1))
14621CHECK(not world:contains(id2))
14631CHECK(not world:contains(id3))
1464N/Aend
1465N/A
1466N/A
14670do
14681local A = world:component()
14691local B = world:component()
14701local C = world:component()
14711local id1 = world:entity()
14721local id2 = world:entity()
14731local id3 = world:entity()
1474N/A
1475N/A
14761world:set(id2, A, true)
14771world:set(id2, pair(B, id1), true)
1478N/A
14791world:set(id3, A, true)
14801world:set(id3, pair(B, id1), true)
14811world:set(id3, C, true)
1482N/A
14831world:delete(id1)
1484N/A
14851CHECK(not world:contains(id1))
14861CHECK(world:contains(id2))
14871CHECK(world:contains(id3))
1488N/A
14891CHECK(world:has(id2, A))
14901CHECK(world:has(id3, A, C))
1491N/A
14921CHECK(not world:target(id2, B))
14931CHECK(not world:target(id3, B))
1494N/Aend
1495N/Aend
1496N/A
14970do
14981CASE("fast delete")
14991local world = jecs.World.new()
1500N/A
15011local entities = {}
15021local Health = world:component()
15031local Poison = world:component()
1504N/A
15051for i = 1, 100 do
1506100local child = world:entity()
1507100world:set(child, Poison, 9999)
1508100world:set(child, Health, 100)
1509100table.insert(entities, child)
1510N/Aend
1511N/A
15121BENCH("simple deletion of entity", function()
15131for i = 1, START(100) do
1514100local e = entities[i]
1515100world:delete(e)
1516N/Aend
1517N/Aend)
1518N/A
15191for _, entity in entities do
1520100CHECK(not world:contains(entity))
1521N/Aend
1522N/Aend
1523N/A
15240do
15251CASE("cycle")
15261local world = jecs.World.new()
15271local Likes = world:component()
15281world:add(Likes, pair(jecs.OnDeleteTarget, jecs.Delete))
15291local bob = world:entity()
15301local alice = world:entity()
1531N/A
15321world:add(bob, pair(Likes, alice))
15331world:add(alice, pair(Likes, bob))
1534N/A
15351world:delete(bob)
15361CHECK(not world:contains(bob))
15371CHECK(not world:contains(alice))
1538N/Aend
1539N/Aend)
1540N/A
15411TEST("world:target", function()
15421do CASE("nth index")
15431local world = world_new()
15441local A = world:component()
15451world:set(A, jecs.Name, "A")
15461local B = world:component()
15471world:set(B, jecs.Name, "B")
15481local C = world:component()
15491world:set(C, jecs.Name, "C")
15501local D = world:component()
15511world:set(D, jecs.Name, "D")
15521local E = world:component()
15531world:set(E, jecs.Name, "E")
15541local e = world:entity()
1555N/A
15561world:add(e, pair(A, B))
15571world:add(e, pair(A, C))
15581world:add(e, pair(A, D))
15591world:add(e, pair(A, E))
15601world:add(e, pair(B, C))
15611world:add(e, pair(B, D))
15621world:add(e, pair(C, D))
1563N/A
15641CHECK(pair(A, B) < pair(A, C))
15651CHECK(pair(A, C) < pair(A, D))
15661CHECK(pair(C, A) < pair(C, D))
1567N/A
15681local records = debug_world_inspect(world).records(e)
15691CHECK(jecs.pair_first(world, pair(B, C)) == B)
15701local r = jecs.entity_index_try_get(world.entity_index, e)
15711local archetype = r.archetype
15721local counts = archetype.counts
15731CHECK(counts[pair(A, __)] == 4)
15741CHECK(records[pair(B, C)] > records[pair(A, E)])
15751CHECK(world:target(e, A, 0) == B)
15761CHECK(world:target(e, A, 1) == C)
15771CHECK(world:target(e, A, 2) == D)
15781CHECK(world:target(e, A, 3) == E)
15791CHECK(world:target(e, B, 0) == C)
15801CHECK(world:target(e, B, 1) == D)
15811CHECK(world:target(e, C, 0) == D)
15821CHECK(world:target(e, C, 1) == nil)
1583N/A
15841CHECK(archetype.records[pair(A, B)] == 1)
15851CHECK(archetype.records[pair(A, C)] == 2)
15861CHECK(archetype.records[pair(A, D)] == 3)
15871CHECK(archetype.records[pair(A, E)] == 4)
1588N/A
15891CHECK(world:target(e, C, 0) == D)
15901CHECK(world:target(e, C, 1) == nil)
1591N/Aend
1592N/A
15930do
15941CASE("infer index when unspecified")
15951local world = world_new()
15961local A = world:component()
15971local B = world:component()
15981local C = world:component()
15991local D = world:component()
16001local e = world:entity()
1601N/A
16021world:add(e, pair(A, B))
16031world:add(e, pair(A, C))
16041world:add(e, pair(B, C))
16051world:add(e, pair(B, D))
16061world:add(e, pair(C, D))
1607N/A
16081CHECK(world:target(e, A) == world:target(e, A, 0))
16091CHECK(world:target(e, B) == world:target(e, B, 0))
16101CHECK(world:target(e, C) == world:target(e, C, 0))
1611N/Aend
1612N/A
16130do
16141CASE("loop until no target")
16151local world = world_new()
1616N/A
16171local ROOT = world:entity()
16181local e1 = world:entity()
16191local targets = {}
1620N/A
16211for i = 1, 10 do
162210local target = world:entity()
162310targets[i] = target
162410world:add(e1, pair(ROOT, target))
1625N/Aend
1626N/A
16271local i = 0
16281local target = world:target(e1, ROOT, 0)
16291while target do
163010i += 1
163110CHECK(targets[i] == target)
163210target = world:target(e1, ROOT, i)
1633N/Aend
1634N/A
16351CHECK(i == 10)
1636N/Aend
1637N/Aend)
1638N/A
16391TEST("world:contains", function()
16401local world = jecs.World.new()
16411local id = world:entity()
16421CHECK(world:contains(id))
1643N/A
16440do
16451CASE("should not exist after delete")
16461world:delete(id)
16471CHECK(not world:contains(id))
1648N/Aend
1649N/Aend)
1650N/A
16511TEST("Hooks", function()
16521do CASE "OnAdd"
16531local world = jecs.World.new()
16541local Transform = world:component()
16551local e1 = world:entity()
16561world:set(Transform, jecs.OnAdd, function(entity)
16571CHECK(e1 == entity)
1658N/Aend)
16591world:add(e1, Transform)
1660N/Aend
1661N/A
16621do CASE "OnSet"
16631local world = jecs.World.new()
16641local Number = world:component()
16651local e1 = world:entity()
1666N/A
16671world:set(Number, jecs.OnSet, function(entity, data)
16681CHECK(e1 == entity)
16691CHECK(data == world:get(entity, Number))
16701CHECK(data == 1)
1671N/Aend)
16721world:set(e1, Number, 1)
1673N/Aend
1674N/A
16751do CASE("OnRemove")
16760do
1677N/A-- basic
16781local world = jecs.World.new()
16791local A = world:component() :: Entity
16801local e1 = world:entity()
16811world:set(A, jecs.OnRemove, function(entity)
16821CHECK(e1 == entity)
16831CHECK(world:has(e1, A))
1684N/Aend)
16851world:add(e1, A)
1686N/A
16871world:remove(e1, A)
16881CHECK(not world:has(e1, A))
1689N/Aend
16900do
1691N/A-- [BUG] https://github.com/Ukendio/jecs/issues/118
16921local world = world_new()
16931local A = world:component()
16941local B = world:component()
16951local e = world:entity()
1696N/A
16971world:set(A, jecs.OnRemove, function(entity)
16981world:set(entity, B, true)
16991CHECK(world:get(entity, A))
17001CHECK(world:get(entity, B))
1701N/Aend)
1702N/A
17031world:set(e, A, true)
17041world:remove(e, A)
17051CHECK(not world:get(e, A))
17061CHECK(world:get(e, B))
1707N/Aend
1708N/Aend
1709N/Aend)
1710N/A
17111TEST("change tracking", function()
17121do CASE "#1"
17131local world = world_new()
17141local Foo = world:component() :: Entity
17151local Previous = jecs.Rest
1716N/A
17171local q1 = world
17181:query(Foo)
17191:without(pair(Previous, Foo))
17200:cached()
1721N/A
17221local e1 = world:entity()
17231world:set(e1, Foo, 1)
17241local e2 = world:entity()
17251world:set(e2, Foo, 2)
1726N/A
17271local i = 0
17281for e, new in q1 :: any do
17292i += 1
17302world:set(e, pair(Previous, Foo), new)
1731N/Aend
1732N/A
17331CHECK(i == 2)
17341local j = 0
17351for e, new in q1 :: any do
17360j += 1
17370world:set(e, pair(Previous, Foo), new)
1738N/Aend
1739N/A
17401CHECK(j == 0)
1741N/Aend
1742N/A
17431do CASE "#2"
17441local world = world_new()
17451local component = world:component() :: Entity
17461local tag = world:entity()
17471local previous = jecs.Rest
1748N/A
17491local q1 = world:query(component):without(pair(previous, component), tag):cached()
1750N/A
17511local testEntity = world:entity()
1752N/A
17531world:set(testEntity, component, 10)
1754N/A
17551local i = 0
17561for entity, number in q1 :: any do
17571i += 1
17581world:add(testEntity, tag)
1759N/Aend
1760N/A
17611CHECK(i == 1)
1762N/A
17631for e, n in q1 :: any do
17640world:set(e, pair(previous, component), n)
1765N/Aend
1766N/Aend
1767N/A
1768N/Aend)
1769N/A
17701TEST("repro", function()
17711do CASE "#1"
17721local world = world_new()
17731local reproEntity = world:component()
17741local components = { Cooldown = world:component() :: jecs.Entity }
17751world:set(reproEntity, components.Cooldown, 2)
1776N/A
17771local function updateCooldowns(dt: number)
17782local toRemove = {}
1779N/A
17802local it = world:query(components.Cooldown):iter()
17812for id, cooldown in it do
17822cooldown -= dt
1783N/A
17842if cooldown <= 0 then
17851table.insert(toRemove, id)
1786N/A-- world:remove(id, components.Cooldown)
17870else
17881world:set(id, components.Cooldown, cooldown)
1789N/Aend
1790N/Aend
1791N/A
17922for _, id in toRemove do
17931world:remove(id, components.Cooldown)
17941CHECK(not world:get(id, components.Cooldown))
1795N/Aend
1796N/Aend
1797N/A
17981updateCooldowns(1.5)
17991updateCooldowns(1.5)
1800N/Aend
1801N/A
18021do CASE "#2" -- ISSUE #171
18031local world = world_new()
18041local component1 = world:component()
18051local tag1 = world:entity()
1806N/A
18071local query = world:query(component1):with(tag1):cached()
1808N/A
18091local entity = world:entity()
18101world:set(entity, component1, "some data")
1811N/A
18121local counter = 0
18131for x in query:iter() do
18140counter += 1
1815N/Aend
18161CHECK(counter == 0)
1817N/Aend
1818N/Aend)
1819N/A
18201TEST("wildcard query", function()
18211do CASE "#1"
18221local world = world_new()
18231local pair = jecs.pair
1824N/A
18251local Relation = world:entity()
18261local Wildcard = jecs.Wildcard
18271local A = world:entity()
1828N/A
18291local relationship = pair(Relation, Wildcard)
18301local query = world:query(relationship):cached()
1831N/A
18321local entity = world:entity()
1833N/A
18341local p = pair(Relation, A)
18351CHECK(jecs.pair_first(world, p) == Relation)
18361CHECK(jecs.pair_second(world, p) == A)
18371local w = dwi(world)
18381world:add(entity, pair(Relation, A))
1839N/A
18401local counter = 0
18411for e in query:iter() do
18421counter += 1
1843N/Aend
18441CHECK(counter == 1)
1845N/Aend
18461do CASE "#2"
18471local world = world_new()
18481local pair = jecs.pair
1849N/A
18501local Relation = world:entity()
18511local Wildcard = jecs.Wildcard
18521local A = world:entity()
1853N/A
18541local relationship = pair(Relation, Wildcard)
1855N/A
18561local entity = world:entity()
1857N/A
18581world:add(entity, pair(Relation, A))
1859N/A
18601local counter = 0
18611for e in world:query(relationship):iter() do
18621counter += 1
1863N/Aend
18641CHECK(counter == 1)
1865N/Aend
18661do CASE "#3"
18671local world = world_new()
18681local pair = jecs.pair
1869N/A
18701local Relation = world:entity()
18711local Wildcard = jecs.Wildcard
18721local A = world:entity()
1873N/A
18741local entity = world:entity()
1875N/A
18761world:add(entity, pair(Relation, A))
1877N/A
18781local relationship = pair(Relation, Wildcard)
18791local query = world:query(relationship):cached()
1880N/A
18811local counter = 0
18821for e in query:iter() do
18831counter += 1
1884N/Aend
18851CHECK(counter == 1)
1886N/Aend
1887N/Aend)
1888N/A
18891TEST("world:delete() invokes OnRemove hook", function()
18901do CASE "#1"
18911local world = world_new()
1892N/A
18931local A = world:entity()
18941local entity = world:entity()
1895N/A
18961local called = false
18971world:set(A, jecs.OnRemove, function(e)
18981called = true
1899N/Aend)
1900N/A
19011world:add(entity, A)
19021world:delete(entity)
1903N/A
19041CHECK(called)
1905N/Aend
19061do CASE "#2"
19071local world = world_new()
19081local pair = jecs.pair
1909N/A
19101local Relation = world:entity()
19111local A = world:entity()
19121local B = world:entity()
1913N/A
19141world:add(Relation, pair(jecs.OnDelete, jecs.Delete))
1915N/A
19161local entity = world:entity()
1917N/A
19181local called = false
19191world:set(A, jecs.OnRemove, function(e)
19201called = true
1921N/Aend)
1922N/A
19231world:add(entity, A)
19241world:add(entity, pair(Relation, B))
1925N/A
19261world:delete(B)
1927N/A
19281CHECK(called)
1929N/Aend
19301do CASE "#3"
19311local world = world_new()
19321local pair = jecs.pair
1933N/A
19341local viewingContainer = world:entity()
19351local character = world:entity()
19361local container = world:entity()
1937N/A
19381local called = false
19391world:set(viewingContainer, jecs.OnRemove, function(e)
19401called = true
1941N/Aend)
1942N/A
19431world:add(character, pair(viewingContainer, container))
1944N/A
19451world:delete(container)
1946N/A
19471CHECK(called)
1948N/Aend
1949N/Aend)
19501FINISH()
\ No newline at end of file diff --git a/default.project.json b/default.project.json new file mode 100644 index 0000000..d4531a0 --- /dev/null +++ b/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "jecs", + "tree": { + "$path": "jecs.luau" + } +} diff --git a/demo/.config/blink b/demo/.config/blink new file mode 100644 index 0000000..3675cf7 --- /dev/null +++ b/demo/.config/blink @@ -0,0 +1,18 @@ +option ClientOutput = "../net/client.luau" +option ServerOutput = "../net/server.luau" + +event UpdateTransform { + From: Server, + Type: Unreliable, + Call: SingleSync, + Poll: true, + Data: (f64, CFrame) +} + +event SpawnMob { + From: Server, + Type: Reliable, + Call: SingleSync, + Poll: true, + Data: (f64, CFrame, u8) +} diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 0000000..cf9d94d --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,6 @@ +# Project place file +/example.rbxlx + +# Roblox Studio lock files +/*.rbxlx.lock +/*.rbxl.lock \ No newline at end of file diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..241cc25 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,15 @@ +# Demo + +## Build with Rojo +To build the place, run the following commands from the root of the repository: + +```bash +cd demo +rojo build -o "demo.rbxl" +``` + +Next, open `demo.rbxl` in Roblox Studio and start the Rojo server: + +```bash +rojo serve +``` diff --git a/demo/default.project.json b/demo/default.project.json new file mode 100644 index 0000000..b3b41f9 --- /dev/null +++ b/demo/default.project.json @@ -0,0 +1,76 @@ +{ + "name": "demo", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + "$path": "src/ReplicatedStorage", + "ecs": { + "$path": "../jecs.luau" + }, + "net": { + "$path": "net/client.luau" + }, + "Packages": { + "$path": "Packages" + } + }, + "ServerScriptService": { + "$className": "ServerScriptService", + "$path": "src/ServerScriptService", + "net": { + "$path": "net/server.luau" + } + }, + "Workspace": { + "$properties": { + "FilteringEnabled": true + }, + "Baseplate": { + "$className": "Part", + "$properties": { + "Anchored": true, + "Color": [ + 0.38823, + 0.37254, + 0.38823 + ], + "Locked": true, + "Position": [ + 0, + -10, + 0 + ], + "Size": [ + 512, + 20, + 512 + ] + } + } + }, + "Lighting": { + "$properties": { + "Ambient": [ + 0, + 0, + 0 + ], + "Brightness": 2, + "GlobalShadows": true, + "Outlines": false, + "Technology": "Voxel" + } + }, + "SoundService": { + "$properties": { + "RespectFilteringEnabled": true + } + }, + "StarterPlayer": { + "StarterPlayerScripts": { + "$path": "src/StarterPlayer/StarterPlayerScripts" + } + } + } +} diff --git a/demo/net/client.luau b/demo/net/client.luau new file mode 100644 index 0000000..062dcab --- /dev/null +++ b/demo/net/client.luau @@ -0,0 +1,336 @@ +--!strict +--!native +--!optimize 2 +--!nolint LocalShadow +--#selene: allow(shadowing) +-- File generated by Blink v0.14.1 (https://github.com/1Axen/Blink) +-- This file is not meant to be edited + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") + +if not RunService:IsClient() then + error("Client network module can only be required from the client.") +end + +local Reliable: RemoteEvent = ReplicatedStorage:WaitForChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent +local Unreliable: UnreliableRemoteEvent = + ReplicatedStorage:WaitForChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent + +local Invocations = 0 + +local SendSize = 64 +local SendOffset = 0 +local SendCursor = 0 +local SendBuffer = buffer.create(64) +local SendInstances = {} + +local RecieveCursor = 0 +local RecieveBuffer = buffer.create(64) + +local RecieveInstances = {} +local RecieveInstanceCursor = 0 + +type Entry = { + value: any, + next: Entry?, +} + +type Queue = { + head: Entry?, + tail: Entry?, +} + +type BufferSave = { + Size: number, + Cursor: number, + Buffer: buffer, + Instances: { Instance }, +} + +local function Read(Bytes: number) + local Offset = RecieveCursor + RecieveCursor += Bytes + return Offset +end + +local function Save(): BufferSave + return { + Size = SendSize, + Cursor = SendCursor, + Buffer = SendBuffer, + Instances = SendInstances, + } +end + +local function Load(Save: BufferSave?) + if Save then + SendSize = Save.Size + SendCursor = Save.Cursor + SendOffset = Save.Cursor + SendBuffer = Save.Buffer + SendInstances = Save.Instances + return + end + + SendSize = 64 + SendCursor = 0 + SendOffset = 0 + SendBuffer = buffer.create(64) + SendInstances = {} +end + +local function Invoke() + if Invocations == 255 then + Invocations = 0 + end + + local Invocation = Invocations + Invocations += 1 + return Invocation +end + +local function Allocate(Bytes: number) + local InUse = (SendCursor + Bytes) + if InUse > SendSize then + --> Avoid resizing the buffer for every write + while InUse > SendSize do + SendSize *= 1.5 + end + + local Buffer = buffer.create(SendSize) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + SendBuffer = Buffer + end + + SendOffset = SendCursor + SendCursor += Bytes + + return SendOffset +end + +local function CreateQueue(): Queue + return { + head = nil, + tail = nil, + } +end + +local function Pop(queue: Queue): any + local head = queue.head + if head == nil then + return + end + + queue.head = head.next + return head.value +end + +local function Push(queue: Queue, value: any) + local entry: Entry = { + value = value, + next = nil, + } + + if queue.tail ~= nil then + queue.tail.next = entry + end + + queue.tail = entry + + if queue.head == nil then + queue.head = entry + end +end + +local Types = {} +local Calls = table.create(256) + +local Events: any = { + Reliable = table.create(256), + Unreliable = table.create(256), +} + +local Queue: any = { + Reliable = table.create(256), + Unreliable = table.create(256), +} + +Queue.Unreliable[0] = CreateQueue() +Queue.Reliable[0] = CreateQueue() + +function Types.ReadEVENT_UpdateTransform(): (number, CFrame) + -- Read BLOCK: 32 bytes + local BLOCK_START = Read(32) + local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0) + local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8) + local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12) + local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16) + local Position = Vector3.new(X, Y, Z) + local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20) + local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24) + local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28) + local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ) + return Value1, Value2 +end + +function Types.WriteEVENT_UpdateTransform(Value1: number, Value2: CFrame): () + -- Allocate BLOCK: 33 bytes + local BLOCK_START = Allocate(33) + buffer.writeu8(SendBuffer, BLOCK_START + 0, 0) + buffer.writef64(SendBuffer, BLOCK_START + 1, Value1) + local Vector = Value2.Position + buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X) + buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y) + buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z) + local rX, rY, rZ = Value2:ToOrientation() + buffer.writef32(SendBuffer, BLOCK_START + 21, rX) + buffer.writef32(SendBuffer, BLOCK_START + 25, rY) + buffer.writef32(SendBuffer, BLOCK_START + 29, rZ) +end + +function Types.ReadEVENT_SpawnMob(): (number, CFrame, number) + -- Read BLOCK: 33 bytes + local BLOCK_START = Read(33) + local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0) + local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8) + local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12) + local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16) + local Position = Vector3.new(X, Y, Z) + local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20) + local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24) + local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28) + local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ) + local Value3 = buffer.readu8(RecieveBuffer, BLOCK_START + 32) + return Value1, Value2, Value3 +end + +function Types.WriteEVENT_SpawnMob(Value1: number, Value2: CFrame, Value3: number): () + -- Allocate BLOCK: 34 bytes + local BLOCK_START = Allocate(34) + buffer.writeu8(SendBuffer, BLOCK_START + 0, 0) + buffer.writef64(SendBuffer, BLOCK_START + 1, Value1) + local Vector = Value2.Position + buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X) + buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y) + buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z) + local rX, rY, rZ = Value2:ToOrientation() + buffer.writef32(SendBuffer, BLOCK_START + 21, rX) + buffer.writef32(SendBuffer, BLOCK_START + 25, rY) + buffer.writef32(SendBuffer, BLOCK_START + 29, rZ) + buffer.writeu8(SendBuffer, BLOCK_START + 33, Value3) +end + +local function StepReplication() + if SendCursor <= 0 then + return + end + + local Buffer = buffer.create(SendCursor) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + Reliable:FireServer(Buffer, SendInstances) + + SendSize = 64 + SendCursor = 0 + SendOffset = 0 + SendBuffer = buffer.create(64) + table.clear(SendInstances) +end + +local Elapsed = 0 +RunService.Heartbeat:Connect(function(DeltaTime: number) + Elapsed += DeltaTime + if Elapsed >= (1 / 61) then + Elapsed -= (1 / 61) + StepReplication() + end +end) + +Reliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: { Instance }) + RecieveCursor = 0 + RecieveBuffer = Buffer + RecieveInstances = Instances + RecieveInstanceCursor = 0 + local Size = buffer.len(RecieveBuffer) + while RecieveCursor < Size do + -- Read BLOCK: 1 bytes + local BLOCK_START = Read(1) + local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0) + if Index == 0 then + Push(Queue.Reliable[0], table.pack(Types.ReadEVENT_SpawnMob())) + end + end +end) + +Unreliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: { Instance }) + RecieveCursor = 0 + RecieveBuffer = Buffer + RecieveInstances = Instances + RecieveInstanceCursor = 0 + local Size = buffer.len(RecieveBuffer) + while RecieveCursor < Size do + -- Read BLOCK: 1 bytes + local BLOCK_START = Read(1) + local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0) + if Index == 0 then + Push(Queue.Unreliable[0], table.pack(Types.ReadEVENT_UpdateTransform())) + end + end +end) + +return { + StepReplication = StepReplication, + + UpdateTransform = { + Iter = function(): () -> (number, number, CFrame) + local index = 0 + local queue = Queue.Unreliable[0] + return function(): (number, number, CFrame) + index += 1 + local arguments = Pop(queue) + if arguments ~= nil then + return index, unpack(arguments, 1, arguments.n) + end + return + end + end, + Next = function(): () -> (number, number, CFrame) + local index = 0 + local queue = Queue.Unreliable[0] + return function(): (number, number, CFrame) + index += 1 + local arguments = Pop(queue) + if arguments ~= nil then + return index, unpack(arguments, 1, arguments.n) + end + return + end + end, + }, + SpawnMob = { + Iter = function(): () -> (number, number, CFrame, number) + local index = 0 + local queue = Queue.Reliable[0] + return function(): (number, number, CFrame, number) + index += 1 + local arguments = Pop(queue) + if arguments ~= nil then + return index, unpack(arguments, 1, arguments.n) + end + return + end + end, + Next = function(): () -> (number, number, CFrame, number) + local index = 0 + local queue = Queue.Reliable[0] + return function(): (number, number, CFrame, number) + index += 1 + local arguments = Pop(queue) + if arguments ~= nil then + return index, unpack(arguments, 1, arguments.n) + end + return + end + end, + }, +} diff --git a/demo/net/server.luau b/demo/net/server.luau new file mode 100644 index 0000000..8a8755c --- /dev/null +++ b/demo/net/server.luau @@ -0,0 +1,372 @@ +--!strict +--!native +--!optimize 2 +--!nolint LocalShadow +--#selene: allow(shadowing) +-- File generated by Blink v0.14.1 (https://github.com/1Axen/Blink) +-- This file is not meant to be edited + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") + +if not RunService:IsServer() then + error("Server network module can only be required from the server.") +end + +local Reliable: RemoteEvent = ReplicatedStorage:FindFirstChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent +if not Reliable then + local RemoteEvent = Instance.new("RemoteEvent") + RemoteEvent.Name = "BLINK_RELIABLE_REMOTE" + RemoteEvent.Parent = ReplicatedStorage + Reliable = RemoteEvent +end + +local Unreliable: UnreliableRemoteEvent = + ReplicatedStorage:FindFirstChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent +if not Unreliable then + local UnreliableRemoteEvent = Instance.new("UnreliableRemoteEvent") + UnreliableRemoteEvent.Name = "BLINK_UNRELIABLE_REMOTE" + UnreliableRemoteEvent.Parent = ReplicatedStorage + Unreliable = UnreliableRemoteEvent +end + +local Invocations = 0 + +local SendSize = 64 +local SendOffset = 0 +local SendCursor = 0 +local SendBuffer = buffer.create(64) +local SendInstances = {} + +local RecieveCursor = 0 +local RecieveBuffer = buffer.create(64) + +local RecieveInstances = {} +local RecieveInstanceCursor = 0 + +type Entry = { + value: any, + next: Entry?, +} + +type Queue = { + head: Entry?, + tail: Entry?, +} + +type BufferSave = { + Size: number, + Cursor: number, + Buffer: buffer, + Instances: { Instance }, +} + +local function Read(Bytes: number) + local Offset = RecieveCursor + RecieveCursor += Bytes + return Offset +end + +local function Save(): BufferSave + return { + Size = SendSize, + Cursor = SendCursor, + Buffer = SendBuffer, + Instances = SendInstances, + } +end + +local function Load(Save: BufferSave?) + if Save then + SendSize = Save.Size + SendCursor = Save.Cursor + SendOffset = Save.Cursor + SendBuffer = Save.Buffer + SendInstances = Save.Instances + return + end + + SendSize = 64 + SendCursor = 0 + SendOffset = 0 + SendBuffer = buffer.create(64) + SendInstances = {} +end + +local function Invoke() + if Invocations == 255 then + Invocations = 0 + end + + local Invocation = Invocations + Invocations += 1 + return Invocation +end + +local function Allocate(Bytes: number) + local InUse = (SendCursor + Bytes) + if InUse > SendSize then + --> Avoid resizing the buffer for every write + while InUse > SendSize do + SendSize *= 1.5 + end + + local Buffer = buffer.create(SendSize) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + SendBuffer = Buffer + end + + SendOffset = SendCursor + SendCursor += Bytes + + return SendOffset +end + +local function CreateQueue(): Queue + return { + head = nil, + tail = nil, + } +end + +local function Pop(queue: Queue): any + local head = queue.head + if head == nil then + return + end + + queue.head = head.next + return head.value +end + +local function Push(queue: Queue, value: any) + local entry: Entry = { + value = value, + next = nil, + } + + if queue.tail ~= nil then + queue.tail.next = entry + end + + queue.tail = entry + + if queue.head == nil then + queue.head = entry + end +end + +local Types = {} +local Calls = table.create(256) + +local Events: any = { + Reliable = table.create(256), + Unreliable = table.create(256), +} + +local Queue: any = { + Reliable = table.create(256), + Unreliable = table.create(256), +} + +function Types.ReadEVENT_UpdateTransform(): (number, CFrame) + -- Read BLOCK: 32 bytes + local BLOCK_START = Read(32) + local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0) + local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8) + local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12) + local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16) + local Position = Vector3.new(X, Y, Z) + local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20) + local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24) + local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28) + local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ) + return Value1, Value2 +end + +function Types.WriteEVENT_UpdateTransform(Value1: number, Value2: CFrame): () + -- Allocate BLOCK: 33 bytes + local BLOCK_START = Allocate(33) + buffer.writeu8(SendBuffer, BLOCK_START + 0, 0) + buffer.writef64(SendBuffer, BLOCK_START + 1, Value1) + local Vector = Value2.Position + buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X) + buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y) + buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z) + local rX, rY, rZ = Value2:ToOrientation() + buffer.writef32(SendBuffer, BLOCK_START + 21, rX) + buffer.writef32(SendBuffer, BLOCK_START + 25, rY) + buffer.writef32(SendBuffer, BLOCK_START + 29, rZ) +end + +function Types.ReadEVENT_SpawnMob(): (number, CFrame, number) + -- Read BLOCK: 33 bytes + local BLOCK_START = Read(33) + local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0) + local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8) + local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12) + local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16) + local Position = Vector3.new(X, Y, Z) + local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20) + local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24) + local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28) + local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ) + local Value3 = buffer.readu8(RecieveBuffer, BLOCK_START + 32) + return Value1, Value2, Value3 +end + +function Types.WriteEVENT_SpawnMob(Value1: number, Value2: CFrame, Value3: number): () + -- Allocate BLOCK: 34 bytes + local BLOCK_START = Allocate(34) + buffer.writeu8(SendBuffer, BLOCK_START + 0, 0) + buffer.writef64(SendBuffer, BLOCK_START + 1, Value1) + local Vector = Value2.Position + buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X) + buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y) + buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z) + local rX, rY, rZ = Value2:ToOrientation() + buffer.writef32(SendBuffer, BLOCK_START + 21, rX) + buffer.writef32(SendBuffer, BLOCK_START + 25, rY) + buffer.writef32(SendBuffer, BLOCK_START + 29, rZ) + buffer.writeu8(SendBuffer, BLOCK_START + 33, Value3) +end + +local PlayersMap: { [Player]: BufferSave } = {} + +Players.PlayerRemoving:Connect(function(Player) + PlayersMap[Player] = nil +end) + +local function StepReplication() + for Player, Send in PlayersMap do + if Send.Cursor <= 0 then + continue + end + + local Buffer = buffer.create(Send.Cursor) + buffer.copy(Buffer, 0, Send.Buffer, 0, Send.Cursor) + Reliable:FireClient(Player, Buffer, Send.Instances) + + Send.Size = 64 + Send.Cursor = 0 + Send.Buffer = buffer.create(64) + table.clear(Send.Instances) + end +end + +RunService.Heartbeat:Connect(StepReplication) + +Reliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: { Instance }) + RecieveCursor = 0 + RecieveBuffer = Buffer + RecieveInstances = Instances + RecieveInstanceCursor = 0 + local Size = buffer.len(RecieveBuffer) + while RecieveCursor < Size do + -- Read BLOCK: 1 bytes + local BLOCK_START = Read(1) + local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0) + end +end) + +Unreliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: { Instance }) + RecieveCursor = 0 + RecieveBuffer = Buffer + RecieveInstances = Instances + RecieveInstanceCursor = 0 + local Size = buffer.len(RecieveBuffer) + while RecieveCursor < Size do + -- Read BLOCK: 1 bytes + local BLOCK_START = Read(1) + local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0) + end +end) + +return { + StepReplication = StepReplication, + + UpdateTransform = { + Fire = function(Player: Player, Value1: number, Value2: CFrame): () + Load() + Types.WriteEVENT_UpdateTransform(Value1, Value2) + local Buffer = buffer.create(SendCursor) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + Unreliable:FireClient(Player, Buffer, SendInstances) + end, + FireAll = function(Value1: number, Value2: CFrame): () + Load() + Types.WriteEVENT_UpdateTransform(Value1, Value2) + local Buffer = buffer.create(SendCursor) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + Unreliable:FireAllClients(Buffer, SendInstances) + end, + FireList = function(List: { Player }, Value1: number, Value2: CFrame): () + Load() + Types.WriteEVENT_UpdateTransform(Value1, Value2) + local Buffer = buffer.create(SendCursor) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + for _, Player in List do + Unreliable:FireClient(Player, Buffer, SendInstances) + end + end, + FireExcept = function(Except: Player, Value1: number, Value2: CFrame): () + Load() + Types.WriteEVENT_UpdateTransform(Value1, Value2) + local Buffer = buffer.create(SendCursor) + buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor) + for _, Player in Players:GetPlayers() do + if Player == Except then + continue + end + Unreliable:FireClient(Player, Buffer, SendInstances) + end + end, + }, + SpawnMob = { + Fire = function(Player: Player, Value1: number, Value2: CFrame, Value3: number): () + Load(PlayersMap[Player]) + Types.WriteEVENT_SpawnMob(Value1, Value2, Value3) + PlayersMap[Player] = Save() + end, + FireAll = function(Value1: number, Value2: CFrame, Value3: number): () + Load() + Types.WriteEVENT_SpawnMob(Value1, Value2, Value3) + local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances + for _, Player in Players:GetPlayers() do + Load(PlayersMap[Player]) + local Position = Allocate(Size) + buffer.copy(SendBuffer, Position, Buffer, 0, Size) + table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances) + PlayersMap[Player] = Save() + end + end, + FireList = function(List: { Player }, Value1: number, Value2: CFrame, Value3: number): () + Load() + Types.WriteEVENT_SpawnMob(Value1, Value2, Value3) + local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances + for _, Player in List do + Load(PlayersMap[Player]) + local Position = Allocate(Size) + buffer.copy(SendBuffer, Position, Buffer, 0, Size) + table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances) + PlayersMap[Player] = Save() + end + end, + FireExcept = function(Except: Player, Value1: number, Value2: CFrame, Value3: number): () + Load() + Types.WriteEVENT_SpawnMob(Value1, Value2, Value3) + local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances + for _, Player in Players:GetPlayers() do + if Player == Except then + continue + end + Load(PlayersMap[Player]) + local Position = Allocate(Size) + buffer.copy(SendBuffer, Position, Buffer, 0, Size) + table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances) + PlayersMap[Player] = Save() + end + end, + }, +} diff --git a/demo/src/ReplicatedStorage/ecs_init.luau b/demo/src/ReplicatedStorage/ecs_init.luau new file mode 100644 index 0000000..d454567 --- /dev/null +++ b/demo/src/ReplicatedStorage/ecs_init.luau @@ -0,0 +1,4 @@ +_G.JECS_DEBUG = true +_G.JECS_HI_COMPONENT_ID = 32 +require(game:GetService("ReplicatedStorage").ecs) +return diff --git a/demo/src/ReplicatedStorage/start.luau b/demo/src/ReplicatedStorage/start.luau new file mode 100644 index 0000000..1c4f373 --- /dev/null +++ b/demo/src/ReplicatedStorage/start.luau @@ -0,0 +1,34 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local UserInputService = game:GetService("UserInputService") +local jabby = require(ReplicatedStorage.Packages.jabby) +local std = ReplicatedStorage.std +local scheduler = require(std.scheduler) +local world = require(std.world) + +local function start(modules) + for _, module in modules do + require(module) + end + local events = scheduler.COLLECT() + scheduler.BEGIN(events) + jabby.set_check_function(function(player) + return true + end) + if RunService:IsClient() then + local player = game:GetService("Players").LocalPlayer + local playergui = player:WaitForChild("PlayerGui") + local client = jabby.obtain_client() + UserInputService.InputBegan:Connect(function(input) + if input.KeyCode == Enum.KeyCode.F4 then + local home = playergui:FindFirstChild("Home") + if home then + home:Destroy() + end + client.spawn_app(client.apps.home) + end + end) + end +end + +return start diff --git a/demo/src/ReplicatedStorage/std/bt.luau b/demo/src/ReplicatedStorage/std/bt.luau new file mode 100644 index 0000000..81d11c5 --- /dev/null +++ b/demo/src/ReplicatedStorage/std/bt.luau @@ -0,0 +1,40 @@ +--!optimize 2 +--!native + +-- original author @centau + +local FAILURE = -1 +local RUNNING = 0 +local SUCCESS = 1 + +local function SEQUENCE(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status <= RUNNING then + return status + end + end + return SUCCESS + end +end + +local function FALLBACK(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status > FAILURE then + return status + end + end + return FAILURE + end +end + +local bt = { + SEQUENCE = SEQUENCE, + FALLBACK = FALLBACK, + RUNNING = RUNNING, +} + +return bt diff --git a/demo/src/ReplicatedStorage/std/collect.luau b/demo/src/ReplicatedStorage/std/collect.luau new file mode 100644 index 0000000..edd9870 --- /dev/null +++ b/demo/src/ReplicatedStorage/std/collect.luau @@ -0,0 +1,67 @@ +--!nonstrict + +--[[ + local signal = Signal.new() :: Signal.Signal + local events = collect(signal) + local function system(world) + for id, str1, str2 in events do + -- + end + end +]] + +--[[ +original author by @memorycode + +MIT License + +Copyright (c) 2024 Michael + +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. +--]] + +type Signal = { [any]: any } +local function collect(event: Signal) + local storage = {} + local mt = {} + local iter = function() + local n = #storage + return function() + if n <= 0 then + mt.__iter = nil + return nil + end + + n -= 1 + return n + 1, unpack(table.remove(storage, 1) :: any) + end + end + + local disconnect = event:Connect(function(...) + table.insert(storage, { ... }) + mt.__iter = iter + end) + + setmetatable(storage, mt) + return (storage :: any) :: () -> (number, T...), function() + disconnect() + end +end + +return collect diff --git a/demo/src/ReplicatedStorage/std/components.luau b/demo/src/ReplicatedStorage/std/components.luau new file mode 100644 index 0000000..0e2d406 --- /dev/null +++ b/demo/src/ReplicatedStorage/std/components.luau @@ -0,0 +1,30 @@ +local jecs = require(game:GetService("ReplicatedStorage").ecs) +local world = require(script.Parent.world) + +type Entity = jecs.Entity +local components: { + Character: Entity, + Mob: Entity, + Model: Entity, + Player: Entity, + Target: Entity, + Transform: Entity<{ new: CFrame, old: CFrame }>, + Velocity: Entity, + Previous: Entity, +} = + { + Character = world:component(), + Mob = world:component(), + Model = world:component(), + Player = world:component(), + Target = world:component(), + Transform = world:component(), + Velocity = world:component(), + Previous = world:component(), + } + +for name, component in components :: {[string]: jecs.Entity} do + world:set(component, jecs.Name, name) +end + +return table.freeze(components) diff --git a/demo/src/ReplicatedStorage/std/interval.luau b/demo/src/ReplicatedStorage/std/interval.luau new file mode 100644 index 0000000..99cda27 --- /dev/null +++ b/demo/src/ReplicatedStorage/std/interval.luau @@ -0,0 +1,19 @@ +local function interval(s) + local pin + + local function throttle() + if not pin then + pin = os.clock() + end + + local elapsed = os.clock() - pin > s + if elapsed then + pin = os.clock() + end + + return elapsed + end + return throttle +end + +return interval \ No newline at end of file diff --git a/demo/src/ReplicatedStorage/std/phases.luau b/demo/src/ReplicatedStorage/std/phases.luau new file mode 100644 index 0000000..30e6bbe --- /dev/null +++ b/demo/src/ReplicatedStorage/std/phases.luau @@ -0,0 +1,14 @@ +local std = game:GetService("ReplicatedStorage").std +local Players = game:GetService("Players") + +local scheduler = require(std.scheduler) +local PHASE = scheduler.PHASE + +return { + PlayerAdded = PHASE({ + event = Players.PlayerAdded + }), + PlayerRemoved = PHASE({ + event = Players.PlayerRemoving + }) +} diff --git a/demo/src/ReplicatedStorage/std/ref.luau b/demo/src/ReplicatedStorage/std/ref.luau new file mode 100644 index 0000000..6700f14 --- /dev/null +++ b/demo/src/ReplicatedStorage/std/ref.luau @@ -0,0 +1,26 @@ +local world = require(script.Parent.world) +local jecs = require(game:GetService("ReplicatedStorage").ecs) +local refs: {[any]: jecs.Entity} = {} + +local function fini(key): () -> () + return function() + refs[key] = nil + end +end + +local function noop() end + +local function ref(key): (jecs.Entity, () -> ()) + if not key then + return world:entity(), noop + end + local e = refs[key] + if not e then + e = world:entity() + refs[key] = e + end + -- Cannot cache handles because they will get invalidated + return e, fini(key) +end + +return ref diff --git a/demo/src/ReplicatedStorage/std/scheduler.luau b/demo/src/ReplicatedStorage/std/scheduler.luau new file mode 100644 index 0000000..5c0092a --- /dev/null +++ b/demo/src/ReplicatedStorage/std/scheduler.luau @@ -0,0 +1,171 @@ +--!native +--!optimize 2 +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local jabby = require(ReplicatedStorage.Packages.jabby) +local jecs = require(ReplicatedStorage.ecs) +local pair = jecs.pair +local Name = jecs.Name + +type World = jecs.World +type Entity = jecs.Entity +type Id = jecs.Id + +type System = { + callback: (world: World) -> (), + id: number, +} + +type Systems = { System } + +type Events = { + RenderStepped: Systems, + Heartbeat: Systems, +} + +local world = require(script.Parent.world) +local Disabled = world:entity() +local System = world:component() :: Id<{ callback: (any) -> (), name: string}> +local DependsOn = world:entity() +local Event = world:component() :: Id +local Phase = world:entity() + +local PreRender = world:entity() +local Heartbeat = world:entity() +local PreAnimation = world:entity() +local PreSimulation = world:entity() + +local sys: System +local dt: number + +local jabby_scheduler = jabby.scheduler.create("Scheduler") + +local a, b, c, d +local function run() + local id = sys.id + jabby_scheduler:run(id, sys.callback, a, b, c, d) + return nil +end + +world:add(Heartbeat, Phase) +world:set(Heartbeat, Event, RunService.Heartbeat) + +world:add(PreSimulation, Phase) +world:set(PreSimulation, Event, RunService.PreSimulation) + +world:add(PreAnimation, Phase) +world:set(PreAnimation, Event, RunService.PreAnimation) + +jabby.register({ + applet = jabby.applets.world, + name = "MyWorld", + configuration = { + world = world, + }, +}) + +jabby.register({ + applet = jabby.applets.scheduler, + name = "Scheduler", + configuration = { + scheduler = jabby_scheduler, + }, +}) + +if RunService:IsClient() then + world:add(PreRender, Phase) + world:set(PreRender, Event, (RunService :: RunService).PreRender) +end + +local function begin(events: { [RBXScriptSignal]: Systems }) + local connections = {} + for event, systems in events do + if not event then + continue + end + local event_name = tostring(event) + connections[event] = event:Connect(function(...) + debug.profilebegin(event_name) + for _, s in systems do + sys = s + a, b, c, d = ... + + for _ in run do + break + end + + end + debug.profileend() + end) + end + return connections +end + +local function scheduler_collect_systems_under_phase_recursive(systems, phase: Entity) + local phase_name = world:get(phase, Name) + for _, s in world:query(System):with(pair(DependsOn, phase)) do + table.insert(systems, { + id = jabby_scheduler:register_system({ + name = s.name, + phase = phase_name, + } :: any), + callback = s.callback, + }) + end + for after in world:query(Phase):with(pair(DependsOn, phase)):iter() do + scheduler_collect_systems_under_phase_recursive(systems, after) + end +end + +local function scheduler_collect_systems_under_event(event) + local systems = {} + scheduler_collect_systems_under_phase_recursive(systems, event) + return systems +end + +local function scheduler_collect_systems_all() + local events = {} + for phase, event in world:query(Event):with(Phase) do + events[event] = scheduler_collect_systems_under_event(phase) + end + return events +end + +local function scheduler_phase_new(d: { after: Entity?, event: RBXScriptSignal? }) + local phase = world:entity() + world:add(phase, Phase) + local after = d.after + if after then + local dependency = pair(DependsOn, after :: Entity) + world:add(phase, dependency) + end + + local event = d.event + if event then + world:set(phase, Event, event) + end + return phase +end + +local function scheduler_systems_new(callback: (any) -> (), phase: Entity?) + local system = world:entity() + world:set(system, System, { callback = callback, name = debug.info(callback, "n") }) + local depends_on = DependsOn :: jecs.Entity + local p: Entity = phase or Heartbeat + world:add(system, pair(depends_on, p)) + + return system +end + +return { + SYSTEM = scheduler_systems_new, + BEGIN = begin, + PHASE = scheduler_phase_new, + COLLECT = scheduler_collect_systems_all, + phases = { + Heartbeat = Heartbeat, + PreSimulation = PreSimulation, + PreAnimation = PreAnimation, + PreRender = PreRender + } +} diff --git a/demo/src/ReplicatedStorage/std/world.luau b/demo/src/ReplicatedStorage/std/world.luau new file mode 100644 index 0000000..553370b --- /dev/null +++ b/demo/src/ReplicatedStorage/std/world.luau @@ -0,0 +1,4 @@ +local jecs = require(game:GetService("ReplicatedStorage").ecs) + +-- I like the idea of only having the world be a singleton. +return jecs.World.new() :: jecs.World diff --git a/demo/src/ReplicatedStorage/track.luau b/demo/src/ReplicatedStorage/track.luau new file mode 100644 index 0000000..79c4006 --- /dev/null +++ b/demo/src/ReplicatedStorage/track.luau @@ -0,0 +1,48 @@ +local events = {} + +local function trackers_invoke(event, component, entity, ...) + local trackers = events[event][component] + if not trackers then + return + end + + for _, tracker in trackers do + tracker(entity, data) + end +end + +local function trackers_init(event, component, fn) + local ob = events[event] + + return { + connect = function(component, fn) + local trackers = ob[component] + if not trackers then + trackers = {} + ob[component] = trackers + end + + table.insert(trackers, fn) + end, + invoke = function(component, ...) + trackers_invoke(event, component, ...) + end + } + return function(component, fn) + local trackers = ob[component] + if not trackers then + trackers = {} + ob[component] = trackers + end + + table.insert(trackers, fn) + end +end + +local trackers = { + emplace = trackers_init("emplace"), + add = trackers_init("added"), + remove = trackers_init("removed") +} + +return trackers diff --git a/demo/src/ServerScriptService/main.server.luau b/demo/src/ServerScriptService/main.server.luau new file mode 100644 index 0000000..6d6a5b1 --- /dev/null +++ b/demo/src/ServerScriptService/main.server.luau @@ -0,0 +1,4 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local start = require(ReplicatedStorage.start) + +start(script.Parent:WaitForChild("systems"):GetChildren()) diff --git a/demo/src/ServerScriptService/systems/mobs.luau b/demo/src/ServerScriptService/systems/mobs.luau new file mode 100644 index 0000000..07b0d9f --- /dev/null +++ b/demo/src/ServerScriptService/systems/mobs.luau @@ -0,0 +1,88 @@ +--!optimize 2 +--!native +--!strict + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local blink = require(game:GetService("ServerScriptService").net) +local jecs = require(ReplicatedStorage.ecs) +local __ = jecs.Wildcard +local std = ReplicatedStorage.std +local ref = require(std.ref) +local interval = require(std.interval) + +local world = require(std.world) +local cts = require(std.components) + +local Mob = cts.Mob +local Transform = cts.Transform +local Velocity = cts.Velocity +local Player = cts.Player +local Character = cts.Character + +local characters = world + :query(Character) + :with(Player) + :cached() + + +local moving_mobs = world + :query(Transform, Velocity) + :with(Mob) + :cached() + + +local function mobsMove(dt: number) + local targets = {} + + for _, character in characters do + table.insert(targets, (character.PrimaryPart :: Part).Position) + end + + for mob, transform, v in moving_mobs do + local cf = transform.new + local p = cf.Position + + local target + local closest + + for _, pos in targets do + local distance = (p - pos).Magnitude + if not target or distance < closest then + target = pos + closest = distance + end + end + + if not target then + continue + end + + local moving = CFrame.new(p + (target - p).Unit * dt * v) + transform.new = moving + blink.UpdateTransform.FireAll(mob, moving) + end +end + +local throttle = interval(5) + +local function spawnMobs() + if throttle() then + local p = Vector3.new(0, 5, 0) + local cf = CFrame.new(p) + local v = 5 + + local e = world:entity() + world:set(e, Velocity, v) + world:set(e, Transform, { new = cf }) + world:add(e, Mob) + + blink.SpawnMob.FireAll(e, cf, v) + end +end + +local scheduler = require(std.scheduler) + +scheduler.SYSTEM(spawnMobs) +scheduler.SYSTEM(mobsMove) + +return 0 \ No newline at end of file diff --git a/demo/src/ServerScriptService/systems/players.luau b/demo/src/ServerScriptService/systems/players.luau new file mode 100644 index 0000000..27dcc8b --- /dev/null +++ b/demo/src/ServerScriptService/systems/players.luau @@ -0,0 +1,40 @@ +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local std = ReplicatedStorage.std +local ref = require(std.ref) +local collect = require(std.collect) + +local cts = require(std.components) +local world = require(std.world) +local Player = cts.Player +local Character = cts.Character + +local conn = {} + +local function playersAdded(player: Player) + local e = ref(player.UserId) + world:set(e, Player, player) + local characterAdd = player.CharacterAdded + conn[e] = characterAdd:Connect(function(rig) + while rig.Parent ~= workspace do + task.wait() + end + world:set(e, Character, rig) + end) +end + +local function playersRemoved(player: Player) + local e = ref(player.UserId) + world:clear(e) + local connection = conn[e] + connection:Disconnect() + conn[e] = nil +end + +local scheduler = require(std.scheduler) +local phases = require(std.phases) +scheduler.SYSTEM(playersAdded, phases.PlayerAdded) +scheduler.SYSTEM(playersRemoved, phases.PlayerRemoved) + +return 0 \ No newline at end of file diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/main.client.luau b/demo/src/StarterPlayer/StarterPlayerScripts/main.client.luau new file mode 100644 index 0000000..6d6a5b1 --- /dev/null +++ b/demo/src/StarterPlayer/StarterPlayerScripts/main.client.luau @@ -0,0 +1,4 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local start = require(ReplicatedStorage.start) + +start(script.Parent:WaitForChild("systems"):GetChildren()) diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/lol.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/lol.luau new file mode 100644 index 0000000..6e09edb --- /dev/null +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/lol.luau @@ -0,0 +1,67 @@ +--!optimize 2 +--!native +--!strict + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local jecs = require(ReplicatedStorage.ecs) +local __ = jecs.Wildcard +local std = ReplicatedStorage.std + +local world = require(std.world) + +local Position = world:component() :: jecs.Entity +local Previous = jecs.Rest +local pre = jecs.pair(Position, Previous) + +local added = world + :query(Position) + :without(pre) + :cached() +local changed = world + :query(Position, pre) + :cached() +local removed = world + :query(pre) + :without(Position) + :cached() + +local children = {} +for i = 1, 10 do + local e = world:entity() + world:set(e, Position, vector.create(i, i, i)) + table.insert(children, e) +end +local function flip() + return math.random() > 0.5 +end +local function system() + for i, child in children do + world:set(child, Position, vector.create(i,i,i)) + end + for e, p in added:iter() do + world:set(e, pre, p) + end + for i, child in children do + if flip() then + world:set(child, Position, vector.create(i + 1, i + 1, i + 1)) + end + end + for e, new, old in changed:iter() do + if new ~= old then + world:set(e, pre, new) + end + end + + for i, child in children do + world:remove(child, Position) + end + + for e in removed:iter() do + world:remove(e, pre) + end +end +local scheduler = require(std.scheduler) + +scheduler.SYSTEM(system) + +return 0 diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/lol2.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/lol2.luau new file mode 100644 index 0000000..dcb695a --- /dev/null +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/lol2.luau @@ -0,0 +1,90 @@ +--!optimize 2 +--!native +--!strict + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local jecs = require(ReplicatedStorage.ecs) +local __ = jecs.Wildcard +local std = ReplicatedStorage.std + +local world = require(std.world) + +local Position = world:component() :: jecs.Entity +local Previous = jecs.Rest +local pre = jecs.pair(Position, Previous) + +local added = world + :query(Position) + :without(pre) + :cached() +local changed = world + :query(Position, pre) + :cached() +local removed = world + :query(pre) + :without(Position) + :cached() + +local children = {} +for i = 1, 10 do + local e = world:entity() + world:set(e, Position, vector.create(i, i, i)) + table.insert(children, e) +end +local function flip() + return math.random() > 0.5 +end +local entity_index = world.entity_index +local function copy(archetypes, id) + for _, archetype in archetypes do + + local to = jecs.archetype_traverse_add(world, pre, archetype) + local columns = to.columns + local records = to.records + local old = columns[records[pre].column] + local new = columns[records[id].column] + + if to ~= archetype then + for _, entity in archetype.entities do + local r = jecs.entity_index_try_get_fast(entity_index, entity) + jecs.entity_move(entity_index, entity, r, to) + end + end + + table.move(new, 1, #new, 1, old) + + end +end +local function system2() + for i, child in children do + world:set(child, Position, vector.create(i,i,i)) + end + for e, p in added:iter() do + end + copy(added:archetypes(), Position) + for i, child in children do + if flip() then + world:set(child, Position, vector.create(i + 1, i + 1, i + 1)) + end + end + + for e, new, old in changed:iter() do + if new ~= old then + end + end + + copy(changed:archetypes(), Position) + + for i, child in children do + world:remove(child, Position) + end + + for e in removed:iter() do + world:remove(e, pre) + end +end +local scheduler = require(std.scheduler) + +scheduler.SYSTEM(system2) + +return 0 diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau new file mode 100644 index 0000000..a00b703 --- /dev/null +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau @@ -0,0 +1,46 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local blink = require(ReplicatedStorage.net) +local std = ReplicatedStorage.std +local world = require(std.world) +local ref = require(std.ref) + +local cts = require(std.components) + +local Model = cts.Model +local Transform = cts.Transform + +local moved_models = world:query(Model, Transform):cached() +local updated_models = {} +local i = 0 +local function processed(n) + i += 1 + if i > n then + i = 0 + return true + end + return false +end + +local function move(dt: number) + for entity, model in moved_models do + if updated_models[entity] then + updated_models[entity] = nil + model.PrimaryPart.CFrame = transform + end + end +end + +local function syncTransforms() + for _, id, cf in blink.UpdateTransform.Iter() do + local e = ref("server-" .. tostring(id)) + world:set(e, Transform, cf) + moved_models[e] = true + end +end + +local scheduler = require(std.scheduler) + +scheduler.SYSTEM(move) +scheduler.SYSTEM(syncTransforms) + +return 0 diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau new file mode 100644 index 0000000..c28bbfa --- /dev/null +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau @@ -0,0 +1,31 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local blink = require(ReplicatedStorage.net) +local std = ReplicatedStorage.std +local ref = require(std.ref) +local world = require(std.world) +local cts = require(std.components) + +local function syncMobs() + for _, id, cf, vel in blink.SpawnMob.Iter() do + local part = Instance.new("Part") + part.Size = Vector3.one * 5 + part.BrickColor = BrickColor.Red() + part.Anchored = true + local model = Instance.new("Model") + model.PrimaryPart = part + part.Parent = model + model.Parent = workspace + + local e = ref("server-" .. tostring(id)) + world:set(e, cts.Transform, { new = cf, old = cf }) + world:set(e, cts.Velocity, vel) + world:set(e, cts.Model, model) + world:add(e, cts.Mob) + end +end + +local scheduler = require(std.scheduler) +scheduler.SYSTEM(syncMobs) + +return 0 + diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/test.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/test.luau new file mode 100644 index 0000000..250bbf1 --- /dev/null +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/test.luau @@ -0,0 +1,44 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local std = ReplicatedStorage.std +local world = require(std.world) + +local A = world:component() +local B = world:component() +local C = world:component() +local D = world:component() + +local function flip() + return math.random() >= 0.15 +end + +for i = 1, 2^8 do + local e = world:entity() + if flip() then + world:set(e, A, true) + end + if flip() then + world:set(e, B, true) + end + if flip() then + world:set(e, C, true) + end + if flip() then + world:set(e, D, true) + end +end + +local function uncached() + for _ in world:query(A, B, C, D) do + end +end + +local q = world:query(A, B, C, D):cached() +local function cached() + for _ in q do + end +end + +local scheduler = require(std.scheduler) +scheduler.SYSTEM(uncached) +scheduler.SYSTEM(cached) +return 0 \ No newline at end of file diff --git a/demo/wally.toml b/demo/wally.toml new file mode 100644 index 0000000..a663a77 --- /dev/null +++ b/demo/wally.toml @@ -0,0 +1,8 @@ +[package] +name = "marcus/demo" +version = "0.1.0" +registry = "https://github.com/UpliftGames/wally-index" +realm = "shared" + +[dependencies] +jabby = "alicesaidhi/jabby@0.2.2" diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..dcd7c9d --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,69 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Jecs", + base: "/jecs/", + description: "A VitePress Site", + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Learn', link: '/' }, + { text: 'API', link: '/api/jecs.md' }, + { text: 'Examples', link: 'https://github.com/Ukendio/jecs/tree/main/examples' }, + ], + + sidebar: { + "/api/": [ + { + text: "API reference", + items: [ + { text: "jecs", link: "/api/jecs" }, + { text: "World", link: "/api/world" }, + { text: "Query", link: "/api/query" } + ] + } + ], + "/learn/": [ + { + text: "Introduction", + items: [ + { text: 'Getting Started', link: '/learn/overview/get-started' }, + { text: 'First Jecs Project', link: '/learn/overview/first-jecs-project' } + ] + }, + { + text: 'Concepts', + items: [ + { text: 'Entities and Components', link: '/learn/concepts/entities-and-components' }, + { text: 'Queries', link: '/learn/concepts/queries' }, + { text: 'Relationships', link: '/learn/concepts/relationships' }, + { text: 'Component Traits', link: 'learn/concepts/component-traits' }, + { text: 'Addons', link: '/learn/concepts/addons' } + ] + }, + { + text: "FAQ", + items: [ + { text: 'How can I contribute?', link: '/learn/faq/contributing' } + ] + }, + + ], + "/contributing/": [ + { + text: 'Contributing', + items: [ + { text: 'Contribution Guidelines', link: '/learn/contributing/guidelines' }, + { text: 'Submitting Issues', link: '/learn/contributing/issues' }, + { text: 'Submitting Pull Requests', link: '/learn/contributing/pull-requests' }, + ] + } + ] + }, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/ukendio/jecs' } + ] + } +}) diff --git a/docs/api/jecs.md b/docs/api/jecs.md new file mode 100644 index 0000000..75012f7 --- /dev/null +++ b/docs/api/jecs.md @@ -0,0 +1,50 @@ +# Jecs + +Jecs. Just an Entity Component System. + +# Properties + +## World +```luau +jecs.World: World +``` +A world is a container of all ECS data. Games can have multiple worlds but component IDs may conflict between worlds. Ensure to register the same component IDs in the same order for each world. + +## Wildcard +```luau +jecs.Wildcard: Entity +``` +Builtin component type. This ID is used for wildcard queries. + +## Component +```luau +jecs.Component: Entity +``` +Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID. + +## ChildOf +```luau +jecs.ChildOf: Entity +``` +Builtin component type. This ID is for creating parent-child hierarchies. + +## Rest +```luau +jecs.Rest: Entity +``` + +# Functions + +## pair() +```luau +function jecs.pair( + first: Entity, -- The first element of the pair, referred to as the relationship of the relationship pair. + object: Entity, -- The second element of the pair, referred to as the target of the relationship pair. +): number -- Returns the ID with those two elements + +``` +::: info + +Note that while relationship pairs can be used as components, meaning you can add data with it as an ID, however they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding. + +::: diff --git a/docs/api/query.md b/docs/api/query.md new file mode 100644 index 0000000..5cc5ea3 --- /dev/null +++ b/docs/api/query.md @@ -0,0 +1,110 @@ +# Query + +A World contains entities which have components. The World is queryable and can be used to get entities with a specific set of components. + +# Methods + +## iter + +Returns an iterator that can be used to iterate over the query. + +```luau +function Query:iter(): () -> (Entity, ...) +``` + +## with + +Adds components (IDs) to query with, but will not use their data. This is useful for Tags or generally just data you do not care for. + +```luau +function Query:with( + ...: Entity -- The IDs to query with +): Query +``` + +Example: +::: code-group + +```luau [luau] +for id, position in world:query(Position):with(Velocity) do + -- Do something +end +``` + +```ts [typescript] +for (const [id, position] of world.query(Position).with(Velocity)) { + // Do something +} +``` + +::: + +:::info +Put the IDs inside of `world:query()` instead if you need the data. +::: + +## without + +Removes entities with the provided components from the query. + +```luau +function Query:without( + ...: Entity -- The IDs to filter against. +): Query -- Returns the Query +``` + +Example: + +::: code-group + +```luau [luau] +for entity, position in world:query(Position):without(Velocity) do + -- Do something +end +``` + +```ts [typescript] +for (const [entity, position] of world.query(Position).without(Velocity)) { + // Do something +} +``` + +::: + +## archetypes + +Returns the matching archetypes of the query. + +```luau +function Query:archetypes(): { Archetype } +``` + +Example: + +```luau [luau] +for i, archetype in world:query(Position, Velocity):archetypes() do + local columns = archetype.columns + local field = archetype.records + + local P = field[Position] + local V = field[Velocity] + + for row, entity in archetype.entities do + local position = columns[P][row] + local velocity = columns[V][row] + -- Do something + end +end +``` + +:::info +This function is meant for people who want to really customize their query behaviour at the archetype-level +::: + +## cached + +Returns a cached version of the query. This is useful if you want to iterate over the same query multiple times. + +```luau +function Query:cached(): Query -- Returns the cached Query +``` diff --git a/docs/api/world.md b/docs/api/world.md new file mode 100644 index 0000000..ddfaf7c --- /dev/null +++ b/docs/api/world.md @@ -0,0 +1,500 @@ +# World + +A World contains entities which have components. The World is queryable and can be used to get entities with a specific set of components and to perform different kinds of operations on them. + +# Functions + +## new + +`World` utilizes a class, meaning JECS allows you to create multiple worlds. + +```luau +function World.new(): World +``` + +Example: + +::: code-group + +```luau [luau] +local world = jecs.World.new() +local myOtherWorld = jecs.World.new() +``` + +```ts [typescript] +import { World } from "@rbxts/jecs"; + +const world = new World(); +const myOtherWorld = new World(); +``` + +::: + +# Methods + +## entity + +Creates a new entity. + +```luau +function World:entity(): Entity +``` + +Example: + +::: code-group + +```luau [luau] +local entity = world:entity() +``` + +```ts [typescript] +const entity = world.entity(); +``` + +::: + +## component + +Creates a new component. Do note components are entities as well, meaning JECS allows you to add other components onto them. + +These are meant to be added onto other entities through `add` and `set` + +```luau +function World:component(): Entity -- The new componen. +``` + +Example: + +::: code-group + +```luau [luau] +local Health = world:component() :: jecs.Entity -- Typecasting this will allow us to know what kind of data the component holds! +``` + +```ts [typescript] +const Health = world.component(); +``` + +::: + +## get + +Returns the data present in the component that was set in the entity. Will return nil if the component was a tag or is not present. + +```luau +function World:get( + entity: Entity, -- The entity + id: Entity -- The component ID to fetch +): T? +``` + +Example: + +::: code-group + +```luau [luau] +local Health = world:component() :: jecs.Entity + +local Entity = world:entity() +world:set(Entity, Health, 100) + +print(world:get(Entity, Health)) + +-- Outputs: +-- 100 +``` + +```ts [typescript] +const Health = world.component(); + +const Entity = world.entity(); +world.set(Entity, Health, 100); + +print(world.get(Entity, Health)); + +// Outputs: +// 100 +``` + +::: + +## has + +Returns whether an entity has a component (ID). Useful for checking if an entity has a tag or if you don't care of the data that is inside the component. + +```luau +function World:has( + entity: Entity, -- The entity + id: Entity -- The component ID to check +): boolean +``` + +Example: + +::: code-group + +```luau [luau] +local IsMoving = world:component() +local Ragdolled = world:entity() -- This is a tag, meaning it won't contain data +local Health = world:component() :: jecs.Entity + +local Entity = world:entity() +world:set(Entity, Health, 100) +world:add(Entity, Ragdolled) + +print(world:has(Entity, Health)) +print(world:has(Entity, IsMoving) + +print(world:get(Entity, Ragdolled)) +print(world:has(Entity, Ragdolled)) + +-- Outputs: +-- true +-- false +-- nil +-- true +``` + +```ts [typescript] +const IsMoving = world.component(); +const Ragdolled = world.entity(); // This is a tag, meaning it won't contain data +const Health = world.component(); + +const Entity = world.entity(); +world.set(Entity, Health, 100); +world.add(Entity, Ragdolled); + +print(world.has(Entity, Health)); +print(world.has(Entity, IsMoving)); + +print(world.get(Entity, Ragdolled)); +print(world.has(Entity, Ragdolled)); + +// Outputs: +// true +// false +// nil +// true +``` + +::: + +## add + +Adds a component (ID) to the entity. Useful for adding a tag to an entity, as this adds the component to the entity without any additional values inside + +```luau +function World:add( + entity: Entity, -- The entity + id: Entity -- The component ID to add +): void +``` + +::: info +This function is idempotent, meaning if the entity already has the id, this operation will have no side effects. +::: + +## set + +Adds or changes data in the entity's component. + +```luau +function World:set( + entity: Entity, -- The entity + id: Entity, -- The component ID to set + data: T -- The data of the component's type +): void +``` + +Example: + +::: code-group + +```luau [luau] +local Health = world:component() :: jecs.Entity + +local Entity = world:entity() +world:set(Entity, Health, 100) + +print(world:get(Entity, Health)) + +world:set(Entity, Health, 50) +print(world:get(Entity, Health)) + +-- Outputs: +-- 100 +-- 50 +``` + +```ts [typescript] +const Health = world.component(); + +const Entity = world.entity(); +world.set(Entity, Health, 100); + +print(world.get(Entity, Health)); + +world.set(Entity, Health, 50); +print(world.get(Entity, Health)); + +// Outputs: +// 100 +// 50 +``` + +::: + +## query + +Creates a [`query`](query) with the given components (IDs). Entities that satisfies the conditions of the query will be returned and their corresponding data. + +```luau +function World:query( + ...: Entity -- The components to query with +): Query +``` + +Example: + +::: code-group + +```luau [luau] +-- Entity could also be a component if a component also meets the requirements, since they are also entities which you can add more components onto +for entity, position, velocity in world:query(Position, Velocity) do + +end +``` + +```ts [typescript] +// Roblox-TS allows to deconstruct tuples on the act like if they were arrays! +// Entity could also be a component if a component also meets the requirements, since they are also entities which you can add more components onto +for (const [entity, position, velocity] of world.query(Position, Velocity) { + // Do something +} +``` + +::: + +:::info +Queries are uncached by default, this is generally very cheap unless you have high fragmentation from e.g. relationships. + +::: + +## target + +Get the target of a relationship. +This will return a target (second element of a pair) of the entity for the specified relationship. The index allows for iterating through the targets, if a single entity has multiple targets for the same relationship. +If the index is larger than the total number of instances the entity has for the relationship or if there is no pair with the specified relationship on the entity, the operation will return nil. + +```luau +function World:target( + entity: Entity, -- The entity + relation: Entity, -- The relationship between the entity and the target + nth: number, -- The index +): Entity? -- The target for the relationship at the specified index. +``` + +## parent + +Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. + +```luau +function World:parent( + child: Entity -- The child ID to find the parent of +): Entity? -- Returns the parent of the child +``` + +This operation is the same as calling: + +```luau +world:target(entity, jecs.ChildOf, 0) +``` + +## contains + +Checks if an entity or component (id) exists in the world. + +```luau +function World:contains( + entity: Entity, +): boolean +``` + +Example: + +::: code-group + +```luau [luau] +local entity = world:entity() +print(world:contains(entity)) +print(world:contains(1)) +print(world:contains(2)) + +-- Outputs: +-- true +-- true +-- false +``` + +```ts [typescript] +const entity = world.entity(); +print(world.contains(entity)); +print(world.contains(1)); +print(world.contains(2)); + +// Outputs: +// true +// true +// false +``` + +::: + +## remove + +Removes a component (ID) from an entity + +```luau +function World:remove( + entity: Entity, + component: Entity +): void +``` + +Example: + +::: code-group + +```luau [luau] +local IsMoving = world:component() + +local entity = world:entity() +world:add(entity, IsMoving) + +print(world:has(entity, IsMoving)) + +world:remove(entity, IsMoving) +print(world:has(entity, IsMoving)) + +-- Outputs: +-- true +-- false +``` + +```ts [typescript] +const IsMoving = world.component(); + +const entity = world.entity(); +world.add(entity, IsMoving); + +print(world.has(entity, IsMoving)); + +world.remove(entity, IsMoving); +print(world.has(entity, IsMoving)); + +// Outputs: +// true +// false +``` + +::: + +## delete + +Deletes an entity and all of its related components and relationships. + +```luau +function World:delete( + entity: Entity +): void +``` + +Example: + +::: code-group + +```luau [luau] +local entity = world:entity() +print(world:has(entity)) + +world:delete(entity) + +print(world:has(entity)) + +-- Outputs: +-- true +-- false +``` + +```ts [typescript] +const entity = world.entity(); +print(world.has(entity)); + +world.delete(entity); + +print(world.has(entity)); + +// Outputs: +// true +// false +``` + +::: + +## clear + +Clears all of the components and relationships of the entity without deleting it. + +```luau +function World:clear( + entity: Entity +): void +``` + +## each + +Iterate over all entities with the specified component. +Useful when you only need the entity for a specific ID and you want to avoid creating a query. + +```luau +function World:each( + id: Entity -- The component ID +): () -> Entity +``` + +Example: + +::: code-group + +```luau [luau] +local id = world:entity() +for entity in world:each(id) do + -- Do something +end +``` + +```ts [typescript] +const id = world.entity(); +for (const entity of world.each(id)) { + // Do something +} +``` + +::: + +## children + +Iterate entities in root of parent + +```luau +function World:children( + parent: Entity -- The parent entity +): () -> Entity +``` + +This is the same as calling: + +```luau +world:each(pair(ChildOf, parent)) +``` diff --git a/docs/contributing/guidelines.md b/docs/contributing/guidelines.md new file mode 100644 index 0000000..2e03428 --- /dev/null +++ b/docs/contributing/guidelines.md @@ -0,0 +1,3 @@ +## TODO + +This is a TODO stub. \ No newline at end of file diff --git a/docs/contributing/issues.md b/docs/contributing/issues.md new file mode 100644 index 0000000..2e03428 --- /dev/null +++ b/docs/contributing/issues.md @@ -0,0 +1,3 @@ +## TODO + +This is a TODO stub. \ No newline at end of file diff --git a/docs/contributing/pull-requests.md b/docs/contributing/pull-requests.md new file mode 100644 index 0000000..2e03428 --- /dev/null +++ b/docs/contributing/pull-requests.md @@ -0,0 +1,3 @@ +## TODO + +This is a TODO stub. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7ed59b7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,29 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "Jecs" + tagline: Just a stupidly fast ECS + image: + src: /jecs_logo.svg + alt: Jecs logo + actions: + - theme: brand + text: Get Started + link: learn/overview/get-started.md + - theme: alt + text: API References + link: /api/jecs.md + +features: + - title: Stupidly Fast + icon: 🔥 + details: Iterates 500,000 entities at 60 frames per second. + - title: Strictly Typed API + icon: 🔒 + details: Has typings for both Luau and Typescript. + - title: Zero-Dependencies + icon: 📦 + details: Jecs doesn't rely on anything other than itself. +--- diff --git a/docs/learn/concepts/addons.md b/docs/learn/concepts/addons.md new file mode 100644 index 0000000..4a667fc --- /dev/null +++ b/docs/learn/concepts/addons.md @@ -0,0 +1,28 @@ +# Addons + +A collection of third-party jecs addons made by the community. If you would like to share what you're working on, [submit a pull request](https://github.com/Ukendio/jecs)! + +# Debuggers + +## [jabby](https://github.com/alicesaidhi/jabby) + +A jecs debugger with a string-based query language and entity editing capabilities. + +# Schedulers + +## [lockstep scheduler](https://gist.github.com/1Axen/6d4f78b3454cf455e93794505588354b) + +A simple fixed step system scheduler. + +## [rubine](https://github.com/Mark-Marks/rubine) + +An ergonomic, runtime agnostic scheduler for Jecs + +## [jam](https://github.com/revvy02/Jam) + +Provides hooks and a scheduler that implements jabby and a topographical runtime + +## [planck](https://github.com/YetAnotherClown/planck) + +An agnostic scheduler inspired by Bevy and Flecs, with core features including phases, pipelines, run conditions, and startup systems. +Planck also provides plugins for Jabby, Matter Hooks, and more. \ No newline at end of file diff --git a/docs/learn/concepts/component-traits.md b/docs/learn/concepts/component-traits.md new file mode 100644 index 0000000..591883d --- /dev/null +++ b/docs/learn/concepts/component-traits.md @@ -0,0 +1,185 @@ +# Component Traits + +Component traits are IDs and pairs that can be added to components to modify their behavior. Although it is possible to create custom traits, this manual only contains an overview of all builtin component traits supported by Jecs. + +# Component + +Every (component) ID comes with a `Component` which helps with the distinction between normal entities and component IDs. + +# Tag + +A (component) ID can be marked with `Tag´ in which the component will never contain any data. This allows for zero-cost components which improves performance for structural changes. + +# Hooks + +Hooks are part of the "interface" of a component. You could consider hooks as the counterpart to OOP methods in ECS. They define the behavior of a component, but can only be invoked through mutations on the component data. You can only configure a single `OnAdd`, `OnRemove` and `OnSet` hook per component, just like you can only have a single constructor and destructor. + +## Examples + +::: code-group + +```luau [luau] +local Transform= world:component() +world:set(Transform, OnAdd, function(entity) + -- A transform component has been added to an entity +end) +world:set(Transform, OnRemove, function(entity) + -- A transform component has been removed from the entity +end) +world:set(Transform, OnSet, function(entity, value) + -- A transform component has been assigned/changed to value on the entity +end) +``` + +```typescript [typescript] +const Transform = world.component(); +world.set(Transform, OnAdd, (entity) => { + // A transform component has been added to an entity +}); +world.set(Transform, OnRemove, (entity) => { + // A transform component has been removed from the entity +}); +world.set(Transform, OnSet, (entity, value) => { + // A transform component has been assigned/changed to value on the entity +}); +``` + +::: + +# Cleanup Traits + +When entities that are used as tags, components, relationships or relationship targets are deleted, cleanup traits ensure that the store does not contain any dangling references. Any cleanup policy provides this guarantee, so while they are configurable, games cannot configure traits that allows for dangling references. + +We also want to specify this per relationship. If an entity has `(Likes, parent)` we may not want to delete that entity, meaning the cleanup we want to perform for `Likes` and `ChildOf` may not be the same. + +This is what cleanup traits are for: to specify which action needs to be executed under which condition. They are applied to entities that have a reference to the entity being deleted: if I delete the `Archer` tag I remove the tag from all entities that have it. + +To configure a cleanup policy for an entity, a `(Condition, Action)` pair can be added to it. If no policy is specified, the default cleanup action (`Remove`) is performed. + +There are two cleanup actions: + +- `Remove`: removes instances of the specified (component) id from all entities (default) +- `Delete`: deletes all entities with specified id + +There are two cleanup conditions: + +- `OnDelete`: the component, tag or relationship is deleted +- `OnDeleteTarget`: a target used with the relationship is deleted + +## Examples + +The following examples show how to use cleanup traits + +### (OnDelete, Remove) + +::: code-group + +```luau [luau] +local Archer = world:component() +world:add(Archer, pair(jecs.OnDelete, jecs.Remove)) + +local e = world:entity() +world:add(e, Archer) + +-- This will remove Archer from e +world:delete(Archer) +``` + +```typescript [typescript] +const Archer = world.component(); +world.add(Archer, pair(jecs.OnDelete, jecs.Remove)); + +const e = world.entity(); +world.add(e, Archer); + +// This will remove Archer from e +world.delete(Archer); +``` + +::: + +### (OnDelete, Delete) + +::: code-group + +```luau [luau] +local Archer = world:component() +world:add(Archer, pair(jecs.OnDelete, jecs.Delete)) + +local e = world:entity() +world:add(e, Archer) + +-- This will delete entity e because the Archer component has a (OnDelete, Delete) cleanup trait +world:delete(Archer) +``` + +```typescript [typescript] +const Archer = world.component(); +world.add(Archer, pair(jecs.OnDelete, jecs.Delete)); + +const e = world.entity(); +world.add(e, Archer); + +// This will delete entity e because the Archer component has a (OnDelete, Delete) cleanup trait +world.delete(Archer); +``` + +::: + +### (OnDeleteTarget, Remove) + +::: code-group + +```luau [luau] +local OwnedBy = world:component() +world:add(OwnedBy, pair(jecs.OnDeleteTarget, jecs.Remove)) +local loot = world:entity() +local player = world:entity() +world:add(loot, pair(OwnedBy, player)) + +-- This will remove (OwnedBy, player) from loot +world:delete(player) +``` + +```typescript [typescript] +const OwnedBy = world.component(); +world.add(OwnedBy, pair(jecs.OnDeleteTarget, jecs.Remove)); +const loot = world.entity(); +const player = world.entity(); +world.add(loot, pair(OwnedBy, player)); + +// This will remove (OwnedBy, player) from loot +world.delete(player); +``` + +### (OnDeleteTarget, Delete) + +::: code-group + +```luau [luau] +local ChildOf = world:component() +world:add(ChildOf, pair(jecs.OnDeleteTarget, jecs.Delete)) + +local parent = world:entity() +local child = world:entity() +world:add(child, pair(ChildOf, parent)) + +-- This will delete both parent and child +world:delete(parent) +``` + +```typescript [typescript] +const ChildOf = world.component(); +world.add(ChildOf, pair(jecs.OnDeleteTarget, jecs.Delete)); + +const parent = world.entity(); +const child = world.entity(); +world.add(child, pair(ChildOf, parent)); + +// This will delete both parent and child +world.delete(parent); +``` + +::: + +This page takes wording and terminology directly from Flecs [documentation](https://www.flecs.dev/flecs/md_docs_2ComponentTraits.html) diff --git a/docs/learn/concepts/entities-and-components.md b/docs/learn/concepts/entities-and-components.md new file mode 100644 index 0000000..5e7e0e9 --- /dev/null +++ b/docs/learn/concepts/entities-and-components.md @@ -0,0 +1,140 @@ +# Entities and Components + +## Entities + +Entities represent things in a game. In a game there may be entities of characters, buildings, projectiles, particle effects etc. + +By itself, an entity is just an unique identifier without any data + +## Components + +A component is something that is added to an entity. Components can simply tag an entity ("this entity is an `Npc`"), attach data to an entity ("this entity is at `Position` `Vector3.new(10, 20, 30)`") and create relationships between entities ("bob `Likes` alice") that may also contain data ("bob `Eats` `10` apples"). + +## Operations + +| Operation | Description | +| --------- | ---------------------------------------------------------------------------------------------- | +| `get` | Get a specific component or set of components from an entity. | +| `add` | Adds component to an entity. If entity already has the component, `add` does nothing. | +| `set` | Sets the value of a component for an entity. `set` behaves as a combination of `add` and `get` | +| `remove` | Removes component from entity. If entity doesn't have the component, `remove` does nothing. | +| `clear` | Remove all components from an entity. Clearing is more efficient than removing one by one. | + +## Components are entities + +In an ECS, components need to be uniquely identified. In Jecs this is done by making each component its own unique entity. If a game has a component Position and Velocity, there will be two entities, one for each component. Component entities can be distinguished from "regular" entities as they have a `Component` component. An example: + +::: code-group + +```luau [luau] +local Position = world:component() :: jecs.Entity +print(world:has(Position, jecs.Component)) +``` + +```typescript [typescript] +const Position = world.component(); +print(world.has(Position, jecs.Component)); +``` + +::: + +All of the APIs that apply to regular entities also apply to component entities. This means it is possible to contexualize components with logic by adding traits to components + +::: code-group + +```luau [luau] +local Networked = world:component() +local Type = world:component() +local Name = world:component() +local Position = world:component() :: jecs.Entity +world:add(Position, Networked) +world:set(Position, Name, "Position") +world:set(Position, Type, { size = 12, type = "Vector3" } ) -- 12 bytes to represent a Vector3 + +for id, ty, name in world:query(Type, Name, Networked) do + local batch = {} + for entity, data in world:query(id) do + table.insert(batch, { entity = entity, data = data }) + end + -- entities are sized f64 + local packet = buffer.create(#batch * (8 + ty.size)) + local offset = 0 + for _, entityData in batch do + offset+=8 + buffer.writef64(packet, offset, entityData.entity) + if ty.type == "Vector3" then + local vec3 = entity.data :: Vector3 + offset += 4 + buffer.writei32(packet, offset, vec3.X) + offset += 4 + buffer.writei32(packet, offset, vec3.Y) + offset += 4 + buffer.writei32(packet, offset, vec3.Z) + end + end + + updatePositions:FireServer(packet) +end +``` + +```typescript [typescript] +const Networked = world.component(); +const Type = world.component(); +const Name = world.component(); +const Position = world.component(); +world.add(Position, Networked); +world.set(Position, Name, "Position"); +world.set(Position, Type, { size: 12, type: "Vector3" }); // 12 bytes to represent a Vector3 + +for (const [id, ty, name] of world.query(Type, Name, Networked)) { + const batch = new Array<{ entity: Entity; data: unknown }>(); + + for (const [entity, data] of world.query(id)) { + batch.push({ entity, data }); + } + // entities are sized f64 + const packet = buffer.create(batch.size() * (8 + ty.size)); + const offset = 0; + for (const [_, entityData] of batch) { + offset += 8; + buffer.writef64(packet, offset, entityData.entity); + if (ty.type == "Vector3") { + const vec3 = entity.data as Vector3; + offset += 4; + buffer.writei32(packet, offsetm, vec3.X); + offset += 4; + buffer.writei32(packet, offset, vec3.Y); + offset += 4; + buffer.writei32(packet, offset, vec3.Z); + } + } + + updatePositions.FireServer(packet); +} +``` + +::: + +## Singletons + +Singletons are components for which only a single instance +exists on the world. They can be accessed on the +world directly and do not require providing an entity. +Singletons are useful for global game resources, such as +game state, a handle to a physics engine or a network socket. An example: + +::: code-group + +```luau [luau] +local TimeOfDay = world:component() :: jecs.Entity +world:set(TimeOfDay, TimeOfDay, 0.5) +local t = world:get(TimeOfDay, TimeOfDay) +``` + +```typescript [typescript] +const TimeOfDay = world.component(); +world.set(TimeOfDay, TimeOfDay, 0.5); +const t = world.get(TimeOfDay, TimeOfDay); +``` + +::: diff --git a/docs/learn/concepts/queries.md b/docs/learn/concepts/queries.md new file mode 100644 index 0000000..38bd20a --- /dev/null +++ b/docs/learn/concepts/queries.md @@ -0,0 +1,186 @@ +# Queries + +## Introductiuon + +Queries enable games to quickly find entities that satifies provided conditions. + +Jecs queries can do anything from returning entities that match a simple list of components, to matching against entity graphs. + +This manual contains a full overview of the query features available in Jecs. Some of the features of Jecs queries are: + +- Queries have support for relationships pairs which allow for matching against entity graphs without having to build complex data structures for it. +- Queries support filters such as [`query:with(...)`](../../api/query.md#with) if entities are required to have the components but you don’t actually care about components value. And [`query:without(...)`](../../api/query.md#without) which selects entities without the components. +- Queries can be drained or reset on when called, which lets you choose iterator behaviour. +- Queries can be called with any ID, including entities created dynamically, this is useful for pairs. +- Queries are already fast but can be futher inlined via [`query:archetypes()`](../../api/query.md#archetypes) for maximum performance to eliminate function call overhead which is roughly 70-80% of the cost for iteration. + +## Performance and Caching + +Understanding the basic architecture of queries helps to make the right tradeoffs when using queries in games. +The biggest impact on query performance is whether a query is cached or not. +This section goes over what caching is, how it can be used and when it makes sense to use it. + +### Caching: what is it? + +Jecs is an archetype ECS, which means that entities with exactly the same components are +grouped together in an "archetype". Archetypes are created on the fly +whenever a new component combination is created in the ECS. For example: + +:::code-group + +```luau [luau] +local e1 = world:entity() +world:set(e1, Position, Vector3.new(10, 20, 30)) -- create archetype [Position] +world:set(e1, Velocity, Vector3.new(1, 2, 3)) -- create archetype [Position, Velocity] + +local e2 = world:entity() +world:set(e2, Position, Vector3.new(10, 20, 30)) -- archetype [Position] already exists +world:set(e2, Velocity, Vector3.new(1, 2, 3)) -- archetype [Position, Velocity] already exists +world:set(e3, Mass, 100) -- create archetype [Position, Velocity, Mass] + +-- e1 is now in archetype [Position, Velocity] +-- e2 is now in archetype [Position, Velocity, Mass] +``` + +```typescript [typescript] +const e1 = world.entity(); +world.set(e1, Position, new Vector3(10, 20, 30)); // create archetype [Position] +world.set(e1, Velocity, new Vector3(1, 2, 3)); // create archetype [Position, Velocity] + +const e2 = world.entity(); +world.set(e2, Position, new Vector3(10, 20, 30)); // archetype [Position] already exists +world.set(e2, Velocity, new Vector3(1, 2, 3)); // archetype [Position, Velocity] already exists +world.set(e3, Mass, 100); // create archetype [Position, Velocity, Mass] + +// e1 is now in archetype [Position, Velocity] +// e2 is now in archetype [Position, Velocity, Mass] +``` + +::: + +Archetypes are important for queries. Since all entities in an archetype have the same components, and a query matches entities with specific components, a query can often match entire archetypes instead of individual entities. This is one of the main reasons why queries in an archetype ECS are fast. + +The second reason that queries in an archetype ECS are fast is that they are cheap to cache. While an archetype is created for each unique component combination, games typically only use a finite set of component combinations which are created quickly after game assets are loaded. + +This means that instead of searching for archetypes each time a query is evaluated, a query can instead cache the list of matching archetypes. This is a cheap cache to maintain: even though entities can move in and out of archetypes, the archetypes themselves are often stable. + +If none of that made sense, the main thing to remember is that a cached query does not actually have to search for entities. Iterating a cached query just means iterating a list of prematched results, and this is really, really fast. + +### Tradeoffs + +Jecs has both cached and uncached queries. If cached queries are so fast, why even bother with uncached queries? There are four main reasons: + +- Cached queries are really fast to iterate, but take more time to create because the cache must be initialized first. +- Cached queries have a higher RAM utilization, whereas uncached queries have very little overhead and are stateless. +- Cached queries add overhead to archetype creation/deletion, as these changes have to get propagated to caches. +- While caching archetypes is fast, some query features require matching individual entities, which are not efficient to cache (and aren't cached). + +As a rule of thumb, if you have a query that is evaluated each frame (as is typically the case with systems), they will benefit from being cached. If you need to create a query ad-hoc, an uncached query makes more sense. + +Ad-hoc queries are often necessary when a game needs to find entities that match a condition that is only known at runtime, for example to find all child entities for a specific parent. + +## Creating Queries + +This section explains how to create queries in the different language bindings. + +:::code-group + +```luau [luau] +for _ in world:query(Position, Velocity) do end +``` + +```typescript [typescript] +for (const [_] of world.query(Position, Velocity)) { +} +``` + +::: + +### Components + +A component is any single ID that can be added to an entity. This includes tags and regular entities, which are IDs that do not have the builtin `Component` component. To match a query, an entity must have all the requested components. An example: + +```luau +local e1 = world:entity() +world:add(e1, Position) + +local e2 = world:entity() +world:add(e2, Position) +world:add(e2, Velocity) + +local e3 = world:entity() +world:add(e3, Position) +world:add(e3, Velocity) +world:add(e3, Mass) + +``` + +Only entities `e2` and `e3` match the query Position, Velocity. + +### Wildcards + +Jecs currently only supports the `Any` type of wildcards which a single result for the first component that it matches. + +When using the `Any` type wildcard it is undefined which component will be matched, as this can be influenced by other parts of the query. It is guaranteed that iterating the same query twice on the same dataset will produce the same result. + +If you want to iterate multiple targets for the same relation on a pair, then use [`world:target`](../../api/world.md#target) + +Wildcards are particularly useful when used in combination with pairs (next section). + +### Pairs + +A pair is an ID that encodes two elements. Pairs, like components, can be added to entities and are the foundation for [Relationships](relationships.md). + +The elements of a pair are allowed to be wildcards. When a query pair returns an `Any` type wildcard, the query returns at most a single matching pair on an entity. + +The following sections describe how to create queries for pairs in the different language bindings. + +:::code-group + +```luau [luau] +local Likes = world:entity() +local bob = world:entity() +for _ in world:query(pair(Likes, bob)) do end +``` + +```typescript [typescript] +const Likes = world.entity(); +const bob = world.entity(); +for (const [_] of world.query(pair(Likes, bob))) { +} +``` + +::: + +When a query pair contains a wildcard, the `world:target()` function can be used to determine the target of the pair element that matched the query: + +:::code-group + +```luau [luau] +for id in world:query(pair(Likes, jecs.Wildcard)) do + print(`entity {getName(id)} likes {getName(world, world:target(id, Likes))}`) +end +``` + +```typescript [typescript] +const Likes = world.entity(); +const bob = world.entity(); +for (const [_] of world.query(pair(Likes, jecs.Wildcard))) { + print(`entity ${getName(id)} likes ${getName(world.target(id, Likes))}`); +} +``` + +::: + +### Filters + +Filters are extensions to queries which allow you to select entities from a more complex pattern but you don't actually care about the component values. + +The following filters are supported by queries: + +| Identifier | Description | +| ---------- | ----------------------------------- | +| With | Must match with all terms. | +| Without | Must not match with provided terms. | + +This page takes wording and terminology directly from Flecs [documentation](https://www.flecs.dev/flecs/md_docs_2Queries.html) diff --git a/docs/learn/concepts/relationships.md b/docs/learn/concepts/relationships.md new file mode 100644 index 0000000..f9aff16 --- /dev/null +++ b/docs/learn/concepts/relationships.md @@ -0,0 +1,198 @@ +# Relationships +Relationships makes it possible to describe entity graphs natively in ECS. + +Adding/removing relationships is similar to adding/removing regular components, with as difference that instead of a single component id, a relationship adds a pair of two things to an entity. In this pair, the first element represents the relationship (e.g. "Eats"), and the second element represents the relationship target (e.g. "Apples"). + +Relationships can be used to describe many things, from hierarchies to inventory systems to trade relationships between players in a game. The following sections go over how to use relationships, and what features they support. + +## Definitions + +Name | Description +----------|------------ +Id | An id that can be added and removed +Component | Id with a single element (same as an entity id) +Relationship | Used to refer to first element of a pair +Target | Used to refer to second element of a pair +Source | Entity to which an id is added + +## Relationship queries +There are a number of ways a game can query for relationships. The following kinds of queries are available for all (unidirectional) relationships, and are all constant time: + +Test if entity has a relationship pair + +:::code-group +```luau [luau] +world:has(bob, pair(Eats, Apples)) +``` +```typescript [typescript] +world.has(bob, pair(Eats, Apples)) +``` +::: + +Test if entity has a relationship wildcard + +:::code-group +```luau [luau] +world:has(bob, pair(Eats, jecs.Wildcard) +``` +```typescript [typescript] +world.has(bob, pair(Eats, jecs.Wildcard) +``` +::: + +Get parent for entity + +:::code-group +```luau [luau] +world:parent(bob) +``` +```typescript [typescript] +world.parent(bob) +``` +::: + +Find first target of a relationship for entity + +:::code-group +```luau [luau] +world:target(bob, Eats) +``` +```typescript [typescript] +world.target(bob, Eats) +``` +::: + +Find all entities with a pair + +:::code-group +```luau [luau] +for id in world:query(pair(Eats, Apples)) do + -- ... +end +``` +```typescript [typescript] +for (const [id] of world.query(pair(Eats, Apples)) { + // ... +} +``` +::: + +Find all entities with a pair wildcard + +:::code-group +```luau [luau] +for id in world:query(pair(Eats, jecs.Wildcard)) do + local food = world:target(id, Eats) -- Apples, ... +end +``` +```typescript [typescript] +for (const [id] of world.query(pair(Eats, jecs.Wildcard)) { + const food = world.target(id, Eats) // Apples, ... +} +``` +::: + +Iterate all children for a parent + +:::code-group +```luau [luau] +for child in world:query(pair(jecs.ChildOf, parent)) do + -- ... +end +``` +```typescript [typescript] +for (const [child] of world.query(pair(jecs.ChildOf, parent)) { + // ... +} +``` +::: + +Relationship components + +Relationship pairs, just like regular component, can be associated with data. + +:::code-group +```luau [luau] +local Position = world:component() +local Eats = world:component() +local Apples = world:entity() +local Begin = world:entity() +local End = world:entity() + +local e = world:entity() +world:set(e, pair(Eats, Apples), { amount = 1 }) + +world:set(e, pair(Begin, Position), Vector3.new(0, 0, 0)) +world:set(e, pair(End, Position), Vector3.new(10, 20, 30)) + +world:add(e, jecs.ChildOf, Position) + +``` +```typescript [typescript] +const Position = world.component() +const Eats = world.component() +const Apples = world.entity() +const Begin = world.entity() +const End = world.entity() + +const e = world.entity() +world.set(e, pair(Eats, Apples), { amount: 1 }) + +world.set(e, pair(Begin, Position), new Vector3(0, 0, 0)) +world.set(e, pair(End, Position), new Vector3(10, 20, 30)) + +world.add(e, jecs.ChildOf, Position) +``` +::: + +## Relationship wildcards + +When querying for relationship pairs, it is often useful to be able to find all instances for a given relationship or target. To accomplish this, an game can use wildcard expressions. + +Wildcards may used for the relationship or target part of a pair + +```luau +pair(Likes, jecs.Wildcard) -- Matches all Likes relationships +pair(jecs.Wildcard, Alice) -- Matches all relationships with Alice as target +``` + +## Relationship performance +This section goes over the performance implications of using relationships. + +### Introduction +The ECS storage needs to know two things in order to store components for entities: +- Which IDs are associated with an entity +- Which types are associated with those ids +Ids represent anything that can be added to an entity. An ID that is not associated with a type is called a tag. An ID associated with a type is a component. For regular components, the ID is a regular entity that has the builtin `Component` component. + +### Storing relationships +Relationships do not fundamentally change or extend the capabilities of the storage. Relationship pairs are two elements encoded into a single 53-bit ID, which means that on the storage level they are treated the same way as regular component IDs. What changes is the function that determines which type is associated with an id. For regular components this is simply a check on whether an entity has `Component`. To support relationships, new rules are added to determine the type of an id. + +Because of this, adding/removing relationships to entities has the same performance as adding/removing regular components. This becomes more obvious when looking more closely at a function that adds a relationship pair. + +### Id ranges +Jecs reserves entity ids under a threshold (HI_COMPONENT_ID, default is 256) for components. This low id range is used by the storage to more efficiently encode graph edges between archetypes. Graph edges for components with low ids use direct array indexing, whereas graph edges for high ids use a hashmap. Graph edges are used to find the next archetype when adding/removing component ids, and are a contributing factor to the performance overhead of add/remove operations. + +Because of the way pair IDs are encoded, a pair will never be in the low id range. This means that adding/removing a pair ID always uses a hashmap to find the next archetype. This introduces a small overhead. + +### Fragmentation +Fragmentation is a property of archetype-based ECS implementations where entities are spread out over more archetypes as the number of different component combinations increases. The overhead of fragmentation is visible in two areas: +- Archetype creation +- Queries (queries have to match & iterate more archetypes) +Games that make extensive use of relationships might observe high levels of fragmentation, as relationships can introduce many different combinations of components. While the Jecs storage is optimized for supporting large amounts (hundreds of thousands) of archetypes, fragmentation is a factor to consider when using relationships. + +Union relationships are planned along with other improvements to decrease the overhead of fragmentation introduced by relationships. + +### Archetype Creation + +When an ID added to an entity is deleted, all references to that ID are deleted from the storage. For example, when the component Position is deleted it is removed from all entities, and all archetypes with the Position component are deleted. While not unique to relationships, it is more common for relationships to trigger cleanup actions, as relationship pairs contain regular entities. + +The opposite is also true. Because relationship pairs can contain regular entities which can be created on the fly, archetype creation is more common than in games that do not use relationships. While Jecs is optimized for fast archetypes creation, creating and cleaning up archetypes is inherently more expensive than creating/deleting an entity. Therefore archetypes creation is a factor to consider, especially for games that make extensive use of relationships. + +### Indexing + +To improve the speed of evaluating queries, Jecs has indices that store all archetypes for a given component ID. Whenever a new archetype is created, it is registered with the indices for the IDs the archetype has, including IDs for relationship pairs. + +While registering an archetype for a relationship index is not more expensive than registering an archetype for a regular index, an archetype with relationships has to also register itself with the appropriate wildcard indices for its relationships. For example, an archetype with relationship `pair(Likes, Apples)` registers itself with the `pair(Likes, Apples)`, `pair(Likes, jecs.Wildcard)` and `pair(jecs.Wildcard, Apples)` indices. For this reason, creating new archetypes with relationships has a higher overhead than an archetype without relationships. + +This page takes wording and terminology directly from Flecs, the first ECS with full support for [Entity Relationships](https://www.flecs.dev/flecs/md_docs_2Relationships.html). diff --git a/docs/learn/faq/contributing.md b/docs/learn/faq/contributing.md new file mode 100644 index 0000000..1346d9f --- /dev/null +++ b/docs/learn/faq/contributing.md @@ -0,0 +1,3 @@ +## TODO + +This is a TODO stub. \ No newline at end of file diff --git a/docs/learn/overview/first-jecs-project.md b/docs/learn/overview/first-jecs-project.md new file mode 100644 index 0000000..324d170 --- /dev/null +++ b/docs/learn/overview/first-jecs-project.md @@ -0,0 +1,71 @@ +# First Jecs project + +Now that you have installed Jecs, you can create your [World](https://ukendio.github.io/jecs/api/world.html). + +:::code-group +```luau [luau] +local jecs = require(path/to/jecs) +local world = jecs.World.new() +``` +```typescript [typescript] +import { World } from "@rbxts/jecs" +const world = new World() +``` +::: + +Let's create a couple components. + +:::code-group +```luau [luau] +local jecs = require(path/to/jecs) +local world = jecs.World.new() + +local Position = world:component() +local Velocity = world:component() +``` + +```typescript [typescript] +import { World } from "@rbxts/jecs" +const world = new World() + +const Position = world.component() +const Velocity = world.component() +``` +::: + +Systems can be as simple as a query in a function or a more contextualized construct. Let's make a system that moves an entity and decelerates over time. + +:::code-group +```luau [luau] +local jecs = require(path/to/jecs) +local world = jecs.World.new() + +local Position = world:component() +local Velocity = world:component() + +for id, position, velocity in world:query(Position, Velocity) do + world:set(id, Position, position + velocity) + world:set(id, Velocity, velocity * 0.9) +end +``` + +```typescript [typescript] +import { World } from "@rbxts/jecs" +const world = new World() + +const Position = world.component() +const Velocity = world.component() + +for (const [id, position, velocity] of world.query(Position, Velocity)) { + world.set(id, Position, position.add(velocity)) + world.set(id, Velocity, velocity.mul(0.9)) +} +``` +::: + +## Where To Get Help + +If you are encountering problems, there are resources for you to get help: +- [Roblox OSS Discord server](https://discord.gg/h2NV8PqhAD) has a [#jecs](https://discord.com/channels/385151591524597761/1248734074940559511) thread under the [#projects](https://discord.com/channels/385151591524597761/1019724676265676930) channel +- [Open an issue](https://github.com/ukendio/jecs/issues) if you run into bugs or have feature requests +- Dive into the nitty gritty in the [thesis paper](https://raw.githubusercontent.com/Ukendio/jecs/main/thesis/drafts/1/paper.pdf) diff --git a/docs/learn/overview/get-started.md b/docs/learn/overview/get-started.md new file mode 100644 index 0000000..86554af --- /dev/null +++ b/docs/learn/overview/get-started.md @@ -0,0 +1,130 @@ +# Getting Started + +## Installation + +### Installing Standalone + +Navigate to the [releases page](https://github.com/Ukendio/jecs/releases) and download `jecs.rbxm` from the assets. + +![jecs.rbxm](rbxm.png) + +### Using Wally + +Add the following to your wally configuration: + +::: code-group + +```toml [wally.toml] +jecs = "ukendio/jecs@0.5.3" +``` + +::: + +### Using npm (roblox-ts) + +Use one of the following commands on your root project directory: + +::: code-group +```bash [npm] +npm i https://github.com/Ukendio/jecs.git +``` +```bash [yarn] +yarn add https://github.com/Ukendio/jecs.git +``` +```bash [pnpm] +pnpm add https://github.com/Ukendio/jecs.git +``` + +::: + +## Example Usage + +::: code-group + +```luau [Luau] +local world = jecs.World.new() +local pair = jecs.pair +local Wildcard = jecs.Wildcard + +local Name = world:component() + +local function getName(e) + return world:get(e, Name) +end + +local Eats = world:component() + +-- Relationship objects +local Apples = world:component() +-- components are entities, so you can add components to components +world:set(Apples, Name, "apples") +local Oranges = world:component() +world:set(Oranges, Name, "oranges") + +local bob = world:entity() +-- Pairs can be constructed from two entities + +world:set(bob, pair(Eats, Apples), 10) +world:set(bob, pair(Eats, Oranges), 5) +world:set(bob, Name, "bob") + +local alice = world:entity() +world:set(alice, pair(Eats, Apples), 4) +world:set(alice, Name, "alice") + +for id, amount in world:query(pair(Eats, Wildcard)) do + -- get the second target of the pair + local food = world:target(id, Eats) + print(string.format("%s eats %d %s", getName(id), amount, getName(food))) +end + +-- Output: +-- bob eats 10 apples +-- bob eats 5 pears +-- alice eats 4 apples +``` + + +```ts [Typescript] +import { Wildcard, pair, World } from "@rbxts/jecs" + + +const world = new World() +const Name = world.component() +function getName(e) { + return world.get(e, Name) +} + +const Eats = world.component() + +// Relationship objects +const Apples = world.component() +// components are entities, so you can add components to components +world.set(Apples, Name, "apples") +const Oranges = world.component() +world.set(Oranges, Name, "oranges") + +const bob = world.entity() +// Pairs can be constructed from two entities + +world.set(bob, pair(Eats, Apples), 10) +world.set(bob, pair(Eats, Oranges), 5) +world.set(bob, Name, "bob") + +const alice = world.entity() +world.set(alice, pair(Eats, Apples), 4) +world.set(alice, Name, "alice") + +for (const [id, amount] of world.query(pair(Eats, Wildcard))) { + // get the second target of the pair + const food = world:target(id, Eats) + print(string.format("%s eats %d %s", getName(id), amount, getName(food))) +} + +// Output: +// bob eats 10 apples +// bob eats 5 pears +// alice eats 4 apples + +``` + diff --git a/docs/learn/overview/rbxm.png b/docs/learn/overview/rbxm.png new file mode 100644 index 0000000..ad8f38d Binary files /dev/null and b/docs/learn/overview/rbxm.png differ diff --git a/docs/learn/public/jecs_logo.svg b/docs/learn/public/jecs_logo.svg new file mode 100644 index 0000000..befe822 --- /dev/null +++ b/docs/learn/public/jecs_logo.svg @@ -0,0 +1,41 @@ + + + +Created with Fabric.js 5.2.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/public/jecs_logo.svg b/docs/public/jecs_logo.svg new file mode 100644 index 0000000..befe822 --- /dev/null +++ b/docs/public/jecs_logo.svg @@ -0,0 +1,41 @@ + + + +Created with Fabric.js 5.2.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..68b04aa --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# Examples + +This folder contains code examples for the Luau/Typescript APIs. + +## Run with Luau +To run the examples with Luau, run the following commands from the root of the repository: + +```sh +cd examples/luau +luau path/to/file.luau +``` diff --git a/examples/luau/entities/basics.luau b/examples/luau/entities/basics.luau new file mode 100644 index 0000000..170431e --- /dev/null +++ b/examples/luau/entities/basics.luau @@ -0,0 +1,43 @@ +local jecs = require("@jecs") +local world = jecs.World.new() + +local Position = world:component() +local Walking = world:component() +local Name = world:component() + +-- Create an entity with name Bob +local bob = world:entity() + +-- The set operation finds or creates a component, and sets it. +world:set(bob, Position, Vector3.new(10, 20, 30)) +-- Name the entity Bob +world:set(bob, Name, "Bob") +-- The add operation adds a component without setting a value. This is +-- useful for tags, or when adding a component with its default value. +world:add(bob, Walking) + +-- Get the value for the Position component +local pos = world:get(bob, Position) +print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`) + +-- Overwrite the value of the Position component +world:set(bob, Position, Vector3.new(40, 50, 60)) + +local alice = world:entity() +-- Create another named entity +world:set(alice, Name, "Alice") +world:set(alice, Position, Vector3.new(10, 20, 30)) +world:add(alice, Walking) + +-- Remove tag +world:remove(alice, Walking) + +-- Iterate all entities with Position +for entity, p in world:query(Position) do + print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`) +end + +-- Output: +-- {10, 20, 30} +-- Alice: {10, 20, 30} +-- Bob: {40, 50, 60} diff --git a/examples/luau/entities/hierarchy.luau b/examples/luau/entities/hierarchy.luau new file mode 100644 index 0000000..40e3727 --- /dev/null +++ b/examples/luau/entities/hierarchy.luau @@ -0,0 +1,112 @@ +local jecs = require("@jecs") +local pair = jecs.pair +local ChildOf = jecs.ChildOf +local world = jecs.World.new() + +local Name = world:component() +local Position = world:component() +local Star = world:component() +local Planet = world:component() +local Moon = world:component() + +local Vector3 +do + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new(left.X + right.X, left.Y + right.Y, left.Z + right.Z) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new(left.X * right, left.Y * right, left.Z * right) + end + return Vector3.new(left.X * right.X, left.Y * right.Y, left.Z * right.Z) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() +end + +local function path(entity) + local str = world:get(entity, Name) + local parent + while true do + parent = world:parent(entity) + if not parent then + break + end + entity = parent + str = world:get(parent, Name) .. "/" .. str + end + return str +end + +local function iterate(entity, parent) + local p = world:get(entity, Position) + local actual = p + parent + print(path(entity)) + print(`\{{actual.X}, {actual.Y}, {actual.Z}}`) + + for child in world:query(pair(ChildOf, entity)) do + --print(world:get(child, Name)) + iterate(child, actual) + end +end + +local sun = world:entity() +world:add(sun, Star) +world:set(sun, Position, Vector3.one) +world:set(sun, Name, "Sun") +do + local earth = world:entity() + world:set(earth, Name, "Earth") + world:add(earth, pair(ChildOf, sun)) + world:add(earth, Planet) + world:set(earth, Position, Vector3.one * 3) + + do + local moon = world:entity() + world:set(moon, Name, "Moon") + world:add(moon, pair(ChildOf, earth)) + world:add(moon, Moon) + world:set(moon, Position, Vector3.one * 0.1) + + print(`Child of Earth? {world:has(moon, pair(ChildOf, earth))}`) + end + + local venus = world:entity() + world:set(venus, Name, "Venus") + world:add(venus, pair(ChildOf, sun)) + world:add(venus, Planet) + world:set(venus, Position, Vector3.one * 2) + + local mercury = world:entity() + world:set(mercury, Name, "Mercury") + world:add(mercury, pair(ChildOf, sun)) + world:add(mercury, Planet) + world:set(mercury, Position, Vector3.one) + + iterate(sun, Vector3.zero) +end + +-- Output: +-- Child of Earth? true +-- Sun +-- {1, 1, 1} +-- Sun/Mercury +-- {2, 2, 2} +-- Sun/Venus +-- {3, 3, 3} +-- Sun/Earth +-- {4, 4, 4} +-- Sun/Earth/Moon +-- {4.1, 4.1, 4.1} diff --git a/examples/luau/hooks/cleanup.luau b/examples/luau/hooks/cleanup.luau new file mode 100644 index 0000000..9088ad2 --- /dev/null +++ b/examples/luau/hooks/cleanup.luau @@ -0,0 +1,21 @@ +local jecs = require("@jecs") +local world = jecs.World.new() + +local Model = world:component() + +-- It is important to define hooks for the component before the component is ever used +-- otherwise the hooks will never invoke! +world:set(Model, jecs.OnRemove, function(entity) + -- OnRemove is invoked before the component and its value is removed + -- which provides a stable reference to the entity at deletion. + -- This means that it is safe to retrieve the data inside of a hook + local model = world:get(entity, Model) + model:Destroy() +end) + +world:set(Model, jecs.OnSet, function(entity, model) + -- OnSet is invoked after the data has been assigned. + -- It also returns the data for faster access. + -- There may be some logic to do some side effects on reassignments + model:SetAttribute("entityId", entity) +end) diff --git a/examples/luau/queries/basics.luau b/examples/luau/queries/basics.luau new file mode 100644 index 0000000..21e7cae --- /dev/null +++ b/examples/luau/queries/basics.luau @@ -0,0 +1,63 @@ +local jecs = require("@jecs") +local world = jecs.World.new() + +local Position = world:component() +local Velocity = world:component() +local Name = world:component() + +local Vector3 +do + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new(left.X + right.X, left.Y + right.Y, left.Z + right.Z) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new(left.X * right, left.Y * right, left.Z * right) + end + return Vector3.new(left.X * right.X, left.Y * right.Y, left.Z * right.Z) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() +end + +-- Create a few test entities for a Position, Velocity query +local e1 = world:entity() +world:set(e1, Name, "e1") +world:set(e1, Position, Vector3.new(10, 20, 30)) +world:set(e1, Velocity, Vector3.new(1, 2, 3)) + +local e2 = world:entity() +world:set(e2, Name, "e2") +world:set(e2, Position, Vector3.new(10, 20, 30)) +world:set(e2, Velocity, Vector3.new(4, 5, 6)) + +-- This entity will not match as it does not have Position, Velocity +local e3 = world:entity() +world:set(e3, Name, "e3") +world:set(e3, Position, Vector3.new(10, 20, 30)) + +-- Create an uncached query for Position, Velocity. +for entity, p, v in world:query(Position, Velocity) do + -- Iterate entities matching the query + p.X += v.X + p.Y += v.Y + p.Z += v.Z + + print(`{world:get(entity, Name)}: \{{p.X}, {p.Y}, {p.Z}}`) +end + +-- Output: +-- e2: {14, 25, 36} +-- e1: {11, 22, 33} diff --git a/examples/luau/queries/changetracking.luau b/examples/luau/queries/changetracking.luau new file mode 100644 index 0000000..e8e23ea --- /dev/null +++ b/examples/luau/queries/changetracking.luau @@ -0,0 +1,61 @@ +local jecs = require("@jecs") +local pair = jecs.pair + +local world = jecs.World.new() +local Name = world:component() + +local function named(ctr, name) + local e = ctr(world) + world:set(e, Name, name) + return e +end +local function name(e) + return world:get(e, Name) +end + +local Position = named(world.component, "Position") :: jecs.Entity +local Previous = jecs.Rest + +local added = world + :query(Position) + :without(pair(Previous, Position)) + :cached() +local changed = world + :query(Position, pair(Previous, Position)) + :cached() +local removed = world + :query(pair(Previous, Position)) + :without(Position) + :cached() + + +local e1 = named(world.entity, "e1") +world:set(e1, Position, vector.create(10, 20, 30)) +local e2 = named(world.entity, "e2") +world:set(e2, Position, vector.create(10, 20, 30)) +for entity, p in added do + print(`Added {name(entity)}: \{{p.x}, {p.y}, {p.z}}`) + world:set(entity, pair(Previous, Position), p) +end + +world:set(e1, Position, vector.create(999, 999, 1998)) + +for _, archetype in changed:archetypes() do + if new ~= old then + print(`{name(e)}'s Position changed from \{{old.x}, {old.y}, {old.z}\} to \{{new.x}, {new.y}, {new.z}\}`) + world:set(e, pair(Previous, Position), new) + end +end + +world:remove(e2, Position) + +for e in removed:iter() do + print(`Position was removed from {name(e)}`) + world:remove(e, pair(Previous, Position)) +end + +-- Output: +-- Added 265: {10, 20, 30} +-- Added 264: {10, 20, 30} +-- e1's Position changed from {10, 20, 30} to {999, 999, 1998} +-- Position was removed from e2 diff --git a/examples/luau/queries/spatial_grids.luau b/examples/luau/queries/spatial_grids.luau new file mode 100644 index 0000000..6dcf72f --- /dev/null +++ b/examples/luau/queries/spatial_grids.luau @@ -0,0 +1,125 @@ +local jecs = require("@jecs") +local pair = jecs.pair +local ChildOf = jecs.ChildOf +local __ = jecs.Wildcard +local Name = jecs.Name +local world = jecs.World.new() + +type Id = number & { __T: T } +local Voxel = world:component() :: Id +local Position = world:component() :: Id +local Perception = world:component() :: Id<{ + range: number, + fov: number, + dir: Vector3, +}> +local PrimaryPart = world:component() :: Id + +local local_player = game:GetService("Players").LocalPlayer + +local function distance(a: Vector3, b: Vector3) + return (b - a).Magnitude +end + +local function is_in_fov(a: Vector3, b: Vector3, forward_dir: Vector3, fov_angle: number) + local to_target = b - a + + local forward_xz = Vector3.new(forward_dir.X, 0, forward_dir.Z).Unit + local to_target_xz = Vector3.new(to_target.X, 0, to_target.Z).Unit + + local angle_to_target = math.deg(math.atan2(to_target_xz.Z, to_target_xz.X)) + local forward_angle = math.deg(math.atan2(forward_xz.Z, forward_xz.X)) + + local angle_difference = math.abs(forward_angle - angle_to_target) + + if angle_difference > 180 then + angle_difference = 360 - angle_difference + end + + return angle_difference <= (fov_angle / 2) +end + +local map = {} +local grid = 50 + +local function add_to_voxel(source: number, position: Vector3, prev_voxel_id: number?) + local hash = position // grid + local voxel_id = map[hash] + if not voxel_id then + voxel_id = world:entity() + world:add(voxel_id, Voxel) + world:set(voxel_id, Position, hash) + map[hash] = voxel_id + end + if prev_voxel_id ~= nil then + world:remove(source, pair(ChildOf, prev_voxel_id)) + end + world:add(source, pair(ChildOf, voxel_id)) +end + +local function reconcile_client_owned_assembly_to_voxel(dt: number) + for e, part, position in world:query(PrimaryPart, Position) do + local p = part.Position + if p ~= position then + world:set(e, Position, p) + local voxel_id = world:target(e, ChildOf, 0) + if map[p // grid] == voxel_id then + continue + end + + add_to_voxel(e, p, voxel_id) + end + end +end + +local function update_camera_direction(dt: number) + for _, perception in world:query(Perception) do + perception.dir = workspace.CurrentCamera.CFrame.LookVector + end +end + +local function perceive_enemies(dt: number) + local it = world:query(Perception, Position, PrimaryPart) + -- There is only going to be one entity matching the query + local e, self_perception, self_position, self_primary_part = it() + + local voxel_id = map[self_primary_part.Position // grid] + local nearby_entities_query = world:query(Position, pair(ChildOf, voxel_id)) + + for enemy, target_position in nearby_entities_query do + if distance(self_position, target_position) > self_perception.range then + continue + end + + if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then + local p = target_position + print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.X}, {p.Y}, {p.Z})`) + end + end +end + +local player = world:entity() +world:set(player, Perception, { + range = 100, + fov = 90, + dir = Vector3.new(1, 0, 0), +}) +world:set(player, Name, "LocalPlayer") +local primary_part = (local_player.Character :: Model).PrimaryPart :: Part +world:set(player, PrimaryPart, primary_part) +world:set(player, Position, Vector3.zero) + +local enemy = world:entity() +world:set(enemy, Name, "Enemy $1") +world:set(enemy, Position, Vector3.new(50, 0, 20)) + +add_to_voxel(player, primary_part.Position) +add_to_voxel(enemy, world) + +local dt = 1 / 60 +reconcile_client_owned_assembly_to_voxel(dt) +update_camera_direction(dt) +perceive_enemies(dt) + +-- Output: +-- LocalPlayer can see target Enemy $1 diff --git a/examples/luau/queries/wildcards.luau b/examples/luau/queries/wildcards.luau new file mode 100644 index 0000000..5d33e3d --- /dev/null +++ b/examples/luau/queries/wildcards.luau @@ -0,0 +1,37 @@ +local jecs = require("@jecs") +local pair = jecs.pair +local world = jecs.World.new() +local Name = world:component() + +local function named(ctr, name) + local e = ctr(world) + world:set(e, Name, name) + return e +end +local function name(e) + return world:get(e, Name) +end + +local Eats = world:component() +local Apples = named(world.entity, "Apples") +local Oranges = named(world.entity, "Oranges") + +local bob = named(world.entity, "Bob") +world:set(bob, pair(Eats, Apples), 10) + +local alice = named(world.entity, "Alice") +world:set(alice, pair(Eats, Oranges), 5) + +-- Aliasing the wildcard to symbols improves readability and ease of writing +local __ = jecs.Wildcard + +-- Create a query that matches edible components +for entity, amount in world:query(pair(Eats, __)) do + -- Iterate the query + local food = world:target(entity, Eats) + print(`{name(entity)} eats {amount} {name(food)}`) +end + +-- Output: +-- Alice eats 5 Oranges +-- Bob eats 10 Apples diff --git a/jecs.d.ts b/jecs.d.ts new file mode 100644 index 0000000..eed8b79 --- /dev/null +++ b/jecs.d.ts @@ -0,0 +1,270 @@ +/** + * A unique identifier in the world, entity. + * The generic type T defines the data type when this entity is used as a component + */ +export type Entity = number & { + readonly __nominal_Entity: unique symbol; + readonly __type_TData: TData; +}; + +/** + * An entity with no associated data when used as a component + */ +export type Tag = Entity; + +/** + * A pair of entities: + * - `pred` is the type of the "predicate" entity. + * - `obj` is the type of the "object" entity. + */ +export type Pair

= number & { + readonly __nominal_Pair: unique symbol; + readonly __pred: P; + readonly __obj: O; +}; +/** + * An `Id` can be either a single Entity or a Pair of Entities. + * By providing `TData`, you can specifically require an Id that yields that type. + */ +export type Id = Entity | Pair | Pair; + +export type InferComponent = E extends Entity + ? D + : E extends Pair + ? P extends undefined + ? O + : P + : never; + +type FlattenTuple = T extends [infer U] ? U : LuaTuple; +type Nullable = { [K in keyof T]: T[K] | undefined }; +type InferComponents = { [K in keyof A]: InferComponent }; + +type ArchetypeId = number; +type Column = unknown[]; + +export type Archetype = { + id: number; + types: number[]; + type: string; + entities: number[]; + columns: Column[]; + records: number[]; + counts: number[]; +}; + +type Iter = IterableFunction>; + +export type CachedQuery = { + /** + * Returns an iterator that produces a tuple of [Entity, ...queriedComponents]. + */ + iter(): Iter; + + /** + * Returns the matched archetypes of the query + * @returns An array of archetypes of the query + */ + archetypes(): Archetype[]; +} & Iter; + +export type Query = { + /** + * Returns an iterator that produces a tuple of [Entity, ...queriedComponents]. + */ + iter(): Iter; + + /** + * Creates and returns a cached version of this query for efficient reuse. + * Call refinement methods (with/without) on the query before caching. + * @returns A cached query + */ + cached(): CachedQuery; + + /** + * Modifies the query to include specified components. + * @param components The components to include. + * @returns A new Query with the inclusion applied. + */ + with(...components: Id[]): Query; + + /** + * Modifies the Query to exclude specified components. + * @param components The components to exclude. + * @returns A new Query with the exclusion applied. + */ + without(...components: Id[]): Query; + + /** + * Returns the matched archetypes of the query + * @returns An array of archetypes of the query + */ + archetypes(): Archetype[]; +} & Iter; + +export class World { + /** + * Creates a new World. + */ + constructor(); + + /** + * Creates a new entity. + * @returns An entity (Tag) with no data. + */ + entity(): Tag; + + /** + * Creates a new entity in the first 256 IDs, typically used for static + * components that need fast access. + * @returns A typed Entity with `TData`. + */ + component(): Entity; + + /** + * Gets the target of a relationship. For example, if we say + * `world.target(entity, ChildOf)`, this returns the parent entity. + * @param entity The entity using a relationship pair. + * @param relation The "relationship" component/tag (e.g., ChildOf). + * @param index If multiple targets exist, specify an index. Defaults to 0. + */ + target(entity: Entity, relation: Entity, index?: number): Entity | undefined; + + /** + * Cleans up the world by removing empty archetypes and rebuilding the archetype collections. + * This helps maintain memory efficiency by removing unused archetype definitions. + */ + cleanup(): void; + + /** + * Clears all components and relationships from the given entity, but + * does not delete the entity from the world. + * @param entity The entity to clear. + */ + clear(entity: Entity): void; + + /** + * Deletes an entity (and its components/relationships) from the world entirely. + * @param entity The entity to delete. + */ + delete(entity: Entity): void; + + /** + * Adds a component (with no value) to the entity. + * @param entity The target entity. + * @param component The component (or tag) to add. + */ + add(entity: Entity, component: Id): void; + + /** + * Assigns a value to a component on the given entity. + * @param entity The target entity. + * @param component The component definition (could be a Pair or Entity). + * @param value The value to store with that component. + */ + set>(entity: Entity, component: E, value: InferComponent): void; + + /** + * Removes a component from the given entity. + * @param entity The target entity. + * @param component The component to remove. + */ + remove(entity: Entity, component: Id): void; + + /** + * Retrieves the values of up to 4 components on a given entity. Missing + * components will return `undefined`. + * @param entity The entity to query. + * @param components Up to 4 components/tags to retrieve. + * @returns A tuple of data (or a single value), each possibly undefined. + */ + get( + entity: Entity, + ...components: T + ): FlattenTuple>>; + + /** + * Returns `true` if the given entity has all of the specified components. + * A maximum of 4 components can be checked at once. + * @param entity The entity to check. + * @param components Upto 4 components to check for. + */ + has(entity: Entity, ...components: Id[]): boolean; + + /** + * Checks if an entity exists in the world. + * @param entity The entity to verify. + */ + contains(entity: Entity): boolean; + + /** + * Gets the parent (the target of a `ChildOf` relationship) for an entity, + * if such a relationship exists. + * @param entity The entity whose parent is queried. + */ + parent(entity: Entity): Entity | undefined; + + /** + * Searches the world for entities that match specified components. + * @param components The list of components to query. + * @returns A Query object to iterate over results. + */ + query(...components: T): Query>; + + /** + * Returns an iterator that yields all entities that have the specified component or relationship. + * @param id The component or relationship ID to search for + * @returns An iterator function that yields entities + */ + each(id: Id): IterableFunction; + + /** + * Returns an iterator that yields all child entities of the specified parent entity. + * Uses the ChildOf relationship internally. + * @param parent The parent entity to get children for + * @returns An iterator function that yields child entities + */ + children(parent: Entity): IterableFunction; +} + +/** + * Creates a composite key (pair) + * @param pred The first entity (predicate) + * @param obj The second entity (object) + * @returns The composite key (pair) + */ +export function pair(pred: Entity

, obj: Entity): Pair; + +/** + * Checks if the entity is a composite key (pair) + * @param value The entity to check + * @returns If the entity is a pair + */ +export function IS_PAIR(value: Id): value is Pair; + +/** + * Gets the first entity (predicate) of a pair + * @param pair The pair to get the first entity from + * @returns The first entity (predicate) of the pair + */ +export function pair_first(world: World, p: Pair): Entity

; + +/** + * Gets the second entity (object) of a pair + * @param pair The pair to get the second entity from + * @returns The second entity (object) of the pair + */ +export function pair_second(world: World, p: Pair): Entity; + +export declare const OnAdd: Entity<(e: Entity) => void>; +export declare const OnRemove: Entity<(e: Entity) => void>; +export declare const OnSet: Entity<(e: Entity, value: unknown) => void>; +export declare const ChildOf: Tag; +export declare const Wildcard: Entity; +export declare const w: Entity; +export declare const OnDelete: Tag; +export declare const OnDeleteTarget: Tag; +export declare const Delete: Tag; +export declare const Remove: Tag; +export declare const Name: Entity; +export declare const Rest: Entity; diff --git a/jecs.luau b/jecs.luau new file mode 100644 index 0000000..596293a --- /dev/null +++ b/jecs.luau @@ -0,0 +1,2674 @@ +--!optimize 2 +--!native +--!strict +--draft 4 + +type i53 = number +type i24 = number + +type Ty = { i53 } +type ArchetypeId = number + +type Column = { any } + +type Map = { [K]: V } + +type ecs_graph_edge_t = { + from: ecs_archetype_t, + to: ecs_archetype_t?, + id: number, + prev: ecs_graph_edge_t?, + next: ecs_graph_edge_t?, +} + +type ecs_graph_edges_t = Map + +type ecs_graph_node_t = { + add: ecs_graph_edges_t, + remove: ecs_graph_edges_t, + refs: ecs_graph_edge_t, +} + +type ecs_archetype_t = { + id: number, + types: Ty, + type: string, + entities: { number }, + columns: { Column }, + records: { [i53]: number }, + counts: { [i53]: number }, +} & ecs_graph_node_t + +export type Archetype = { + id: number, + types: Ty, + type: string, + entities: { number }, + columns: { Column }, + records: { [Id]: number }, + counts: { [Id]: number }, +} + +type ecs_record_t = { + archetype: ecs_archetype_t, + row: number, + dense: i24, +} + +type ecs_id_record_t = { + cache: { number }, + counts: { number }, + flags: number, + size: number, + hooks: { + on_add: ((entity: i53) -> ())?, + on_set: ((entity: i53, data: any) -> ())?, + on_remove: ((entity: i53) -> ())?, + }, +} + +type ecs_id_index_t = Map + +type ecs_archetypes_map_t = { [string]: ecs_archetype_t } + +type ecs_archetypes_t = { ecs_archetype_t } + +type ecs_entity_index_t = { + dense_array: Map, + sparse_array: Map, + alive_count: number, + max_id: number, +} + +type ecs_query_data_t = { + compatible_archetypes: { ecs_archetype_t }, + ids: { i53 }, + filter_with: { i53 }, + filter_without: { i53 }, + next: () -> (number, ...any), + world: ecs_world_t, +} + +type ecs_observer_t = { + callback: (archetype: ecs_archetype_t) -> (), + query: ecs_query_data_t, +} + +type ecs_observable_t = Map> + +type ecs_world_t = { + entity_index: ecs_entity_index_t, + component_index: ecs_id_index_t, + archetypes: ecs_archetypes_t, + archetype_index: ecs_archetypes_map_t, + max_archetype_id: number, + max_component_id: number, + ROOT_ARCHETYPE: ecs_archetype_t, + observable: Map>, +} + +local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 +-- stylua: ignore start +local EcsOnAdd = HI_COMPONENT_ID + 1 +local EcsOnRemove = HI_COMPONENT_ID + 2 +local EcsOnSet = HI_COMPONENT_ID + 3 +local EcsWildcard = HI_COMPONENT_ID + 4 +local EcsChildOf = HI_COMPONENT_ID + 5 +local EcsComponent = HI_COMPONENT_ID + 6 +local EcsOnDelete = HI_COMPONENT_ID + 7 +local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 +local EcsDelete = HI_COMPONENT_ID + 9 +local EcsRemove = HI_COMPONENT_ID + 10 +local EcsName = HI_COMPONENT_ID + 11 +local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12 +local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13 +local EcsRest = HI_COMPONENT_ID + 14 + +local ECS_ID_DELETE = 0b0000_0001 +local ECS_ID_IS_TAG = 0b0000_0010 +local ECS_ID_HAS_ON_ADD = 0b0000_0100 +local ECS_ID_HAS_ON_SET = 0b0000_1000 +local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 +local ECS_ID_MASK = 0b0000_0000 + +local ECS_ENTITY_MASK = bit32.lshift(1, 24) +local ECS_GENERATION_MASK = bit32.lshift(1, 16) + +local NULL_ARRAY = table.freeze({}) +local ECS_INTERNAL_ERROR = [[ + This is an internal error, please file a bug report via the following link: + + https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md +]] + +local function ECS_COMBINE(id: number, generation: number): i53 + return id + (generation * ECS_ENTITY_MASK) +end +local ECS_PAIR_OFFSET = 2^48 + +local function ECS_IS_PAIR(e: number): boolean + return e > ECS_PAIR_OFFSET +end + +local function ECS_GENERATION_INC(e: i53): i53 + if e > ECS_ENTITY_MASK then + local id = e % ECS_ENTITY_MASK + local generation = e // ECS_ENTITY_MASK + + local next_gen = generation + 1 + if next_gen >= ECS_GENERATION_MASK then + return id + end + + return ECS_COMBINE(id, next_gen) + end + return ECS_COMBINE(e, 1) +end + +local function ECS_ENTITY_T_LO(e: i53): i24 + return e % ECS_ENTITY_MASK +end + +local function ECS_GENERATION(e: i53) + return e // ECS_ENTITY_MASK +end + +local function ECS_ENTITY_T_HI(e: i53): i24 + return e // ECS_ENTITY_MASK +end + +local function ECS_PAIR(pred: i53, obj: i53): i53 + pred %= ECS_ENTITY_MASK + obj %= ECS_ENTITY_MASK + + return obj + (pred * ECS_ENTITY_MASK) + ECS_PAIR_OFFSET +end + +local function ECS_PAIR_FIRST(e: i53): i24 + return (e - ECS_PAIR_OFFSET) // ECS_ENTITY_MASK +end + +local function ECS_PAIR_SECOND(e: i53): i24 + return (e - ECS_PAIR_OFFSET) % ECS_ENTITY_MASK +end + +local function entity_index_try_get_any( + entity_index: ecs_entity_index_t, + entity: number +): ecs_record_t? + local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] + + if not r or r.dense == 0 then + return nil + end + + return r +end + +local function entity_index_try_get(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? + local r = entity_index_try_get_any(entity_index, entity) + if r then + local r_dense = r.dense + if r_dense > entity_index.alive_count then + return nil + end + if entity_index.dense_array[r_dense] ~= entity then + return nil + end + end + return r +end + +local function entity_index_try_get_fast(entity_index: ecs_entity_index_t, entity: number): ecs_record_t? + local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] + if r then + if entity_index.dense_array[r.dense] ~= entity then + return nil + end + end + return r +end + +local function entity_index_is_alive(entity_index: ecs_entity_index_t, entity: i53) + return entity_index_try_get(entity_index, entity) ~= nil +end + +local function entity_index_get_alive(index: ecs_entity_index_t, entity: i53): i53? + local r = entity_index_try_get_any(index, entity) + if r then + return index.dense_array[r.dense] + end + return nil +end + +local function ecs_get_alive(world, entity) + if entity == 0 then + return 0 + end + + local eindex = world.entity_index + + if entity_index_is_alive(eindex, entity) then + return entity + end + + if entity > ECS_ENTITY_MASK then + return 0 + end + + local current = entity_index_get_alive(eindex, entity) + if not current or not entity_index_is_alive(eindex, current) then + return 0 + end + + return current +end + +local function entity_index_new_id(entity_index: ecs_entity_index_t): i53 + local dense_array = entity_index.dense_array + local alive_count = entity_index.alive_count + local max_id = entity_index.max_id + if alive_count ~= max_id then + alive_count += 1 + entity_index.alive_count = alive_count + local id = dense_array[alive_count] + return id + end + + local id = max_id + 1 + entity_index.max_id = id + alive_count += 1 + entity_index.alive_count = alive_count + dense_array[alive_count] = id + entity_index.sparse_array[id] = { dense = alive_count } :: ecs_record_t + + return id +end + +local function ecs_pair_first(world: ecs_world_t, e: i53) + local pred = ECS_PAIR_FIRST(e) + return ecs_get_alive(world, pred) +end + +local function ecs_pair_second(world: ecs_world_t, e: i53) + local obj = ECS_PAIR_SECOND(e) + return ecs_get_alive(world, obj) +end + +local function query_match(query: ecs_query_data_t, + archetype: ecs_archetype_t) + local records = archetype.records + local with = query.filter_with + + for _, id in with do + if not records[id] then + return false + end + end + + local without = query.filter_without + if without then + for _, id in without do + if records[id] then + return false + end + end + end + + return true +end + +local function find_observers(world: ecs_world_t, event: i53, + component: i53): { ecs_observer_t }? + local cache = world.observable[event] + if not cache then + return nil + end + return cache[component] :: any +end + +local function archetype_move( + entity_index: ecs_entity_index_t, + to: ecs_archetype_t, + dst_row: i24, + from: ecs_archetype_t, + src_row: i24 +) + local src_columns = from.columns + local dst_columns = to.columns + local dst_entities = to.entities + local src_entities = from.entities + + local last = #src_entities + local id_types = from.types + local records = to.records + + for i, column in src_columns do + if column == NULL_ARRAY then + continue + end + -- Retrieves the new column index from the source archetype's record from each component + -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. + local tr = records[id_types[i]] + + -- Sometimes target column may not exist, e.g. when you remove a component. + if tr then + dst_columns[tr][dst_row] = column[src_row] + end + + -- If the entity is the last row in the archetype then swapping it would be meaningless. + if src_row ~= last then + -- Swap rempves columns to ensure there are no holes in the archetype. + column[src_row] = column[last] + end + column[last] = nil + end + + local moved = #src_entities + + -- Move the entity from the source to the destination archetype. + -- Because we have swapped columns we now have to update the records + -- corresponding to the entities' rows that were swapped. + local e1 = src_entities[src_row] + local e2 = src_entities[moved] + + if src_row ~= moved then + src_entities[src_row] = e2 + end + + src_entities[moved] = nil :: any + dst_entities[dst_row] = e1 + + local sparse_array = entity_index.sparse_array + + local record1 = sparse_array[ECS_ENTITY_T_LO(e1)] + local record2 = sparse_array[ECS_ENTITY_T_LO(e2)] + record1.row = dst_row + record2.row = src_row +end + +local function archetype_append( + entity: i53, + archetype: ecs_archetype_t +): number + local entities = archetype.entities + local length = #entities + 1 + entities[length] = entity + return length +end + +local function new_entity( + entity: i53, + record: ecs_record_t, + archetype: ecs_archetype_t +): ecs_record_t + local row = archetype_append(entity, archetype) + record.archetype = archetype + record.row = row + return record +end + +local function entity_move( + entity_index: ecs_entity_index_t, + entity: i53, + record: ecs_record_t, + to: ecs_archetype_t +) + local sourceRow = record.row + local from = record.archetype + local dst_row = archetype_append(entity, to) + archetype_move(entity_index, to, dst_row, from, sourceRow) + record.archetype = to + record.row = dst_row +end + +local function hash(arr: { number }): string + return table.concat(arr, "_") +end + +local function fetch(id: i53, records: { number }, + columns: { Column }, row: number): any + local tr = records[id] + + if not tr then + return nil + end + + return columns[tr][row] +end + +local function world_get(world: ecs_world_t, entity: i53, + a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any + local record = entity_index_try_get_fast(world.entity_index, entity) + if not record then + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local records = archetype.records + local columns = archetype.columns + local row = record.row + + local va = fetch(a, records, columns, row) + + if not b then + return va + elseif not c then + return va, fetch(b, records, columns, row) + elseif not d then + return va, fetch(b, records, columns, row), fetch(c, records, columns, row) + elseif not e then + return va, fetch(b, records, columns, row), fetch(c, records, columns, row), fetch(d, records, columns, row) + else + error("args exceeded") + end +end + +local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): boolean + local record = entity_index_try_get_fast(world.entity_index, entity) + if not record then + return false + end + + local archetype = record.archetype + if not archetype then + return false + end + + local records = archetype.records + + return records[id] ~= nil +end + +local function world_has(world: ecs_world_t, entity: i53, ...: i53): boolean + local record = entity_index_try_get_fast(world.entity_index, entity) + if not record then + return false + end + + local archetype = record.archetype + if not archetype then + return false + end + + local records = archetype.records + + for i = 1, select("#", ...) do + if not records[select(i, ...)] then + return false + end + end + + return true +end + +local function world_target(world: ecs_world_t, entity: i53, relation: i24, index: number?): i24? + local nth = index or 0 + local record = entity_index_try_get_fast(world.entity_index, entity) + if not record then + return nil + end + + local archetype = record.archetype + if not archetype then + return nil + end + + local r = ECS_PAIR(relation, EcsWildcard) + + local count = archetype.counts[r] + if not count then + return nil + end + + if nth >= count then + nth = nth + count + 1 + end + + nth = archetype.types[nth + archetype.records[r]] + if not nth then + return nil + end + + return entity_index_get_alive(world.entity_index, + ECS_PAIR_SECOND(nth)) +end + +local function ECS_ID_IS_WILDCARD(e: i53): boolean + local first = ECS_ENTITY_T_HI(e) + local second = ECS_ENTITY_T_LO(e) + return first == EcsWildcard or second == EcsWildcard +end + +local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t + local component_index = world.component_index + local entity_index = world.entity_index + local idr: ecs_id_record_t = component_index[id] + + if not idr then + local flags = ECS_ID_MASK + local relation = id + local target = 0 + local is_pair = ECS_IS_PAIR(id) + if is_pair then + relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53 + assert(relation and entity_index_is_alive( + entity_index, relation), ECS_INTERNAL_ERROR) + target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53 + assert(target and entity_index_is_alive( + entity_index, target), ECS_INTERNAL_ERROR) + end + + local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) + local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) + + local has_delete = false + + if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then + has_delete = true + end + + local on_add, on_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, EcsOnRemove) + + local is_tag = not world_has_one_inline(world, relation, EcsComponent) + + if is_tag and is_pair then + is_tag = not world_has_one_inline(world, target, EcsComponent) + end + + flags = bit32.bor( + flags, + if on_add then ECS_ID_HAS_ON_ADD else 0, + if on_remove then ECS_ID_HAS_ON_REMOVE else 0, + if on_set then ECS_ID_HAS_ON_SET else 0, + if has_delete then ECS_ID_DELETE else 0, + if is_tag then ECS_ID_IS_TAG else 0 + ) + + idr = { + size = 0, + cache = {}, + counts = {}, + flags = flags, + hooks = { + on_add = on_add, + on_set = on_set, + on_remove = on_remove, + }, + } + + component_index[id] = idr + end + + return idr +end + +local function archetype_append_to_records( + idr: ecs_id_record_t, + archetype: ecs_archetype_t, + id: i53, + index: number +) + local archetype_id = archetype.id + local archetype_records = archetype.records + local archetype_counts = archetype.counts + local idr_columns = idr.cache + local idr_counts = idr.counts + local tr = idr_columns[archetype_id] + if not tr then + idr_columns[archetype_id] = index + idr_counts[archetype_id] = 1 + + archetype_records[id] = index + archetype_counts[id] = 1 + else + local max_count = idr_counts[archetype_id] + 1 + idr_counts[archetype_id] = max_count + archetype_counts[id] = max_count + end +end + +local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: i53?): ecs_archetype_t + local archetype_id = (world.max_archetype_id :: number) + 1 + world.max_archetype_id = archetype_id + + local length = #id_types + local columns = (table.create(length) :: any) :: { Column } + + local records: { number } = {} + local counts: {number} = {} + + local archetype: ecs_archetype_t = { + columns = columns, + entities = {}, + id = archetype_id, + records = records, + counts = counts, + type = ty, + types = id_types, + + add = {}, + remove = {}, + refs = {} :: ecs_graph_edge_t, + } + + for i, component_id in id_types do + local idr = id_record_ensure(world, component_id) + archetype_append_to_records(idr, archetype, component_id, i) + + if ECS_IS_PAIR(component_id) then + local relation = ECS_PAIR_FIRST(component_id) + local object = ECS_PAIR_SECOND(component_id) + local r = ECS_PAIR(relation, EcsWildcard) + local idr_r = id_record_ensure(world, r) + archetype_append_to_records(idr_r, archetype, r, i) + + local t = ECS_PAIR(EcsWildcard, object) + local idr_t = id_record_ensure(world, t) + archetype_append_to_records(idr_t, archetype, t, i) + end + + if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then + columns[i] = {} + else + columns[i] = NULL_ARRAY + end + end + + for id in records do + local observer_list = find_observers(world, EcsOnArchetypeCreate, id) + if not observer_list then + continue + end + for _, observer in observer_list do + if query_match(observer.query, archetype) then + observer.callback(archetype) + end + end + end + + world.archetype_index[ty] = archetype + world.archetypes[archetype_id] = archetype + + return archetype +end + +local function world_entity(world: ecs_world_t): i53 + return entity_index_new_id(world.entity_index) +end + +local function world_parent(world: ecs_world_t, entity: i53) + return world_target(world, entity, EcsChildOf, 0) +end + +local function archetype_ensure(world: ecs_world_t, id_types): ecs_archetype_t + if #id_types < 1 then + return world.ROOT_ARCHETYPE + end + + local ty = hash(id_types) + local archetype = world.archetype_index[ty] + if archetype then + return archetype + end + + return archetype_create(world, id_types, ty) +end + +local function find_insert(id_types: { i53 }, toAdd: i53): number + for i, id in id_types do + if id == toAdd then + return -1 + end + if id > toAdd then + return i + end + end + return #id_types + 1 +end + +local function find_archetype_with(world: ecs_world_t, node: ecs_archetype_t, id: i53): ecs_archetype_t + local id_types = node.types + -- Component IDs are added incrementally, so inserting and sorting + -- them each time would be expensive. Instead this insertion sort can find the insertion + -- point in the types array. + + local dst = table.clone(node.types) :: { i53 } + local at = find_insert(id_types, id) + if at == -1 then + -- If it finds a duplicate, it just means it is the same archetype so it can return it + -- directly instead of needing to hash types for a lookup to the archetype. + return node + end + table.insert(dst, at, id) + + return archetype_ensure(world, dst) +end + +local function find_archetype_without( + world: ecs_world_t, + node: ecs_archetype_t, + id: i53 +): ecs_archetype_t + local id_types = node.types + local at = table.find(id_types, id) + if at == nil then + return node + end + + local dst = table.clone(id_types) + table.remove(dst, at) + + return archetype_ensure(world, dst) +end + +local function archetype_init_edge( + archetype: ecs_archetype_t, + edge: ecs_graph_edge_t, + id: i53, + to: ecs_archetype_t +) + edge.from = archetype + edge.to = to + edge.id = id +end + +local function archetype_ensure_edge( + world: ecs_world_t, + edges: ecs_graph_edges_t, + id: i53 +): ecs_graph_edge_t + local edge = edges[id] + if not edge then + edge = {} :: ecs_graph_edge_t + edges[id] = edge + end + + return edge +end + +local function init_edge_for_add(world, archetype: ecs_archetype_t, edge: ecs_graph_edge_t, id, to: ecs_archetype_t) + archetype_init_edge(archetype, edge, id, to) + archetype_ensure_edge(world, archetype.add, id) + if archetype ~= to then + local to_refs = to.refs + local next_edge = to_refs.next + + to_refs.next = edge + edge.prev = to_refs + edge.next = next_edge + + if next_edge then + next_edge.prev = edge + end + end +end + +local function init_edge_for_remove( + world: ecs_world_t, + archetype: ecs_archetype_t, + edge: ecs_graph_edge_t, + id: number, + to: ecs_archetype_t +) + archetype_init_edge(archetype, edge, id, to) + archetype_ensure_edge(world, archetype.remove, id) + if archetype ~= to then + local to_refs = to.refs + local prev_edge = to_refs.prev + + to_refs.prev = edge + edge.next = to_refs + edge.prev = prev_edge + + if prev_edge then + prev_edge.next = edge + end + end +end + +local function create_edge_for_add( + world: ecs_world_t, + node: ecs_archetype_t, + edge: ecs_graph_edge_t, + id: i53 +): ecs_archetype_t + local to = find_archetype_with(world, node, id) + init_edge_for_add(world, node, edge, id, to) + return to +end + +local function create_edge_for_remove( + world: ecs_world_t, + node: ecs_archetype_t, + edge: ecs_graph_edge_t, + id: i53 +): ecs_archetype_t + local to = find_archetype_without(world, node, id) + init_edge_for_remove(world, node, edge, id, to) + return to +end + +local function archetype_traverse_add( + world: ecs_world_t, + id: i53, + from: ecs_archetype_t +): ecs_archetype_t + from = from or world.ROOT_ARCHETYPE + local edge = archetype_ensure_edge(world, from.add, id) + + local to = edge.to + if not to then + to = create_edge_for_add(world, from, edge, id) + end + + return to :: ecs_archetype_t +end + +local function archetype_traverse_remove( + world: ecs_world_t, + id: i53, + from: ecs_archetype_t +): ecs_archetype_t + from = from or world.ROOT_ARCHETYPE + + local edge = archetype_ensure_edge(world, from.remove, id) + + local to = edge.to + if not to then + to = create_edge_for_remove(world, from, edge, id) + end + + return to :: ecs_archetype_t +end + +local function world_add( + world: ecs_world_t, + entity: i53, + id: i53 +): () + local entity_index = world.entity_index + local record = entity_index_try_get_fast(entity_index, entity) + if not record then + return + end + + local from = record.archetype + local to = archetype_traverse_add(world, id, from) + if from == to then + return + end + if from then + entity_move(entity_index, entity, record, to) + else + if #to.types > 0 then + new_entity(entity, record, to) + end + end + + local idr = world.component_index[id] + local on_add = idr.hooks.on_add + + if on_add then + on_add(entity) + end +end + +local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown): () + local entity_index = world.entity_index + local record = entity_index_try_get_fast(entity_index, entity) + if not record then + return + end + + local from: ecs_archetype_t = record.archetype + local to: ecs_archetype_t = archetype_traverse_add(world, id, from) + local idr = world.component_index[id] + local idr_hooks = idr.hooks + + if from == to then + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. + local tr = to.records[id] + local column = from.columns[tr] + column[record.row] = data + local on_set = idr_hooks.on_set + if on_set then + on_set(entity, data) + end + + return + end + + if from then + -- If there was a previous archetype, then the entity needs to move the archetype + entity_move(entity_index, entity, record, to) + else + if #to.types > 0 then + -- When there is no previous archetype it should create the archetype + new_entity(entity, record, to) + end + end + + local tr = to.records[id] + local column = to.columns[tr] + + column[record.row] = data + + local on_add = idr_hooks.on_add + if on_add then + on_add(entity) + end + + local on_set = idr_hooks.on_set + if on_set then + on_set(entity, data) + end +end + +local function world_component(world: World): i53 + local id = (world.max_component_id :: number) + 1 + if id > 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.max_component_id = id + + return id +end + +local function world_remove(world: ecs_world_t, entity: i53, id: i53) + local entity_index = world.entity_index + local record = entity_index_try_get_fast(entity_index, entity) + if not record then + return + end + local from = record.archetype + + if not from then + return + end + + if from.records[id] then + local idr = world.component_index[id] + local on_remove = idr.hooks.on_remove + if on_remove then + on_remove(entity) + end + + local to = archetype_traverse_remove(world, id, record.archetype) + + entity_move(entity_index, entity, record, to) + end +end + +local function archetype_fast_delete_last(columns: { Column }, column_count: number, types: { i53 }, entity: i53) + for i, column in columns do + if column ~= NULL_ARRAY then + column[column_count] = nil + end + end +end + +local function archetype_fast_delete(columns: { Column }, column_count: number, row, types, entity) + for i, column in columns do + if column ~= NULL_ARRAY then + column[row] = column[column_count] + column[column_count] = nil + end + end +end + +local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, row: number) + local entity_index = world.entity_index + local component_index = world.component_index + local columns = archetype.columns + local id_types = archetype.types + local entities = archetype.entities + local column_count = #entities + local last = #entities + local move = entities[last] + -- We assume first that the entity is the last in the archetype + local delete = move + + if row ~= last then + local record_to_move = entity_index_try_get_any(entity_index, move) + if record_to_move then + record_to_move.row = row + end + + delete = entities[row] + entities[row] = move + end + + for _, id in id_types do + local idr = component_index[id] + local on_remove = idr.hooks.on_remove + if on_remove then + on_remove(delete) + end + end + + entities[last] = nil :: any + + if row == last then + archetype_fast_delete_last(columns, column_count, id_types, delete) + else + archetype_fast_delete(columns, column_count, row, id_types, delete) + end +end + +local function world_clear(world: ecs_world_t, entity: i53) + local entity_index = world.entity_index + local component_index = world.component_index + local archetypes = world.archetypes + local tgt = ECS_PAIR(EcsWildcard, entity) + local idr_t = component_index[tgt] + local idr = component_index[entity] + local rel = ECS_PAIR(entity, EcsWildcard) + local idr_r = component_index[rel] + + if idr then + local count = 0 + local queue = {} + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + count += n + table.move(entities, 1, n, #queue + 1, queue) + end + for _, e in queue do + world_remove(world, e, entity) + end + end + + if idr_t then + local queue + local ids + + local count = 0 + local archetype_ids = idr_t.cache + for archetype_id in archetype_ids do + local idr_t_archetype = archetypes[archetype_id] + local idr_t_types = idr_t_archetype.types + local entities = idr_t_archetype.entities + local removal_queued = false + + for _, id in idr_t_types do + if not ECS_IS_PAIR(id) then + continue + end + local object = entity_index_get_alive( + entity_index, ECS_PAIR_SECOND(id)) + if object ~= entity then + continue + end + if not ids then + ids = {} + end + ids[id] = true + removal_queued = true + end + + if not removal_queued then + continue + end + + if not queue then + queue = {} + end + + local n = #entities + table.move(entities, 1, n, count + 1, queue) + count += n + end + + for id in ids do + for _, child in queue do + world_remove(world, child, id) + end + end + end + + if idr_r then + local count = 0 + local archetype_ids = idr_r.cache + local ids = {} + local queue = {} + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local tr = idr_r_archetype.records[rel] + local tr_count = idr_r_archetype.counts[rel] + local types = idr_r_archetype.types + for i = tr, tr + tr_count - 1 do + ids[types[i]] = true + end + local n = #entities + table.move(entities, 1, n, count + 1, queue) + count += n + end + + for _, e in queue do + for id in ids do + world_remove(world, e, id) + end + end + end +end + +local function archetype_disconnect_edge(edge: ecs_graph_edge_t) + local edge_next = edge.next + local edge_prev = edge.prev + if edge_next then + edge_next.prev = edge_prev + end + if edge_prev then + edge_prev.next = edge_next + end +end + +local function archetype_remove_edge(edges: ecs_graph_edges_t, id: i53, edge: ecs_graph_edge_t) + archetype_disconnect_edge(edge) + edges[id] = nil :: any +end + +local function archetype_clear_edges(archetype: ecs_archetype_t) + local add: ecs_graph_edges_t = archetype.add + local remove: ecs_graph_edges_t = archetype.remove + local node_refs = archetype.refs + for id, edge in add do + archetype_disconnect_edge(edge) + add[id] = nil :: any + end + for id, edge in remove do + archetype_disconnect_edge(edge) + remove[id] = nil :: any + end + + local cur = node_refs.next + while cur do + local edge = cur :: ecs_graph_edge_t + local next_edge = edge.next + archetype_remove_edge(edge.from.add, edge.id, edge) + cur = next_edge + end + + cur = node_refs.prev + while cur do + local edge: ecs_graph_edge_t = cur + local next_edge = edge.prev + archetype_remove_edge(edge.from.remove, edge.id, edge) + cur = next_edge + end + + node_refs.next = nil + node_refs.prev = nil +end + +local function archetype_destroy(world: ecs_world_t, archetype: ecs_archetype_t) + if archetype == world.ROOT_ARCHETYPE then + return + end + + local component_index = world.component_index + archetype_clear_edges(archetype) + local archetype_id = archetype.id + world.archetypes[archetype_id] = nil :: any + world.archetype_index[archetype.type] = nil :: any + local records = archetype.records + + for id in records do + local observer_list = find_observers(world, EcsOnArchetypeDelete, id) + if not observer_list then + continue + end + for _, observer in observer_list do + if query_match(observer.query, archetype) then + observer.callback(archetype) + end + end + end + + for id in records do + local idr = component_index[id] + idr.cache[archetype_id] = nil :: any + idr.counts[archetype_id] = nil + idr.size -= 1 + records[id] = nil :: any + if idr.size == 0 then + component_index[id] = nil :: any + end + end +end + +local function world_cleanup(world: ecs_world_t) + local archetypes = world.archetypes + + for _, archetype in archetypes do + if #archetype.entities == 0 then + archetype_destroy(world, archetype) + end + end + + local new_archetypes = table.create(#archetypes) :: { ecs_archetype_t } + local new_archetype_map = {} + + for index, archetype in archetypes do + new_archetypes[index] = archetype + new_archetype_map[archetype.type] = archetype + end + + world.archetypes = new_archetypes + world.archetype_index = new_archetype_map +end + +local function world_delete(world: ecs_world_t, entity: i53) + local entity_index = world.entity_index + local record = entity_index_try_get(entity_index, entity) + if not record then + return + end + + local archetype = record.archetype + local row = record.row + + if archetype then + -- In the future should have a destruct mode for + -- deleting archetypes themselves. Maybe requires recycling + archetype_delete(world, archetype, row) + end + + local delete = entity + local component_index = world.component_index + local archetypes = world.archetypes + local tgt = ECS_PAIR(EcsWildcard, delete) + local rel = ECS_PAIR(delete, EcsWildcard) + + local idr_t = component_index[tgt] + local idr = component_index[delete] + local idr_r = component_index[rel] + + if idr then + local flags = idr.flags + if bit32.band(flags, ECS_ID_DELETE) ~= 0 then + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_delete(world, entities[i]) + end + + archetype_destroy(world, idr_archetype) + end + else + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_remove(world, entities[i], delete) + end + + archetype_destroy(world, idr_archetype) + end + end + end + + if idr_t then + local children + local ids + + local count = 0 + local archetype_ids = idr_t.cache + for archetype_id in archetype_ids do + local idr_t_archetype = archetypes[archetype_id] + local idr_t_types = idr_t_archetype.types + local entities = idr_t_archetype.entities + local removal_queued = false + + for _, id in idr_t_types do + if not ECS_IS_PAIR(id) then + continue + end + local object = entity_index_get_alive( + entity_index, ECS_PAIR_SECOND(id)) + if object ~= delete then + continue + end + local id_record = component_index[id] + local flags = id_record.flags + local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) + if flags_delete_mask ~= 0 then + for i = #entities, 1, -1 do + local child = entities[i] + world_delete(world, child) + end + break + else + if not ids then + ids = {} + end + ids[id] = true + removal_queued = true + end + end + + if not removal_queued then + continue + end + if not children then + children = {} + end + local n = #entities + table.move(entities, 1, n, count + 1, children) + count += n + end + + if ids then + for _, child in children do + for id in ids do + world_remove(world, child, id) + end + end + end + + for archetype_id in archetype_ids do + archetype_destroy(world, archetypes[archetype_id]) + end + end + + if idr_r then + local archetype_ids = idr_r.cache + local flags = idr_r.flags + if bit32.band(flags, ECS_ID_DELETE) ~= 0 then + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_delete(world, entities[i]) + end + archetype_destroy(world, idr_r_archetype) + end + else + local children = {} + local count = 0 + local ids = {} + for archetype_id in archetype_ids do + local idr_r_archetype = archetypes[archetype_id] + local entities = idr_r_archetype.entities + local tr = idr_r_archetype.records[rel] + local tr_count = idr_r_archetype.counts[rel] + local types = idr_r_archetype.types + for i = tr, tr_count - 1 do + ids[types[tr]] = true + end + local n = #entities + table.move(entities, 1, n, count + 1, children) + count += n + end + + for _, child in children do + for id in ids do + world_remove(world, child, id) + end + end + + for archetype_id in archetype_ids do + archetype_destroy(world, archetypes[archetype_id]) + end + end + end + + local dense_array = entity_index.dense_array + local index_of_deleted_entity = record.dense + local index_of_last_alive_entity = entity_index.alive_count + entity_index.alive_count = index_of_last_alive_entity - 1 + + local last_alive_entity = dense_array[index_of_last_alive_entity] + local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: ecs_record_t + r_swap.dense = index_of_deleted_entity + record.archetype = nil :: any + record.row = nil :: any + record.dense = index_of_last_alive_entity + + dense_array[index_of_deleted_entity] = last_alive_entity + dense_array[index_of_last_alive_entity] = ECS_GENERATION_INC(entity) +end + +local function world_contains(world: ecs_world_t, entity): boolean + return entity_index_is_alive(world.entity_index, entity) +end + +local function NOOP() end + +export type QueryInner = { + compatible_archetypes: { Archetype }, + ids: { i53 }, + filter_with: { i53 }, + filter_without: { i53 }, + next: () -> (number, ...any), + world: World, +} + +local function query_iter_init(query: ecs_query_data_t): () -> (number, ...any) + local world_query_iter_next + + local compatible_archetypes = query.compatible_archetypes + local lastArchetype = 1 + local archetype = compatible_archetypes[1] + if not archetype then + return NOOP :: () -> (number, ...any) + end + local columns = archetype.columns + local entities = archetype.entities + local i = #entities + local records = archetype.records + + local ids = query.ids + local A, B, C, D, E, F, G, H, I = unpack(ids) + local a: Column, b: Column, c: Column, d: Column + local e: Column, f: Column, g: Column, h: Column + + if not B then + a = columns[records[A]] + elseif not C then + a = columns[records[A]] + b = columns[records[B]] + elseif not D then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + elseif not E then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + elseif not F then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + elseif not G then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + elseif not H then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + elseif not I then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + h = columns[records[H]] + end + + if not B then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + end + + local row = i + i -= 1 + + return entity, a[row] + end + elseif not C then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row] + end + elseif not D then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row] + end + elseif not E then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row] + end + elseif not F then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row] + end + elseif not G then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row] + end + elseif not H then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + end + elseif not I then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + h = columns[records[H]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + else + local output = {} + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + end + + local row = i + i -= 1 + + for j, id in ids do + output[j] = columns[records[id]][row] + end + + return entity, unpack(output) + end + end + + query.next = world_query_iter_next + return world_query_iter_next +end + +local function query_iter(query): () -> (number, ...any) + local query_next = query.next + if not query_next then + query_next = query_iter_init(query) + end + return query_next +end + +local function query_without(query: ecs_query_data_t, ...: i53) + local without = { ... } + query.filter_without = without + local compatible_archetypes = query.compatible_archetypes + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] + local records = archetype.records + local matches = true + + for _, id in without do + if records[id] then + matches = false + break + end + end + + if matches then + continue + end + + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any + end + + return query :: any +end + +local function query_with(query: ecs_query_data_t, ...: i53) + local compatible_archetypes = query.compatible_archetypes + local with = { ... } + query.filter_with = with + + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] + local records = archetype.records + local matches = true + + for _, id in with do + if not records[id] then + matches = false + break + end + end + + if matches then + continue + end + + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any + end + + return query :: any +end + +-- Meant for directly iterating over archetypes to minimize +-- function call overhead. Should not be used unless iterating over +-- hundreds of thousands of entities in bulk. +local function query_archetypes(query) + return query.compatible_archetypes +end + +local function query_cached(query: ecs_query_data_t) + local with = query.filter_with + local ids = query.ids + if with then + table.move(ids, 1, #ids, #with + 1, with) + else + query.filter_with = ids + end + + local compatible_archetypes = query.compatible_archetypes + local lastArchetype = 1 + + local A, B, C, D, E, F, G, H, I = unpack(ids) + local a: Column, b: Column, c: Column, d: Column + local e: Column, f: Column, g: Column, h: Column + + local world_query_iter_next + local columns: { Column } + local entities: { number } + local i: number + local archetype: ecs_archetype_t + local records: { number } + local archetypes = query.compatible_archetypes + + local world = query.world :: { observable: ecs_observable_t } + -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively + -- because the event will be emitted for all components of that Archetype. + local observable = world.observable :: ecs_observable_t + local on_create_action = observable[EcsOnArchetypeCreate] + if not on_create_action then + on_create_action = {} + observable[EcsOnArchetypeCreate] = on_create_action + end + local query_cache_on_create = on_create_action[A] + if not query_cache_on_create then + query_cache_on_create = {} + on_create_action[A] = query_cache_on_create + end + + local on_delete_action = observable[EcsOnArchetypeDelete] + if not on_delete_action then + on_delete_action = {} + observable[EcsOnArchetypeDelete] = on_delete_action + end + local query_cache_on_delete = on_delete_action[A] + if not query_cache_on_delete then + query_cache_on_delete = {} + on_delete_action[A] = query_cache_on_delete + end + + local function on_create_callback(archetype) + table.insert(archetypes, archetype) + end + + local function on_delete_callback(archetype) + local i = table.find(archetypes, archetype) :: number + local n = #archetypes + archetypes[i] = archetypes[n] + archetypes[n] = nil + end + + local observer_for_create = { query = query, callback = on_create_callback } + local observer_for_delete = { query = query, callback = on_delete_callback } + + table.insert(query_cache_on_create, observer_for_create) + table.insert(query_cache_on_delete, observer_for_delete) + + local function cached_query_iter() + lastArchetype = 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return NOOP + end + entities = archetype.entities + i = #entities + records = archetype.records + columns = archetype.columns + if not B then + a = columns[records[A]] + elseif not C then + a = columns[records[A]] + b = columns[records[B]] + elseif not D then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + elseif not E then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + elseif not F then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + elseif not G then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + elseif not H then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + elseif not I then + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + h = columns[records[H]] + end + + return world_query_iter_next + end + + if not B then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + end + + local row = i + i -= 1 + + return entity, a[row] + end + elseif not C then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row] + end + elseif not D then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row] + end + elseif not E then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row] + end + elseif not F then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row] + end + elseif not G then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row] + end + elseif not H then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + end + elseif not I then + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A]] + b = columns[records[B]] + c = columns[records[C]] + d = columns[records[D]] + e = columns[records[E]] + f = columns[records[F]] + g = columns[records[G]] + h = columns[records[H]] + end + + local row = i + i -= 1 + + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + else + local queryOutput = {} + function world_query_iter_next(): any + local entity = entities[i] + while entity == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + if i == 0 then + continue + end + entity = entities[i] + columns = archetype.columns + records = archetype.records + end + + local row = i + i -= 1 + + if not F then + return entity, a[row], b[row], c[row], d[row], e[row] + elseif not G then + return entity, a[row], b[row], c[row], d[row], e[row], f[row] + elseif not H then + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + elseif not I then + return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + + for j, id in ids do + queryOutput[j] = columns[records[id]][row] + end + + return entity, unpack(queryOutput) + end + end + + local cached_query = query :: any + cached_query.archetypes = query_archetypes + cached_query.__iter = cached_query_iter + cached_query.iter = cached_query_iter + setmetatable(cached_query, cached_query) + return cached_query +end + +local Query = {} +Query.__index = Query +Query.__iter = query_iter +Query.iter = query_iter_init +Query.without = query_without +Query.with = query_with +Query.archetypes = query_archetypes +Query.cached = query_cached + +local function world_query(world: ecs_world_t, ...) + local compatible_archetypes = {} + local length = 0 + + local ids = { ... } + + local archetypes = world.archetypes + + local idr: ecs_id_record_t? + local component_index = world.component_index + + local q = setmetatable({ + ids = ids, + compatible_archetypes = compatible_archetypes, + world = world, + }, Query) + + for _, id in ids do + local map = component_index[id] + if not map then + return q + end + + if idr == nil or map.size < idr.size then + idr = map + end + end + + if not idr then + return q + end + + for archetype_id in idr.cache do + local compatibleArchetype = archetypes[archetype_id] + if #compatibleArchetype.entities == 0 then + continue + end + local records = compatibleArchetype.records + + local skip = false + + for i, id in ids do + local tr = records[id] + if not tr then + skip = true + break + end + end + + if skip then + continue + end + + length += 1 + compatible_archetypes[length] = compatibleArchetype + end + + return q +end + +local function world_each(world: ecs_world_t, id: i53): () -> () + local idr = world.component_index[id] + if not idr then + return NOOP + end + + local idr_cache = idr.cache + local archetypes = world.archetypes + local archetype_id = next(idr_cache, nil) :: number + local archetype = archetypes[archetype_id] + if not archetype then + return NOOP + end + + local entities = archetype.entities + local row = #entities + + return function(): any + local entity = entities[row] + while not entity do + archetype_id = next(idr_cache, archetype_id) :: number + if not archetype_id then + return + end + archetype = archetypes[archetype_id] + entities = archetype.entities + row = #entities + entity = entities[row] + end + row -= 1 + return entity + end +end + +local function world_children(world: ecs_world_t, parent: i53) + return world_each(world, ECS_PAIR(EcsChildOf, parent)) +end + +export type Record = { + archetype: Archetype, + row: number, + dense: i24, +} +export type ComponentRecord = { + cache: { [Id]: number }, + counts: { [Id]: number }, + flags: number, + size: number, + hooks: { + on_add: ((entity: Entity) -> ())?, + on_set: ((entity: Entity, data: any) -> ())?, + on_remove: ((entity: Entity) -> ())?, + }, +} +export type ComponentIndex = Map +export type Archetypes = { [Id]: Archetype } + +export type EntityIndex = { + dense_array: Map, + sparse_array: Map, + alive_count: number, + max_id: number, +} + +local World = {} +World.__index = World + +World.entity = world_entity +World.query = world_query +World.remove = world_remove +World.clear = world_clear +World.delete = world_delete +World.component = world_component +World.add = world_add +World.set = world_set +World.get = world_get +World.has = world_has +World.target = world_target +World.parent = world_parent +World.contains = world_contains +World.cleanup = world_cleanup +World.each = world_each +World.children = world_children + +local function world_new() + local entity_index = { + dense_array = {}, + sparse_array = {}, + alive_count = 0, + max_id = 0, + } :: ecs_entity_index_t + local self = setmetatable({ + archetype_index = {} :: { [string]: Archetype }, + archetypes = {} :: Archetypes, + component_index = {} :: ComponentIndex, + entity_index = entity_index, + ROOT_ARCHETYPE = (nil :: any) :: Archetype, + + max_archetype_id = 0, + max_component_id = 0, + + observable = {} :: Observable, + }, World) :: any + + self.ROOT_ARCHETYPE = archetype_create(self, {}, "") + + for i = 1, HI_COMPONENT_ID do + local e = entity_index_new_id(entity_index) + world_add(self, e, EcsComponent) + end + + for i = HI_COMPONENT_ID + 1, EcsRest do + -- Initialize built-in components + entity_index_new_id(entity_index) + end + + world_add(self, EcsName, EcsComponent) + world_add(self, EcsOnSet, EcsComponent) + world_add(self, EcsOnAdd, EcsComponent) + world_add(self, EcsOnRemove, EcsComponent) + world_add(self, EcsWildcard, EcsComponent) + world_add(self, EcsRest, EcsComponent) + + world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd") + world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove") + world_set(self, EcsOnSet, EcsName, "jecs.OnSet") + world_set(self, EcsWildcard, EcsName, "jecs.Wildcard") + world_set(self, EcsChildOf, EcsName, "jecs.ChildOf") + world_set(self, EcsComponent, EcsName, "jecs.Component") + world_set(self, EcsOnDelete, EcsName, "jecs.OnDelete") + world_set(self, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") + world_set(self, EcsDelete, EcsName, "jecs.Delete") + world_set(self, EcsRemove, EcsName, "jecs.Remove") + world_set(self, EcsName, EcsName, "jecs.Name") + world_set(self, EcsRest, EcsRest, "jecs.Rest") + + world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) + + return self +end + +World.new = world_new + +export type Entity = { __T: T } +export type Id = { __T: T } +export type Pair = Id

+type ecs_id_t = Id | Pair | Pair<"Tag", T> +export type Item = (self: Query) -> (Entity, T...) +export type Iter = (query: Query) -> () -> (Entity, T...) + +export type Query = typeof(setmetatable({}, { + __iter = (nil :: any) :: Iter, +})) & { + iter: Iter, + with: (self: Query, ...Id) -> Query, + without: (self: Query, ...Id) -> Query, + archetypes: (self: Query) -> { Archetype }, + cached: (self: Query) -> Query, +} + +export type Observer = { + callback: (archetype: Archetype) -> (), + query: QueryInner, +} + +export type Observable = { + [Id]: { + [Id]: { + { Observer } + } + } +} + +export type World = { + archetype_index: { [string]: Archetype }, + archetypes: Archetypes, + component_index: ComponentIndex, + entity_index: EntityIndex, + ROOT_ARCHETYPE: Archetype, + + max_component_id: number, + max_archetype_id: number, + + observable: any, + + --- Creates a new entity + entity: (self: World, id: Entity?) -> Entity, + --- Creates a new entity located in the first 256 ids. + --- These should be used for static components for fast access. + component: (self: World) -> Entity, + --- Gets the target of an relationship. For example, when a user calls + --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. + target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?, + --- Deletes an entity and all it's related components and relationships. + delete: (self: World, id: Entity) -> (), + + --- Adds a component to the entity with no value + add: (self: World, id: Entity, component: Id) -> (), + --- Assigns a value to a component on the given entity + set: (self: World, id: Entity, component: Id, data: T) -> (), + + cleanup: (self: World) -> (), + -- Clears an entity from the world + clear: (self: World, id: Entity) -> (), + --- Removes a component from the given entity + remove: (self: World, id: Entity, component: Id) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: ((self: World, id: Entity, Id) -> A?) + & ((self: World, id: Entity, Id, Id) -> (A?, B?)) + & ((self: World, id: Entity, Id, Id, Id) -> (A?, B?, C?)) + & (self: World, id: Entity, Id, Id, Id, Id) -> (A?, B?, C?, D?), + + --- Returns whether the entity has the ID. + has: (self: World, entity: Entity, ...Id) -> boolean, + + --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. + parent:(self: World, entity: Entity) -> Entity, + + --- Checks if the world contains the given entity + contains:(self: World, entity: Entity) -> boolean, + + each: (self: World, id: Id) -> () -> Entity, + + children: (self: World, id: Id) -> () -> Entity, + + --- Searches the world for entities that match a given query + query: ((World, Id) -> Query) + & ((World, Id, Id) -> Query) + & ((World, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) +} +-- type function ecs_id_t(entity) +-- local ty = entity:components()[2] +-- local __T = ty:readproperty(types.singleton("__T")) +-- if not __T then +-- return ty:readproperty(types.singleton("__jecs_pair_value")) +-- end +-- return __T +-- end + +-- type function ecs_pair_t(first, second) +-- if ecs_id_t(first):is("nil") then +-- return second +-- else +-- return first +-- end +-- end + +return { + World = World :: { new: () -> World }, + world = World.new :: () -> World, + + OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, + OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, + OnSet = EcsOnSet :: Entity<(entity: Entity, data: any) -> ()>, + ChildOf = EcsChildOf :: Entity, + Component = EcsComponent :: Entity, + Wildcard = EcsWildcard :: Entity, + w = EcsWildcard :: Entity, + OnDelete = EcsOnDelete :: Entity, + OnDeleteTarget = EcsOnDeleteTarget :: Entity, + Delete = EcsDelete :: Entity, + Remove = EcsRemove :: Entity, + Name = EcsName :: Entity, + Rest = EcsRest :: Entity, + + pair = (ECS_PAIR :: any) :: (first: Id

, second: Id) -> Pair, + + -- Inwards facing API for testing + ECS_ID = ECS_ENTITY_T_LO, + ECS_GENERATION_INC = ECS_GENERATION_INC, + ECS_GENERATION = ECS_GENERATION, + ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD, + + ECS_ID_DELETE = ECS_ID_DELETE, + + IS_PAIR = ECS_IS_PAIR, + pair_first = ecs_pair_first, + pair_second = ecs_pair_second, + entity_index_get_alive = entity_index_get_alive, + + archetype_append_to_records = archetype_append_to_records, + id_record_ensure = id_record_ensure, + archetype_create = archetype_create, + archetype_ensure = archetype_ensure, + find_insert = find_insert, + find_archetype_with = find_archetype_with, + find_archetype_without = find_archetype_without, + archetype_init_edge = archetype_init_edge, + archetype_ensure_edge = archetype_ensure_edge, + init_edge_for_add = init_edge_for_add, + init_edge_for_remove = init_edge_for_remove, + create_edge_for_add = create_edge_for_add, + create_edge_for_remove = create_edge_for_remove, + archetype_traverse_add = archetype_traverse_add, + archetype_traverse_remove = archetype_traverse_remove, + + entity_move = entity_move, + + entity_index_try_get = entity_index_try_get, + entity_index_try_get_any = entity_index_try_get_any, + entity_index_try_get_fast = entity_index_try_get_fast, + entity_index_is_alive = entity_index_is_alive, + entity_index_new_id = entity_index_new_id, + + query_iter = query_iter, + query_iter_init = query_iter_init, + query_with = query_with, + query_without = query_without, + query_archetypes = query_archetypes, + query_match = query_match, + + find_observers = find_observers, +} diff --git a/mirror.luau b/mirror.luau new file mode 100644 index 0000000..c2ceac6 --- /dev/null +++ b/mirror.luau @@ -0,0 +1,659 @@ +--!optimize 2 +--!native +--!strict +--draft 4 + +type i53 = number +type i24 = number + +type Ty = { i53 } +type ArchetypeId = number + +type Column = { any } + +type Archetype = { + id: number, + edges: { + [i24]: { + add: Archetype, + remove: Archetype, + }, + }, + types: Ty, + type: string | number, + entities: { number }, + columns: { Column }, + records: {}, +} + +type Record = { + archetype: Archetype, + row: number, +} + +type EntityIndex = { [i24]: Record } +type ComponentIndex = { [i24]: ArchetypeMap } + +type ArchetypeRecord = number +type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord }, size: number } +type Archetypes = { [ArchetypeId]: Archetype } + +type ArchetypeDiff = { + added: Ty, + removed: Ty, +} + +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 REST = HI_COMPONENT_ID + 4 + +local function transitionArchetype( + entityIndex: EntityIndex, + to: Archetype, + destinationRow: i24, + from: Archetype, + sourceRow: i24 +) + local columns = from.columns + local sourceEntities = from.entities + local destinationEntities = to.entities + local destinationColumns = to.columns + local tr = to.records + local types = from.types + + for i, column in columns do + -- Retrieves the new column index from the source archetype's record from each component + -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. + local targetColumn = destinationColumns[tr[types[i]]] + + -- Sometimes target column may not exist, e.g. when you remove a component. + if targetColumn then + targetColumn[destinationRow] = column[sourceRow] + end + -- If the entity is the last row in the archetype then swapping it would be meaningless. + local last = #column + if sourceRow ~= last then + -- Swap rempves columns to ensure there are no holes in the archetype. + column[sourceRow] = column[last] + end + column[last] = nil + end + + -- Move the entity from the source to the destination archetype. + local atSourceRow = sourceEntities[sourceRow] + destinationEntities[destinationRow] = atSourceRow + entityIndex[atSourceRow].row = destinationRow + + -- Because we have swapped columns we now have to update the records + -- corresponding to the entities' rows that were swapped. + local movedAway = #sourceEntities + if sourceRow ~= movedAway then + local atMovedAway = sourceEntities[movedAway] + sourceEntities[sourceRow] = atMovedAway + entityIndex[atMovedAway].row = sourceRow + end + + sourceEntities[movedAway] = nil +end + +local function archetypeAppend(entity: number, archetype: Archetype): number + local entities = archetype.entities + local length = #entities + 1 + entities[length] = entity + return length +end + +local function newEntity(entityId: i53, record: Record, archetype: Archetype) + local row = archetypeAppend(entityId, archetype) + record.archetype = archetype + record.row = row + return record +end + +local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype) + local sourceRow = record.row + local from = record.archetype + local destinationRow = archetypeAppend(entityId, to) + transitionArchetype(entityIndex, to, destinationRow, from, sourceRow) + record.archetype = to + record.row = destinationRow +end + +local function hash(arr): string | number + return table.concat(arr, "_") +end + +local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?) + local destinationIds = to.types + local records = to.records + local id = to.id + + for i, destinationId in destinationIds do + local archetypesMap = componentIndex[destinationId] + + if not archetypesMap then + archetypesMap = { size = 0, sparse = {} } + componentIndex[destinationId] = archetypesMap + end + + archetypesMap.sparse[id] = i + records[destinationId] = i + end +end + +local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype + local ty = hash(types) + + local id = world.nextArchetypeId + 1 + world.nextArchetypeId = id + + local length = #types + local columns = table.create(length) :: { any } + + for index in types do + columns[index] = {} + end + + local archetype = { + columns = columns, + edges = {}, + entities = {}, + id = id, + records = {}, + type = ty, + types = types, + } + world.archetypeIndex[ty] = archetype + world.archetypes[id] = archetype + if length > 0 then + createArchetypeRecords(world.componentIndex, archetype, prev) + end + + return archetype +end + +local World = {} +World.__index = World +function World.new() + local self = setmetatable({ + archetypeIndex = {}, + archetypes = {}, + componentIndex = {}, + entityIndex = {}, + hooks = { + [ON_ADD] = {}, + }, + nextArchetypeId = 0, + nextComponentId = 0, + nextEntityId = 0, + ROOT_ARCHETYPE = (nil :: any) :: Archetype, + }, World) + return self +end + +local function emit(world, eventDescription) + local event = eventDescription.event + + table.insert(world.hooks[event], { + archetype = eventDescription.archetype, + ids = eventDescription.ids, + offset = eventDescription.offset, + otherArchetype = eventDescription.otherArchetype, + }) +end + +local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) + if #added > 0 then + emit(world, { + archetype = archetype, + event = ON_ADD, + ids = added, + offset = row, + otherArchetype = otherArchetype, + }) + end +end + +export type World = typeof(World.new()) + +local function ensureArchetype(world: World, types, prev) + if #types < 1 then + return world.ROOT_ARCHETYPE + end + + local ty = hash(types) + local archetype = world.archetypeIndex[ty] + if archetype then + return archetype + end + + return archetypeOf(world, types, prev) +end + +local function findInsert(types: { i53 }, toAdd: i53) + for i, id in types do + if id == toAdd then + return -1 + end + if id > toAdd then + return i + end + end + return #types + 1 +end + +local function findArchetypeWith(world: World, node: Archetype, componentId: i53) + local types = node.types + -- Component IDs are added incrementally, so inserting and sorting + -- them each time would be expensive. Instead this insertion sort can find the insertion + -- point in the types array. + local at = findInsert(types, componentId) + if at == -1 then + -- If it finds a duplicate, it just means it is the same archetype so it can return it + -- directly instead of needing to hash types for a lookup to the archetype. + return node + end + + local destinationType = table.clone(node.types) + table.insert(destinationType, at, componentId) + return ensureArchetype(world, destinationType, node) +end + +local function ensureEdge(archetype: Archetype, componentId: i53) + local edges = archetype.edges + local edge = edges[componentId] + if not edge then + edge = {} :: any + edges[componentId] = edge + end + return edge +end + +local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype + if not from then + -- If there was no source archetype then it should return the ROOT_ARCHETYPE + local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE + if not ROOT_ARCHETYPE then + ROOT_ARCHETYPE = archetypeOf(world, {}, nil) + world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never + end + from = ROOT_ARCHETYPE + end + + local edge = ensureEdge(from, componentId) + local add = edge.add + if not add then + -- Save an edge using the component ID to the archetype to allow + -- faster traversals to adjacent archetypes. + add = findArchetypeWith(world, from, componentId) + edge.add = add :: never + end + + return add +end + +local function ensureRecord(entityIndex, entityId: i53): Record + local record = entityIndex[entityId] + + if not record then + record = {} + entityIndex[entityId] = record + end + + return record :: Record +end + +function World.set(world: World, entityId: i53, componentId: i53, data: unknown) + local record = ensureRecord(world.entityIndex, entityId) + local from = record.archetype + local to = archetypeTraverseAdd(world, componentId, from) + + if from == to then + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. + local archetypeRecord = to.records[componentId] + from.columns[archetypeRecord][record.row] = data + -- Should fire an OnSet event here. + return + end + + if from then + -- If there was a previous archetype, then the entity needs to move the archetype + moveEntity(world.entityIndex, entityId, record, to) + else + if #to.types > 0 then + -- When there is no previous archetype it should create the archetype + newEntity(entityId, record, to) + onNotifyAdd(world, to, from, record.row, { componentId }) + end + end + + local archetypeRecord = to.records[componentId] + to.columns[archetypeRecord][record.row] = data +end + +local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype + local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype + local edge = ensureEdge(from, componentId) + + local remove = edge.remove + if not remove then + local to = table.clone(from.types) + table.remove(to, table.find(to, componentId)) + remove = ensureArchetype(world, to, from) + edge.remove = remove :: never + end + + return remove +end + +function World.remove(world: World, entityId: i53, componentId: i53) + local entityIndex = world.entityIndex + local record = ensureRecord(entityIndex, entityId) + local sourceArchetype = record.archetype + local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) + + if sourceArchetype and not (sourceArchetype == destinationArchetype) then + moveEntity(entityIndex, entityId, record, destinationArchetype) + end +end + +-- Keeping the function as small as possible to enable inlining +local function get(record: Record, componentId: i24) + local archetype = record.archetype + local archetypeRecord = archetype.records[componentId] + + if not archetypeRecord then + return nil + end + + return archetype.columns[archetypeRecord][record.row] +end + +function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) + local id = entityId + local record = world.entityIndex[id] + if not record then + return nil + end + + local va = get(record, a) + + if b == nil then + return va + elseif c == nil then + return va, get(record, b) + elseif d == nil then + return va, get(record, b), get(record, c) + elseif e == nil then + return va, get(record, b), get(record, c), get(record, d) + else + error("args exceeded") + end +end + +-- the less creation the better +local function actualNoOperation() end +local function noop(_self: Query, ...: i53): () -> (number, ...any) + return actualNoOperation :: any +end + +local EmptyQuery = { + __iter = noop, + without = noop, +} +EmptyQuery.__index = EmptyQuery +setmetatable(EmptyQuery, EmptyQuery) + +export type Query = typeof(EmptyQuery) + +function World.query(world: World, ...: i53): Query + -- breaking? + if (...) == nil then + error("Missing components") + end + + local compatibleArchetypes = {} + local length = 0 + + local components = { ... } + local archetypes = world.archetypes + local queryLength = #components + + local firstArchetypeMap + local componentIndex = world.componentIndex + + for _, componentId in components do + local map = componentIndex[componentId] + if not map then + return EmptyQuery + end + + if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then + firstArchetypeMap = map + end + end + + for id in firstArchetypeMap.sparse 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 + indices[i] = index + end + + if skip then + continue + end + + length += 1 + compatibleArchetypes[length] = { archetype, indices } + end + + local lastArchetype, compatibleArchetype = next(compatibleArchetypes) + if not lastArchetype then + return EmptyQuery + end + + local preparedQuery = {} + preparedQuery.__index = preparedQuery + + function preparedQuery:without(...) + local withoutComponents = { ... } + for i = #compatibleArchetypes, 1, -1 do + local archetype = compatibleArchetypes[i][1] + 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() + local archetype = compatibleArchetype[1] + local row = next(archetype.entities, lastRow) + while row == nil do + lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) + if lastArchetype == nil then + return + end + archetype = compatibleArchetype[1] + row = next(archetype.entities, row) + end + lastRow = row + + local entityId = archetype.entities[row :: number] + local columns = archetype.columns + local tr = compatibleArchetype[2] + + if queryLength == 1 then + return entityId, columns[tr[1]][row] + elseif queryLength == 2 then + return entityId, columns[tr[1]][row], columns[tr[2]][row] + elseif queryLength == 3 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] + elseif queryLength == 4 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] + elseif queryLength == 5 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row] + elseif queryLength == 6 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row] + elseif queryLength == 7 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row], + columns[tr[7]][row] + elseif queryLength == 8 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row], + columns[tr[7]][row], + columns[tr[8]][row] + end + + for i in components do + queryOutput[i] = columns[tr[i]][row] + end + + return entityId, unpack(queryOutput, 1, queryLength) + end + end + + return setmetatable({}, preparedQuery) :: any +end + +function World.component(world: World) + local componentId = world.nextComponentId + 1 + if componentId > HI_COMPONENT_ID then + -- IDs are partitioned into ranges because component IDs are not nominal, + -- so it needs to error when IDs intersect into the entity range. + error("Too many components, consider using world:entity() instead to create components.") + end + world.nextComponentId = componentId + return componentId +end + +function World.entity(world: World) + local nextEntityId = world.nextEntityId + 1 + world.nextEntityId = nextEntityId + return nextEntityId + REST +end + +function World.delete(world: World, entityId: i53) + local entityIndex = world.entityIndex + local record = entityIndex[entityId] + moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE) + -- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from + -- the entities array and delete the record. We know there won't be the hole since + -- we are always removing the last row. + --world.ROOT_ARCHETYPE.entities[record.row] = nil + --entityIndex[entityId] = nil +end + +function World.observer(world: World, ...) + local componentIds = { ... } + local idsCount = #componentIds + local hooks = world.hooks + + return { + event = function(event) + local hook = hooks[event] + hooks[event] = nil + + local last, change + return function() + last, change = next(hook, last) + if not last then + return + end + + local matched = false + local ids = change.ids + + while not matched do + local skip = false + for _, id in ids do + if not table.find(componentIds, id) then + skip = true + break + end + end + + if skip then + last, change = next(hook, last) + ids = change.ids + continue + end + + matched = true + end + + local queryOutput = table.create(idsCount) + local row = change.offset + local archetype = change.archetype + local columns = archetype.columns + local archetypeRecords = archetype.records + for index, id in componentIds do + queryOutput[index] = columns[archetypeRecords[id]][row] + end + + return archetype.entities[row], unpack(queryOutput, 1, idsCount) + end + end, + } +end + +return table.freeze({ + World = World, + ON_ADD = ON_ADD, + ON_REMOVE = ON_REMOVE, + ON_SET = ON_SET, +}) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1bb8abc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4464 @@ +{ + "name": "@rbxts/jecs", + "version": "0.5.5", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@rbxts/jecs", + "version": "0.5.5", + "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": "^3.0.0", + "typescript": "^5.4.2", + "vitepress": "^1.3.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.23.0.tgz", + "integrity": "sha512-AyZ+9CUgWXwaaJ2lSwOJSy+/w0MFBPFqLrjWYs/HEpYMzBuFfGNZ7gEM9a7h4j7jY8hSBARBl8qdvInmj5vOEQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.23.0.tgz", + "integrity": "sha512-oeKCPwLBnTEPF/RWr0aaJnrfRDfFRLT5O7KV0OF1NmpEXvmzLmN7RwnwDKsNtPUHNfpJ6esP9xzkPEtJabrZ2w==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.23.0.tgz", + "integrity": "sha512-9jacdC44vXLSaYKNLkFpbU1J4BbBPi/N7uoPhcGO//8ubRuVzigH6+RfK5FbudmQlqFt0J5DGUCVeTlHtgyUeg==", + "dev": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.23.0.tgz", + "integrity": "sha512-/Gw5UitweRsnyb24Td4XhjXmsx8PxFzCI0oW6FZZvyr4kjzB9ECP2IjO+PdDq1A2fzDl/LXQ+u8ROudoVnXnQg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.23.0.tgz", + "integrity": "sha512-ivrEZBoXfDatpqpifgHauydxHEe4udNqJ0gy7adR2KODeQ+39MQeaT10I24mu+eylIuiQKJRqORgEdLZycq2qQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.23.0.tgz", + "integrity": "sha512-DjSgJWqTcsnlXEKqDsU7Y2vB/W/VYLlr6UfkzJkMuKB554Ia7IJr4keP2AlHVjjbBG62IDpdh5OkEs/+fbWsOA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.23.0.tgz", + "integrity": "sha512-XAYWUYUhEG4OIdo/N7H/OFFRD9fokfv3bBTky+4Y4/q07bxhnrGSUvcrU6JQ2jJTQyg6kv0ke1EIfiTO/Xxb+g==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.23.0.tgz", + "integrity": "sha512-ULbykzzhhLVofCDU1m/CqSzTyKmjaxA/z1d6o6hgUuR6X7/dll9/G0lu0e4vmWIOItklWWrhU2V8sXD0YGBIHg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.23.0.tgz", + "integrity": "sha512-oB3wG7CgQJQr+uoijV7bWBphiSHkvGX43At8RGgkDyc7Aeabcp9ik5HgLC1YDgbHVOlQI+tce5HIbDCifzQCIg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.23.0.tgz", + "integrity": "sha512-4PWvCV6VGhnCMAbv2zfQUAlc3ofMs6ovqKlC/xcp7tWaucYd//piHg9CcCM4S0p9OZznEGQMRYPt2uqbk6V9vg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.23.0.tgz", + "integrity": "sha512-bacOsX41pnsupNB0k0Ny+1JDchQxIsZIcp69GKDBT0NgTHG8OayEO141eFalNmGil+GXPY0NUPRpx+5s4RdhGA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.23.0.tgz", + "integrity": "sha512-tVNFREexJWDrvc23evmRgAcb2KLZuVilOIB/rVnQCl0GDbqIWJuQ1lG22HKqvCEQFthHkgVFGLYE74wQ96768g==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.23.0.tgz", + "integrity": "sha512-XXHbq2heOZc9EFCc4z+uyHS9YRBygZbYQVsWjWZWx8hdAz+tkBX/jLHM9Xg+3zO0/v8JN6pcZzqYEVsdrLeNLg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "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/@iconify-json/simple-icons": { + "version": "1.2.29", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.29.tgz", + "integrity": "sha512-KYrxmxtRz6iOAulRiUsIBMUuXek+H+Evwf8UvYPIkbQ+KDoOqTegHx3q/w3GDDVC0qJYB+D3hXPMZcpm78qIuA==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "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.2", + "resolved": "https://registry.npmjs.org/@rbxts/compiler-types/-/compiler-types-2.3.0-types.2.tgz", + "integrity": "sha512-ZfCn1LNrfMS0GCfnXvLVOPE9oNNBf5851JRBGy0ptuuOK1bDvV0fljP0Xx9w6LcLFb+NLTM2GKXX+gkoCh7CvA==", + "dev": true + }, + "node_modules/@rbxts/types": { + "version": "1.0.844", + "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.844.tgz", + "integrity": "sha512-qiUe7FDF8hyY2oxD+MgShgw4irAGethZWxy3APll4xd4IA9RuPo/shJknYoHvTA8XRXgxlKdT6wV1D3hek0YcQ==", + "dev": true + }, + "node_modules/@roblox-ts/luau-ast": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@roblox-ts/luau-ast/-/luau-ast-2.0.0.tgz", + "integrity": "sha512-cmMi093IdwBOLVxwuordhM8AmtbyTIyRpsTbB0D/JauidW4SXsQRQowSwWjHo4QP0DRJBXvOIlxtqEQi50uNzQ==", + "dev": true + }, + "node_modules/@roblox-ts/path-translator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@roblox-ts/path-translator/-/path-translator-1.1.0.tgz", + "integrity": "sha512-D0akTmnNYqBw+ZIek5JxocT3BjmbgGOuOy0x1nIIxHBPNLGCpzseToY8jyYs/0mlvnN2xnSP/k8Tv+jvGOQSwQ==", + "dev": true, + "dependencies": { + "ajv": "^8.12.0", + "fs-extra": "^11.2.0" + } + }, + "node_modules/@roblox-ts/path-translator/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/@roblox-ts/rojo-resolver/-/rojo-resolver-1.1.0.tgz", + "integrity": "sha512-QmvVryu1EeME+3QUoG5j/gHGJoJUaffCgZ92mhlG7cJSd1uyhgpY4CNWriZAwZJYkTlzd5Htkpn+18yDFbOFXA==", + "dev": true, + "dependencies": { + "ajv": "^8.17.1", + "fs-extra": "^11.2.0" + } + }, + "node_modules/@roblox-ts/rojo-resolver/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", + "integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz", + "integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz", + "integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz", + "integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz", + "integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz", + "integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz", + "integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz", + "integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz", + "integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz", + "integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz", + "integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz", + "integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz", + "integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz", + "integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz", + "integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz", + "integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz", + "integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz", + "integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz", + "integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz", + "integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "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/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.2.tgz", + "integrity": "sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==", + "dev": true, + "dependencies": { + "@vue/devtools-kit": "^7.7.2" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.2.tgz", + "integrity": "sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==", + "dev": true, + "dependencies": { + "@vue/devtools-shared": "^7.7.2", + "birpc": "^0.2.19", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.2.tgz", + "integrity": "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==", + "dev": true, + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "dev": true, + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "dev": true + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "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/algoliasearch": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.23.0.tgz", + "integrity": "sha512-7TCj+hLx6fZKppLL74lYGDEltSBNSu4vqRwgqeIKZ3VQ0q3aOrdEN0f1sDWcvU1b+psn2wnl7aHt9hWtYatUUA==", + "dev": true, + "dependencies": { + "@algolia/client-abtesting": "5.23.0", + "@algolia/client-analytics": "5.23.0", + "@algolia/client-common": "5.23.0", + "@algolia/client-insights": "5.23.0", + "@algolia/client-personalization": "5.23.0", + "@algolia/client-query-suggestions": "5.23.0", + "@algolia/client-search": "5.23.0", + "@algolia/ingestion": "1.23.0", + "@algolia/monitoring": "1.23.0", + "@algolia/recommend": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "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/birpc": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "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.1", + "@humanwhocodes/config-array": "^0.13.0", + "@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.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "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.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "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.8" + }, + "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/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "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.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", + "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "dev": true, + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "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/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "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/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "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/minisearch": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz", + "integrity": "sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==", + "dev": true + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "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/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "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/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.26.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.4.tgz", + "integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "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/property-information": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", + "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", + "dev": true, + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true + }, + "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.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/roblox-ts/-/roblox-ts-3.0.0.tgz", + "integrity": "sha512-hwAC2frIFlLJOtHd6F+5opMEhBgfAMK9z5l1mP+bykLBbMO5cn1q5lIwhhXbeh9Pq07rlhF8uGHlmeRLPd/3AA==", + "dev": true, + "dependencies": { + "@roblox-ts/luau-ast": "=2.0.0", + "@roblox-ts/path-translator": "=1.1.0", + "@roblox-ts/rojo-resolver": "=1.1.0", + "chokidar": "^3.6.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "resolve": "^1.22.6", + "typescript": "=5.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "rbxtsc": "out/CLI/cli.js" + } + }, + "node_modules/roblox-ts/node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/rollup": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz", + "integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.37.0", + "@rollup/rollup-android-arm64": "4.37.0", + "@rollup/rollup-darwin-arm64": "4.37.0", + "@rollup/rollup-darwin-x64": "4.37.0", + "@rollup/rollup-freebsd-arm64": "4.37.0", + "@rollup/rollup-freebsd-x64": "4.37.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", + "@rollup/rollup-linux-arm-musleabihf": "4.37.0", + "@rollup/rollup-linux-arm64-gnu": "4.37.0", + "@rollup/rollup-linux-arm64-musl": "4.37.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", + "@rollup/rollup-linux-riscv64-gnu": "4.37.0", + "@rollup/rollup-linux-riscv64-musl": "4.37.0", + "@rollup/rollup-linux-s390x-gnu": "4.37.0", + "@rollup/rollup-linux-x64-gnu": "4.37.0", + "@rollup/rollup-linux-x64-musl": "4.37.0", + "@rollup/rollup-win32-arm64-msvc": "4.37.0", + "@rollup/rollup-win32-ia32-msvc": "4.37.0", + "@rollup/rollup-win32-x64-msvc": "4.37.0", + "fsevents": "~2.3.2" + } + }, + "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/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "peer": true + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "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/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "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/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, + "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/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vitepress": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.3.tgz", + "integrity": "sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==", + "dev": true, + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vitepress/node_modules/@types/node": { + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/vitepress/node_modules/@vitejs/plugin-vue": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", + "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/vitepress/node_modules/vite": { + "version": "5.4.15", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz", + "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..328a361 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "@rbxts/jecs", + "version": "0.5.5", + "description": "Stupidly fast Entity Component System", + "main": "jecs.luau", + "repository": { + "type": "git", + "url": "git+https://github.com/ukendio/jecs.git" + }, + "keywords": [], + "author": "Ukendio", + "contributors": [ + "Ukendio", + "EncodedVenom" + ], + "homepage": "https://github.com/ukendio/jecs", + "license": "MIT", + "types": "jecs.d.ts", + "files": [ + "jecs.luau", + "jecs.d.ts", + "LICENSE.md", + "README.md" + ], + "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": "^3.0.0", + "typescript": "^5.4.2", + "vitepress": "^1.3.0" + }, + "scripts": { + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + } +} diff --git a/rokit.toml b/rokit.toml new file mode 100644 index 0000000..0f38210 --- /dev/null +++ b/rokit.toml @@ -0,0 +1,5 @@ +[tools] +wally = "upliftgames/wally@0.3.2" +rojo = "rojo-rbx/rojo@7.4.4" +stylua = "johnnymorganz/stylua@2.0.1" +Blink = "1Axen/Blink@0.14.1" diff --git a/test/devtools_test.luau b/test/devtools_test.luau new file mode 100644 index 0000000..31ed1df --- /dev/null +++ b/test/devtools_test.luau @@ -0,0 +1,25 @@ +local jecs = require("@jecs") +local pair = jecs.pair +local ChildOf = jecs.ChildOf +local lifetime_tracker_add = require("@tools/lifetime_tracker") +local pe = require("@tools/entity_visualiser").prettify +local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false}) +local FriendsWith = world:component() +world:print_snapshot() +local e1 = world:entity() +local e2 = world:entity() +world:delete(e2) + +world:print_snapshot() +local e3 = world:entity() +world:add(e3, pair(ChildOf, e1)) +local e4 = world:entity() +world:add(e4, pair(FriendsWith, e3)) +world:print_snapshot() +world:delete(e1) +world:delete(e3) +world:print_snapshot() +world:print_entity_index() +world:entity() +world:entity() +world:print_snapshot() diff --git a/test/lol.luau b/test/lol.luau new file mode 100644 index 0000000..0749543 --- /dev/null +++ b/test/lol.luau @@ -0,0 +1,158 @@ +local c = { + white_underline = function(s: any) + return `\27[1;4m{s}\27[0m` + end, + + white = function(s: any) + return `\27[37;1m{s}\27[0m` + end, + + green = function(s: any) + return `\27[32;1m{s}\27[0m` + end, + + red = function(s: any) + return `\27[31;1m{s}\27[0m` + end, + + yellow = function(s: any) + return `\27[33;1m{s}\27[0m` + end, + + red_highlight = function(s: any) + return `\27[41;1;30m{s}\27[0m` + end, + + green_highlight = function(s: any) + return `\27[42;1;30m{s}\27[0m` + end, + + gray = function(s: any) + return `\27[30;1m{s}\27[0m` + end, +} + + +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) + +type i53 = number +type i24 = number + +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 ECS_GENERATION(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 +end + +local ECS_ID = ECS_ENTITY_T_LO + +local function ECS_COMBINE(source: number, target: number): i53 + return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) +end + +local function ECS_GENERATION_INC(e: i53) + if e > ECS_ENTITY_MASK then + local flags = e // ECS_ID_FLAGS_MASK + local id = flags // ECS_ENTITY_MASK + local generation = flags % ECS_GENERATION_MASK + + local next_gen = generation + 1 + if next_gen > ECS_GENERATION_MASK then + return id + end + + return ECS_COMBINE(id, next_gen) + flags + end + return ECS_COMBINE(e, 1) +end + +local function bl() + print("") +end + +local function pe(e) + local gen = ECS_GENERATION(e) + return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`) +end + +local function dprint(tbl: { [number]: number }) + bl() + print("--------") + for i, e in tbl do + print("| "..pe(e).." |") + print("--------") + end + bl() +end + +local max_id = 0 +local alive_count = 0 +local dense = {} +local sparse = {} +local function alloc() + if alive_count ~= #dense then + alive_count += 1 + print("*recycled", pe(dense[alive_count])) + return dense[alive_count] + end + max_id += 1 + local id = max_id + alive_count += 1 + dense[alive_count] = id + sparse[id] = { + dense = alive_count + } + print("*allocated", pe(id)) + return id +end + +local function remove(entity) + local id = ECS_ID(entity) + local r = sparse[id] + local index_of_deleted_entity = r.dense + local last_entity_alive_at_index = alive_count -- last entity alive + alive_count -= 1 + local last_alive_entity = dense[last_entity_alive_at_index] + local r_swap = sparse[ECS_ID(last_alive_entity)] + r_swap.dense = r.dense + r.dense = last_entity_alive_at_index + dense[index_of_deleted_entity] = last_alive_entity + dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity) + print("*dellocated", pe(id)) +end + +local function alive(e) + local r = sparse[ECS_ID(e)] + + return dense[r.dense] == e +end + +local function pa(e) + print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`) +end + +local tprint = require("@testkit").print +local e1v0 = alloc() +local e2v0 = alloc() +local e3v0 = alloc() +local e4v0 = alloc() +local e5v0 = alloc() +pa(e1v0) +pa(e4v0) +remove(e5v0) +pa(e5v0) + +local e5v1 = alloc() +pa(e5v0) +pa(e5v1) +pa(e2v0) +print(ECS_ID(e2v0)) + +dprint(dense) +remove(e2v0) +dprint(dense) diff --git a/test/stress.client.luau b/test/stress.client.luau new file mode 100644 index 0000000..9bc7775 --- /dev/null +++ b/test/stress.client.luau @@ -0,0 +1,122 @@ +local RunService = game:GetService("RunService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +_G.__JECS_HI_COMPONENT_ID = 300 +local ecs = require(ReplicatedStorage.ecs) + +-- 500 entities +-- 2-30 components on each entity +-- 300 unique components +-- 200 systems +-- 1-10 components to query per system + +local startTime = os.clock() + +local world = ecs.World.new() + +local components = {} + +for i = 1, 300 do -- 300 components + components[i] = world:component() +end + +local archetypes = {} +for i = 1, 50 do -- 50 archetypes + local archetype = {} + + for _ = 1, math.random(2, 30) do + local componentId = math.random(1, #components) + + table.insert(archetype, components[componentId]) + end + + archetypes[i] = archetype +end + +for _ = 1, 1000 do -- 1000 entities in the world + local componentsToAdd = {} + + local archetypeId = math.random(1, #archetypes) + local e = world:entity() + for _, component in ipairs(archetypes[archetypeId]) do + world:set(e, component, { + DummyData = math.random(1, 5000), + }) + end +end + +local function values(t) + local array = {} + for _, v in t do + table.insert(array, v) + end + return array +end + +local contiguousComponents = values(components) +local systemComponentsToQuery = {} + +for _ = 1, 200 do -- 200 systems + local numComponentsToQuery = math.random(1, 10) + local componentsToQuery = {} + + for _ = 1, numComponentsToQuery do + table.insert(componentsToQuery, contiguousComponents[math.random(1, #contiguousComponents)]) + end + + table.insert(systemComponentsToQuery, componentsToQuery) +end + +local worldCreateTime = os.clock() - startTime +local results = {} +startTime = os.clock() + +RunService.Heartbeat:Connect(function() + local added = 0 + local systemStartTime = os.clock() + debug.profilebegin("systems") + for _, componentsToQuery in ipairs(systemComponentsToQuery) do + debug.profilebegin("system") + for entityId, firstComponent in world:query(unpack(componentsToQuery)) do + world:set( + entityId, + { + DummyData = firstComponent.DummyData + 1, + } + ) + added += 1 + end + debug.profileend() + end + debug.profileend() + + if os.clock() - startTime < 4 then + -- discard first 4 seconds + return + end + + if results == nil then + return + elseif #results < 1000 then + table.insert(results, os.clock() - systemStartTime) + else + print("added", added) + print("World created in", worldCreateTime * 1000, "ms") + local sum = 0 + for _, result in ipairs(results) do + sum += result + end + print(("Average frame time: %fms"):format((sum / #results) * 1000)) + + results = nil + + local n = #world.archetypes + + print( + ("X entities\n%d components\n%d systems\n%d archetypes"):format( + #components, + #systemComponentsToQuery, + n + ) + ) + end +end) diff --git a/test/stress.project.json b/test/stress.project.json new file mode 100644 index 0000000..7962bad --- /dev/null +++ b/test/stress.project.json @@ -0,0 +1,19 @@ +{ + "name": "Stress", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + "$path": "DevPackages", + "ecs": { + "$path": "jecs.luau" + } + }, + "ReplicatedFirst": { + "$className": "ReplicatedFirst", + "stres": { + "$path": "stress.client.luau" + } + } + } +} diff --git a/test/tests.luau b/test/tests.luau new file mode 100644 index 0000000..9fe76af --- /dev/null +++ b/test/tests.luau @@ -0,0 +1,1950 @@ +local jecs = require("@jecs") + +local testkit = require("@testkit") +local BENCH, START = testkit.benchmark() +local __ = jecs.Wildcard +local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION +local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC +local IS_PAIR = jecs.IS_PAIR +local pair = jecs.pair +local ecs_pair_first = jecs.pair_first +local ecs_pair_second = jecs.pair_second +local entity_index_try_get_any = jecs.entity_index_try_get_any +local entity_index_get_alive = jecs.entity_index_get_alive +local entity_index_is_alive = jecs.entity_index_is_alive +local ChildOf = jecs.ChildOf +local world_new = jecs.World.new + +local it = testkit.test() +local TEST, CASE = it.TEST, it.CASE +local CHECK, FINISH = it.CHECK, it.FINISH +local SKIP, FOCUS = it.SKIP, it.FOCUS +local CHECK_EXPECT_ERR = it.CHECK_EXPECT_ERR + +local N = 2 ^ 8 + +type World = jecs.World +type Entity = jecs.Entity + +local c = { + white_underline = function(s: any) + return `\27[1;4m{s}\27[0m` + end, + + white = function(s: any) + return `\27[37;1m{s}\27[0m` + end, + + green = function(s: any) + return `\27[32;1m{s}\27[0m` + end, + + red = function(s: any) + return `\27[31;1m{s}\27[0m` + end, + + yellow = function(s: any) + return `\27[33;1m{s}\27[0m` + end, + + red_highlight = function(s: any) + return `\27[41;1;30m{s}\27[0m` + end, + + green_highlight = function(s: any) + return `\27[42;1;30m{s}\27[0m` + end, + + gray = function(s: any) + return `\27[30;1m{s}\27[0m` + end, +} + +local function pe(e) + local gen = ECS_GENERATION(e) + return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`) +end + +local function pp(e) + local gen = ECS_GENERATION(e) + return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{jecs.ECS_ENTITY_T_HI(e)}`) +end + +local function debug_world_inspect(world: World) + local function record(e): jecs.Record + return entity_index_try_get_any(world.entity_index, e) :: any + end + local function tbl(e) + return record(e).archetype + end + local function archetype(e) + return tbl(e).type + end + local function records(e) + return tbl(e).records + end + local function columns(e) + return tbl(e).columns + end + local function row(e) + return record(e).row + end + + -- Important to order them in the order of their columns + local function tuple(e, ...) + for i, column in columns(e) do + if select(i, ...) ~= column[row(e)] then + return false + end + end + return true + end + + return { + record = record, + tbl = tbl, + archetype = archetype, + records = records, + row = row, + tuple = tuple, + columns = columns + } +end + +local dwi = debug_world_inspect + +local function name(world, e) + return world:get(e, jecs.Name) +end + +TEST("#adding a recycled target", function() + local world = world_new() + local R = world:component() + + local e = world:entity() + local T = world:entity() + world:add(e, pair(R, T)) + world:delete(T) + CHECK(not world:has(e, pair(R, T))) + local T2 = world:entity() + world:add(e, pair(R, T2)) + CHECK(world:target(e, R) ~= T) + CHECK(world:target(e, R) ~= 0) + +end) + +TEST("#repro2", function() + local world = world_new() + local Lifetime = world:component() :: jecs.Id + local Particle = world:entity() + local Beam = world:entity() + + local entity = world:entity() + world:set(entity, pair(Lifetime, Particle), 1) + world:set(entity, pair(Lifetime, Beam), 2) + world:set(entity, pair(4, 5), 6) -- noise + + local entity_visualizer = require("@tools/entity_visualiser") + -- entity_visualizer.components(world, entity) + + for e in world:each(pair(Lifetime, __)) do + local i = 0 + local nth = world:target(e, Lifetime, i) + while nth do + -- entity_visualizer.components(world, e) + + local data = world:get(e, pair(Lifetime, nth)) + data -= 1 + if data <= 0 then + world:remove(e, pair(Lifetime, nth)) + else + world:set(e, pair(Lifetime, nth), data) + end + i += 1 + nth = world:target(e, Lifetime, i) + end + end + + CHECK(not world:has(entity, pair(Lifetime, Particle))) + CHECK(world:get(entity, pair(Lifetime, Beam)) == 1) +end) + +local lifetime_tracker_add = require("@tools/lifetime_tracker") + +TEST("another", function() + local world = world_new() + -- world = lifetime_tracker_add(world, {padding_enabled=false}) + local e1 = world:entity() + local e2 = world:entity() + local e3 = world:entity() + world:delete(e2) + local e2_e3 = pair(e2, e3) + CHECK(jecs.pair_first(world, e2_e3) == 0) + CHECK(jecs.pair_second(world, e2_e3) == e3) + CHECK_EXPECT_ERR(function() + world:add(e1, pair(e2, e3)) + end) +end) + +TEST("#repro", function() + local world = world_new() + + local function getTargets(relation) + local tgts = {} + local pairwildcard = pair(relation, jecs.Wildcard) + for _, archetype in world:query(pairwildcard):archetypes() do + local tr = archetype.records[pairwildcard] + local count = archetype.counts[pairwildcard] + local types = archetype.types + for _, entity in archetype.entities do + for i = 0, count - 1 do + local tgt = jecs.pair_second(world, types[i + tr]) + table.insert(tgts, tgt) + end + end + end + return tgts + end + + local Attacks = world:component() + local Eats = world:component() + + local function setAttacksAndEats(entity1, entity2) + world:add(entity1, pair(Attacks, entity2)) + world:add(entity1, pair(Eats, entity2)) + end + + local e1 = world:entity() + local e2 = world:entity() + local e3 = world:entity() + setAttacksAndEats(e3, e1) + setAttacksAndEats(e3, e2) + setAttacksAndEats(e1, e2) + local d = dwi(world) + world:delete(e2) + local types1 = { pair(Attacks, e1), pair(Eats, e1) } + table.sort(types1) + + + CHECK(d.tbl(e1).type == "") + CHECK(d.tbl(e3).type == table.concat(types1, "_")) + + for _, entity in getTargets(Attacks) do + CHECK(entity == e1) + end + for _, entity in getTargets(Eats) do + CHECK(entity == e1) + end +end) + +TEST("archetype", function() + local archetype_traverse_add = jecs.archetype_traverse_add + local archetype_traverse_remove = jecs.archetype_traverse_remove + + local world = world_new() + local root = world.ROOT_ARCHETYPE + local c1 = world:component() + local c2 = world:component() + local c3 = world:component() + + local a1 = archetype_traverse_add(world, c1, nil :: any) + local a2 = archetype_traverse_remove(world, c1, a1) + CHECK(root.add[c1].to == a1) + CHECK(root == a2) +end) + +TEST("world:cleanup()", function() + local world = world_new() + local A = world:component() :: jecs.Id + local B = world:component() :: jecs.Id + local C = world:component() :: jecs.Id + + local e1 = world:entity() + local e2 = world:entity() + local e3 = world:entity() + + world:set(e1, A, true) + + world:set(e2, A, true) + world:set(e2, B, true) + + + world:set(e3, A, true) + world:set(e3, B, true) + world:set(e3, C, true) + + local archetype_index = world.archetype_index + + CHECK(#archetype_index["1"].entities == 1) + CHECK(#archetype_index["1_2"].entities == 1) + CHECK(#archetype_index["1_2_3"].entities == 1) + + world:delete(e1) + world:delete(e2) + world:delete(e3) + + world:cleanup() + + archetype_index = world.archetype_index + + CHECK((archetype_index["1"] :: jecs.Archetype?) == nil) + CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil) + CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil) + + local e4 = world:entity() + world:set(e4, A, true) + CHECK(#archetype_index["1"].entities == 1) + CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil) + CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil) + world:set(e4, B, true) + CHECK(#archetype_index["1"].entities == 0) + CHECK(#archetype_index["1_2"].entities == 1) + CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil) + world:set(e4, C, true) + CHECK(#archetype_index["1"].entities == 0) + CHECK(#archetype_index["1_2"].entities == 0) + CHECK(#archetype_index["1_2_3"].entities == 1) +end) + +local pe = require("@tools/entity_visualiser").prettify +local lifetime_tracker_add = require("@tools/lifetime_tracker") + +TEST("world:entity()", function() + do + CASE("unique IDs") + local world = jecs.World.new() + local set = {} + for i = 1, N do + local e = world:entity() + CHECK(not set[e]) + set[e] = true + end + end + do + CASE("generations") + local world = jecs.World.new() + local e = world:entity() :: number + CHECK(ECS_ID(e) == 1 + jecs.Rest :: number) + CHECK(ECS_GENERATION(e) == 0) -- 0 + e = ECS_GENERATION_INC(e) + CHECK(ECS_GENERATION(e) == 1) -- 1 + end + + do CASE "pairs" + local world = jecs.World.new() + local _e = world:entity() + local e2 = world:entity() + local e3 = world:entity() + + -- Incomplete pair, must have a bit flag that notes it is a pair + CHECK(IS_PAIR(world:entity()) == false) + + local p = pair(e2, e3) + CHECK(IS_PAIR(p) == true) + + CHECK(ecs_pair_first(world, p) == e2 :: number) + CHECK(ecs_pair_second(world, p) == e3 :: number) + + world:delete(e2) + local e2v2 = world:entity() + CHECK(IS_PAIR(e2v2) == false) + + CHECK(IS_PAIR(pair(e2v2, e3)) == true) + end + + do CASE "Recycling" + local world = world_new() + local e = world:entity() + world:delete(e) + local e1 = world:entity() + world:delete(e1) + local e2 = world:entity() + CHECK(ECS_ID(e2) == e :: number) + CHECK(ECS_GENERATION(e2) == 2) + CHECK(world:contains(e2)) + CHECK(not world:contains(e1)) + CHECK(not world:contains(e)) + end + + do CASE "Recycling max generation" + local world = world_new() + local pin = (jecs.Rest :: any) :: number + 1 + for i = 1, 2^16-1 do + local e = world:entity() + world:delete(e) + end + local e = world:entity() + CHECK(ECS_ID(e) == pin) + CHECK(ECS_GENERATION(e) == 2^16-1) + world:delete(e) + e = world:entity() + CHECK(ECS_ID(e) == pin) + CHECK(ECS_GENERATION(e) == 0) + end +end) + +TEST("world:set()", function() + do CASE "archetype move" + do + local world = jecs.World.new() + + local d = debug_world_inspect(world) + + local _1 = world:component() + local _2 = world:component() + local e = world:entity() + -- An entity starts without an archetype or row + -- should therefore not need to copy over data + CHECK(d.tbl(e) == nil) + CHECK(d.row(e) == nil) + + local archetypes = #world.archetypes + -- This should create a new archetype since it is the first + -- entity to have moved there + world:set(e, _1, 1) + local oldRow = d.row(e) + local oldArchetype = d.archetype(e) + CHECK(#world.archetypes == archetypes + 1) + CHECK(oldArchetype == "1") + CHECK(d.tbl(e)) + CHECK(oldRow == 1) + + world:set(e, _2, 2) + CHECK(d.archetype(e) == "1_2") + -- Should have tuple of fields to the next archetype and set the component data + CHECK(d.tuple(e, 1, 2)) + -- Should have moved the data from the old archetype + CHECK(world.archetype_index[oldArchetype].columns[_1][oldRow] == nil) + end + end + + do CASE "pairs" + local world = jecs.World.new() + + local C1 = world:component() + local C2 = world:component() + local T1 = world:entity() + local T2 = world:entity() + + local e = world:entity() + + world:set(e, pair(C1, C2), true) + world:set(e, pair(C1, T1), true) + world:set(e, pair(T1, C1), true) + + CHECK_EXPECT_ERR(function() + world:set(e, pair(T1, T2), true :: any) + end) + + CHECK(world:get(e, pair(C1, C2))) + CHECK(world:get(e, pair(C1, T1))) + CHECK(world:get(e, pair(T1, C1))) + CHECK(not world:get(e, pair(T1, T2))) + + local e2 = world:entity() + + CHECK_EXPECT_ERR(function() + world:set(e2, pair(jecs.ChildOf, e), true :: any) + end) + CHECK(not world:get(e2, pair(jecs.ChildOf, e))) + end +end) + +TEST("world:remove()", function() + do + CASE("should allow remove a component that doesn't exist on entity") + local world = jecs.World.new() + + local Health = world:component() + local Poison = world:component() + + local id = world:entity() + do + world:remove(id, Poison) + CHECK(true) -- Didn't error + end + + world:set(id, Health, 50) + world:remove(id, Poison) + + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == 50) + end +end) + +TEST("world:add()", function() + do + CASE("idempotent") + local world = jecs.World.new() + local d = debug_world_inspect(world) + local _1, _2 = world:component(), world:component() + + local e = world:entity() + world:add(e, _1) + world:add(e, _2) + world:add(e, _2) -- should have 0 effects + CHECK(d.archetype(e) == "1_2") + end + + do + CASE("archetype move") + do + local world = jecs.World.new() + + local d = debug_world_inspect(world) + + local _1 = world:component() + local e = world:entity() + -- An entity starts without an archetype or row + -- should therefore not need to copy over data + CHECK(d.tbl(e) == nil) + CHECK(d.row(e) == nil) + + local archetypes = #world.archetypes + -- This should create a new archetype + world:add(e, _1) + CHECK(#world.archetypes == archetypes + 1) + + CHECK(d.archetype(e) == "1") + CHECK(d.tbl(e)) + end + end +end) + +TEST("world:query()", function() + do CASE "cached" + local world = world_new() + local Foo = world:component() + local Bar = world:component() + local Baz = world:component() + local e = world:entity() + local q = world:query(Foo, Bar):without(Baz):cached() + world:set(e, Foo, true) + world:set(e, Bar, false) + local i = 0 + + local iter = 0 + for _, e in q:iter() do + iter += 1 + i=1 + end + CHECK (iter == 1) + CHECK(i == 1) + for _, e in q:iter() do + i=2 + end + CHECK(i == 2) + for _, e in q :: any do + i=3 + end + CHECK(i == 3) + for _, e in q :: any do + i=4 + end + CHECK(i == 4) + + CHECK(#q:archetypes() == 1) + CHECK(not table.find(q:archetypes(), world.archetype_index[table.concat({Foo, Bar, Baz}, "_")])) + world:delete(Foo) + CHECK(#q:archetypes() == 0) + end + do CASE "multiple iter" + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local e = world:entity() + world:add(e, A) + world:add(e, B) + local q = world:query(A, B) + local counter = 0 + for x in q:iter() do + counter += 1 + end + for x in q:iter() do + counter += 1 + end + CHECK(counter == 2) + end + do CASE "tag" + local world = jecs.World.new() + local A = world:entity() + local e = world:entity() + CHECK_EXPECT_ERR(function() + world:set(e, A, "test" :: any) + end) + local count = 0 + for id, a in world:query(A) :: any do + count += 1 + CHECK(a == nil) + end + CHECK(count == 1) + end + do CASE "pairs" + local world = jecs.World.new() + + local C1 = world:component() :: jecs.Id + local C2 = world:component() :: jecs.Id + local T1 = world:entity() + local T2 = world:entity() + + local e = world:entity() + + local C1_C2 = pair(C1, C2) + world:set(e, C1_C2, true) + world:set(e, pair(C1, T1), true) + world:set(e, pair(T1, C1), true) + CHECK_EXPECT_ERR(function() + world:set(e, pair(T1, T2), true :: any) + end) + + for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)):iter() do + CHECK(a == true) + CHECK(b == true) + CHECK(c == true) + CHECK(d == nil) + end + end + do + CASE("query single component") + do + local world = jecs.World.new() + local A = world:component() + local B = world:component() + + local entities = {} + for i = 1, N do + local id = world:entity() + + world:set(id, A, true) + if i > 5 then + world:set(id, B, true) + end + entities[i] = id + end + + for id in world:query(A) :: any do + table.remove(entities, CHECK(table.find(entities, id))) + end + + CHECK(#entities == 0) + end + + do + 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) + + -- Should drain the iterator + local q = world:query(A) + + local i = 0 + local j = 0 + for _ in q :: any do + i += 1 + end + for _ in q :: any do + j += 1 + end + CHECK(i == 2) + CHECK(j == 0) + end + end + + do + CASE("query missing component") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() + + local e1 = world:entity() + local e2 = world:entity() + + world:set(e1, A, "abc") + world:set(e2, A, "def") + world:set(e1, B, 123) + world:set(e2, B, 457) + + local counter = 0 + for _ in world:query(B, C) :: any do + counter += 1 + end + CHECK(counter == 0) + end + + do + CASE("query more than 8 components") + local world = jecs.World.new() + local components = {} + + for i = 1, 9 do + local id = world:component() + components[i] = id + end + local e = world:entity() + for i, id in components do + world:set(e, id, 13 ^ i) + end + + for entity, a, b, c, d, e, f, g, h, i in world:query(unpack(components)) :: any do + CHECK(a == 13 ^ 1) + CHECK(b == 13 ^ 2) + CHECK(c == 13 ^ 3) + CHECK(d == 13 ^ 4) + CHECK(e == 13 ^ 5) + CHECK(f == 13 ^ 6) + CHECK(g == 13 ^ 7) + CHECK(h == 13 ^ 8) + CHECK(i == 13 ^ 9) + end + 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 it = world:query(A):iter() + + local e: number, data = it() + while e do + if e == eA :: number then + CHECK(data) + elseif e == eAB :: number then + CHECK(data) + else + CHECK(false) + end + + e, data = it() + end + CHECK(true) + end + + do CASE "should query all matching entities when irrelevant component is removed" + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() + + local entities = {} + for i = 1, N do + local id = world:entity() + + -- specifically put them in disorder to track regression + -- https://github.com/Ukendio/jecs/pull/15 + world:set(id, B, true) + world:set(id, A, true) + if i > 5 then + world:remove(id, B) + end + entities[i] = id + end + + local added = 0 + for id in world:query(A) :: any do + added += 1 + table.remove(entities, CHECK(table.find(entities, id))) + end + + CHECK(added == N) + end + + do + CASE("should query all entities without B") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + + local entities = {} + for i = 1, N do + local id = world:entity() + + world:set(id, A, true) + if i < 5 then + entities[i] = id + else + world:set(id, B, true) + end + end + + for id in world:query(A):without(B) :: any do + table.remove(entities, CHECK(table.find(entities, id))) + end + + CHECK(#entities == 0) + end + + do + CASE("should allow querying for relations") + local world = jecs.World.new() + local Eats = world:component() + local Apples = world:component() + local bob = world:entity() + + world:set(bob, pair(Eats, Apples), true) + for e, bool in world:query(pair(Eats, Apples)) :: any do + CHECK(e == bob) + CHECK(bool) + end + end + + do + CASE("should allow wildcards in queries") + local world = jecs.World.new() + local Eats = world:component() + local Apples = world:entity() + local bob = world:entity() + + world:set(bob, pair(Eats, Apples), "bob eats apples") + + local w = jecs.Wildcard + for e, data in world:query(pair(Eats, w)) :: any do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + for e, data in world:query(pair(w, Apples)) :: any do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + end + + do + CASE("should match against multiple pairs") + local world = jecs.World.new() + local Eats = world:component() + local Apples = world:entity() + local Oranges = world:entity() + local bob = world:entity() + local alice = world:entity() + + world:set(bob, pair(Eats, Apples), "bob eats apples") + world:set(alice, pair(Eats, Oranges), "alice eats oranges") + + local w = jecs.Wildcard + local count = 0 + for e, data in world:query(pair(Eats, w)) :: any do + count += 1 + if e == bob then + CHECK(data == "bob eats apples") + else + CHECK(data == "alice eats oranges") + end + end + + CHECK(count == 2) + count = 0 + + for e, data in world:query(pair(w, Apples)) :: any do + count += 1 + CHECK(data == "bob eats apples") + end + CHECK(count == 1) + end + + do CASE "should only relate alive entities" + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:component() + local Oranges = world:component() + local bob = world:entity() + local alice = world:entity() + + world:set(bob, Apples, "apples") + world:set(bob, pair(Eats, Apples), "bob eats apples") + world:set(alice, pair(Eats, Oranges) :: Entity, "alice eats oranges") + + world:delete(Apples) + local Wildcard = jecs.Wildcard + + local count = 0 + for _, data in world:query(pair(Wildcard, Apples)) :: any do + count += 1 + end + + world:delete(pair(Eats, Apples)) + + CHECK(count == 0) + CHECK(world:get(bob, pair(Eats, Apples)) == nil) + + end + + do + CASE("should error when setting invalid pair") + local world = jecs.World.new() + local Eats = world:component() + local Apples = world:component() + local bob = world:entity() + + world:delete(Apples) + CHECK_EXPECT_ERR(function() + world:set(bob, pair(Eats, Apples), "bob eats apples") + end) + end + + do + CASE("should find target for ChildOf") + local world = jecs.World.new() + local ChildOf = jecs.ChildOf + + local Name = world:component() + + local bob = world:entity() + local alice = world:entity() + local sara = world:entity() + + world:add(bob, pair(ChildOf, alice)) + world:set(bob, Name, "bob") + world:add(sara, pair(ChildOf, alice)) + world:set(sara, Name, "sara") + CHECK(world:parent(bob) :: number == alice :: number) -- O(1) + + local count = 0 + for _, name in world:query(Name, pair(ChildOf, alice)) :: any do + count += 1 + end + CHECK(count == 2) + end + + do + CASE("despawning while iterating") + 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) :: any do + world:clear(id) + count += 1 + end + CHECK(count == 2) + end + + do CASE("iterator invalidation") + do CASE("adding") + SKIP() + 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) :: any do + world:add(id, B) + + count += 1 + end + + CHECK(count == 2) + end + + do CASE("spawning") + 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) + + for id in world:query(A) :: any do + local e = world:entity() + world:add(e, A) + world:add(e, B) + end + + CHECK(true) + end + end + + do CASE("should not find any entities") + local world = jecs.World.new() + + local Hello = world:component() + local Bob = world:component() + + local helloBob = world:entity() + world:add(helloBob, pair(Hello, Bob)) + world:add(helloBob, Bob) + + local withoutCount = 0 + for _ in world:query(pair(Hello, Bob)):without(Bob) :: any do + withoutCount += 1 + end + + CHECK(withoutCount == 0) + end + + do CASE("without") + -- REGRESSION TEST + local world = jecs.World.new() + local _1, _2, _3 = world:component(), world:component(), world:component() + + local counter = 0 + for e, a, b in world:query(_1, _2):without(_3) :: any do + counter += 1 + end + CHECK(counter == 0) + end +end) + +TEST("world:each", function() + local world = world_new() + local A = world:component() + local B = world:component() + local C = world:component() + + local e3 = world:entity() + local e1 = world:entity() + local e2 = world:entity() + + world:set(e1, A, true) + + world:set(e2, A, true) + world:set(e2, B, true) + + world:set(e3, A, true) + world:set(e3, B, true) + world:set(e3, C, true) + + for entity: number in world:each(A) do + if entity == e1 :: number or entity == e2 :: number or entity == e3 :: number then + CHECK(true) + continue + end + CHECK(false) + end +end) + +TEST("world:children", function() + local world = world_new() + local C = world:component() + local T = world:entity() + + local e1 = world:entity() + world:set(e1, C, true) + + local e2 = world:entity() :: number + + world:add(e2, T) + world:add(e2, pair(ChildOf, e1)) + + local e3 = world:entity() :: number + world:add(e3, pair(ChildOf, e1)) + + local count = 0 + for entity: number in world:children(e1) do + count += 1 + if entity == e2 or entity == e3 then + CHECK(true) + continue + end + CHECK(false) + end + CHECK(count == 2) + + world:remove(e2, pair(ChildOf, e1)) + + count = 0 + for entity in world:children(e1) do + count += 1 + end + + CHECK(count == 1) +end) + +TEST("world:clear()", function() + do CASE("should remove its components") + local world = jecs.World.new() :: World + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + + local e = world:entity() + local e1 = world:entity() + local e2 = world:entity() + + world:set(e, A, true) + world:set(e, B, true) + + world:set(e1, A, true) + world:set(e1, B, true) + + CHECK(world:get(e, A)) + CHECK(world:get(e, B)) + + world:clear(A) + CHECK(world:get(e, A) == nil) + CHECK(world:get(e, B)) + CHECK(world:get(e1, A) == nil) + CHECK(world:get(e1, B)) + end + + do CASE("remove cleared ID from entities") + local world = world_new() + local A = world:component() + local B = world:component() + local C = world:component() + + do + local id1 = world:entity() + local id2 = world:entity() + local id3 = world:entity() + + world:set(id1, A, true) + + world:set(id2, A, true) + world:set(id2, B, true) + + world:set(id3, A, true) + world:set(id3, B, true) + world:set(id3, C, true) + + world:clear(A) + + CHECK(not world:has(id1, A)) + CHECK(not world:has(id2, A)) + CHECK(not world:has(id3, A)) + + CHECK(world:has(id2, B)) + CHECK(world:has(id3, B, C)) + + world:clear(C) + + CHECK(world:has(id2, B)) + CHECK(world:has(id3, B)) + + CHECK(world:contains(A)) + CHECK(world:contains(C)) + CHECK(world:has(A, jecs.Component)) + CHECK(world:has(B, jecs.Component)) + end + + do + local id1 = world:entity() + local id2 = world:entity() + local id3 = world:entity() + + local tgt = world:entity() + + world:add(id1, pair(A, tgt)) + world:add(id1, pair(B, tgt)) + world:add(id1, pair(C, tgt)) + + world:add(id2, pair(A, tgt)) + world:add(id2, pair(B, tgt)) + world:add(id2, pair(C, tgt)) + + world:add(id3, pair(A, tgt)) + world:add(id3, pair(B, tgt)) + world:add(id3, pair(C, tgt)) + + world:clear(B) + CHECK(world:has(id1, pair(A, tgt), pair(C, tgt))) + CHECK(not world:has(id1, pair(B, tgt))) + CHECK(world:has(id2, pair(A, tgt), pair(C, tgt))) + CHECK(not world:has(id1, pair(B, tgt))) + CHECK(world:has(id3, pair(A, tgt), pair(C, tgt))) + + end + + end +end) + +TEST("world:has()", function() + do CASE("should find Tag on entity") + local world = jecs.World.new() + + local Tag = world:entity() + + local e = world:entity() + world:add(e, Tag) + + CHECK(world:has(e, Tag)) + end + + do CASE("should return false when missing one tag") + local world = jecs.World.new() + + local A = world:entity() + local B = world:entity() + local C = world:entity() + local D = world:entity() + + local e = world:entity() + world:add(e, A) + world:add(e, C) + world:add(e, D) + + CHECK(world:has(e, A, B, C, D) == false) + end +end) + +TEST("world:component()", function() + do CASE("only components should have EcsComponent trait") + local world = jecs.World.new() :: World + local A = world:component() + local e = world:entity() + + CHECK(world:has(A, jecs.Component)) + CHECK(not world:has(e, jecs.Component)) + end + + do CASE("tag") + local world = jecs.World.new() :: World + local A = world:component() + local B = world:entity() + local C = world:entity() + local e = world:entity() + world:set(e, A, "test") + world:add(e, B) + CHECK_EXPECT_ERR(function() + world:set(e, C, 11 :: any) + end) + + CHECK(world:has(e, A)) + CHECK(world:get(e, A) == "test") + CHECK(world:get(e, B) == nil) + CHECK(world:get(e, C) == nil) + end +end) + +TEST("world:delete", function() + do CASE "invoke OnRemove hooks" + local world = world_new() + + local e1 = world:entity() + local e2 = world:entity() + + local Stable = world:component() + world:set(Stable, jecs.OnRemove, function(e) + CHECK(e == e1) + end) + + world:set(e1, Stable, true) + world:set(e2, Stable, true) + + world:delete(e1) + end + do CASE "delete recycled entity id used as component" + local world = world_new() + local id = world:entity() + world:add(id, jecs.Component) + + local e = world:entity() + world:set(e, id, 1) + CHECK(world:get(e, id) == 1) + world:delete(id) + local recycled = world:entity() + world:add(recycled, jecs.Component) + world:set(e, recycled, 1) + CHECK(world:has(recycled, jecs.Component)) + CHECK(world:get(e, recycled) == 1) + end + do + CASE("bug: Empty entity does not respect cleanup policy") + local world = world_new() + local parent = world:entity() + local tag = world:entity() + + local child = world:entity() + world:add(child, jecs.pair(jecs.ChildOf, parent)) + world:delete(parent) + + CHECK(not world:contains(parent)) + CHECK(not world:contains(child)) + + local entity = world:entity() + world:add(entity, tag) + world:delete(tag) + CHECK(world:contains(entity)) + CHECK(not world:contains(tag)) + CHECK(not world:has(entity, tag)) -- => true + end + do CASE("should allow deleting components") + local world = jecs.World.new() + + local Health = world:component() + local Poison = world:component() + + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) + local id1 = world:entity() + world:set(id1, Poison, 500) + world:set(id1, Health, 50) + + world:delete(id) + CHECK(not world:contains(id)) + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == nil) + + CHECK(world:get(id1, Poison) == 500) + CHECK(world:get(id1, Health) == 50) + end + + do CASE("delete entities using another Entity as component with Delete cleanup action") + local world = jecs.World.new() + + local Health = world:entity() + world:add(Health, pair(jecs.OnDelete, jecs.Delete)) + local Poison = world:component() + + local id = world:entity() + world:set(id, Poison, 5) + CHECK_EXPECT_ERR(function() + world:set(id, Health, 50 :: any) + end) + local id1 = world:entity() + world:set(id1, Poison, 500) + CHECK_EXPECT_ERR(function() + world:set(id1, Health, 50 :: any) + end) + + CHECK(world:has(id, Poison, Health)) + CHECK(world:has(id1, Poison, Health)) + world:delete(Poison) + + CHECK(world:contains(id)) + CHECK(not world:has(id, Poison)) + CHECK(not world:has(id1, Poison)) + + world:delete(Health) + CHECK(not world:contains(id)) + CHECK(not world:contains(id1)) + CHECK(not world:has(id, Health)) + CHECK(not world:has(id1, Health)) + end + + + do CASE("delete children") + local world = jecs.World.new() + + local Health = world:component() + local Poison = world:component() + local FriendsWith = world:component() + + local e = world:entity() + world:set(e, Poison, 5) + world:set(e, Health, 50) + + local children = {} + for i = 1, 10 do + local child = world:entity() + world:set(child, Poison, 9999) + world:set(child, Health, 100) + world:add(child, pair(jecs.ChildOf, e)) + table.insert(children, child) + end + + BENCH("delete children of entity", function() + world:delete(e) + end) + + for i, child in children do + CHECK(not world:contains(child)) + CHECK(not world:has(child, pair(jecs.ChildOf, e))) + CHECK(not world:has(child, Health)) + end + + e = world:entity() + + local friends = {} + for i = 1, 10 do + local friend = world:entity() + world:set(friend, Poison, 9999) + world:set(friend, Health, 100) + world:add(friend, pair(FriendsWith, e)) + table.insert(friends, friend) + end + + BENCH("remove friends of entity", function() + world:delete(e) + end) + + local d = debug_world_inspect(world) + for i, friend in friends do + CHECK(not world:has(friend, pair(FriendsWith, e))) + CHECK(world:has(friend, Health)) + CHECK(world:contains(friend)) + end + end + + do CASE("remove deleted ID from entities") + local world = world_new() + do + local A = world:component() + local B = world:component() + local C = world:component() + local id1 = world:entity() + local id2 = world:entity() + local id3 = world:entity() + + world:set(id1, A, true) + + world:set(id2, A, true) + world:set(id2, B, true) + + world:set(id3, A, true) + world:set(id3, B, true) + world:set(id3, C, true) + + world:delete(A) + + CHECK(not world:has(id1, A)) + CHECK(not world:has(id2, A)) + CHECK(not world:has(id3, A)) + + CHECK(world:has(id2, B)) + CHECK(world:has(id3, B, C)) + + world:delete(C) + + CHECK(world:has(id2, B)) + CHECK(world:has(id3, B)) + + CHECK(not world:contains(A)) + CHECK(not world:contains(C)) + end + + do + local A = world:component() + world:add(A, pair(jecs.OnDeleteTarget, jecs.Delete)) + local B = world:component() + local C = world:component() + world:add(C, pair(jecs.OnDeleteTarget, jecs.Delete)) + + local id1 = world:entity() + local id2 = world:entity() + local id3 = world:entity() + + world:set(id1, C, true) + + world:set(id2, pair(A, id1), true) + world:set(id2, B, true) + + world:set(id3, B, true) + world:set(id3, pair(C, id2), true) + + world:delete(id1) + + CHECK(not world:contains(id1)) + CHECK(not world:contains(id2)) + CHECK(not world:contains(id3)) + end + + + do + local A = world:component() + local B = world:component() + local C = world:component() + local id1 = world:entity() + local id2 = world:entity() + local id3 = world:entity() + + + world:set(id2, A, true) + world:set(id2, pair(B, id1), true) + + world:set(id3, A, true) + world:set(id3, pair(B, id1), true) + world:set(id3, C, true) + + world:delete(id1) + + CHECK(not world:contains(id1)) + CHECK(world:contains(id2)) + CHECK(world:contains(id3)) + + CHECK(world:has(id2, A)) + CHECK(world:has(id3, A, C)) + + CHECK(not world:target(id2, B)) + CHECK(not world:target(id3, B)) + end + end + + do + CASE("fast delete") + local world = jecs.World.new() + + local entities = {} + local Health = world:component() + local Poison = world:component() + + for i = 1, 100 do + local child = world:entity() + world:set(child, Poison, 9999) + world:set(child, Health, 100) + table.insert(entities, child) + end + + BENCH("simple deletion of entity", function() + for i = 1, START(100) do + local e = entities[i] + world:delete(e) + end + end) + + for _, entity in entities do + CHECK(not world:contains(entity)) + end + end + + do + CASE("cycle") + local world = jecs.World.new() + local Likes = world:component() + world:add(Likes, pair(jecs.OnDeleteTarget, jecs.Delete)) + local bob = world:entity() + local alice = world:entity() + + world:add(bob, pair(Likes, alice)) + world:add(alice, pair(Likes, bob)) + + world:delete(bob) + CHECK(not world:contains(bob)) + CHECK(not world:contains(alice)) + end +end) + +TEST("world:target", function() + do CASE("nth index") + local world = world_new() + local A = world:component() + world:set(A, jecs.Name, "A") + local B = world:component() + world:set(B, jecs.Name, "B") + local C = world:component() + world:set(C, jecs.Name, "C") + local D = world:component() + world:set(D, jecs.Name, "D") + local E = world:component() + world:set(E, jecs.Name, "E") + local e = world:entity() + + world:add(e, pair(A, B)) + world:add(e, pair(A, C)) + world:add(e, pair(A, D)) + world:add(e, pair(A, E)) + world:add(e, pair(B, C)) + world:add(e, pair(B, D)) + world:add(e, pair(C, D)) + + CHECK(pair(A, B) < pair(A, C)) + CHECK(pair(A, C) < pair(A, D)) + CHECK(pair(C, A) < pair(C, D)) + + local records = debug_world_inspect(world).records(e) + CHECK(jecs.pair_first(world, pair(B, C)) == B) + local r = jecs.entity_index_try_get(world.entity_index, e) + local archetype = r.archetype + local counts = archetype.counts + CHECK(counts[pair(A, __)] == 4) + CHECK(records[pair(B, C)] > records[pair(A, E)]) + CHECK(world:target(e, A, 0) == B) + CHECK(world:target(e, A, 1) == C) + CHECK(world:target(e, A, 2) == D) + CHECK(world:target(e, A, 3) == E) + CHECK(world:target(e, B, 0) == C) + CHECK(world:target(e, B, 1) == D) + CHECK(world:target(e, C, 0) == D) + CHECK(world:target(e, C, 1) == nil) + + CHECK(archetype.records[pair(A, B)] == 1) + CHECK(archetype.records[pair(A, C)] == 2) + CHECK(archetype.records[pair(A, D)] == 3) + CHECK(archetype.records[pair(A, E)] == 4) + + CHECK(world:target(e, C, 0) == D) + CHECK(world:target(e, C, 1) == nil) + end + + do + CASE("infer index when unspecified") + local world = world_new() + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + local e = world:entity() + + world:add(e, pair(A, B)) + world:add(e, pair(A, C)) + world:add(e, pair(B, C)) + world:add(e, pair(B, D)) + world:add(e, pair(C, D)) + + CHECK(world:target(e, A) == world:target(e, A, 0)) + CHECK(world:target(e, B) == world:target(e, B, 0)) + CHECK(world:target(e, C) == world:target(e, C, 0)) + end + + do + CASE("loop until no target") + local world = world_new() + + local ROOT = world:entity() + local e1 = world:entity() + local targets = {} + + for i = 1, 10 do + local target = world:entity() + targets[i] = target + world:add(e1, pair(ROOT, target)) + end + + local i = 0 + local target = world:target(e1, ROOT, 0) + while target do + i += 1 + CHECK(targets[i] == target) + target = world:target(e1, ROOT, i) + end + + CHECK(i == 10) + end +end) + +TEST("world:contains", function() + local world = jecs.World.new() + local id = world:entity() + CHECK(world:contains(id)) + + do + CASE("should not exist after delete") + world:delete(id) + CHECK(not world:contains(id)) + end +end) + +TEST("Hooks", function() + do CASE "OnAdd" + local world = jecs.World.new() + local Transform = world:component() + local e1 = world:entity() + world:set(Transform, jecs.OnAdd, function(entity) + CHECK(e1 == entity) + end) + world:add(e1, Transform) + end + + do CASE "OnSet" + local world = jecs.World.new() + local Number = world:component() + local e1 = world:entity() + + world:set(Number, jecs.OnSet, function(entity, data) + CHECK(e1 == entity) + CHECK(data == world:get(entity, Number)) + CHECK(data == 1) + end) + world:set(e1, Number, 1) + end + + do CASE("OnRemove") + do + -- basic + local world = jecs.World.new() + local A = world:component() :: Entity + local e1 = world:entity() + world:set(A, jecs.OnRemove, function(entity) + CHECK(e1 == entity) + CHECK(world:has(e1, A)) + end) + world:add(e1, A) + + world:remove(e1, A) + CHECK(not world:has(e1, A)) + end + do + -- [BUG] https://github.com/Ukendio/jecs/issues/118 + local world = world_new() + local A = world:component() + local B = world:component() + local e = world:entity() + + world:set(A, jecs.OnRemove, function(entity) + world:set(entity, B, true) + CHECK(world:get(entity, A)) + CHECK(world:get(entity, B)) + end) + + world:set(e, A, true) + world:remove(e, A) + CHECK(not world:get(e, A)) + CHECK(world:get(e, B)) + end + end +end) + +TEST("change tracking", function() + do CASE "#1" + local world = world_new() + local Foo = world:component() :: Entity + local Previous = jecs.Rest + + local q1 = world + :query(Foo) + :without(pair(Previous, Foo)) + :cached() + + local e1 = world:entity() + world:set(e1, Foo, 1) + local e2 = world:entity() + world:set(e2, Foo, 2) + + local i = 0 + for e, new in q1 :: any do + i += 1 + world:set(e, pair(Previous, Foo), new) + end + + CHECK(i == 2) + local j = 0 + for e, new in q1 :: any do + j += 1 + world:set(e, pair(Previous, Foo), new) + end + + CHECK(j == 0) + end + + do CASE "#2" + local world = world_new() + local component = world:component() :: Entity + local tag = world:entity() + local previous = jecs.Rest + + local q1 = world:query(component):without(pair(previous, component), tag):cached() + + local testEntity = world:entity() + + world:set(testEntity, component, 10) + + local i = 0 + for entity, number in q1 :: any do + i += 1 + world:add(testEntity, tag) + end + + CHECK(i == 1) + + for e, n in q1 :: any do + world:set(e, pair(previous, component), n) + end + end + +end) + +TEST("repro", function() + do CASE "#1" + local world = world_new() + local reproEntity = world:component() + local components = { Cooldown = world:component() :: jecs.Entity } + world:set(reproEntity, components.Cooldown, 2) + + local function updateCooldowns(dt: number) + local toRemove = {} + + local it = world:query(components.Cooldown):iter() + for id, cooldown in it do + cooldown -= dt + + if cooldown <= 0 then + table.insert(toRemove, id) + -- world:remove(id, components.Cooldown) + else + world:set(id, components.Cooldown, cooldown) + end + end + + for _, id in toRemove do + world:remove(id, components.Cooldown) + CHECK(not world:get(id, components.Cooldown)) + end + end + + updateCooldowns(1.5) + updateCooldowns(1.5) + end + + do CASE "#2" -- ISSUE #171 + local world = world_new() + local component1 = world:component() + local tag1 = world:entity() + + local query = world:query(component1):with(tag1):cached() + + local entity = world:entity() + world:set(entity, component1, "some data") + + local counter = 0 + for x in query:iter() do + counter += 1 + end + CHECK(counter == 0) + end +end) + +TEST("wildcard query", function() + do CASE "#1" + local world = world_new() + local pair = jecs.pair + + local Relation = world:entity() + local Wildcard = jecs.Wildcard + local A = world:entity() + + local relationship = pair(Relation, Wildcard) + local query = world:query(relationship):cached() + + local entity = world:entity() + + local p = pair(Relation, A) + CHECK(jecs.pair_first(world, p) == Relation) + CHECK(jecs.pair_second(world, p) == A) + local w = dwi(world) + world:add(entity, pair(Relation, A)) + + local counter = 0 + for e in query:iter() do + counter += 1 + end + CHECK(counter == 1) + end + do CASE "#2" + local world = world_new() + local pair = jecs.pair + + local Relation = world:entity() + local Wildcard = jecs.Wildcard + local A = world:entity() + + local relationship = pair(Relation, Wildcard) + + local entity = world:entity() + + world:add(entity, pair(Relation, A)) + + local counter = 0 + for e in world:query(relationship):iter() do + counter += 1 + end + CHECK(counter == 1) + end + do CASE "#3" + local world = world_new() + local pair = jecs.pair + + local Relation = world:entity() + local Wildcard = jecs.Wildcard + local A = world:entity() + + local entity = world:entity() + + world:add(entity, pair(Relation, A)) + + local relationship = pair(Relation, Wildcard) + local query = world:query(relationship):cached() + + local counter = 0 + for e in query:iter() do + counter += 1 + end + CHECK(counter == 1) + end +end) + +TEST("world:delete() invokes OnRemove hook", function() + do CASE "#1" + local world = world_new() + + local A = world:entity() + local entity = world:entity() + + local called = false + world:set(A, jecs.OnRemove, function(e) + called = true + end) + + world:add(entity, A) + world:delete(entity) + + CHECK(called) + end + do CASE "#2" + local world = world_new() + local pair = jecs.pair + + local Relation = world:entity() + local A = world:entity() + local B = world:entity() + + world:add(Relation, pair(jecs.OnDelete, jecs.Delete)) + + local entity = world:entity() + + local called = false + world:set(A, jecs.OnRemove, function(e) + called = true + end) + + world:add(entity, A) + world:add(entity, pair(Relation, B)) + + world:delete(B) + + CHECK(called) + end + do CASE "#3" + local world = world_new() + local pair = jecs.pair + + local viewingContainer = world:entity() + local character = world:entity() + local container = world:entity() + + local called = false + world:set(viewingContainer, jecs.OnRemove, function(e) + called = true + end) + + world:add(character, pair(viewingContainer, container)) + + world:delete(container) + + CHECK(called) + end +end) +FINISH() diff --git a/thesis/drafts/1/listings-rust.sty b/thesis/drafts/1/listings-rust.sty new file mode 100644 index 0000000..569e4b2 --- /dev/null +++ b/thesis/drafts/1/listings-rust.sty @@ -0,0 +1,74 @@ +\NeedsTeXFormat{LaTeX2e}[1994/06/01] +\ProvidesPackage{listings-rust}[2018/01/23 Custom Package] +\RequirePackage{color} +\RequirePackage{listings} + +\lstdefinelanguage{Rust}{% + sensitive% +, morecomment=[l]{//}% +, morecomment=[s]{/*}{*/}% +, moredelim=[s][{\itshape\color[rgb]{0,0,0.75}}]{\#[}{]}% +, morestring=[b]{"}% +, alsodigit={}% +, alsoother={}% +, alsoletter={!}% +% +% +% [1] reserve keywords +% [2] traits +% [3] primitive types +% [4] type and value constructors +% [5] identifier +% +, morekeywords={struct, break, continue, else, for, if, in, loop, match, return, while} % control flow keywords +, morekeywords={as, const, let, move, mut, ref, static} % in the context of variables +, morekeywords={dyn, enum, fn, impl, Self, self, struct, trait, type, union, use, where} % in the context of declarations +, morekeywords={crate, extern, mod, pub, super} % in the context of modularisation +, morekeywords={unsafe} % markers +, morekeywords={abstract, alignof, become, box, do, final, macro, offsetof, override, priv, proc, pure, sizeof, typeof, unsized, virtual, yield} % reserved identifiers +% +% grep 'pub trait [A-Za-z][A-Za-z0-9]*' -r . | sed 's/^.*pub trait \([A-Za-z][A-Za-z0-9]*\).*/\1/g' | sort -u | tr '\n' ',' | sed 's/^\(.*\),$/{\1}\n/g' | sed 's/,/, /g' +, morekeywords=[2]{Add, AddAssign, Any, AsciiExt, AsInner, AsInnerMut, AsMut, AsRawFd, AsRawHandle, AsRawSocket, AsRef, Binary, BitAnd, BitAndAssign, Bitor, BitOr, BitOrAssign, BitXor, BitXorAssign, Borrow, BorrowMut, Boxed, BoxPlace, BufRead, BuildHasher, CastInto, CharExt, Clone, CoerceUnsized, CommandExt, Copy, Debug, DecodableFloat, Default, Deref, DerefMut, DirBuilderExt, DirEntryExt, Display, Div, DivAssign, DoubleEndedIterator, DoubleEndedSearcher, Drop, EnvKey, Eq, Error, ExactSizeIterator, ExitStatusExt, Extend, FileExt, FileTypeExt, Float, Fn, FnBox, FnMut, FnOnce, Freeze, From, FromInner, FromIterator, FromRawFd, FromRawHandle, FromRawSocket, FromStr, FullOps, FusedIterator, Generator, Hash, Hasher, Index, IndexMut, InPlace, Int, Into, IntoCow, IntoInner, IntoIterator, IntoRawFd, IntoRawHandle, IntoRawSocket, IsMinusOne, IsZero, Iterator, JoinHandleExt, LargeInt, LowerExp, LowerHex, MetadataExt, Mul, MulAssign, Neg, Not, Octal, OpenOptionsExt, Ord, OsStrExt, OsStringExt, Packet, PartialEq, PartialOrd, Pattern, PermissionsExt, Place, Placer, Pointer, Product, Put, RangeArgument, RawFloat, Read, Rem, RemAssign, Seek, Shl, ShlAssign, Shr, ShrAssign, Sized, SliceConcatExt, SliceExt, SliceIndex, Stats, Step, StrExt, Sub, SubAssign, Sum, Sync, TDynBenchFn, Terminal, Termination, ToOwned, ToSocketAddrs, ToString, Try, TryFrom, TryInto, UnicodeStr, Unsize, UpperExp, UpperHex, WideInt, Write} +, morekeywords=[2]{Send} % additional traits +% +, morekeywords=[3]{bool, char, f32, f64, i8, i16, i32, i64, isize, str, u8, u16, u32, u64, unit, usize, i128, u128} % primitive types +% +, morekeywords=[4]{Err, false, None, Ok, Some, true} % prelude value constructors +% grep 'pub \(type\|struct\|enum\) [A-Za-z][A-Za-z0-9]*' -r . | sed 's/^.*pub \(type\|struct\|enum\) \([A-Za-z][A-Za-z0-9]*\).*/\2/g' | sort -u | tr '\n' ',' | sed 's/^\(.*\),$/{\1}\n/g' | sed 's/,/, /g' +, morekeywords=[3]{AccessError, Adddf3, AddI128, AddoI128, AddoU128, ADDRESS, ADDRESS64, addrinfo, ADDRINFOA, AddrParseError, Addsf3, AddU128, advice, aiocb, Alignment, AllocErr, AnonPipe, Answer, Arc, Args, ArgsInnerDebug, ArgsOs, Argument, Arguments, ArgumentV1, Ashldi3, Ashlti3, Ashrdi3, Ashrti3, AssertParamIsClone, AssertParamIsCopy, AssertParamIsEq, AssertUnwindSafe, AtomicBool, AtomicPtr, Attr, auxtype, auxv, BackPlace, BacktraceContext, Barrier, BarrierWaitResult, Bencher, BenchMode, BenchSamples, BinaryHeap, BinaryHeapPlace, blkcnt, blkcnt64, blksize, BOOL, boolean, BOOLEAN, BoolTrie, BorrowError, BorrowMutError, Bound, Box, bpf, BTreeMap, BTreeSet, Bucket, BucketState, Buf, BufReader, BufWriter, Builder, BuildHasherDefault, BY, BYTE, Bytes, CannotReallocInPlace, cc, Cell, Chain, CHAR, CharIndices, CharPredicateSearcher, Chars, CharSearcher, CharsError, CharSliceSearcher, CharTryFromError, Child, ChildPipes, ChildStderr, ChildStdin, ChildStdio, ChildStdout, Chunks, ChunksMut, ciovec, clock, clockid, Cloned, cmsgcred, cmsghdr, CodePoint, Color, ColorConfig, Command, CommandEnv, Component, Components, CONDITION, condvar, Condvar, CONSOLE, CONTEXT, Count, Cow, cpu, CRITICAL, CStr, CString, CStringArray, Cursor, Cycle, CycleIter, daddr, DebugList, DebugMap, DebugSet, DebugStruct, DebugTuple, Decimal, Decoded, DecodeUtf16, DecodeUtf16Error, DecodeUtf8, DefaultEnvKey, DefaultHasher, dev, device, Difference, Digit32, DIR, DirBuilder, dircookie, dirent, dirent64, DirEntry, Discriminant, DISPATCHER, Display, Divdf3, Divdi3, Divmoddi4, Divmodsi4, Divsf3, Divsi3, Divti3, dl, Dl, Dlmalloc, Dns, DnsAnswer, DnsQuery, dqblk, Drain, DrainFilter, Dtor, Duration, DwarfReader, DWORD, DWORDLONG, DynamicLibrary, Edge, EHAction, EHContext, Elf32, Elf64, Empty, EmptyBucket, EncodeUtf16, EncodeWide, Entry, EntryPlace, Enumerate, Env, epoll, errno, Error, ErrorKind, EscapeDebug, EscapeDefault, EscapeUnicode, event, Event, eventrwflags, eventtype, ExactChunks, ExactChunksMut, EXCEPTION, Excess, ExchangeHeapSingleton, exit, exitcode, ExitStatus, Failure, fd, fdflags, fdsflags, fdstat, ff, fflags, File, FILE, FileAttr, filedelta, FileDesc, FilePermissions, filesize, filestat, FILETIME, filetype, FileType, Filter, FilterMap, Fixdfdi, Fixdfsi, Fixdfti, Fixsfdi, Fixsfsi, Fixsfti, Fixunsdfdi, Fixunsdfsi, Fixunsdfti, Fixunssfdi, Fixunssfsi, Fixunssfti, Flag, FlatMap, Floatdidf, FLOATING, Floatsidf, Floatsisf, Floattidf, Floattisf, Floatundidf, Floatunsidf, Floatunsisf, Floatuntidf, Floatuntisf, flock, ForceResult, FormatSpec, Formatted, Formatter, Fp, FpCategory, fpos, fpos64, fpreg, fpregset, FPUControlWord, Frame, FromBytesWithNulError, FromUtf16Error, FromUtf8Error, FrontPlace, fsblkcnt, fsfilcnt, fsflags, fsid, fstore, fsword, FullBucket, FullBucketMut, FullDecoded, Fuse, GapThenFull, GeneratorState, gid, glob, glob64, GlobalDlmalloc, greg, group, GROUP, Guard, GUID, Handle, HANDLE, Handler, HashMap, HashSet, Heap, HINSTANCE, HMODULE, hostent, HRESULT, id, idtype, if, ifaddrs, IMAGEHLP, Immut, in, in6, Incoming, Infallible, Initializer, ino, ino64, inode, input, InsertResult, Inspect, Instant, int16, int32, int64, int8, integer, IntermediateBox, Internal, Intersection, intmax, IntoInnerError, IntoIter, IntoStringError, intptr, InvalidSequence, iovec, ip, IpAddr, ipc, Ipv4Addr, ipv6, Ipv6Addr, Ipv6MulticastScope, Iter, IterMut, itimerspec, itimerval, jail, JoinHandle, JoinPathsError, KDHELP64, kevent, kevent64, key, Key, Keys, KV, l4, LARGE, lastlog, launchpad, Layout, Lazy, lconv, Leaf, LeafOrInternal, Lines, LinesAny, LineWriter, linger, linkcount, LinkedList, load, locale, LocalKey, LocalKeyState, Location, lock, LockResult, loff, LONG, lookup, lookupflags, LookupHost, LPBOOL, LPBY, LPBYTE, LPCSTR, LPCVOID, LPCWSTR, LPDWORD, LPFILETIME, LPHANDLE, LPOVERLAPPED, LPPROCESS, LPPROGRESS, LPSECURITY, LPSTARTUPINFO, LPSTR, LPVOID, LPWCH, LPWIN32, LPWSADATA, LPWSAPROTOCOL, LPWSTR, Lshrdi3, Lshrti3, lwpid, M128A, mach, major, Map, mcontext, Metadata, Metric, MetricMap, mflags, minor, mmsghdr, Moddi3, mode, Modsi3, Modti3, MonitorMsg, MOUNT, mprot, mq, mqd, msflags, msghdr, msginfo, msglen, msgqnum, msqid, Muldf3, Mulodi4, Mulosi4, Muloti4, Mulsf3, Multi3, Mut, Mutex, MutexGuard, MyCollection, n16, NamePadding, NativeLibBoilerplate, nfds, nl, nlink, NodeRef, NoneError, NonNull, NonZero, nthreads, NulError, OccupiedEntry, off, off64, oflags, Once, OnceState, OpenOptions, Option, Options, OptRes, Ordering, OsStr, OsString, Output, OVERLAPPED, Owned, Packet, PanicInfo, Param, ParseBoolError, ParseCharError, ParseError, ParseFloatError, ParseIntError, ParseResult, Part, passwd, Path, PathBuf, PCONDITION, PCONSOLE, Peekable, PeekMut, Permissions, PhantomData, pid, Pipes, PlaceBack, PlaceFront, PLARGE, PoisonError, pollfd, PopResult, port, Position, Powidf2, Powisf2, Prefix, PrefixComponent, PrintFormat, proc, Process, PROCESS, processentry, protoent, PSRWLOCK, pthread, ptr, ptrdiff, PVECTORED, Queue, radvisory, RandomState, Range, RangeFrom, RangeFull, RangeInclusive, RangeMut, RangeTo, RangeToInclusive, RawBucket, RawFd, RawHandle, RawPthread, RawSocket, RawTable, RawVec, Rc, ReadDir, Receiver, recv, RecvError, RecvTimeoutError, ReentrantMutex, ReentrantMutexGuard, Ref, RefCell, RefMut, REPARSE, Repeat, Result, Rev, Reverse, riflags, rights, rlim, rlim64, rlimit, rlimit64, roflags, Root, RSplit, RSplitMut, RSplitN, RSplitNMut, RUNTIME, rusage, RwLock, RWLock, RwLockReadGuard, RwLockWriteGuard, sa, SafeHash, Scan, sched, scope, sdflags, SearchResult, SearchStep, SECURITY, SeekFrom, segment, Select, SelectionResult, sem, sembuf, send, Sender, SendError, servent, sf, Shared, shmatt, shmid, ShortReader, ShouldPanic, Shutdown, siflags, sigaction, SigAction, sigevent, sighandler, siginfo, Sign, signal, signalfd, SignalToken, sigset, sigval, Sink, SipHasher, SipHasher13, SipHasher24, size, SIZE, Skip, SkipWhile, Slice, SmallBoolTrie, sockaddr, SOCKADDR, sockcred, Socket, SOCKET, SocketAddr, SocketAddrV4, SocketAddrV6, socklen, speed, Splice, Split, SplitMut, SplitN, SplitNMut, SplitPaths, SplitWhitespace, spwd, SRWLOCK, ssize, stack, STACKFRAME64, StartResult, STARTUPINFO, stat, Stat, stat64, statfs, statfs64, StaticKey, statvfs, StatVfs, statvfs64, Stderr, StderrLock, StderrTerminal, Stdin, StdinLock, Stdio, StdioPipes, Stdout, StdoutLock, StdoutTerminal, StepBy, String, StripPrefixError, StrSearcher, subclockflags, Subdf3, SubI128, SuboI128, SuboU128, subrwflags, subscription, Subsf3, SubU128, Summary, suseconds, SYMBOL, SYMBOLIC, SymmetricDifference, SyncSender, sysinfo, System, SystemTime, SystemTimeError, Take, TakeWhile, tcb, tcflag, TcpListener, TcpStream, TempDir, TermInfo, TerminfoTerminal, termios, termios2, TestDesc, TestDescAndFn, TestEvent, TestFn, TestName, TestOpts, TestResult, Thread, threadattr, threadentry, ThreadId, tid, time, time64, timespec, TimeSpec, timestamp, timeval, timeval32, timezone, tm, tms, ToLowercase, ToUppercase, TraitObject, TryFromIntError, TryFromSliceError, TryIter, TryLockError, TryLockResult, TryRecvError, TrySendError, TypeId, U64x2, ucontext, ucred, Udivdi3, Udivmoddi4, Udivmodsi4, Udivmodti4, Udivsi3, Udivti3, UdpSocket, uid, UINT, uint16, uint32, uint64, uint8, uintmax, uintptr, ulflags, ULONG, ULONGLONG, Umoddi3, Umodsi3, Umodti3, UnicodeVersion, Union, Unique, UnixDatagram, UnixListener, UnixStream, Unpacked, UnsafeCell, UNWIND, UpgradeResult, useconds, user, userdata, USHORT, Utf16Encoder, Utf8Error, Utf8Lossy, Utf8LossyChunk, Utf8LossyChunksIter, utimbuf, utmp, utmpx, utsname, uuid, VacantEntry, Values, ValuesMut, VarError, Variables, Vars, VarsOs, Vec, VecDeque, vm, Void, WaitTimeoutResult, WaitToken, wchar, WCHAR, Weak, whence, WIN32, WinConsole, Windows, WindowsEnvKey, winsize, WORD, Wrapping, wrlen, WSADATA, WSAPROTOCOL, WSAPROTOCOLCHAIN, Wtf8, Wtf8Buf, Wtf8CodePoints, xsw, xucred, Zip, zx} +% +, morekeywords=[5]{assert!, assert_eq!, assert_ne!, cfg!, column!, compile_error!, concat!, concat_idents!, debug_assert!, debug_assert_eq!, debug_assert_ne!, env!, eprint!, eprintln!, file!, format!, format_args!, include!, include_bytes!, include_str!, line!, module_path!, option_env!, panic!, print!, println!, select!, stringify!, thread_local!, try!, unimplemented!, unreachable!, vec!, write!, writeln!} % prelude macros +}% + +\lstdefinestyle{colouredRust}% +{ basicstyle=\ttfamily% +, identifierstyle=% +, commentstyle=\color[gray]{0.4}% +, stringstyle=\color[rgb]{0, 0, 0.5}% +, keywordstyle=\bfseries% reserved keywords +, keywordstyle=[2]\color[rgb]{0.75, 1, 1}% traits +, keywordstyle=[3]\color[rgb]{1, 0.5, 0}% primitive types +, keywordstyle=[4]\color[rgb]{0, 0.5, 1}% type and value constructors +, keywordstyle=[5]\color[rgb]{1, 0.5, 0}% macros +, columns=spaceflexible% +, keepspaces=true% +, showspaces=false% +, showtabs=false% +, showstringspaces=true% +}% + +\lstdefinestyle{boxed}{ + style=colouredRust% +, numbers=left% +, firstnumber=auto% +, numberblanklines=true% +, frame=trbL% +, numberstyle=\tiny% +, frame=leftline% +, numbersep=7pt% +, framesep=5pt% +, framerule=10pt% +, xleftmargin=15pt% +, backgroundcolor=\color[gray]{0.97}% +, rulecolor=\color[gray]{0.90}% +} \ No newline at end of file diff --git a/thesis/drafts/1/paper.aux b/thesis/drafts/1/paper.aux new file mode 100644 index 0000000..0666408 --- /dev/null +++ b/thesis/drafts/1/paper.aux @@ -0,0 +1,71 @@ +\relax +\providecommand\hyper@newdestlabel[2]{} +\providecommand\HyField@AuxAddToFields[1]{} +\providecommand\HyField@AuxAddToCoFields[2]{} +\citation{Martin} +\citation{Muratori} +\citation{Flecs} +\@writefile{toc}{\contentsline {section}{\tocsection {}{1}{Introduction}}{2}{section.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{1.1}{Background}}{2}{subsection.1.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{1.2}{ECS Libraries}}{2}{subsection.1.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\tocsubsubsection {}{1.2.1}{Matter}}{2}{subsubsection.1.2.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\tocsubsubsection {}{1.2.2}{Flecs}}{2}{subsubsection.1.2.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\tocsubsubsection {}{1.2.3}{Hecs}}{3}{subsubsection.1.2.3}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{1.3}{Purpose}}{3}{subsection.1.3}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{1.4}{Research Question}}{3}{subsection.1.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\tocsection {}{2}{Method}}{3}{section.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{2.1}{Research Approach}}{3}{subsection.2.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{2.2}{Research Process}}{3}{subsection.2.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\tocsection {}{3}{Theory}}{3}{section.3}\protected@file@percent } +\citation{Nystrom} +\citation{Flecs} +\citation{Flecs} +\citation{ABC} +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{3.1}{Entity Component System Architecture}}{4}{subsection.3.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{3.2}{Cache Locality}}{4}{subsection.3.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{3.3}{Data Layouts}}{4}{subsection.3.3}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\tocsubsubsection {}{3.3.1}{Array Of Structs}}{4}{subsubsection.3.3.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\tocsubsubsection {}{3.3.2}{Struct of Arrays}}{4}{subsubsection.3.3.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{3.4}{SIMD}}{4}{subsection.3.4}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{3.5}{Vectorization}}{4}{subsection.3.5}\protected@file@percent } +\citation{Anderson} +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{3.6}{Archetype}}{5}{subsection.3.6}\protected@file@percent } +\newlabel{Fig 1: Archetype Graph}{{3.6}{6}{Archetype}{subsection.3.6}{}} +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{3.7}{Sparse Set}}{6}{subsection.3.7}\protected@file@percent } +\newlabel{Fig 2: Sparse Set}{{3.7}{6}{Sparse Set}{subsection.3.7}{}} +\citation{Caini} +\citation{Luau} +\newlabel{Fig 3: Removing Entity}{{3.7}{7}{Sparse Set}{subsection.3.7}{}} +\@writefile{toc}{\contentsline {section}{\tocsection {}{4}{Implementation}}{7}{section.4}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{4.1}{Data Structures}}{7}{subsection.4.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{4.2}{Functions}}{8}{subsection.4.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\tocsubsubsection {}{4.2.1}{get(entityId, \ldots )}}{8}{subsubsection.4.2.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\tocsubsubsection {}{4.2.2}{entity()}}{8}{subsubsection.4.2.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\tocsubsubsection {}{4.2.3}{add(entityId, componentId, data)}}{9}{subsubsection.4.2.3}\protected@file@percent } +\@writefile{toc}{\contentsline {subsubsection}{\tocsubsubsection {}{4.2.4}{query(\ldots )}}{9}{subsubsection.4.2.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\tocsection {}{5}{Analysis}}{10}{section.5}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{5.1}{Random Access}}{10}{subsection.5.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{5.2}{Updating Component Data}}{10}{subsection.5.2}\protected@file@percent } +\newlabel{Fig 4: Random Access}{{5.1}{11}{Random Access}{subsection.5.1}{}} +\newlabel{Fig 5: Insertion}{{5.2}{11}{Updating Component Data}{subsection.5.2}{}} +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{5.3}{Queries}}{11}{subsection.5.3}\protected@file@percent } +\bibcite{Martin}{1} +\bibcite{Muratori}{2} +\bibcite{ABC}{3} +\newlabel{Fig 6: Queries}{{5.3}{12}{Queries}{subsection.5.3}{}} +\@writefile{toc}{\contentsline {section}{\tocsection {}{6}{Conclusions}}{12}{section.6}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\tocsection {}{7}{Acknowledgments}}{12}{section.7}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{12}{section*.2}\protected@file@percent } +\bibcite{Archetypes}{4} +\bibcite{Anderson}{5} +\bibcite{Caini}{6} +\bibcite{Nystrom}{7} +\bibcite{gdc}{8} +\bibcite{matter}{9} +\bibcite{luau}{10} +\newlabel{tocindent-1}{0pt} +\newlabel{tocindent0}{12.7778pt} +\newlabel{tocindent1}{17.77782pt} +\newlabel{tocindent2}{29.38873pt} +\newlabel{tocindent3}{0pt} +\gdef \@abspage@last{13} diff --git a/thesis/drafts/1/paper.fdb_latexmk b/thesis/drafts/1/paper.fdb_latexmk new file mode 100644 index 0000000..f990f20 --- /dev/null +++ b/thesis/drafts/1/paper.fdb_latexmk @@ -0,0 +1,167 @@ +# Fdb version 4 +["pdflatex"] 1724165754.00483 "c:/Users/Marcus/Documents/packages/jecs/thesis/drafts/1/paper.tex" "paper.pdf" "paper" 1724165755.21979 0 + "../../images/archetype_graph.png" 1709688578 50172 8f93f7d24d4920bd8720f4b480771eb4 "" + "../../images/insertion.png" 1720373630 158773 c2f9fb7fae25fea3afb7e426b1d318d6 "" + "../../images/queries.png" 1720373630 205571 d976c9319fb29ae7dffc46ded3de4e55 "" + "../../images/random_access.png" 1712278385 64975 e6fbe06298c59f52a21da1b89efe1d12 "" + "../../images/removed.png" 1709688578 10876 4c5ce75a368dfc9581164c9b1ace0382 "" + "../../images/sparseset.png" 1709688578 9733 da4c27a8a932697883c764373b0b4e9e "" + "C:/Users/Marcus/AppData/Local/MiKTeX/fonts/map/pdftex/pdftex.map" 1722382141 81939 3d80a3cba051aa49603173dafcdf1492 "" + "C:/Users/Marcus/AppData/Local/MiKTeX/fonts/tfm/public/rsfs/rsfs10.tfm" 1712242763 688 37338d6ab346c2f1466b29e195316aa4 "" + "C:/Users/Marcus/AppData/Local/MiKTeX/fonts/tfm/public/rsfs/rsfs5.tfm" 1712242764 684 3a51bd4fd9600428d5264cf25f04bb9a "" + "C:/Users/Marcus/AppData/Local/MiKTeX/fonts/tfm/public/rsfs/rsfs7.tfm" 1712242763 692 1b6510779f0f05e9cbf03e0f6c8361e6 "" + "C:/Users/Marcus/AppData/Local/MiKTeX/miktex/data/le/pdftex/pdflatex.fmt" 1712242950 24225517 8c13a3ac174c54eedd305f71bafb3a9e "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1233951848 1004 54797486969f23fa377b128694d548df "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1233951848 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/amsfonts/symbols/msam10.tfm" 1233951854 916 f87d7c45f9c908e672703b83b72241a3 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/amsfonts/symbols/msam5.tfm" 1233951854 924 9904cf1d39e9767e7a3622f2a125a565 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/amsfonts/symbols/msam7.tfm" 1233951854 928 2dc8d444221b7a635bb58038579b861a "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/amsfonts/symbols/msbm10.tfm" 1233951854 908 2921f8a10601f252058503cc6570e581 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/amsfonts/symbols/msbm5.tfm" 1233951854 940 75ac932a52f80982a9f8ea75d03a34cf "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/amsfonts/symbols/msbm7.tfm" 1233951854 940 228d6584342e91276bf566bcf9716b83 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmbx10.tfm" 1136765053 1328 c834bbb027764024c09d3d2bf908b5f0 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmcsc10.tfm" 1136765053 1300 63a6111ee6274895728663cf4b4e7e81 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmmi6.tfm" 1136765053 1512 f21f83efb36853c0b70002322c1ab3ad "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmmi8.tfm" 1136765053 1520 eccf95517727cb11801f4f1aee3a21b4 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmr6.tfm" 1136765053 1300 b62933e007d01cfd073f79b963c01526 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmr8.tfm" 1136765053 1292 21c1c5bfeaebccffdb478fd231a0997d "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmr9.tfm" 1136765053 1292 6b21b9c2c7bebb38aa2273f7ca0fb3af "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmss10.tfm" 1136765053 1316 b636689f1933f24d1294acdf6041daaa "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmss8.tfm" 1136765053 1296 d77f431d10d47c8ea2cc18cf45346274 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmsy6.tfm" 1136765053 1116 933a60c408fc0a863a92debe84b2d294 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmsy8.tfm" 1136765053 1120 8b7d695260f3cff42e636090a8002094 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmti10.tfm" 1136765053 1480 aa8e34af0eb6a2941b776984cf1dfdc4 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmti8.tfm" 1136765053 1504 1747189e0441d1c18f3ea56fafc1c480 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmtt10.tfm" 1136765053 768 1321e9409b4137d6fb428ac9dc956269 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmtt8.tfm" 1136765053 768 d7b9a2629a0c353102ad947dc9221d49 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/cm/cmtt9.tfm" 1136765053 764 c98a2af25c99b73a368cf7336e255190 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/latex-fonts/lasy6.tfm" 1136765053 520 4889cce2180234b97cad636b6039c722 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/xypic/xyatip10.tfm" 1381022313 608 50246cc71b0635b0ba0a5c10a0bf4257 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/xypic/xybsql10.tfm" 1381022313 608 4db60f15ea23b4ec2d796c6d568a63fa "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/xypic/xybtip10.tfm" 1381022313 608 50246cc71b0635b0ba0a5c10a0bf4257 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/xypic/xycirc10.tfm" 1381022313 844 3393210079fb4ed9347e214b3bfd7c1a "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/xypic/xycmat10.tfm" 1381022313 608 f124f78ed50a1817738d2adb190cf2bd "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/xypic/xycmbt10.tfm" 1381022313 608 f124f78ed50a1817738d2adb190cf2bd "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/xypic/xydash10.tfm" 1381022313 984 5c01c46b93e3ba8369f3f8edc6e62aef "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/xypic/xyluat10.tfm" 1381022313 608 a3a3bc08980c5126ff2a7a68fb5a64ff "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/tfm/public/xypic/xylubt10.tfm" 1381022313 608 a3a3bc08980c5126ff2a7a68fb5a64ff "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1247596666 34811 78b52f49e893bcba91bd7581cdc144c0 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmcsc10.pfb" 1247596667 32001 6aeea3afe875097b1eb0da29acd61e28 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmmi10.pfb" 1247596667 36299 5f9df58c2139e7edcf37c8fca4bd384d "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmr10.pfb" 1247596667 35752 024fb6c41858982481f6968b5fc26508 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmr6.pfb" 1247596667 32734 69e00a6b65cedb993666e42eedb3d48f "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmr7.pfb" 1247596667 32762 7fee39e011c23b3589931effd97b9702 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmr8.pfb" 1247596667 32726 39f0f9e62e84beb801509898a605dbd5 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmss10.pfb" 1247596666 24457 5cbb7bdf209d5d1ce9892a9b80a307cc "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmsy10.pfb" 1247596667 32569 5e5ddc8df908dea60932f3c484a54c0d "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmti10.pfb" 1247596667 37944 359e864bd06cde3b1cf57bb20757fb06 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmti8.pfb" 1247596666 35660 fb24af7afbadb71801619f1415838111 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmtt10.pfb" 1247596667 31099 342ef5a582aacbd3346f3cf4579679fa "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmtt8.pfb" 1247596666 24287 6b803fa9eb1ddff9112e00519b09dd9e "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/fonts/type1/public/amsfonts/cm/cmtt9.pfb" 1247596667 29078 718ea4567ceff944262b0f5b0800e1d9 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/context/base/mkii/supp-pdf.mkii" 1580390158 71627 94eb9990bed73c364d7f53f960cc8c5b "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/atbegshi/atbegshi.sty" 1575571100 24708 5584a51a7101caf7e6bbf1fc27d8f7b1 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/bigintcalc/bigintcalc.sty" 1576433602 40635 c40361e206be584d448876bba8a64a3b "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/bitset/bitset.sty" 1575926576 33961 6b5c75130e435b2bfdb9f480a09a39f9 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/gettitlestring/gettitlestring.sty" 1576433666 8371 9d55b8bd010bc717624922fb3477d92e "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/iftex/ifpdf.sty" 1643997108 480 5778104efadad304ced77548ca2184b1 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/iftex/iftex.sty" 1643997108 7237 bdd120a32c8fdb4b433cf9ca2e7cd98a "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/infwarerr/infwarerr.sty" 1575399508 8356 7bbb2c2373aa810be568c29e333da8ed "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/intcalc/intcalc.sty" 1576433764 31769 002a487f55041f8e805cfbf6385ffd97 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/kvdefinekeys/kvdefinekeys.sty" 1576763304 5412 d5a2436094cd7be85769db90f29250a6 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/ltxcmds/ltxcmds.sty" 1702206890 17865 1a9bd36b4f98178fa551aca822290953 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/pdfescape/pdfescape.sty" 1575926700 19007 15924f7228aca6c6d184b115f4baa231 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/pdftexcmds/pdftexcmds.sty" 1623005277 20089 80423eac55aa175305d35b49e04fe23b "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/uniquecounter/uniquecounter.sty" 1576434012 7008 f92eaa0a3872ed622bbf538217cd2ab7 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xkeyval/keyval.tex" 1656236919 2725 1a42bd9e7e57e25fc7763c445f4b785b "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xkeyval/xkeyval.tex" 1656236919 19231 27205ee17aaa2902aea3e0c07a3cfc65 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xkeyval/xkvutils.tex" 1656236919 7677 9cb1a74d945bc9331f2181c0a59ff34a "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xy.sty" 1381022313 4692 1e1bcf75c622af1eefd9169948208302 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xy.tex" 1381022313 115380 413d5f789929a45aab7d12ce0d0aee7d "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xy2cell.tex" 1381022313 28208 66beb10e89ca3b367faccdfebe2d3965 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xyall.tex" 1381022313 1449 24340b6befc66d28ee1ebb657efb5892 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xyarc.tex" 1381022313 30224 28134012dafb2972d4c32eb8af3edb2e "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xyarrow.tex" 1381022313 22657 990ce136a3cc15728ba417a2e78b25c8 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xycmtip.tex" 1381022313 1374 43fb8dc80dd748631d78096701166d76 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xycolor.tex" 1381022313 4586 edd672434f45626662368282c0322160 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xycurve.tex" 1381022313 109670 d412ee1ff259daefee5e927172e2f9a8 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xyframe.tex" 1381022313 24249 186931a828664624939ab0b347e3952c "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xygraph.tex" 1381022313 9619 b7e4d9a6936ba2ad6119a280abde9641 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xyidioms.tex" 1381022313 2907 1ee562fde0b53c9cd16f7a604f33fdf0 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xyline.tex" 1381022313 10928 c3a572983ccc9fc596b4e9ce454d5652 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xymatrix.tex" 1381022313 22583 25b1e7edeee41f181ee9733429da4a9c "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xypdf-co.tex" 1381022313 8442 90cb8a3b00c2081384c1ce988d2ba0a3 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xypdf-cu.tex" 1381022313 39762 25a964ebb390bcfcd35c040f477eef1d "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xypdf-fr.tex" 1381022313 16485 5686b19cc46d046c885428794ed9c114 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xypdf-li.tex" 1381022313 2619 1a12b316e2132654e44ba2cd21def637 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xypdf-ro.tex" 1381022313 5290 e16fc85c85f64d0a5c04708bf3312d00 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xypdf.tex" 1381022313 18763 e61049d36bdfccb226f22e582d70d368 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xyrecat.tex" 1381022313 1391 c8763fc8e281cb6ecf697988b6608e4a "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xyrotate.tex" 1381022313 7008 cb768d8d63a12d35607cbb3c4e7ba163 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/generic/xypic/xytips.tex" 1381022313 3689 0d51788a4141bc66ab896f7ac63495fd "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/00miktex/epstopdf-sys.cfg" 1616067285 584 2a1075dd71571459f59146da9f7502ad "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/amscls/amsart.cls" 1591024533 61881 a7369c346c2922a758ae6283cc1ed014 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/amsfonts/amsfonts.sty" 1358197772 5949 3f3fd50a8cc94c3d4cbf4fc66cd3df1c "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/amsfonts/umsa.fd" 1358197772 961 6518c6525a34feb5e8250ffa91731cff "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/amsfonts/umsb.fd" 1358197772 961 d02606146ba5601b5645f987c92e6193 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/amsmath/amsbsy.sty" 1686931788 2222 499d61426192c39efd8f410ee1a52b9c "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/amsmath/amsgen.sty" 1686931787 4173 82ac04dfb1256038fad068287fbb4fe6 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/amsmath/amsmath.sty" 1686931788 88371 d84032c0f422c3d1e282266c01bef237 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/amsmath/amsopn.sty" 1686931788 4474 b811654f4bf125f11506d13d13647efb "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/amsmath/amstext.sty" 1686931788 2444 0d0c1ee65478277e8015d65b86983da2 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/atveryend/atveryend.sty" 1576101110 19336 ce7ae9438967282886b3b036cfad1e4d "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/auxhook/auxhook.sty" 1576538732 3935 57aa3c3e203a5c2effb4d2bd2efbc323 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/base/atbegshi-ltx.sty" 1705273578 3045 273c666a54e60b9f730964f431a56c1b "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/base/atveryend-ltx.sty" 1705273579 2462 6bc53756156dbd71c1ad550d30a3b93f "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/base/inputenc.sty" 1705273578 5048 425739d70251273bf93e3d51f3c40048 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/breakurl/breakurl.sty" 1366019824 8782 9af34887a0e6e535b004c39238830991 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/comment/comment.sty" 1468691282 10197 204f75d5d8d88aa345a8c402e879e63b "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/epstopdf-pkg/epstopdf-base.sty" 1623003186 13886 d1306dcf79a944f6988e688c1785f9ce "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/etoolbox/etoolbox.sty" 1601897756 46845 3b58f70c6e861a13d927bff09d35ecbc "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics-cfg/color.cfg" 1465894292 1213 620bba36b25224fa9b7e1ccb4ecb76fd "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics-cfg/graphics.cfg" 1465894292 1224 978390e9c2234eab29404bc21b268d1e "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics-def/pdftex.def" 1663918690 19448 1e988b341dda20961a6b931bcde55519 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics/color.sty" 1665067579 7233 e46ce9241d2b2ca2a78155475fdd557a "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics/dvipsnam.def" 1665067579 5009 d242512eef244b70f2fc3fde14419206 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics/graphics.sty" 1665067579 18387 8f900a490197ebaf93c02ae9476d4b09 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics/graphicx.sty" 1665067579 8010 a8d949cbdbc5c983593827c9eec252e1 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics/keyval.sty" 1665067579 2671 7e67d78d9b88c845599a85b2d41f2e39 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics/mathcolor.ltx" 1665067579 3171 1cf0d440b5464e2f034398ce4ef36f75 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/graphics/trig.sty" 1665067579 4023 293ea1c16429fc0c4cf605f4da1791a9 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/hycolor/hycolor.sty" 1580380792 18571 4c28a13fc3d975e6e81c9bea1d697276 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/hyperref/hpdftex.def" 1701020798 48154 77bec99bb3bbdf933bcecb211f7f4038 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/hyperref/hyperref.sty" 1701020798 220895 6ca6b57b8bf00b1b056e6f2806b9ff68 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/hyperref/nameref.sty" 1701020798 11026 67c64046f677e9221917813968f2fbc2 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/hyperref/pd1enc.def" 1701020798 14249 0a8695b3ac35d9c3ddf779fe5e2f4acf "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/hyperref/puenc.def" 1701020798 117112 9c2b129a5be8857257127759fbab51ce "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/jknappen/mathrsfs.sty" 930764892 300 12fa6f636b617656f2810ee82cb05015 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/jknappen/ursfs.fd" 930764886 548 cc4e3557704bfed27c7002773fad6c90 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/kvoptions/kvoptions.sty" 1656236481 22555 6d8e155cfef6d82c3d5c742fea7c992e "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/kvsetkeys/kvsetkeys.sty" 1665066333 13815 760b0c02f691ea230f5359c4e1de23a7 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/l3backend/l3backend-pdftex.def" 1704400941 30006 57b07afb710ee2f649c65cfbafda39c1 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/letltxmacro/letltxmacro.sty" 1575399536 5766 13a9e8766c47f30327caf893ece86ac8 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/listings/listings.cfg" 1679057124 1829 d8258b7d94f5f955e70c623e525f9f45 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/listings/listings.sty" 1679057124 80947 75a96bb4c9f40ae31d54a01d924df2ff "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/listings/lstlang1.sty" 1679057124 205154 31132370016e8c97e49bc3862419679b "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/listings/lstlang2.sty" 1679057124 93648 37f37f89a55d35f95036cd331d48114f "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/listings/lstmisc.sty" 1679057124 77021 d05e9115c67855816136d82929db8892 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/refcount/refcount.sty" 1576433952 9878 9e94e8fa600d95f9c7731bb21dfb67a4 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/rerunfilecheck/rerunfilecheck.sty" 1657800696 9714 ba3194bd52c8499b3f1e3eb91d409670 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/tools/enumerate.sty" 1700599895 3468 0ef513f22d965f96b06adb5cff671cd7 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/ulem/ulem.sty" 1578651445 15682 94f55b803e160cf7fb6e4d77d07cfe1d "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/url/url.sty" 1388490452 12796 8edb7d69a20b857904dd0ea757c14ec9 "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/xcolor/xcolor.sty" 1700127522 55487 80a65caedd3722f4c20a14a69e785d8f "" + "C:/Users/Marcus/AppData/Local/Programs/MiKTeX/tex/latex/xkeyval/xkeyval.sty" 1656236919 4937 4ce600ce9bd4ec84d0250eb6892fcf4f "" + "c:/Users/Marcus/Documents/packages/jecs/thesis/drafts/1/paper.tex" 1722532428 33628 2358f35913ab57bac270409214a52615 "" + "listings-rust.sty" 1720461559 12349 f346af5561f91e34970cbe0b79654ec2 "" + "paper.aux" 1724165755 5596 e71f1baf7c13471206b3537d383c78e2 "pdflatex" + "paper.out" 1724165755 3695 a11dbc9d88dd30c22755dc5ebf6964ec "pdflatex" + "paper.tex" 1722532428 33628 2358f35913ab57bac270409214a52615 "" + "paper.toc" 1724165755 3025 f0a34bc8923dbdfdaeb8258045835a7e "pdflatex" + (generated) + "paper.aux" + "paper.log" + "paper.out" + "paper.pdf" + "paper.toc" + (rewritten before read) diff --git a/thesis/drafts/1/paper.fls b/thesis/drafts/1/paper.fls new file mode 100644 index 0000000..ec658e8 --- /dev/null +++ b/thesis/drafts/1/paper.fls @@ -0,0 +1,345 @@ +PWD c:\Users\Marcus\Documents\packages\jecs\thesis\drafts\1 +INPUT C:\Users\Marcus\AppData\Local\MiKTeX\miktex\data\le\pdftex\pdflatex.fmt +INPUT c:\Users\Marcus\Documents\packages\jecs\thesis\drafts\1\paper.tex +OUTPUT paper.log +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amscls\amsart.cls +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amscls\amsart.cls +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amsmath.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\amsfonts.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amsmath.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amsopn.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amstext.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amstext.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amsgen.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amsgen.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amsbsy.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amsbsy.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsmath\amsopn.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\umsa.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\umsa.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\umsa.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\amsfonts.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\jknappen\mathrsfs.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\jknappen\mathrsfs.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\comment\comment.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\comment\comment.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\color.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\color.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-cfg\color.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-cfg\color.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-cfg\color.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-def\pdftex.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-def\pdftex.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-def\pdftex.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\dvipsnam.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\dvipsnam.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\dvipsnam.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\mathcolor.ltx +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\mathcolor.ltx +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\mathcolor.ltx +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\ulem\ulem.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\ulem\ulem.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\latex-fonts\lasy6.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\url\url.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\url\url.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\breakurl\breakurl.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\breakurl\breakurl.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\xkeyval\xkeyval.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\xkeyval\xkeyval.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xkeyval\xkeyval.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xkeyval\xkvutils.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xkeyval\keyval.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\iftex\ifpdf.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\iftex\ifpdf.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\iftex\iftex.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\iftex\iftex.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\base\inputenc.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\base\inputenc.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\hyperref.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\hyperref.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\infwarerr\infwarerr.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\infwarerr\infwarerr.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\keyval.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\kvsetkeys\kvsetkeys.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\kvsetkeys\kvsetkeys.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\kvdefinekeys\kvdefinekeys.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\kvdefinekeys\kvdefinekeys.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\pdfescape\pdfescape.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\pdfescape\pdfescape.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\ltxcmds\ltxcmds.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\ltxcmds\ltxcmds.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\pdftexcmds\pdftexcmds.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\pdftexcmds\pdftexcmds.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hycolor\hycolor.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hycolor\hycolor.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\letltxmacro\letltxmacro.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\letltxmacro\letltxmacro.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\auxhook\auxhook.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\auxhook\auxhook.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\nameref.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\nameref.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\refcount\refcount.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\refcount\refcount.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\gettitlestring\gettitlestring.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\gettitlestring\gettitlestring.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\kvoptions\kvoptions.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\kvoptions\kvoptions.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\etoolbox\etoolbox.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\etoolbox\etoolbox.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\pd1enc.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\pd1enc.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\pd1enc.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\intcalc\intcalc.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\intcalc\intcalc.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\puenc.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\puenc.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\puenc.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\bitset\bitset.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\bitset\bitset.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\bigintcalc\bigintcalc.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\bigintcalc\bigintcalc.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\atbegshi\atbegshi.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\base\atbegshi-ltx.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\base\atbegshi-ltx.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\hpdftex.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\hpdftex.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\hyperref\hpdftex.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\atveryend\atveryend.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\base\atveryend-ltx.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\base\atveryend-ltx.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\rerunfilecheck\rerunfilecheck.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\rerunfilecheck\rerunfilecheck.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\uniquecounter\uniquecounter.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\uniquecounter\uniquecounter.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\listings.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\listings.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstmisc.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstmisc.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstmisc.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\listings.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\listings.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\listings.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xy.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xy.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xy.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyrecat.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyidioms.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\xypic\xydash10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\xypic\xyatip10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\xypic\xybtip10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\xypic\xybsql10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\xypic\xycirc10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyall.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyall.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xycurve.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xycurve.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyframe.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyframe.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xycmtip.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xycmtip.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xytips.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xytips.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\xypic\xycmat10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\xypic\xycmbt10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\xypic\xyluat10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\xypic\xylubt10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyline.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyline.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyrotate.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyrotate.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xycolor.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xycolor.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xymatrix.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xymatrix.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyarrow.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyarrow.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xygraph.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xygraph.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyarc.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xyarc.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xy2cell.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xy2cell.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xypdf.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xypdf.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xypdf-co.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xypdf-cu.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xypdf-fr.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xypdf-li.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\generic\xypic\xypdf-ro.tex +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\tools\enumerate.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\tools\enumerate.sty +INPUT .\listings-rust.sty +INPUT listings-rust.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\xcolor\xcolor.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\xcolor\xcolor.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-cfg\color.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\mathcolor.ltx +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\graphicx.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\graphicx.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\graphics.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\graphics.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\trig.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics\trig.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-cfg\graphics.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-cfg\graphics.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\graphics-cfg\graphics.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\l3backend\l3backend-pdftex.def +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\l3backend\l3backend-pdftex.def +INPUT .\paper.aux +INPUT .\paper.aux +INPUT paper.aux +OUTPUT paper.aux +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmr8.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmr6.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmmi8.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmmi6.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmsy8.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmsy6.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\cmextra\cmex8.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\cmextra\cmex7.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\cmextra\cmex7.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\umsa.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\umsa.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\umsa.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msam10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msam7.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msam5.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\umsb.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\umsb.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\amsfonts\umsb.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msbm10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msbm7.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msbm5.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\jknappen\ursfs.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\jknappen\ursfs.fd +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\jknappen\ursfs.fd +INPUT C:\Users\Marcus\AppData\Local\MiKTeX\fonts\tfm\public\rsfs\rsfs10.tfm +INPUT C:\Users\Marcus\AppData\Local\MiKTeX\fonts\tfm\public\rsfs\rsfs5.tfm +INPUT C:\Users\Marcus\AppData\Local\MiKTeX\fonts\tfm\public\rsfs\rsfs5.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\context\base\mkii\supp-pdf.mkii +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\context\base\mkii\supp-pdf.mkii +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\context\base\mkii\supp-pdf.mkii +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\epstopdf-pkg\epstopdf-base.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\epstopdf-pkg\epstopdf-base.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\00miktex\epstopdf-sys.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\00miktex\epstopdf-sys.cfg +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\00miktex\epstopdf-sys.cfg +INPUT .\paper.out +INPUT .\paper.out +INPUT paper.out +INPUT paper.out +OUTPUT paper.pdf +INPUT .\paper.out +INPUT .\paper.out +OUTPUT paper.out +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\cmextra\cmex7.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msam10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msam7.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msbm10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\amsfonts\symbols\msbm7.tfm +INPUT C:\Users\Marcus\AppData\Local\MiKTeX\fonts\tfm\public\rsfs\rsfs10.tfm +INPUT C:\Users\Marcus\AppData\Local\MiKTeX\fonts\tfm\public\rsfs\rsfs7.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmcsc10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmti8.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmbx10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmcsc10.tfm +INPUT .\paper.toc +INPUT .\paper.toc +INPUT paper.toc +INPUT C:\Users\Marcus\AppData\Local\MiKTeX\fonts\map\pdftex\pdftex.map +OUTPUT paper.toc +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmti10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmtt10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmss10.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmss8.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmss8.tfm +INPUT ..\..\images\archetype_graph.png +INPUT ..\..\images\archetype_graph.png +INPUT ..\..\images\archetype_graph.png +INPUT ..\..\images\archetype_graph.png +INPUT ..\..\images\archetype_graph.png +INPUT ..\..\images\archetype_graph.png +INPUT ..\..\images\archetype_graph.png +INPUT ..\..\images\sparseset.png +INPUT ..\..\images\sparseset.png +INPUT ..\..\images\sparseset.png +INPUT ..\..\images\sparseset.png +INPUT ..\..\images\sparseset.png +INPUT ..\..\images\sparseset.png +INPUT ..\..\images\sparseset.png +INPUT ..\..\images\removed.png +INPUT ..\..\images\removed.png +INPUT ..\..\images\removed.png +INPUT ..\..\images\removed.png +INPUT ..\..\images\removed.png +INPUT ..\..\images\removed.png +INPUT ..\..\images\removed.png +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang1.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang1.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang1.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang2.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang2.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang2.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang1.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang1.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang1.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang2.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang2.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex\latex\listings\lstlang2.sty +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmtt9.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmr9.tfm +INPUT ..\..\images\random_access.png +INPUT ..\..\images\random_access.png +INPUT ..\..\images\random_access.png +INPUT ..\..\images\random_access.png +INPUT ..\..\images\random_access.png +INPUT ..\..\images\random_access.png +INPUT ..\..\images\random_access.png +INPUT ..\..\images\insertion.png +INPUT ..\..\images\insertion.png +INPUT ..\..\images\insertion.png +INPUT ..\..\images\insertion.png +INPUT ..\..\images\insertion.png +INPUT ..\..\images\insertion.png +INPUT ..\..\images\insertion.png +INPUT ..\..\images\queries.png +INPUT ..\..\images\queries.png +INPUT ..\..\images\queries.png +INPUT ..\..\images\queries.png +INPUT ..\..\images\queries.png +INPUT ..\..\images\queries.png +INPUT ..\..\images\queries.png +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmtt8.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmss8.tfm +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\tfm\public\cm\cmss8.tfm +INPUT paper.aux +INPUT .\paper.out +INPUT .\paper.out +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmbx10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmbx10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmcsc10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmcsc10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmmi10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmmi10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmr10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmr10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmr6.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmr6.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmr7.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmr7.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmr8.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmr8.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmss10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmss10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmsy10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmsy10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmti10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmti10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmti8.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmti8.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmtt10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmtt10.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmtt8.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmtt8.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmtt9.pfb +INPUT C:\Users\Marcus\AppData\Local\Programs\MiKTeX\fonts\type1\public\amsfonts\cm\cmtt9.pfb diff --git a/thesis/drafts/1/paper.log b/thesis/drafts/1/paper.log new file mode 100644 index 0000000..4163130 --- /dev/null +++ b/thesis/drafts/1/paper.log @@ -0,0 +1,601 @@ +This is pdfTeX, Version 3.141592653-2.6-1.40.25 (MiKTeX 24.1) (preloaded format=pdflatex 2024.4.4) 20 AUG 2024 16:55 +entering extended mode + restricted \write18 enabled. + file:line:error style messages enabled. + %&-line parsing enabled. +**c:/Users/Marcus/Documents/packages/jecs/thesis/drafts/1/paper.tex +(c:/Users/Marcus/Documents/packages/jecs/thesis/drafts/1/paper.tex +LaTeX2e <2023-11-01> patch level 1 +L3 programming layer <2024-01-04> +(C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amscls\amsart.cls +Document Class: amsart 2020/05/29 v2.20.6 +\linespacing=\dimen140 +\normalparindent=\dimen141 +\normaltopskip=\skip48 +(C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amsmath\amsmath.sty +Package: amsmath 2023/05/13 v2.17o AMS math features +\@mathmargin=\skip49 + +For additional information on amsmath, use the `?' option. +(C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amsmath\amstext.sty +Package: amstext 2021/08/26 v2.01 AMS text + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amsmath\amsgen.sty +File: amsgen.sty 1999/11/30 v2.0 generic functions +\@emptytoks=\toks17 +\ex@=\dimen142 +)) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amsmath\amsbsy.sty +Package: amsbsy 1999/11/29 v1.2d Bold Symbols +\pmbraise@=\dimen143 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amsmath\amsopn.sty +Package: amsopn 2022/04/08 v2.04 operator names +) +\inf@bad=\count187 +LaTeX Info: Redefining \frac on input line 234. +\uproot@=\count188 +\leftroot@=\count189 +LaTeX Info: Redefining \overline on input line 399. +LaTeX Info: Redefining \colon on input line 410. +\classnum@=\count190 +\DOTSCASE@=\count191 +LaTeX Info: Redefining \ldots on input line 496. +LaTeX Info: Redefining \dots on input line 499. +LaTeX Info: Redefining \cdots on input line 620. +\Mathstrutbox@=\box51 +\strutbox@=\box52 +LaTeX Info: Redefining \big on input line 722. +LaTeX Info: Redefining \Big on input line 723. +LaTeX Info: Redefining \bigg on input line 724. +LaTeX Info: Redefining \Bigg on input line 725. +\big@size=\dimen144 +LaTeX Font Info: Redeclaring font encoding OML on input line 743. +LaTeX Font Info: Redeclaring font encoding OMS on input line 744. +\macc@depth=\count192 +LaTeX Info: Redefining \bmod on input line 905. +LaTeX Info: Redefining \pmod on input line 910. +LaTeX Info: Redefining \smash on input line 940. +LaTeX Info: Redefining \relbar on input line 970. +LaTeX Info: Redefining \Relbar on input line 971. +\c@MaxMatrixCols=\count193 +\dotsspace@=\muskip16 +\c@parentequation=\count194 +\dspbrk@lvl=\count195 +\tag@help=\toks18 +\row@=\count196 +\column@=\count197 +\maxfields@=\count198 +\andhelp@=\toks19 +\eqnshift@=\dimen145 +\alignsep@=\dimen146 +\tagshift@=\dimen147 +\tagwidth@=\dimen148 +\totwidth@=\dimen149 +\lineht@=\dimen150 +\@envbody=\toks20 +\multlinegap=\skip50 +\multlinetaggap=\skip51 +\mathdisplay@stack=\toks21 +LaTeX Info: Redefining \[ on input line 2953. +LaTeX Info: Redefining \] on input line 2954. +) +LaTeX Font Info: Trying to load font information for U+msa on input line 397. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amsfonts\umsa.fd +File: umsa.fd 2013/01/14 v3.01 AMS symbols A +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amsfonts\amsfonts.sty +Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support + + +Package amsfonts Warning: The 'psamsfonts' option is obsolete in AMSFonts v3. + +\symAMSa=\mathgroup4 +\symAMSb=\mathgroup5 +LaTeX Font Info: Redeclaring math symbol \hbar on input line 98. +LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold' +(Font) U/euf/m/n --> U/euf/b/n on input line 106. +) +\copyins=\insert199 +\abstractbox=\box53 +\listisep=\skip52 +\c@part=\count266 +\c@section=\count267 +\c@subsection=\count268 +\c@subsubsection=\count269 +\c@paragraph=\count270 +\c@subparagraph=\count271 +\c@figure=\count272 +\c@table=\count273 +\abovecaptionskip=\skip53 +\belowcaptionskip=\skip54 +\captionindent=\dimen151 +\thm@style=\toks22 +\thm@bodyfont=\toks23 +\thm@headfont=\toks24 +\thm@notefont=\toks25 +\thm@headpunct=\toks26 +\thm@preskip=\skip55 +\thm@postskip=\skip56 +\thm@headsep=\skip57 +\dth@everypar=\toks27 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/jknappen\mathrsfs.sty +Package: mathrsfs 1996/01/01 Math RSFS package v1.0 (jk) +\symrsfs=\mathgroup6 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/comment\comment.sty +\CommentStream=\write3 + Excluding comment 'comment') (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics\color.sty +Package: color 2022/01/06 v1.3d Standard LaTeX Color (DPC) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics-cfg\color.cfg +File: color.cfg 2016/01/02 v1.6 sample color configuration +) +Package color Info: Driver file: pdftex.def on input line 149. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics-def\pdftex.def +File: pdftex.def 2022/09/22 v1.2b Graphics/color driver for pdftex +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics\dvipsnam.def +File: dvipsnam.def 2016/06/17 v3.0m Driver-dependent file (DPC,SPQR) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics\mathcolor.ltx)) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/ulem\ulem.sty +\UL@box=\box54 +\UL@hyphenbox=\box55 +\UL@skip=\skip58 +\UL@hook=\toks28 +\UL@height=\dimen152 +\UL@pe=\count274 +\UL@pixel=\dimen153 +\ULC@box=\box56 +Package: ulem 2019/11/18 +\ULdepth=\dimen154 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/url\url.sty +\Urlmuskip=\muskip17 +Package: url 2013/09/16 ver 3.4 Verb mode for urls, etc. +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/breakurl\breakurl.sty +Package: breakurl 2013/04/10 v1.40 Breakable hyperref URLs + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/xkeyval\xkeyval.sty +Package: xkeyval 2022/06/16 v2.9 package option processing (HA) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xkeyval\xkeyval.tex (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xkeyval\xkvutils.tex +\XKV@toks=\toks29 +\XKV@tempa@toks=\toks30 + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xkeyval\keyval.tex)) +\XKV@depth=\count275 +File: xkeyval.tex 2014/12/03 v2.7a key=value parser (HA) +)) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/iftex\ifpdf.sty +Package: ifpdf 2019/10/25 v3.4 ifpdf legacy package. Use iftex instead. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/iftex\iftex.sty +Package: iftex 2022/02/03 v1.0f TeX engine tests +)) + +Package breakurl Warning: You are using breakurl while processing via pdflatex. +(breakurl) \burl will be just a synonym of \url. +(breakurl) on input line 48. + +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/base\inputenc.sty +Package: inputenc 2021/02/14 v1.3d Input encoding file +\inpenc@prehook=\toks31 +\inpenc@posthook=\toks32 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/hyperref\hyperref.sty +Package: hyperref 2023-11-26 v7.01g Hypertext links for LaTeX + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/infwarerr\infwarerr.sty +Package: infwarerr 2019/12/03 v1.5 Providing info/warning/error messages (HO) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/kvsetkeys\kvsetkeys.sty +Package: kvsetkeys 2022-10-05 v1.19 Key value parser (HO) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/kvdefinekeys\kvdefinekeys.sty +Package: kvdefinekeys 2019-12-19 v1.6 Define keys (HO) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/pdfescape\pdfescape.sty +Package: pdfescape 2019/12/09 v1.15 Implements pdfTeX's escape features (HO) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/ltxcmds\ltxcmds.sty +Package: ltxcmds 2023-12-04 v1.26 LaTeX kernel commands for general use (HO) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/pdftexcmds\pdftexcmds.sty +Package: pdftexcmds 2020-06-27 v0.33 Utility functions of pdfTeX for LuaTeX (HO) +Package pdftexcmds Info: \pdf@primitive is available. +Package pdftexcmds Info: \pdf@ifprimitive is available. +Package pdftexcmds Info: \pdfdraftmode found. +)) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/hycolor\hycolor.sty +Package: hycolor 2020-01-27 v1.10 Color options for hyperref/bookmark (HO) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/letltxmacro\letltxmacro.sty +Package: letltxmacro 2019/12/03 v1.6 Let assignment for LaTeX macros (HO) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/auxhook\auxhook.sty +Package: auxhook 2019-12-17 v1.6 Hooks for auxiliary files (HO) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/hyperref\nameref.sty +Package: nameref 2023-11-26 v2.56 Cross-referencing by name of section + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/refcount\refcount.sty +Package: refcount 2019/12/15 v3.6 Data extraction from label references (HO) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/gettitlestring\gettitlestring.sty +Package: gettitlestring 2019/12/15 v1.6 Cleanup title references (HO) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/kvoptions\kvoptions.sty +Package: kvoptions 2022-06-15 v3.15 Key value format for package options (HO) +)) +\c@section@level=\count276 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/etoolbox\etoolbox.sty +Package: etoolbox 2020/10/05 v2.5k e-TeX tools for LaTeX (JAW) +\etb@tempcnta=\count277 +) +\@linkdim=\dimen155 +\Hy@linkcounter=\count278 +\Hy@pagecounter=\count279 + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/hyperref\pd1enc.def +File: pd1enc.def 2023-11-26 v7.01g Hyperref: PDFDocEncoding definition (HO) +Now handling font encoding PD1 ... +... no UTF-8 mapping file for font encoding PD1 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/intcalc\intcalc.sty +Package: intcalc 2019/12/15 v1.3 Expandable calculations with integers (HO) +) +\Hy@SavedSpaceFactor=\count280 + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/hyperref\puenc.def +File: puenc.def 2023-11-26 v7.01g Hyperref: PDF Unicode definition (HO) +Now handling font encoding PU ... +... no UTF-8 mapping file for font encoding PU +) +Package hyperref Info: Hyper figures OFF on input line 4181. +Package hyperref Info: Link nesting OFF on input line 4186. +Package hyperref Info: Hyper index ON on input line 4189. +Package hyperref Info: Plain pages OFF on input line 4196. +Package hyperref Info: Backreferencing OFF on input line 4201. +Package hyperref Info: Implicit mode ON; LaTeX internals redefined. +Package hyperref Info: Bookmarks ON on input line 4448. +\c@Hy@tempcnt=\count281 +LaTeX Info: Redefining \url on input line 4786. +\XeTeXLinkMargin=\dimen156 + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/bitset\bitset.sty +Package: bitset 2019/12/09 v1.3 Handle bit-vector datatype (HO) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/bigintcalc\bigintcalc.sty +Package: bigintcalc 2019/12/15 v1.5 Expandable calculations on big integers (HO) +)) +\Fld@menulength=\count282 +\Field@Width=\dimen157 +\Fld@charsize=\dimen158 +Package hyperref Info: Hyper figures OFF on input line 6065. +Package hyperref Info: Link nesting OFF on input line 6070. +Package hyperref Info: Hyper index ON on input line 6073. +Package hyperref Info: backreferencing OFF on input line 6080. +Package hyperref Info: Link coloring OFF on input line 6085. +Package hyperref Info: Link coloring with OCG OFF on input line 6090. +Package hyperref Info: PDF/A mode OFF on input line 6095. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/base\atbegshi-ltx.sty +Package: atbegshi-ltx 2021/01/10 v1.0c Emulation of the original atbegshi +package with kernel methods +) +\Hy@abspage=\count283 +\c@Item=\count284 +\c@Hfootnote=\count285 +) +Package hyperref Info: Driver (autodetected): hpdftex. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/hyperref\hpdftex.def +File: hpdftex.def 2023-11-26 v7.01g Hyperref driver for pdfTeX + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/base\atveryend-ltx.sty +Package: atveryend-ltx 2020/08/19 v1.0a Emulation of the original atveryend package +with kernel methods +) +\Fld@listcount=\count286 +\c@bookmark@seq@number=\count287 + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/rerunfilecheck\rerunfilecheck.sty +Package: rerunfilecheck 2022-07-10 v1.10 Rerun checks for auxiliary files (HO) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/uniquecounter\uniquecounter.sty +Package: uniquecounter 2019/12/15 v1.4 Provide unlimited unique counter (HO) +) +Package uniquecounter Info: New unique counter `rerunfilecheck' on input line 285. +) +\Hy@SectionHShift=\skip59 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/listings\listings.sty +\lst@mode=\count288 +\lst@gtempboxa=\box57 +\lst@token=\toks33 +\lst@length=\count289 +\lst@currlwidth=\dimen159 +\lst@column=\count290 +\lst@pos=\count291 +\lst@lostspace=\dimen160 +\lst@width=\dimen161 +\lst@newlines=\count292 +\lst@lineno=\count293 +\lst@maxwidth=\dimen162 + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/listings\lstmisc.sty +File: lstmisc.sty 2023/02/27 1.9 (Carsten Heinz) +\c@lstnumber=\count294 +\lst@skipnumbers=\count295 +\lst@framebox=\box58 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/listings\listings.cfg +File: listings.cfg 2023/02/27 1.9 listings configuration +)) +Package: listings 2023/02/27 1.9 (Carsten Heinz) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xy.sty (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xy.tex Bootstrap'ing: catcodes, docmode, (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xyrecat.tex) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xyidioms.tex) + + Xy-pic version 3.8.9 <2013/10/06> + Copyright (c) 1991-2013 by Kristoffer H. Rose and others + Xy-pic is free software: see the User's Guide for details. + +Loading kernel: messages; fonts; allocations: state, +\X@c=\dimen163 +\Y@c=\dimen164 +\U@c=\dimen165 +\D@c=\dimen166 +\L@c=\dimen167 +\R@c=\dimen168 +\Edge@c=\toks34 +\X@p=\dimen169 +\Y@p=\dimen170 +\U@p=\dimen171 +\D@p=\dimen172 +\L@p=\dimen173 +\R@p=\dimen174 +\Edge@p=\toks35 +\X@origin=\dimen175 +\Y@origin=\dimen176 +\X@xbase=\dimen177 +\Y@xbase=\dimen178 +\X@ybase=\dimen179 +\Y@ybase=\dimen180 +\X@min=\dimen181 +\Y@min=\dimen182 +\X@max=\dimen183 +\Y@max=\dimen184 +\lastobjectbox@=\box59 +\zerodotbox@=\box60 +\almostz@=\dimen185 + direction, +\d@X=\dimen186 +\d@Y=\dimen187 +\K@=\count296 +\KK@=\count297 +\Direction=\count298 +\K@dXdY=\dimen188 +\K@dYdX=\dimen189 +\xyread@=\read2 +\xywrite@=\write4 +\csp@=\count299 +\quotPTK@=\dimen190 + utility macros; pictures: \xy, positions, +\swaptoks@@=\toks36 +\connectobjectbox@@=\box61 + objects, +\styletoks@=\toks37 + decorations; kernel objects: directionals, circles, text; options; algorithms: directions, edges, connections; Xy-pic loaded) +Package: xy 2013/10/06 Xy-pic version 3.8.9 + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xyall.tex Xy-pic option: All features v.3.8 (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xycurve.tex Xy-pic option: Curve and Spline extension v.3.12 curve, +\crv@cnt@=\count300 +\crvpts@=\toks38 +\splinebox@=\box62 +\splineval@=\dimen191 +\splinedepth@=\dimen192 +\splinetol@=\dimen193 +\splinelength@=\dimen194 + circles, +\L@=\dimen195 + loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xyframe.tex Xy-pic option: Frame and Bracket extension v.3.14 loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xycmtip.tex Xy-pic option: Computer Modern tip extension v.3.7 (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xytips.tex Xy-pic option: More Tips extension v.3.11 loaded) loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xyline.tex Xy-pic option: Line styles extension v.3.10 +\xylinethick@=\dimen196 + loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xyrotate.tex Xy-pic option: Rotate and Scale extension v.3.8 loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xycolor.tex Xy-pic option: Colour extension v.3.11 loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xymatrix.tex Xy-pic option: Matrix feature v.3.14 +\Row=\count301 +\Col=\count302 +\queue@=\toks39 +\queue@@=\toks40 +\qcount@=\count303 +\qcount@@=\count304 +\matrixsize@=\count305 + loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xyarrow.tex Xy-pic option: Arrow and Path feature v.3.9 path, \ar, loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xygraph.tex Xy-pic option: Graph feature v.3.11 loaded) loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xyarc.tex Xy-pic option: Circle, Ellipse, Arc feature v.3.8 circles, ellipses, elliptical arcs, loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xy2cell.tex Xy-pic option: Two-cell feature v.3.7 two-cells, loaded) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xypdf.tex Xy-pic option: PDF driver v.1.7 Xy-pic pdf driver: `color' extension support (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xypdf-co.tex loaded) Xy-pic pdf driver: `curve' extension support (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xypdf-cu.tex loaded) Xy-pic pdf driver: `frame' extension support (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xypdf-fr.tex loaded) Xy-pic pdf driver: `line' extension support (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xypdf-li.tex loaded) Xy-pic pdf driver: `rotate' extension support (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/generic/xypic\xypdf-ro.tex loaded) loaded)) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/tools\enumerate.sty +Package: enumerate 2023/07/04 v3.00 enumerate extensions (DPC) +\@enLab=\toks41 +) +Package hyperref Info: Option `bookmarksnumbered' set `true' on input line 26. + + +Package hyperref Warning: Option `bookmarks' has already been used, +(hyperref) setting the option has no effect on input line 26. + +Package hyperref Info: Option `colorlinks' set `true' on input line 26. + +Package hyperref Warning: Option `pagecolor' is not available anymore. + +Package hyperref Info: Option `pdfnewwindow' set `true' on input line 26. +(listings-rust.sty +Package: listings-rust 2018/01/23 Custom Package +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/xcolor\xcolor.sty +Package: xcolor 2023/11/15 v3.01 LaTeX color extensions (UK) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics-cfg\color.cfg +File: color.cfg 2016/01/02 v1.6 sample color configuration +) +Package xcolor Info: Driver file: pdftex.def on input line 274. +LaTeX Info: Redefining \color on input line 758. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics\mathcolor.ltx) +Package xcolor Info: Model `cmy' substituted by `cmy0' on input line 1350. +Package xcolor Info: Model `hsb' substituted by `rgb' on input line 1354. +Package xcolor Info: Model `RGB' extended on input line 1366. +Package xcolor Info: Model `HTML' substituted by `rgb' on input line 1368. +Package xcolor Info: Model `Hsb' substituted by `hsb' on input line 1369. +Package xcolor Info: Model `tHsb' substituted by `hsb' on input line 1370. +Package xcolor Info: Model `HSB' substituted by `hsb' on input line 1371. +Package xcolor Info: Model `Gray' substituted by `gray' on input line 1372. +Package xcolor Info: Model `wave' substituted by `hsb' on input line 1373. +) +\c@thm=\count306 +\c@cor=\count307 +\c@prop=\count308 +\c@lem=\count309 +\c@prob=\count310 +\c@conj=\count311 +\c@defn=\count312 +\c@ass=\count313 +\c@asss=\count314 +\c@ax=\count315 +\c@con=\count316 +\c@exmp=\count317 +\c@notn=\count318 +\c@notns=\count319 +\c@pro=\count320 +\c@quest=\count321 +\c@rem=\count322 +\c@warn=\count323 +\c@sch=\count324 +\c@obs=\count325 +\c@conv=\count326 + +\bibstyle{plain} +(C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics\graphicx.sty +Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics\graphics.sty +Package: graphics 2022/03/10 v1.4e Standard LaTeX Graphics (DPC,SPQR) + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics\trig.sty +Package: trig 2021/08/11 v1.11 sin cos tan (DPC) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/graphics-cfg\graphics.cfg +File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration +) +Package graphics Info: Driver file: pdftex.def on input line 107. +) +\Gin@req@height=\dimen197 +\Gin@req@width=\dimen198 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/l3backend\l3backend-pdftex.def +File: l3backend-pdftex.def 2024-01-04 L3 backend support: PDF output (pdfTeX) +\l__color_backend_stack_int=\count327 +\l__pdf_internal_box=\box63 +) + +LaTeX Warning: Unused global option(s): + [openany,amssymb]. + +(paper.aux) +\openout1 = `paper.aux'. + +LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 163. +LaTeX Font Info: ... okay on input line 163. +LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 163. +LaTeX Font Info: ... okay on input line 163. +LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 163. +LaTeX Font Info: ... okay on input line 163. +LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 163. +LaTeX Font Info: ... okay on input line 163. +LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 163. +LaTeX Font Info: ... okay on input line 163. +LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 163. +LaTeX Font Info: ... okay on input line 163. +LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 163. +LaTeX Font Info: ... okay on input line 163. +LaTeX Font Info: Checking defaults for PD1/pdf/m/n on input line 163. +LaTeX Font Info: ... okay on input line 163. +LaTeX Font Info: Checking defaults for PU/pdf/m/n on input line 163. +LaTeX Font Info: ... okay on input line 163. +LaTeX Font Info: Trying to load font information for U+msa on input line 163. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amsfonts\umsa.fd +File: umsa.fd 2013/01/14 v3.01 AMS symbols A +) +LaTeX Font Info: Trying to load font information for U+msb on input line 163. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/amsfonts\umsb.fd +File: umsb.fd 2013/01/14 v3.01 AMS symbols B +) +LaTeX Font Info: Trying to load font information for U+rsfs on input line 163. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/jknappen\ursfs.fd +File: ursfs.fd 1998/03/24 rsfs font definition file (jk) +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/context/base/mkii\supp-pdf.mkii +[Loading MPS to PDF converter (version 2006.09.02).] +\scratchcounter=\count328 +\scratchdimen=\dimen256 +\scratchbox=\box64 +\nofMPsegments=\count329 +\nofMParguments=\count330 +\everyMPshowfont=\toks42 +\MPscratchCnt=\count331 +\MPscratchDim=\dimen257 +\MPnumerator=\count332 +\makeMPintoPDFobject=\count333 +\everyMPtoPDFconversion=\toks43 +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/epstopdf-pkg\epstopdf-base.sty +Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf +Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 485. + (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/00miktex\epstopdf-sys.cfg +File: epstopdf-sys.cfg 2021/03/18 v2.0 Configuration of epstopdf for MiKTeX +)) +Package hyperref Info: Link coloring ON on input line 163. + (paper.out) (paper.out) +\@outlinefile=\write5 +\openout5 = `paper.out'. + +\c@lstlisting=\count334 +Package xypdf Info: Line width: 0.39998pt on input line 163. + (paper.toc [1{C:/Users/Marcus/AppData/Local/MiKTeX/fonts/map/pdftex/pdftex.map}]) +\tf@toc=\write6 +\openout6 = `paper.toc'. + + + +LaTeX Warning: Citation `Flecs' on page 2 undefined on input line 225. + +[2] [3] + +LaTeX Warning: Citation `Flecs' on page 4 undefined on input line 271. + +LaTeX Font Info: Font shape `OT1/cmtt/bx/n' in size <10> not available +(Font) Font shape `OT1/cmtt/m/n' tried instead on input line 274. + +LaTeX Warning: Citation `Flecs' on page 4 undefined on input line 283. + +[4] [5] +<../../images/archetype_graph.png, id=193, 831.105pt x 240.9pt> +File: ../../images/archetype_graph.png Graphic file (type png) + +Package pdftex.def Info: ../../images/archetype_graph.png used on input line 374. +(pdftex.def) Requested size: 332.43611pt x 96.35828pt. +<../../images/sparseset.png, id=194, 484.81125pt x 161.60374pt> +File: ../../images/sparseset.png Graphic file (type png) + +Package pdftex.def Info: ../../images/sparseset.png used on input line 382. +(pdftex.def) Requested size: 290.88899pt x 96.96298pt. + [6 <../../images/archetype_graph.png (PNG copy)> <../../images/sparseset.png>] +<../../images/removed.png, id=205, 484.81125pt x 161.60374pt> +File: ../../images/removed.png Graphic file (type png) + +Package pdftex.def Info: ../../images/removed.png used on input line 402. +(pdftex.def) Requested size: 290.88899pt x 96.96298pt. + + +LaTeX Warning: Citation `Luau' on page 7 undefined on input line 408. + +[7 <../../images/removed.png>] (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/listings\lstlang1.sty +File: lstlang1.sty 2023/02/27 1.9 listings language file +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/listings\lstlang2.sty +File: lstlang2.sty 2023/02/27 1.9 listings language file +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/listings\lstlang1.sty +File: lstlang1.sty 2023/02/27 1.9 listings language file +) (C:\Users\Marcus\AppData\Local\Programs\MiKTeX\tex/latex/listings\lstlang2.sty +File: lstlang2.sty 2023/02/27 1.9 listings language file +) +Overfull \hbox (17.91617pt too wide) in paragraph at lines 472--476 +\OT1/cmr/bx/n/10 Explanation: \OT1/cmr/m/n/10 This func-tion re-trieves the record for the given en-tity from \OT1/cmtt/m/n/10 entityIndex\OT1/cmr/m/n/10 . + [] + +[8] [9] +<../../images/random_access.png, id=288, 653.44125pt x 341.02406pt> +File: ../../images/random_access.png Graphic file (type png) + +Package pdftex.def Info: ../../images/random_access.png used on input line 583. +(pdftex.def) Requested size: 326.71982pt x 170.51161pt. + [10] +<../../images/insertion.png, id=313, 1797.71625pt x 1188.44pt> +File: ../../images/insertion.png Graphic file (type png) + +Package pdftex.def Info: ../../images/insertion.png used on input line 597. +(pdftex.def) Requested size: 359.53688pt x 237.68379pt. +<../../images/queries.png, id=314, 2229.32875pt x 990.70125pt> +File: ../../images/queries.png Graphic file (type png) + +Package pdftex.def Info: ../../images/queries.png used on input line 613. +(pdftex.def) Requested size: 334.38489pt x 148.59877pt. + [11 <../../images/random_access.png> <../../images/insertion.png>] [12 <../../images/queries.png>] [13] (paper.aux) + *********** +LaTeX2e <2023-11-01> patch level 1 +L3 programming layer <2024-01-04> + *********** + + +LaTeX Warning: There were undefined references. + +Package rerunfilecheck Info: File `paper.out' has not changed. +(rerunfilecheck) Checksum: A11DBC9D88DD30C22755DC5EBF6964EC;3695. + ) +Here is how much of TeX's memory you used: + 17163 strings out of 474486 + 250241 string characters out of 5743284 + 1977542 words of memory out of 5000000 + 38859 multiletter control sequences out of 15000+600000 + 569162 words of font info for 85 fonts, out of 8000000 for 9000 + 1302 hyphenation exceptions out of 8191 + 75i,8n,117p,7940b,2340s stack positions out of 10000i,1000n,20000p,200000b,200000s + +Output written on paper.pdf (13 pages, 600187 bytes). +PDF statistics: + 441 PDF objects out of 1000 (max. 8388607) + 156 named destinations out of 1000 (max. 500000) + 239 words of extra memory for PDF output out of 10000 (max. 10000000) + diff --git a/thesis/drafts/1/paper.pdf b/thesis/drafts/1/paper.pdf new file mode 100644 index 0000000..7af265f Binary files /dev/null and b/thesis/drafts/1/paper.pdf differ diff --git a/thesis/drafts/1/paper.synctex.gz b/thesis/drafts/1/paper.synctex.gz new file mode 100644 index 0000000..233caf1 Binary files /dev/null and b/thesis/drafts/1/paper.synctex.gz differ diff --git a/thesis/drafts/1/paper.tex b/thesis/drafts/1/paper.tex new file mode 100644 index 0000000..8557a26 --- /dev/null +++ b/thesis/drafts/1/paper.tex @@ -0,0 +1,692 @@ +\documentclass[openany, amssymb, psamsfonts]{amsart} +\usepackage{mathrsfs,comment} +\usepackage[usenames,dvipsnames]{color} +\usepackage[normalem]{ulem} +\usepackage[hyphens]{url} +\usepackage{breakurl} +\usepackage[utf8]{inputenc} % Ensure proper encoding +\usepackage{hyperref} % For clickable links +\usepackage{listings} +\usepackage[all,arc,2cell]{xy} +\UseAllTwocells +\usepackage{enumerate} +%%% hyperref stuff is taken from AGT style file +\usepackage{hyperref} +\hypersetup{% + bookmarksnumbered=true,% + bookmarks=true,% + colorlinks=true,% + linkcolor=blue,% + citecolor=blue,% + filecolor=blue,% + menucolor=blue,% + pagecolor=blue,% + urlcolor=blue,% + pdfnewwindow=true,% + pdfstartview=FitBH} + +\usepackage{listings, listings-rust} +\usepackage{xcolor} + +% Define Rust language syntax +\lstdefinestyle{lua}{ + language=[5.1]Lua, + basicstyle=\ttfamily\small, + keywordstyle=\color{magenta}, + stringstyle=\color{blue}, + commentstyle=\color{black!50}, + frame=single, +} + +\let\fullref\autoref +% +% \autoref is very crude. It uses counters to distinguish environments +% so that if say {lemma} uses the {theorem} counter, then autrorefs +% which should come out Lemma X.Y in fact come out Theorem X.Y. To +% correct this give each its own counter eg: +% \newtheorem{theorem}{Theorem}[section] +% \newtheorem{lemma}{Lemma}[section] +% and then equate the counters by commands like: +% \makeatletter +% \let\c@lemma\c@theorem +% \makeatother +% +% To work correctly the environment name must have a corrresponding +% \XXXautorefname defined. The following command does the job: +% +\def\makeautorefname#1#2{\expandafter\def\csname#1autorefname\endcsname{#2}} +% +% Some standard autorefnames. If the environment name for an autoref +% you need is not listed below, add a similar line to your TeX file: +% +%\makeautorefname{equation}{Equation}% +\def\equationautorefname~#1\null{(#1)\null} +\makeautorefname{footnote}{footnote}% +\makeautorefname{item}{item}% +\makeautorefname{figure}{Figure}% +\makeautorefname{table}{Table}% +\makeautorefname{part}{Part}% +\makeautorefname{appendix}{Appendix}% +\makeautorefname{chapter}{Chapter}% +\makeautorefname{section}{Section}% +\makeautorefname{subsection}{Section}% +\makeautorefname{subsubsection}{Section}% +\makeautorefname{theorem}{Theorem}% +\makeautorefname{thm}{Theorem}% +\makeautorefname{cor}{Corollary}% +\makeautorefname{lem}{Lemma}% +\makeautorefname{prop}{Proposition}% +\makeautorefname{pro}{Property} +\makeautorefname{conj}{Conjecture}% +\makeautorefname{defn}{Definition}% +\makeautorefname{notn}{Notation} +\makeautorefname{notns}{Notations} +\makeautorefname{rem}{Remark}% +\makeautorefname{quest}{Question}% +\makeautorefname{exmp}{Example}% +\makeautorefname{ax}{Axiom}% +\makeautorefname{claim}{Claim}% +\makeautorefname{ass}{Assumption}% +\makeautorefname{asss}{Assumptions}% +\makeautorefname{con}{Construction}% +\makeautorefname{prob}{Problem}% +\makeautorefname{warn}{Warning}% +\makeautorefname{obs}{Observation}% +\makeautorefname{conv}{Convention}% + + +% +% *** End of hyperref stuff *** + +%theoremstyle{plain} --- default +\newtheorem{thm}{Theorem}[section] +\newtheorem{cor}{Corollary}[section] +\newtheorem{prop}{Proposition}[section] +\newtheorem{lem}{Lemma}[section] +\newtheorem{prob}{Problem}[section] +\newtheorem{conj}{Conjecture}[section] +%\newtheorem{ass}{Assumption}[section] +%\newtheorem{asses}{Assumptions}[section] + +\theoremstyle{definition} +\newtheorem{defn}{Definition}[section] +\newtheorem{ass}{Assumption}[section] +\newtheorem{asss}{Assumptions}[section] +\newtheorem{ax}{Axiom}[section] +\newtheorem{con}{Construction}[section] +\newtheorem{exmp}{Example}[section] +\newtheorem{notn}{Notation}[section] +\newtheorem{notns}{Notations}[section] +\newtheorem{pro}{Property}[section] +\newtheorem{quest}{Question}[section] +\newtheorem{rem}{Remark}[section] +\newtheorem{warn}{Warning}[section] +\newtheorem{sch}{Scholium}[section] +\newtheorem{obs}{Observation}[section] +\newtheorem{conv}{Convention}[section] + +%%%% hack to get fullref working correctly +\makeatletter +\let\c@obs=\c@thm +\let\c@cor=\c@thm +\let\c@prop=\c@thm +\let\c@lem=\c@thm +\let\c@prob=\c@thm +\let\c@con=\c@thm +\let\c@conj=\c@thm +\let\c@defn=\c@thm +\let\c@notn=\c@thm +\let\c@notns=\c@thm +\let\c@exmp=\c@thm +\let\c@ax=\c@thm +\let\c@pro=\c@thm +\let\c@ass=\c@thm +\let\c@warn=\c@thm +\let\c@rem=\c@thm +\let\c@sch=\c@thm +\let\c@equation\c@thm +\numberwithin{equation}{section} +\makeatother + +\bibliographystyle{plain} + +%--------Meta Data: Fill in your info------ +\title{Implementation of Entity Component-Systems in Scripting } + +\author{Marcus} + +\date{DEADLINES: Draft March 2 and Final version April 4, 2024} + +% images +\usepackage{graphicx} + +\begin{document} + +\begin{abstract} + +As game development continues to be a lucrative industry, it has become increasingly +important to make performant experiences while keeping production velocity high\cite{Martin}. +However, this is non-trivial when you have to deal with many objects that have converging +behaviour. The traditional approach is to use object-oriented paradigms to construct +massive and complicated inheritance paths to share a set of behaviour. However, with +the additional indirections from code paths, the reusability of code decreases and +performance suffers\cite{Muratori}. + +To combat these issues, the traditional object-oriented design can be replaced with a more +data-oriented design approach utilising a composition-over-inheritance model where the data +and logic are separated, called Entity-Component-System (ECS). In this approach, where +components and systems can be added to a complex program without interfering with existing logic. +This flexibility sets it apart from the aforementioned traditional object-oriented approaches +based on heterogeneous collections of explicitly defined object types, where implementing new +combinations of behaviours can require far-reaching changes. + +The purpose of this thesis is to research the impacts of the memory arrangement and how that +affects implementation. Then pivot to explore various elements of ECS, highlighting its +advantages, and discussing potential implementations on a conceptual level. + +Through comparative analysis, a well-designed ECS completely separates from a naive implementation +by leveraging optimized memory layouts and caching to achieve significant performance improvements. +These findings provide valuable insights for game developers seeking to build an efficient ECS. + +\end{abstract} + +\maketitle + +\tableofcontents + +\section{Introduction} + +\subsection{Background} In modern game development, optimizing for performance and production +velocity is crucial, However, traditional object-oriented programming (OOP) approaches +often encounter challenges when dealing with the complexity of game systems, particularly +in managing large amounts of data efficiently. One significant issue is the lack of data +locality, which refers to how closely related data elements are stored in memory. + +In 2011, Robert Nystrom published his book Game Programming Patterns. In the chapter on +optimization patterns, Nystrom expounds on the importance of data locality. This concept, +while not revolutionary, underscores the fundamental role of data storage, referencing, and +manipulation is the impetus for creating any program. Without efficient data locality, programs +may suffer from increased cache misses and slower memory access times, leading to performance +bottlenecks and decreased frame rates. This problem becomes more pronounced as games become +more sophisticated and demand higher fidelity graphics, complex physics simulations, and +larger virtual worlds. It was this chapter that motivated the research into the relationship +between data locality and the implementation of Entity Component System. + +\subsection{ECS Libraries} +\subsubsection{Matter} +Matter, an ECS library written in Lua, provided with a debugger and scheduler that has been developed +specifically for Roblox, makes it easy to use and understand. + +Matter was selected for this paper to provide +a baseline threshold to benchmark against. + +\subsubsection{Flecs} +Flecs is an efficient ECS made for games and simulations with many entities. It also has an elaborate +query engine that is capable of finding entities by relationships\cite{Flecs} and can embed multitudes of operations +into its queries. + +Flecs was chosen because of its exhaustive API coupled with an involved community and in-depth documentation. + +\subsubsection{Hecs} +Hecs, a lightweight ECS that aims to be unobtrusive by being a library and not a framework. + +Hecs also has an archetypal storage and is the main inspiration for Matter. This was the reason for why it was chosen for this paper. + + +\subsection{Purpose} +The traditional OOP paradigm, with its emphasis on class hierarchies and inheritance, often results in poor data locality due to how objects and their associated data are stored in memory. As a result, game developers are turning to data-oriented design (DOD) principles to address these performance issues. + +By adopting a data-oriented approach, such as the ECS architecture, developers can restructure their code to prioritize data locality. ECS separates game entities into discrete components, each containing only the data relevant to a specific aspect of gameplay. Systems then operate on these components in a data-driven manner, promoting cache efficiency and reducing memory access overhead. + +However, despite the potential benefits of ECS and other data-oriented techniques, many developers still face challenges in understanding and implementing an ECS efficiently. This gap underscores the need for comprehensive research and documentation to explore the implications of data locality in game development and provide practical solutions for optimizing an ECS implementation. + +\subsection{Research Question} +How can the ECS architecture be optimized to address the limitations of traditional object-oriented techniques? + +\section{Method} +\subsection{Research Approach} +This research project is an exploratory study with the aim of gaining an understanding of the inner workings of ECS. This study will adopt a mixed-methods approach, with both inductive and deductive reasoning. This approach is chosen to provide a comprehensive understanding of the relationship between entity-component-systems, data locality, and performance. + +\subsection{Research Process} +The research process will start with a thorough study of the literature related to the field of ECS. Relevant research concepts will be summarized and presented in the Theory chapter to construct a theoretical framework. This framework will be used for analysis in the empirical part of the study. + +The empirical part of the project consists of a comprehensive case study. Multiple ECS implementations will be tested and analysed, using the framework constructed in the theoretical part. + +An implementation of an ECS from scratch will further be conducted in order to experiment with different storage layouts. Through this iterative process, insights into the optimal design and implementation of ECS will be gained, with a particular focus on addressing performance bottlenecks related to data access and manipulation. + +\section{Theory} +This theory chapter is dedicated to forming the theoretical foundation of ECS architecture. The reader will get a fundamental understanding of what ECS is, what makes it useful, and what the key elements of the architecture are. Together, these parts form a theoretical framework which will be used as the base of both the empirical and implementation part of the study. + +\subsection{Entity Component System Architecture} +The Entity Component System (ECS) architecture provides infrastructure for representing distinct objects with loosely coupled data and behaviour. Data is stored in contiguous storage types to promote cache optimality which benefits performance. An ECS world consists of any number of entities (unique IDs) associated with components, which are pure data. The world is then manipulated by systems that access a set of component types. +\subsection{Cache Locality} +When a CPU loads data from Random Access Memory it is stored in a cache tier (i.e. L1, L2, L3), +where the lower tiers are allowed to operate faster relatively to how closely +embedded it is to the CPU.\cite{Nystrom} When a program requests some memory, the CPU grabs a whole slab, usually from around 64 to 128 bytes starting from the requested address, and puts it in the CPU cache, i.e. cache line. If the next requested data is in the same slab, the CPU reads it straight from the cache, which is faster than hitting RAM. Inversely, when there is a cache miss, i.e. it is not in the same slab then the CPU cannot process the next instruction because it needs said data and waits a couple of CPU cycles until it successfully fetches it. (Nystrom, 2011). + +\subsection{Data Layouts} + +\subsubsection{Array Of Structs} + +Array of Structs organizes data in a way where each struct is stored as elements within an array, arranged in rows (see code snippet). This memory arrangement is frequently utilized in object-oriented programming, mirroring how classes inherently structure their data members.\cite{Flecs} + +\begin{lstlisting}[language=Rust, style=boxed] + struct AoS { + foo: i64; + bar: i32; + } + + values: Vec +\end{lstlisting} + +\subsubsection{Struct of Arrays} +Struct of Arrays organizes data in a way where each field of an entity is stored in separate arrays or "columns" (see code snippet). This memory arrangement in memory in a way that will be more beneficial to CPU performance as it can better predict the next memory access.\cite{Flecs} + +\begin{lstlisting}[language=Rust, style=boxed] + struct SoA { + foo: Vec; + bar: Vec; + } + + values: SoA +\end{lstlisting} + +\subsection{SIMD} +Single Instruction Multiple Data (SIMD) is a type of parallel computing that performs the same operation on multiple values simultaneously. + +\subsection{Vectorization} +Vectorization is where code meets the requirements to use SIMD instructions. Those requirements are that: +- Data must be stored in contiguous arrays +- The code should contain no branches or function calls + +\subsection{Archetype} +Storing data in contiguous arrays to maximize vectorization and SIMD is the ideal situation, +however it is a very complex problem in implementation. Below the ABC problem\cite{ABC} +is demonstrated where 3 entities all have the component \texttt{A} which can be stored in a single +column: + +\begin{verbatim} +0: [A] +1: [A] +2: [A] +\end{verbatim} + +Now suppose entity 0 and entity 2 have the component B, leaving a gap between the lower and upper bound entities in the component *B* array (column). The column is now non-contiguous which means it cannot be vectorized or use SIMD: + +\begin{verbatim} +0: [A, B] +1: [A, ] +2: [A, B] +\end{verbatim} + +The components in the rows are stored contiguously, but the traversal over the entities cannot be vectorized for code that requires both *A* and *B*. To make these components contiguous in memory again, the entities at indexes 1 and 2 are swapped, resulting in the following organization: + +\begin{verbatim} +0: [A, B] +2: [A, B] +1: [A, ] +\end{verbatim} + +However there are no operations that can fix the entity indexes when there are more than two columns and if there are every combination of components present in component storage: + +\begin{verbatim} +0: [ , B, ] +1: [ , B, C] +2: [A, B, C] +3: [A, B, ] +4: [A, , ] +5: [A, , C] +6: [ , , C] +\end{verbatim} + +This problem is called the ``ABC problem'' which requires a relaxation in order to support +vectorization. Which is what developers have found that archetypes solves.\cite{Anderson} +Archetypes are semantically identical to ``tables''. Each archetype contains only one type of +entity, meaning each unique combination of components defining an entity has its own archetype. +Below the following illustration is demonstrated where 2 entities only has component \texttt{A}, +2 entities with \texttt{A} and \texttt{B} and finally 2 entities with both \texttt{A} +and \texttt{C} (see code snippet). It follows the same SoA principles where each component +type has a column in the archetype. Rows in the archetype correspond to specific entities, with each entity intersecting components in the archetype. + +\begin{verbatim} +1: [A] + +2: [A, B] +3: [A, B] + +4: [A, C] +5: [A, C] +\end{verbatim} +This type of organization enables fast querying and iteration, however it also presents different challenges. Modifying entities, such as adding or removing components, or adding new entities, can be costly operations. Each change necessitates searching for the appropriate archetype, potentially creating a new archetype, and updating entity placements in archetypes which is really slow and requires traversal over every entity to find the archetype that the entity is in. + + +% maybe move this into implementation section +To move entities faster between archetypes, a common optimization is to keep +references to the next archetype based on component types (see Figure 1). +Each edge in the graph corresponds to a component that can be added, akin to an +intersection operation on the archetype set. \[A \cap \left( B \cap C \right)\] +Removal of a component from the archetype is akin to a subtraction operation from the set. \[A \cap \left( B \cap C \right) - {C}\]`. +This archetype graph facilitates $\mathsf{O}(1)$ transitions between adjacent archetypes to +mitigate the cost of structual changes. + +\begin{figure}[htbp] +\centering +\includegraphics[scale=0.4]{../../images/archetype_graph.png}\label{Fig 1: Archetype Graph} +\end{figure} + +\subsection{Sparse Set} +Sparse sets organize each component type into its own densely packed array. A sparse set is composed of two arrays, one densely packed and one sparsely populated. The sparse array contains the position of an entity ID that is stored in the dense array (see Figure 2). Components are stored in parallel to entities which allows for insertions and removals of components at $\mathsf{O}(1)$ constant time as it is just setting a single value in an array. The trade-off is that it is less memory efficient as it revolves around many repeated random access and it requires `2n` memory units to store these indices in two arrays. However, they serve different purposes. The dense array is for operations over many entities such as iteration while the sparse array is for single entity lookup. + +\begin{figure}[htbp] +\centering +\includegraphics[scale=0.6]{../../images/sparseset.png}\label{Fig 2: Sparse Set} +\end{figure} + + +To add an entity to the sparse set, it is pushed back onto the dense array and the sparse +array is updated with the entity as the key, while the index representing its position +in the dense array becomes its corresponding value. This ensures constant-time lookups +to see whether an entity is contained in the sparse set: \texttt{dense[sparse[i]] == i}. However, removing an entity is more complicated as it involves swapping it with the last entity in the dense array and updating their respective positions in the sparse array. For instance, if entity 6 (indexed at 3 in the dense array) is to be removed, it is swapped with the last entity (e.g. entity 7), and the corresponding entry in the sparse array is adjusted to reflect this change. The removed entity is then simply removed from the end of the dense array. This operation ensures +that the dense array remains tightly packed, facilitating efficient data management. + +However, removing an entity is more complicated as it involves swapping it with the +last entity in the dense dense array and updating its corresponding position in the +sparse array. Using the previous Figure as an example, if the entity 6 (indexed at 3 +in the dense array) is to be removed then it will be swapped with the last entity which +is entity 7 and the corresponding entry in the sparse array will be updated. The removed +entity is then simply removed from the end of the dense array (see Figure 3). +This method ensures that the dense array remains tightly packed.\cite{Caini} + +\begin{figure}[htbp] +\centering +\includegraphics[scale=0.6]{../../images/removed.png}\label{Fig 3: Removing Entity} +\end{figure} + +The sparse set structure is beneficial for programs that frequently manipulate the component structures of entities. However, querying multiple components can become less efficient due to the need to load and reference each component array individually. In contrast to archetypes, which only needs to iterate over entities matching their query. + +\section{Implementation} +The decision to use Luau\cite{Luau} for the ECS implementation was ultimately chosen because +a pure Lua implementation confers distinct advantages in terms +of compatibility and portability. By eschewing reliance on external C or C++ libraries +or bindings, we ensure that our ECS framework remains platform-agnostic and +compatible across various game engines. While some game engines offer support +for integrating native code written in C or C++, not all engines provide this capability. +Therefore, by keeping our implementation solely within the Lua environment, +we maximize compatibility across different engines and platforms, +including those that may lack native code integration capabilities. + +\subsection{Data Structures} + +The ECS utilize several key data structures to organize and manage entities and components within the ECS framework: + +\begin{itemize} + \item \textbf{Archetype:} Represents a group of entities sharing the same set of component types. Each archetype maintains information about its components, entities, and associated records. + + \item \textbf{Record:} Stores the archetype and row index of an entity to facilitate fast lookups. + + \item \textbf{EntityIndex:} Maps entity IDs to their corresponding records. + + \item \textbf{ComponentIndex}: Maps IDs to archetype maps. + + \item \textbf{ArchetypeMap:} Maps archetype IDs to archetype records which is used to find the column for the corresponding component. + + \item \textbf{ArchetypeIndex}: Maps type hashes to archetype. + + \item \textbf{Archetypes:} Maintains a collection of archetypes indexed by their IDs. +\end{itemize} + +These data structures form the foundation of our ECS implementation, enabling efficient organization and retrieval of entity-component data. + +\subsection{Functions} + +The ECS needs to know which components an entity has and provide an interface to manipulate it and search for +homogenous entities from a set of components quickly. + +\subsubsection{get(entityId, \ldots)} +\textbf{Purpose:} The get function retrieves component data associated with a given entity. It accepts the entity ID and one or more component IDs as arguments and returns the corresponding component data. +\begin{lstlisting}[style=lua] +local function get(entityId: i53, a, b, c, d, e) + local id = entityId + local record = entityIndex[id] + if not record then + return nil + end + + return getComponent(record, a), getComponent(record, b) ... +end + +local function getComponent(record: Record, componentId: i24) + local id = record.archetype.id + local archetypeRecord = componentIndex[componentId][id] + + if not archetypeRecord then + return nil + end + + local column = archetypeRecord.column + + return archetype.data.columns[column][record.row] +end + +\end{lstlisting} +\textbf{Explanation:} +This function retrieves the record for the given entity from \texttt{entityIndex}. It +then calls \texttt{getComponent(record, componentId)} to fetch the data for each specified +component \texttt{(a, b, c, d, e)} from the entity's archetype which is returned. + +\subsubsection{entity()} +\textbf{Purpose:} This function is responsible for generating a unique entity ID. +\begin{lstlisting}[style=lua] + local nextId = 0 + local function entity() + nextId += 1 + return nextId + end +\end{lstlisting} +\textbf{Explanation:} +Generates a unique entity ID by incrementing a counter each time it is called. + +\subsubsection{add(entityId, componentId, data)} +\textbf{Purpose:} Adds a component with associated data to a given entity +\begin{lstlisting}[style=lua] +local function add(entity, id, data) + local record = ensureRecord(entityId) + local source = record.archetype + local destination = archetypeTraverseAdd(id, source) + + if not source == destination then + moveEntity(entityId, record, destination) + -- update query cache + else + if #destination.types > 0 then + newEntity(entityId, record, destination) + end + end + + local archetypeRecord = destination.records[componentId] + local columns = destination.data.columns + columns[archetypeRecord.column][record.row] = data +end + +\end{lstlisting} +\textbf{Explanation:} +This function first ensures that the record exists for the given entity using +\texttt{ensureRecord()}. It then determines the destination archetype from the +current entity archetype and new component using \texttt{archetypeTraverseAdd()}. +It will move the entity to a new archetype or if the entity does not have a record yet, initializes +the record by calling \texttt{newEntity()}. Lastly it updates the data for the component in the +corresponding column of the archetype's data. + +\subsubsection{query(\ldots)} +\textbf{Purpose:} Performs a query against the entities that exists based on the specified components. +\begin{lstlisting}[style=lua] + local function query(a, b, c, ..) + local entities = {} + for archetype in archetypesWith(a) do + if not archetypesWith(b)[archetype] then + continue + end + if not archetypesWith(c)[archetype] then + continue + end + ... -- match archetype if every archetype + ... -- from the specified components are compatible + end + + local i = 0 + return function() + i+=1 + if i > #entities then + return + end + local entity = entities[i] + local record = entityIndex[entity] + local archetype, row = record.archetype, record.row + + local columns = archetype.data.columns + local id = archetype.id + + return entity, + columns[componentIndex[a][id].column][row], + columns[componentIndex[b][id].column][row], + columns[componentIndex[c][id].column][row] + ... + end +end + +\end{lstlisting} +\textbf{Explanation:} +This function through retrieves all archetypes that have the first component +in the specified component set through \texttt{archetypesWith()} that goes through the +\texttt{ArchetypeMap} which maps a component to a set of all of the archetypes with that component. The query stacks +operations that evaluate the conditions one by one with the subsequent components in the set. +When an archetype gets matched, it iterates through the entities in that archetype and fetches +the data for the component for each entity. + +\section{Analysis} +There are three main operational aspects to measure for performance to evaluate the efficiency +of the ECS, namely updating component data, random access and queries. Each of these aspects provide +metrics to examine the performance characteristics and identify key areas for optimization. + +\subsection{Random Access} +Retrieving component data associated with a specific entity is often slow because it requires +multiple random access into memory due to map lookup. This is exemplified by Matter requiring multiple +indirections to look through an entity in all of the storages with two subsequent map lookups +using the entity archetype. + +However, with specific locations of component data memoized by the +column and row respectively as specified by Flecs, constant $\mathsf{O}(1)$ time data retrieval +can be achieved by mostly array lookups as evident by Jade, an alias for the ECS implementation +made during this paper that outperformed Matter by 98.25\% (see below). +\begin{figure}[htbp] +\centering +\includegraphics[scale=0.5]{../../images/random_access.png}\label{Fig 4: Random Access} +\end{figure} + +\subsection{Updating Component Data} +Insertions and Removals of component data being slow was expected due to that moving many overlapping +components between archetypes costs a lot of computation when reconciling the columns and rows. +Matter is especially slow here because it needs to naively look through every storage to find +its old archetype and it has to create a new the new archetype every time an entity is updated. +Instead, Jade has amortized this cost by caching edges to adjacent archetypes on the graph (see Figure 1). + +The result is that updating data was 360\% faster than Matter (see below). + +\begin{figure}[htbp] +\centering +\includegraphics[scale=0.2]{../../images/insertion.png}\label{Fig 5: Insertion} +\end{figure} + +\subsection{Queries} +Matter is incapable leveraging very performant queries due to it is failing cache locality +under adverse conditions as entities data is stored in AoS that requires heaps of random accesses, +including many unnecessary hash lookups. It is also naively populating the query cache by iterating +over every archetype in the world in linear time which scales poorly as there are always going to +be more archetypes than components. + +Jade saw a 93.9\% increase in iteration speed by having memoized the entity locations by their column +and row indices for fast indexing during contiguous traversal over homogeneous entities. Query creations +are also cheaper as populating the cache is cheaper when only iterating over archetypes with a common component. + +\begin{figure}[htbp] +\centering +\includegraphics[scale=0.15]{../../images/queries.png}\label{Fig 6: Queries} +\end{figure} + +\section{Conclusions} +Through the exploration of ECS and its performance characteristics, this research sheds light +on crucial insights on various optimization strategies of highly abstracted memory layouts. +The theoretical framework established highlights the significance of prioritizing data locality +and separating data and logic in game systems. Additionally, the empirical analysis of various +ECS implementations underscores the importance of memory arrangement and efficient data +manipulation strategies. + +Implementations such as Flecs exhibit superior performance by +structuring memory layouts to minimize indirections, resulting in constant-time data +retrieval for random access. Conversely, approaches like Matter, relying heavily on map lookups, +experience performance penalties in random access operations. Implementations with poor cache +locality, exemplified by Matter, struggle with slow query performance due to excessive random +accesses during adverse locality conditions and emphasized the importance of caching strategies. + +In conclusion, the ECS architecture offers a promising solution for addressing performance +challenges in game development, however it needs to be implemented carefully in order to not +have performance penalties. + +\section{Acknowledgments} +I am grateful to Sanders Mertens for insightful discussions on archetypes and +meticulous evaluations of the minimal ECS implementation iterations. +My thanks also extend to Eryn L. K. and Lucien Greathouse for their invaluable +guidance and contributions to the Matter project. + +\sloppy +\tolerance=2000 +\hbadness=10000 +\begin{thebibliography}{10000} + +\bibitem{Martin} +Martin, Adam (2007). +\textit{Entity Systems are the future of MMOG development - Part 1}. +\url{https://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/} + +\bibitem{Muratori} +Muratori, Casey (2014). +\textit{Semantic Compression}. +\url{https://caseymuratori.com/blog_0015} + +\bibitem{ABC} +Mertens, Sanders (2022). \texttt{Building Games in ECS with Entity Relationships. See the ABC problem, component index.} +\url{https://ajmmertens.medium.com/building-an-ecs-1-where-are-my-entities-and-components-63d07c7da742} + +\bibitem{Archetypes} +Mertens, Sanders (2022). \texttt{Building an ECS \#2: Archetypes and Vectorization.} +\url{https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorization-fe21690805f9} + +\bibitem{Anderson} +Anderson, Carter (2022). +\textit{Bevy}. +Available at: \url{https://github.com/bevyengine/bevy} + +\bibitem{Caini} +Caini, Michele (2020). +\textit{ECS back and forth}. +Available at: \url{https://skypjack.github.io/2020-08-02-ecs-baf-part-9/} + +\bibitem{Nystrom} +Nystrom, Robert (2011). +\textit{Game Programming Patterns}. + +\bibitem{gdc} +Bilas, Scott (2002). +\textit{A Data-Driven Object System} (GDC 2002 Talk by Scott Bilas). +Available at: \url{https://www.youtube.com/watch?v=Eb4-0M2a9xE} + +\bibitem{matter} +\textit{Matter, an archetypal ECS for Roblox}. +Available at: \url{https://matter-ecs.github.io/matter/} + +\bibitem{luau} + +\end{thebibliography} + +\end{document} + diff --git a/thesis/drafts/1/paper.toc b/thesis/drafts/1/paper.toc new file mode 100644 index 0000000..a246dd6 --- /dev/null +++ b/thesis/drafts/1/paper.toc @@ -0,0 +1,35 @@ +\contentsline {section}{\tocsection {}{1}{Introduction}}{2}{section.1}% +\contentsline {subsection}{\tocsubsection {}{1.1}{Background}}{2}{subsection.1.1}% +\contentsline {subsection}{\tocsubsection {}{1.2}{ECS Libraries}}{2}{subsection.1.2}% +\contentsline {subsubsection}{\tocsubsubsection {}{1.2.1}{Matter}}{2}{subsubsection.1.2.1}% +\contentsline {subsubsection}{\tocsubsubsection {}{1.2.2}{Flecs}}{2}{subsubsection.1.2.2}% +\contentsline {subsubsection}{\tocsubsubsection {}{1.2.3}{Hecs}}{3}{subsubsection.1.2.3}% +\contentsline {subsection}{\tocsubsection {}{1.3}{Purpose}}{3}{subsection.1.3}% +\contentsline {subsection}{\tocsubsection {}{1.4}{Research Question}}{3}{subsection.1.4}% +\contentsline {section}{\tocsection {}{2}{Method}}{3}{section.2}% +\contentsline {subsection}{\tocsubsection {}{2.1}{Research Approach}}{3}{subsection.2.1}% +\contentsline {subsection}{\tocsubsection {}{2.2}{Research Process}}{3}{subsection.2.2}% +\contentsline {section}{\tocsection {}{3}{Theory}}{3}{section.3}% +\contentsline {subsection}{\tocsubsection {}{3.1}{Entity Component System Architecture}}{4}{subsection.3.1}% +\contentsline {subsection}{\tocsubsection {}{3.2}{Cache Locality}}{4}{subsection.3.2}% +\contentsline {subsection}{\tocsubsection {}{3.3}{Data Layouts}}{4}{subsection.3.3}% +\contentsline {subsubsection}{\tocsubsubsection {}{3.3.1}{Array Of Structs}}{4}{subsubsection.3.3.1}% +\contentsline {subsubsection}{\tocsubsubsection {}{3.3.2}{Struct of Arrays}}{4}{subsubsection.3.3.2}% +\contentsline {subsection}{\tocsubsection {}{3.4}{SIMD}}{4}{subsection.3.4}% +\contentsline {subsection}{\tocsubsection {}{3.5}{Vectorization}}{4}{subsection.3.5}% +\contentsline {subsection}{\tocsubsection {}{3.6}{Archetype}}{5}{subsection.3.6}% +\contentsline {subsection}{\tocsubsection {}{3.7}{Sparse Set}}{6}{subsection.3.7}% +\contentsline {section}{\tocsection {}{4}{Implementation}}{7}{section.4}% +\contentsline {subsection}{\tocsubsection {}{4.1}{Data Structures}}{7}{subsection.4.1}% +\contentsline {subsection}{\tocsubsection {}{4.2}{Functions}}{8}{subsection.4.2}% +\contentsline {subsubsection}{\tocsubsubsection {}{4.2.1}{get(entityId, \ldots )}}{8}{subsubsection.4.2.1}% +\contentsline {subsubsection}{\tocsubsubsection {}{4.2.2}{entity()}}{8}{subsubsection.4.2.2}% +\contentsline {subsubsection}{\tocsubsubsection {}{4.2.3}{add(entityId, componentId, data)}}{9}{subsubsection.4.2.3}% +\contentsline {subsubsection}{\tocsubsubsection {}{4.2.4}{query(\ldots )}}{9}{subsubsection.4.2.4}% +\contentsline {section}{\tocsection {}{5}{Analysis}}{10}{section.5}% +\contentsline {subsection}{\tocsubsection {}{5.1}{Random Access}}{10}{subsection.5.1}% +\contentsline {subsection}{\tocsubsection {}{5.2}{Updating Component Data}}{10}{subsection.5.2}% +\contentsline {subsection}{\tocsubsection {}{5.3}{Queries}}{11}{subsection.5.3}% +\contentsline {section}{\tocsection {}{6}{Conclusions}}{12}{section.6}% +\contentsline {section}{\tocsection {}{7}{Acknowledgments}}{12}{section.7}% +\contentsline {section}{\tocsection {}{}{References}}{12}{section*.2}% diff --git a/thesis/images/archetype_graph.png b/thesis/images/archetype_graph.png new file mode 100644 index 0000000..140def3 Binary files /dev/null and b/thesis/images/archetype_graph.png differ diff --git a/thesis/images/chrome_IdcpbCveiD.png b/thesis/images/chrome_IdcpbCveiD.png new file mode 100644 index 0000000..0bf4848 Binary files /dev/null and b/thesis/images/chrome_IdcpbCveiD.png differ diff --git a/thesis/images/chrome_f5DTavXIka.png b/thesis/images/chrome_f5DTavXIka.png new file mode 100644 index 0000000..935489f Binary files /dev/null and b/thesis/images/chrome_f5DTavXIka.png differ diff --git a/thesis/images/chrome_giChmd5W4Z.png b/thesis/images/chrome_giChmd5W4Z.png new file mode 100644 index 0000000..254871b Binary files /dev/null and b/thesis/images/chrome_giChmd5W4Z.png differ diff --git a/thesis/images/insertion.png b/thesis/images/insertion.png new file mode 100644 index 0000000..f6facf6 Binary files /dev/null and b/thesis/images/insertion.png differ diff --git a/thesis/images/queries.png b/thesis/images/queries.png new file mode 100644 index 0000000..a511784 Binary files /dev/null and b/thesis/images/queries.png differ diff --git a/thesis/images/random_access.png b/thesis/images/random_access.png new file mode 100644 index 0000000..6b122af Binary files /dev/null and b/thesis/images/random_access.png differ diff --git a/thesis/images/removed.png b/thesis/images/removed.png new file mode 100644 index 0000000..03cc9b6 Binary files /dev/null and b/thesis/images/removed.png differ diff --git a/thesis/images/sparseset.png b/thesis/images/sparseset.png new file mode 100644 index 0000000..0cdea79 Binary files /dev/null and b/thesis/images/sparseset.png differ diff --git a/tools/ansi.luau b/tools/ansi.luau new file mode 100644 index 0000000..a391f8e --- /dev/null +++ b/tools/ansi.luau @@ -0,0 +1,33 @@ +return { + white_underline = function(s: any) + return `\27[1;4m{s}\27[0m` + end, + + white = function(s: any) + return `\27[37;1m{s}\27[0m` + end, + + green = function(s: any) + return `\27[32;1m{s}\27[0m` + end, + + red = function(s: any) + return `\27[31;1m{s}\27[0m` + end, + + yellow = function(s: any) + return `\27[33;1m{s}\27[0m` + end, + + red_highlight = function(s: any) + return `\27[41;1;30m{s}\27[0m` + end, + + green_highlight = function(s: any) + return `\27[42;1;30m{s}\27[0m` + end, + + gray = function(s: any) + return `\27[30;1m{s}\27[0m` + end, +} diff --git a/tools/entity_visualiser.luau b/tools/entity_visualiser.luau new file mode 100644 index 0000000..69dc910 --- /dev/null +++ b/tools/entity_visualiser.luau @@ -0,0 +1,43 @@ +local jecs = require("@jecs") +local ECS_GENERATION = jecs.ECS_GENERATION +local ECS_ID = jecs.ECS_ID +local ansi = require("@tools/ansi") + +local function pe(e: any) + local gen = ECS_GENERATION(e) + return ansi.green(`e{ECS_ID(e)}`) .. ansi.yellow(`v{gen}`) +end + +local function name(world: jecs.World, id: any) + return world:get(id, jecs.Name) or `${id}` +end + +local function components(world: jecs.World, entity: any) + local r = jecs.entity_index_try_get(world.entity_index, entity) + if not r then + return false + end + + local archetype = r.archetype + local row = r.row + print(`Entity {pe(entity)}`) + print("-----------------------------------------------------") + for i, column in archetype.columns do + local component = archetype.types[i] + local n + if jecs.IS_PAIR(component) then + n = `({name(world, jecs.pair_first(world, component))}, {name(world, jecs.pair_second(world, component))})` + else + n = name(world, component) + end + local data = column[row] or "TAG" + print(`| {n} | {data} |`) + end + print("-----------------------------------------------------") + return true +end + +return { + components = components, + prettify = pe, +} diff --git a/tools/lifetime_tracker.luau b/tools/lifetime_tracker.luau new file mode 100644 index 0000000..c9abbbf --- /dev/null +++ b/tools/lifetime_tracker.luau @@ -0,0 +1,216 @@ +local jecs = require("@jecs") +local ECS_GENERATION = jecs.ECS_GENERATION +local ECS_ID = jecs.ECS_ID +local __ = jecs.Wildcard +local pair = jecs.pair + +local prettify = require("@tools/entity_visualiser").prettify + +local pe = prettify +local ansi = require("@tools/ansi") + +function print_centered_entity(entity, width: number) + local entity_str = tostring(entity) + local entity_length = #entity_str + + local padding_total = width - 2 - entity_length + + local padding_left = math.floor(padding_total / 2) + local padding_right = padding_total - padding_left + + local centered_str = string.rep(" ", padding_left) .. entity_str .. string.rep(" ", padding_right) + + print("|" .. centered_str .. "|") +end + +local function name(world, e) + return world:get(world, e, jecs.Name) or pe(e) +end +local padding_enabled = false +local function pad() + if padding_enabled then + print("") + end +end + +local function lifetime_tracker_add(world: jecs.World, opt) + local entity_index = world.entity_index + local dense_array = entity_index.dense_array + local component_index = world.component_index + + local ENTITY_RANGE = (jecs.Rest :: any) + 1 + + local w = setmetatable({}, { __index = world }) + + padding_enabled = opt.padding_enabled + + local world_entity = world.entity + w.entity = function(self, entity) + if entity then + return world_entity(world, entity) + end + local will_recycle = entity_index.max_id ~= entity_index.alive_count + local e = world_entity(world) + if will_recycle then + print(`*recycled {pe(e)}`) + else + print(`*created {pe(e)}`) + end + pad() + return e + end + w.print_entity_index = function(self) + local max_id = entity_index.max_id + local alive_count = entity_index.alive_count + local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {}) + local dead = table.move(dense_array, alive_count + 1, max_id, 1, {}) + + local sep = "|--------|" + if #alive > 0 then + print("|-alive--|") + for i = 1, #alive do + local e = pe(alive[i]) + print_centered_entity(e, 32) + print(sep) + end + print("\n") + end + + if #dead > 0 then + print("|--dead--|") + for i = 1, #dead do + print_centered_entity(pe(dead[i]), 32) + print(sep) + end + end + pad() + end + local timelines = {} + w.print_snapshot = function(self) + local timeline = #timelines + 1 + local entity_column_width = 10 + local status_column_width = 8 + + local header = string.format("| %-" .. entity_column_width .. "s |", "Entity") + for i = 1, timeline do + header = header .. string.format(" %-" .. status_column_width .. "s |", string.format("T%d", i)) + end + + local max_id = entity_index.max_id + local alive_count = entity_index.alive_count + local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {}) + local dead = table.move(dense_array, alive_count + 1, max_id, 1, {}) + + local data = {} + print("-------------------------------------------------------------------") + print(header) + + -- Store the snapshot data for this timeline + for i = ENTITY_RANGE, max_id do + if dense_array[i] then + local entity = dense_array[i] + local id = ECS_ID(entity) + local status = "alive" + if not world:contains(entity) then + status = "dead" + end + data[id] = status + end + end + + table.insert(timelines, data) + + -- Create a table to hold entity data for sorting + local entities = {} + for i = ENTITY_RANGE, max_id do + if dense_array[i] then + local entity = dense_array[i] + local id = ECS_ID(entity) + -- Push entity and id into the new `entities` table + table.insert(entities, { entity = entity, id = id }) + end + end + + -- Sort the entities by ECS_ID + table.sort(entities, function(a, b) + return a.id < b.id + end) + + -- Print the sorted rows + for _, entity_data in ipairs(entities) do + local entity = entity_data.entity + local id = entity_data.id + local status = "alive" + if id > alive_count then + status = "dead" + end + local row = string.format("| %-" .. entity_column_width .. "s |", pe(entity)) + for j = 1, timeline do + local timeline_data = timelines[j] + local entity_data = timeline_data[id] + if entity_data then + row = row .. string.format(" %-" .. status_column_width .. "s |", entity_data) + else + row = row .. string.format(" %-" .. status_column_width .. "s |", "-") + end + end + print(row) + end + print("-------------------------------------------------------------------") + pad() + end + local world_add = world.add + local relations = {} + w.add = function(self, entity: any, component: any) + world_add(world, entity, component) + if jecs.IS_PAIR(component) then + local relation = jecs.pair_first(world, component) + local target = jecs.pair_second(world, component) + print(`*added ({pe(relation)}, {pe(target)}) to {pe(entity)}`) + pad() + end + end + + local world_delete = world.delete + w.delete = function(self, e) + world_delete(world, e) + + local idr_t = component_index[pair(__, e)] + if idr_t then + for archetype_id in idr_t.cache do + local archetype = world.archetypes[archetype_id] + for _, id in archetype.types do + if not jecs.IS_PAIR(id) then + continue + end + local object = jecs.pair_second(world, id) + if object ~= e then + continue + end + local id_record = component_index[id] + local flags = id_record.flags + local flags_delete_mask: number = bit32.band(flags, jecs.ECS_ID_DELETE) + if flags_delete_mask ~= 0 then + for _, entity in archetype.entities do + print(`*deleted dependant {pe(entity)} of {pe(e)}`) + pad() + end + break + else + for _, entity in archetype.entities do + print( + `*removed dependency ({pe(jecs.pair_first(world, id))}, {pe(object)}) from {pe(entity)}` + ) + end + end + end + end + end + + print(`*deleted {pe(e)}`) + pad() + end + return w +end + +return lifetime_tracker_add diff --git a/tools/perfgraph.py b/tools/perfgraph.py new file mode 100644 index 0000000..a6bc1dc --- /dev/null +++ b/tools/perfgraph.py @@ -0,0 +1,177 @@ +#!/usr/bin/python3 +# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +# Given a profile dump, this tool generates a flame graph based on the stacks listed in the profile +# The result of analysis is a .svg file which can be viewed in a browser + +import svg +import argparse +import json + +argumentParser = argparse.ArgumentParser(description='Generate flamegraph SVG from Luau sampling profiler dumps') +argumentParser.add_argument('source_file', type=open) +argumentParser.add_argument('--json', dest='useJson',action='store_const',const=1,default=0,help='Parse source_file as JSON') + +class Node(svg.Node): + def __init__(self): + svg.Node.__init__(self) + self.function = "" + self.source = "" + self.line = 0 + self.ticks = 0 + + def text(self): + return self.function + + def title(self): + if self.line > 0: + return "{}\n{}:{}".format(self.function, self.source, self.line) + else: + return self.function + + def details(self, root): + return "Function: {} [{}:{}] ({:,} usec, {:.1%}); self: {:,} usec".format(self.function, self.source, self.line, self.width, self.width / root.width, self.ticks) + + +def nodeFromCallstackListFile(source_file): + dump = source_file.readlines() + root = Node() + + for l in dump: + ticks, stack = l.strip().split(" ", 1) + node = root + + for f in reversed(stack.split(";")): + source, function, line = f.split(",") + + child = node.child(f) + child.function = function + child.source = source + child.line = int(line) if len(line) > 0 else 0 + + node = child + + node.ticks += int(ticks) + + return root + + +def getDuration(nodes, nid): + node = nodes[nid - 1] + total = node['TotalDuration'] + + if 'NodeIds' in node: + for cid in node['NodeIds']: + total -= nodes[cid - 1]['TotalDuration'] + + return total + +def getFunctionKey(fn): + source = fn['Source'] if 'Source' in fn else '' + name = fn['Name'] if 'Name' in fn else '' + line = str(fn['Line']) if 'Line' in fn else '-1' + + return source + "," + name + "," + line + +def recursivelyBuildNodeTree(nodes, functions, parent, fid, nid): + ninfo = nodes[nid - 1] + finfo = functions[fid - 1] + + child = parent.child(getFunctionKey(finfo)) + child.source = finfo['Source'] if 'Source' in finfo else '' + child.function = finfo['Name'] if 'Name' in finfo else '' + child.line = int(finfo['Line']) if 'Line' in finfo and finfo['Line'] > 0 else 0 + + child.ticks = getDuration(nodes, nid) + + if 'FunctionIds' in ninfo: + assert(len(ninfo['FunctionIds']) == len(ninfo['NodeIds'])) + + for i in range(0, len(ninfo['FunctionIds'])): + recursivelyBuildNodeTree(nodes, functions, child, ninfo['FunctionIds'][i], ninfo['NodeIds'][i]) + + return + +def nodeFromJSONV2(dump): + assert(dump['Version'] == 2) + + nodes = dump['Nodes'] + functions = dump['Functions'] + categories = dump['Categories'] + + root = Node() + + for category in categories: + nid = category['NodeId'] + node = nodes[nid - 1] + name = category['Name'] + + child = root.child(name) + child.function = name + child.ticks = getDuration(nodes, nid) + + if 'FunctionIds' in node: + assert(len(node['FunctionIds']) == len(node['NodeIds'])) + + for i in range(0, len(node['FunctionIds'])): + recursivelyBuildNodeTree(nodes, functions, child, node['FunctionIds'][i], node['NodeIds'][i]) + + return root + +def getDurationV1(obj): + total = obj['TotalDuration'] + + if 'Children' in obj: + for key, obj in obj['Children'].items(): + total -= obj['TotalDuration'] + + return total + + +def nodeFromJSONObject(node, key, obj): + source, function, line = key.split(",") + + node.function = function + node.source = source + node.line = int(line) if len(line) > 0 else 0 + + node.ticks = getDurationV1(obj) + + if 'Children' in obj: + for key, obj in obj['Children'].items(): + nodeFromJSONObject(node.child(key), key, obj) + + return node + +def nodeFromJSONV1(dump): + assert(dump['Version'] == 1) + root = Node() + + if 'Children' in dump: + for key, obj in dump['Children'].items(): + nodeFromJSONObject(root.child(key), key, obj) + + return root + +def nodeFromJSONFile(source_file): + dump = json.load(source_file) + + if dump['Version'] == 2: + return nodeFromJSONV2(dump) + elif dump['Version'] == 1: + return nodeFromJSONV1(dump) + + return Node() + + +arguments = argumentParser.parse_args() + +if arguments.useJson: + root = nodeFromJSONFile(arguments.source_file) +else: + root = nodeFromCallstackListFile(arguments.source_file) + + + +svg.layout(root, lambda n: n.ticks) +svg.display(root, "Flame Graph", "hot", flip = True) diff --git a/tools/read_lcov.py b/tools/read_lcov.py new file mode 100644 index 0000000..b93592e --- /dev/null +++ b/tools/read_lcov.py @@ -0,0 +1,153 @@ +import os + +LCOV_FILE = "coverage.out" +OUTPUT_DIR = "coverage" + +os.makedirs(OUTPUT_DIR, exist_ok=True) + +def parse_lcov(content): + """Parses LCOV data from a single string.""" + files = {} + current_file = None + + for line in content.splitlines(): + if line.startswith("SF:"): + current_file = line[3:].strip() + files[current_file] = {"coverage": {}, "functions": []} + elif line.startswith("DA:") and current_file: + parts = line[3:].split(",") + line_num = int(parts[0]) + execution_count = int(parts[1]) + files[current_file]["coverage"][line_num] = execution_count + elif line.startswith("FN:") and current_file: + parts = line[3:].split(",") + line_num = int(parts[0]) + function_name = parts[1].strip() + files[current_file]["functions"].append({"name": function_name, "line": line_num, "hits": 0}) + elif line.startswith("FNDA:") and current_file: + parts = line[5:].split(",") + hit_count = int(parts[0]) + function_name = parts[1].strip() + for func in files[current_file]["functions"]: + if func["name"] == function_name: + func["hits"] = hit_count + break + + return files + +def read_source_file(filepath): + """Reads source file content if available.""" + if not os.path.exists(filepath): + return [] + + with open(filepath, "r", encoding="utf-8") as f: + return f.readlines() + +def generate_file_html(filepath, coverage_data, functions_data): + """Generates an HTML file for a specific source file.""" + filename = os.path.basename(filepath) + source_code = read_source_file(filepath) + html_path = os.path.join(OUTPUT_DIR, f"{filename}.html") + + total_hits = sum(func["hits"] for func in functions_data) + max_hits = max((func["hits"] for func in functions_data), default=0) + + total_functions = len(functions_data) + covered_functions = sum(1 for func in functions_data if func["hits"] > 0) + function_coverage_percent = (covered_functions / total_functions * 100) if total_functions > 0 else 0 + + lines = [ + "", + '', + '', + "", + f'

{filename} Coverage

', + f'

Total Execution Hits: {total_hits}

', + f'

Function Coverage Overview: {function_coverage_percent:.2f}%

', + + '', + + '
', + '

Function Coverage:

' + ] + + longest_name = max((len(func["name"]) for func in functions_data), default=0) + + for func in functions_data: + hit_color = "red" if func["hits"] == 0 else "green" + lines.append( + f'' + f'' + ) + + lines.append('
FunctionHits
{func["name"]}{func["hits"]}
') # Close collapsible div + + lines.append('

Source Code:

') + + for i, line in enumerate(source_code, start=1): + stripped_line = line.strip() + class_name = "text-muted" + if not stripped_line or stripped_line.startswith("end") or stripped_line.startswith("--"): + count_display = "N/A" + lines.append(f'>') + else: + count = coverage_data.get(i, 0) + class_name = "zero-hits" if count == 0 else "low-hits" if count < max_hits * 0.3 else "high-hits" + count_display = f'{count}' + marked_text = f'{line.strip()}' + lines.append(f'') + + lines.append("
LineHitsCode
{i}{count_display}{line.strip()}
{i}{count_display}{marked_text}
") + + with open(html_path, "w", encoding="utf-8") as f: + f.write("\n".join(lines)) + +def generate_index(files): + """Generates an index.html summarizing the coverage.""" + index_html = [ + "", + '', + "", + '

Coverage Report

', + '' + ] + + for filepath, data in files.items(): + filename = os.path.basename(filepath) + total_hits = sum(func["hits"] for func in data["functions"]) + total_functions = len(data["functions"]) + + index_html.append(f'') + + index_html.append("
FileTotal HitsFunctions
{filename}{total_hits}{total_functions}
") + + with open(os.path.join(OUTPUT_DIR, "index.html"), "w", encoding="utf-8") as f: + f.write("\n".join(index_html)) + +with open(LCOV_FILE, "r", encoding="utf-8") as f: + lcov_content = f.read() + +files_data = parse_lcov(lcov_content) + +for file_path, data in files_data.items(): + generate_file_html(file_path, data["coverage"], data["functions"]) + +generate_index(files_data) + +print(f"Coverage report generated in {OUTPUT_DIR}/index.html") diff --git a/tools/runtime_lints.luau b/tools/runtime_lints.luau new file mode 100644 index 0000000..9a38bde --- /dev/null +++ b/tools/runtime_lints.luau @@ -0,0 +1,108 @@ +local function dbg_info(n: number): any + return debug.info(n, "s") +end +local function throw(msg: string) + local s = 1 + local root = dbg_info(1) + repeat + s += 1 + until dbg_info(s) ~= root + if warn then + error(msg, s) + else + print(`[jecs] error: {msg}\n`) + end +end + +local function ASSERT(v: T, msg: string) + if v then + return + end + throw(msg) +end + +local function runtime_lints_add(world) + local function get_name(id) + return world_get_one_inline(world, id, EcsName) + end + + local function bname(id): string + local name: string + if ECS_IS_PAIR(id) then + local first = get_name(world, ecs_pair_first(world, id)) + local second = get_name(world, ecs_pair_second(world, id)) + name = `pair({first}, {second})` + else + return get_name(world, id) + end + if name then + return name + else + return `${id}` + end + end + + local function ID_IS_TAG(world: World, id) + if ECS_IS_PAIR(id) then + id = ecs_pair_first(world, id) + end + return not world_has_one_inline(world, id, EcsComponent) + end + + World.query = function(world: World, ...) + ASSERT((...), "Requires at least a single component") + return world_query(world, ...) + end + + World.set = function(world: World, entity: i53, id: i53, value: any): () + local is_tag = ID_IS_TAG(world, id) + if is_tag and value == nil then + local _1 = bname(world, entity) + local _2 = bname(world, id) + local why = "cannot set component value to nil" + throw(why) + return + elseif value ~= nil and is_tag then + local _1 = bname(world, entity) + local _2 = bname(world, id) + local why = `cannot set a component value because {_2} is a tag` + why ..= `\n[jecs] note: consider using "world:add({_1}, {_2})" instead` + throw(why) + return + end + + world_set(world, entity, id, value) + end + + World.add = function(world: World, entity: i53, id: i53, value: any) + if value ~= nil then + local _1 = bname(world, entity) + local _2 = bname(world, id) + throw("You provided a value when none was expected. " .. `Did you mean to use "world:add({_1}, {_2})"`) + end + + world_add(world, entity, id) + end + + World.get = function(world: World, entity: i53, ...) + local length = select("#", ...) + ASSERT(length < 5, "world:get does not support more than 4 components") + local _1 + for i = 1, length do + local id = select(i, ...) + local id_is_tag = not world_has(world, id, EcsComponent) + if id_is_tag then + local name = get_name(world, id) + if not _1 then + _1 = get_name(world, entity) + end + throw( + `cannot get (#{i}) component {name} value because it is a tag.` + .. `\n[jecs] note: If this was intentional, use "world:has({_1}, {name}) instead"` + ) + end + end + + return world_get(world, entity, ...) + end +end diff --git a/tools/svg.py b/tools/svg.py new file mode 100644 index 0000000..385db91 --- /dev/null +++ b/tools/svg.py @@ -0,0 +1,501 @@ +# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +class Node: + def __init__(self): + self.name = "" + self.children = {} + # computed + self.depth = 0 + self.width = 0 + self.offset = 0 + + def child(self, name): + node = self.children.get(name) + if not node: + node = self.__class__() + node.name = name + self.children[name] = node + return node + + def subtree(self): + result = [self] + offset = 0 + + while offset < len(result): + p = result[offset] + offset += 1 + for c in p.children.values(): + result.append(c) + + return result + +def escape(s): + return s.replace("&", "&").replace("<", "<").replace(">", ">") + +def layout(root, widthcb): + for n in reversed(root.subtree()): + # propagate width to the parent + n.width = widthcb(n) + for c in n.children.values(): + n.width += c.width + + # compute offset from parent for every child in width order (layout order) + offset = 0 + for c in sorted(n.children.values(), key = lambda x: x.width, reverse = True): + c.offset = offset + offset += c.width + + for n in root.subtree(): + for c in n.children.values(): + c.depth = n.depth + 1 + c.offset += n.offset + +# svg template (stolen from framegraph.pl) +template = r""" + + + + + + + + + + + + +$title +Reset Zoom +Search +ic + + + +""" + +def namehash(s): + # FNV-1a + hval = 0x811c9dc5 + for ch in s: + hval = hval ^ ord(ch) + hval = hval * 0x01000193 + hval = hval % (2 ** 32) + return (hval % 31337) / 31337.0 + +def display(root, title, colors, flip = False): + if colors == "cold": + gradient_start = "#eef2ee" + gradient_end = "#e0ffe0" + else: + gradient_start = "#eeeeee" + gradient_end = "#eeeeb0" + + maxdepth = 0 + for n in root.subtree(): + maxdepth = max(maxdepth, n.depth) + + svgheight = maxdepth * 16 + 3 * 16 + 2 * 16 + + print(template + .replace("$title", title) + .replace("$gradient-start", gradient_start) + .replace("$gradient-end", gradient_end) + .replace("$height", str(svgheight)) + .replace("$status", str((svgheight - 16 + 3 if flip else 3 * 16 - 3))) + .replace("$flip", str(int(flip))) + ) + + framewidth = 1200 - 20 + + def pixels(x): + return float(x) / root.width * framewidth if root.width > 0 else 0 + + for n in root.subtree(): + if pixels(n.width) < 0.1: + continue + + x = 10 + pixels(n.offset) + y = (maxdepth - 1 - n.depth if flip else n.depth) * 16 + 3 * 16 + width = pixels(n.width) + height = 15 + + if colors == "cold": + fillr = 0 + fillg = int(190 + 50 * namehash(n.name)) + fillb = int(210 * namehash(n.name[::-1])) + else: + fillr = int(205 + 50 * namehash(n.name)) + fillg = int(230 * namehash(n.name[::-1])) + fillb = int(55 * namehash(n.name[::-2])) + + fill = "rgb({},{},{})".format(fillr, fillg, fillb) + chars = width / (12 * 0.59) + + text = n.text() + + if chars >= 3: + if chars < len(text): + text = text[:int(chars-2)] + ".." + else: + text = "" + + print("") + print("{}".format(escape(n.title()))) + print("
{}
".format(escape(n.details(root)))) + print("".format(x, y, width, height, fill)) + print("{}".format(x + 3, y + 10.5, escape(text))) + print("{}".format(escape(n.text()))) + print("
") + + print("
\n
\n") diff --git a/tools/testkit.luau b/tools/testkit.luau new file mode 100644 index 0000000..4b48202 --- /dev/null +++ b/tools/testkit.luau @@ -0,0 +1,559 @@ +-------------------------------------------------------------------------------- +-- testkit.luau +-- v0.7.3 +-- MIT License +-- Copyright (c) 2022 centau +-------------------------------------------------------------------------------- + +local disable_ansi = false + +local color = { + white_underline = function(s: string): string + return if disable_ansi then s else `\27[1;4m{s}\27[0m` + end, + + white = function(s: string): string + return if disable_ansi then s else `\27[37;1m{s}\27[0m` + end, + + green = function(s: string): string + return if disable_ansi then s else `\27[32;1m{s}\27[0m` + end, + + red = function(s: string): string + return if disable_ansi then s else `\27[31;1m{s}\27[0m` + end, + + yellow = function(s: string): string + return if disable_ansi then s else `\27[33;1m{s}\27[0m` + end, + + red_highlight = function(s: string): string + return if disable_ansi then s else `\27[41;1;30m{s}\27[0m` + end, + + green_highlight = function(s: string): string + return if disable_ansi then s else `\27[42;1;30m{s}\27[0m` + end, + + gray = function(s: string): string + return if disable_ansi then s else `\27[38;1m{s}\27[0m` + end, + + orange = function(s: string): string + return if disable_ansi then s else `\27[38;5;208m{s}\27[0m` + end, +} + +local function convert_units(unit: string, value: number): (number, string) + local sign = math.sign(value) + value = math.abs(value) + + local prefix_colors = { + [4] = color.red, + [3] = color.red, + [2] = color.yellow, + [1] = color.yellow, + [0] = color.green, + [-1] = color.red, + [-2] = color.yellow, + [-3] = color.green, + [-4] = color.red, + } + + local prefixes = { + [4] = "T", + [3] = "G", + [2] = "M", + [1] = "k", + [0] = " ", + [-1] = "m", + [-2] = "u", + [-3] = "n", + [-4] = "p", + } + + local order = 0 + + while value >= 1000 do + order += 1 + value /= 1000 + end + + while value ~= 0 and value < 1 do + order -= 1 + value *= 1000 + end + + if value >= 100 then + value = math.floor(value) + elseif value >= 10 then + value = math.floor(value * 1e1) / 1e1 + elseif value >= 1 then + value = math.floor(value * 1e2) / 1e2 + end + + return value * sign, prefix_colors[order](prefixes[order] .. unit) +end + +local WALL = color.gray("│") + +-------------------------------------------------------------------------------- +-- Testing +-------------------------------------------------------------------------------- + +type Test = { + name: string, + case: Case?, + cases: { Case }, + duration: number, + error: { + message: string, + trace: string, + }?, + focus: boolean, +} + +type Case = { + name: string, + result: number, + line: number?, + focus: boolean, +} + +local PASS, FAIL, NONE, ERROR, SKIPPED = 1, 2, 3, 4, 5 + +local check_for_focused = false +local skip = false +local test: Test? +local tests: { Test } = {} + +local function output_test_result(test: Test) + if check_for_focused then + local any_focused = test.focus + for _, case in test.cases do + any_focused = any_focused or case.focus + end + + if not any_focused then + return + end + end + + print(color.white(test.name)) + + for _, case in test.cases do + local status = ({ + [PASS] = color.green("PASS"), + [FAIL] = color.red("FAIL"), + [NONE] = color.orange("NONE"), + [ERROR] = color.red("FAIL"), + [SKIPPED] = color.yellow("SKIP"), + })[case.result] + + local line = case.result == FAIL and color.red(`{case.line}:`) or "" + if check_for_focused and case.focus == false and test.focus == false then + continue + end + print(`{status}{WALL} {line}{color.gray(case.name)}`) + end + + if test.error then + print(color.gray("error: ") .. color.red(test.error.message)) + print(color.gray("trace: ") .. color.red(test.error.trace)) + else + print() + end +end + +local function CASE(name: string) + skip = false + assert(test, "no active test") + + local case = { + name = name, + result = NONE, + focus = false, + } + + test.case = case + table.insert(test.cases, case) +end + +local function CHECK_EXPECT_ERR(fn, ...) + assert(test, "no active test") + local case = test.case + if not case then + CASE("") + case = test.case + end + assert(case, "no active case") + if case.result ~= FAIL then + local ok, err = pcall(fn, ...) + case.result = if ok then FAIL else PASS + if skip then + case.result = SKIPPED + end + case.line = debug.info(stack and stack + 1 or 2, "l") + end +end + +local function CHECK(value: T, stack: number?): T? + assert(test, "no active test") + + local case = test.case + + if not case then + CASE("") + case = test.case + end + + assert(case, "no active case") + + if case.result ~= FAIL then + case.result = value and PASS or FAIL + if skip then + case.result = SKIPPED + end + case.line = debug.info(stack and stack + 1 or 2, "l") + end + + return value +end + +local function TEST(name: string, fn: () -> ()) + + test = { + name = name, + cases = {}, + duration = 0, + focus = false, + fn = fn + } + + table.insert(tests, test) +end + +local function FOCUS() + assert(test, "no active test") + + check_for_focused = true + if test.case then + test.case.focus = true + else + test.focus = true + end +end + +local function FINISH(): boolean + local success = true + local total_cases = 0 + local passed_cases = 0 + local passed_focus_cases = 0 + local total_focus_cases = 0 + local duration = 0 + + for _, t in tests do + if check_for_focused and not t.focus then + continue + end + test = t + fn = t.fn + local start = os.clock() + local err + local success = xpcall(fn, function(m: string) + err = { message = m, trace = debug.traceback(nil, 2) } + end) + test.duration = os.clock() - start + + if not test.case then + CASE("") + end + assert(test.case, "no active case") + + if not success then + test.case.result = ERROR + test.error = err + end + collectgarbage() + end + + for _, test in tests do + duration += test.duration + for _, case in test.cases do + total_cases += 1 + if case.focus or test.focus then + total_focus_cases += 1 + end + if case.result == PASS or case.result == NONE or case.result == SKIPPED then + if case.focus or test.focus then + passed_focus_cases += 1 + end + passed_cases += 1 + else + success = false + end + end + + output_test_result(test) + end + + print(color.gray(string.format(`{passed_cases}/{total_cases} test cases passed in %.3f ms.`, duration * 1e3))) + if check_for_focused then + print(color.gray(`{passed_focus_cases}/{total_focus_cases} focused test cases passed`)) + end + + local fails = total_cases - passed_cases + + print((fails > 0 and color.red or color.green)(`{fails} {fails == 1 and "fail" or "fails"}`)) + + check_for_focused = false + return success, table.clear(tests) +end + +local function SKIP() + skip = true +end + +-------------------------------------------------------------------------------- +-- Benchmarking +-------------------------------------------------------------------------------- + +type Bench = { + time_start: number?, + memory_start: number?, + iterations: number?, +} + +local bench: Bench? + +function START(iter: number?): number + local n = iter or 1 + assert(n > 0, "iterations must be greater than 0") + assert(bench, "no active benchmark") + assert(not bench.time_start, "clock was already started") + + bench.iterations = n + bench.memory_start = gcinfo() + bench.time_start = os.clock() + return n +end + +local function BENCH(name: string, fn: () -> ()) + local active = bench + assert(not active, "a benchmark is already in progress") + + bench = {} + assert(bench); + (collectgarbage :: any)("collect") + + local mem_start = gcinfo() + local time_start = os.clock() + local err_msg: string? + + local success = xpcall(fn, function(m: string) + err_msg = m .. debug.traceback(nil, 2) + end) + + local time_stop = os.clock() + local mem_stop = gcinfo() + + if not success then + print(`{WALL}{color.red("ERROR")}{WALL} {name}`) + print(color.gray(err_msg :: string)) + else + time_start = bench.time_start or time_start + mem_start = bench.memory_start or mem_start + + local n = bench.iterations or 1 + local d, d_unit = convert_units("s", (time_stop - time_start) / n) + local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3)) + + local function round(x: number): string + return x > 0 and x < 10 and (x - math.floor(x)) > 0 and string.format("%2.1f", x) + or string.format("%3.f", x) + end + + print( + string.format( + `%s %s %s %s{WALL} %s`, + color.gray(round(d)), + d_unit, + color.gray(round(a)), + a_unit, + color.gray(name) + ) + ) + end + + bench = nil +end + +-------------------------------------------------------------------------------- +-- Printing +-------------------------------------------------------------------------------- + +local function print2(v: unknown) + type Buffer = { n: number, [number]: string } + type Cyclic = { n: number, [{}]: number } + + -- overkill concatenationless string buffer + local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic) + local TAB = " " + local indent = table.concat(table.create(stack, TAB)) + + if type(value) == "string" then + local n = str.n + str[n + 1] = "\"" + str[n + 2] = value + str[n + 3] = "\"" + str.n = n + 3 + elseif type(value) ~= "table" then + local n = str.n + str[n + 1] = value == nil and "nil" or tostring(value) + str.n = n + 1 + elseif next(value) == nil then + local n = str.n + str[n + 1] = "{}" + str.n = n + 1 + else -- is table + local tabbed_indent = indent .. TAB + + if cyclic[value] then + str.n += 1 + str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`) + return + else + cyclic.n += 1 + cyclic[value] = cyclic.n + end + + str.n += 3 + str[str.n - 2] = "{ " + str[str.n - 1] = color.gray(tostring(cyclic[value])) + str[str.n - 0] = "\n" + + local i, v = next(value, nil) + while v ~= nil do + local n = str.n + str[n + 1] = tabbed_indent + + if type(i) ~= "string" then + str[n + 2] = "[" + str[n + 3] = tostring(i) + str[n + 4] = "]" + n += 4 + else + str[n + 2] = tostring(i) + n += 2 + end + + str[n + 1] = " = " + str.n = n + 1 + + tos(v, stack + 1, str, cyclic) + + i, v = next(value, i) + + n = str.n + str[n + 1] = v ~= nil and ",\n" or "\n" + str.n = n + 1 + end + + local n = str.n + str[n + 1] = indent + str[n + 2] = "}" + str.n = n + 2 + end + end + + local str = { n = 0 } + local cyclic = { n = 0 } + tos(v, 0, str, cyclic) + print(table.concat(str)) +end + +-------------------------------------------------------------------------------- +-- Equality +-------------------------------------------------------------------------------- + +local function shallow_eq(a: {}, b: {}): boolean + if #a ~= #b then + return false + end + + for i, v in next, a do + if b[i] ~= v then + return false + end + end + + for i, v in next, b do + if a[i] ~= v then + return false + end + end + + return true +end + +local function deep_eq(a: {}, b: {}): boolean + if #a ~= #b then + return false + end + + for i, v in next, a do + if type(b[i]) == "table" and type(v) == "table" then + if deep_eq(b[i], v) == false then + return false + end + elseif b[i] ~= v then + return false + end + end + + for i, v in next, b do + if type(a[i]) == "table" and type(v) == "table" then + if deep_eq(a[i], v) == false then + return false + end + elseif a[i] ~= v then + return false + end + end + + return true +end + +-------------------------------------------------------------------------------- +-- Return +-------------------------------------------------------------------------------- + +return { + test = function() + return { + TEST = TEST, + CASE = CASE, + CHECK = CHECK, + FINISH = FINISH, + SKIP = SKIP, + FOCUS = FOCUS, + CHECK_EXPECT_ERR = CHECK_EXPECT_ERR, + } + end, + + benchmark = function() + return BENCH, START + end, + + disable_formatting = function() + disable_ansi = true + end, + + print = print2, + + seq = shallow_eq, + deq = deep_eq, + + color = color, +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..38f3795 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "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" + ], + "rootDir": "lib", + "outDir": "out", + "baseUrl": "lib", + "incremental": true, + "tsBuildInfoFile": "out/tsconfig.tsbuildinfo", + "moduleDetection": "force" + } +} diff --git a/wally.toml b/wally.toml new file mode 100644 index 0000000..125c85b --- /dev/null +++ b/wally.toml @@ -0,0 +1,15 @@ +[package] +name = "ukendio/jecs" +version = "0.5.5" +registry = "https://github.com/UpliftGames/wally-index" +realm = "shared" +license = "MIT" +include = [ + "default.project.json", + "jecs.luau", + "wally.toml", + "README.md", + "CHANGELOG.md", + "LICENSE", +] +exclude = ["**"]