├── .luaurc ├── todo.md ├── default.project.json ├── .gitignore ├── docs ├── .vitepress │ ├── theme │ │ ├── index.js │ │ └── vars.css │ └── config.ts ├── package.json ├── index.md ├── tut │ ├── partition.md │ ├── tags.md │ ├── context.md │ ├── entity-type.md │ ├── storage.md │ ├── groups.md │ └── crash-course.md ├── public │ ├── logo.svg │ └── favicon.svg ├── api │ ├── Group.md │ ├── Handle.md │ ├── Pool.md │ ├── Signal.md │ ├── Queue.md │ ├── View.md │ ├── restrictions.md │ ├── Observer.md │ ├── ecr.md │ └── Registry.md └── wiki │ ├── entity-identifier.md │ └── component-storage.md ├── wally.toml ├── .github └── workflows │ ├── unit-test.yml │ └── deploy.yml ├── LICENSE.md ├── README.md ├── CHANGELOG.md └── test ├── testkit.luau ├── benchmarks.luau └── tests.luau /.luaurc: -------------------------------------------------------------------------------- 1 | { "languageMode": "strict" } 2 | 3 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # To do 2 | 3 | - Non-owning groups 4 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecr", 3 | "tree": { "$path": "src/ecr.luau" } 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | sourcemap.json 2 | remarks.luau 3 | .vscode 4 | _local 5 | aftman.toml 6 | 7 | docs/.vitepress/dist 8 | docs/.vitepress/cache 9 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | // .vitepress/theme/index.js 2 | import DefaultTheme from 'vitepress/theme' 3 | import './vars.css' 4 | export default DefaultTheme 5 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-brand: #ff3030; 3 | 4 | --vp-c-brand-dark: #ff3030; 5 | --vp-c-brand-darker: #350606; 6 | --vp-c-brand-darkest: #fb4848; 7 | --vp-c-brand-light: #fb4848; 8 | } 9 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | 4 | "scripts": { 5 | "docs:dev": "vitepress dev", 6 | "docs:build": "vitepress build", 7 | "docs:preview": "vitepress preview" 8 | }, 9 | 10 | "devDependencies": { 11 | "vitepress": "1.0.0-rc.4" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "centau/ecr" 3 | description = "An ECS library for Luau." 4 | version = "0.9.0" 5 | registry = "https://github.com/UpliftGames/wally-index" 6 | realm = "shared" 7 | include = ["src", "LICENSE.md", "default.project.json"] 8 | exclude = ["CHANGELOG.md", ".gitignore", ".github", ".gitattributes", "test", "docs", "README.md", ".luaurc"] 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: ECR 7 | text: "" 8 | tagline: A Luau ECS library. 9 | image: 10 | src: /logo.svg 11 | alt: ECR 12 | actions: 13 | - theme: brand 14 | text: Tutorials 15 | link: /tut/crash-course 16 | - theme: alt 17 | text: API Reference 18 | link: /api/ecr 19 | 20 | features: 21 | - title: In Development 22 | details: Not recommended for production use. 23 | --- 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: unit-test 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | unit-test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repo 11 | uses: actions/checkout@v3 12 | 13 | - name: Install Luau zip 14 | uses: robinraju/release-downloader@v1.6 15 | with: 16 | repository: Roblox/luau 17 | tag: "0.655" 18 | fileName: luau-ubuntu.zip 19 | out-file-path: bin 20 | 21 | - name: Unzip Luau 22 | run: unzip bin/luau-ubuntu.zip -d bin 23 | 24 | - name: Run unit tests 25 | run: bin/luau test/tests.luau 26 | -------------------------------------------------------------------------------- /docs/tut/partition.md: -------------------------------------------------------------------------------- 1 | # Partitions 2 | 3 | Currently, a registry by default can create entities occupying a range 4 | between [1, 65534]. 5 | You can optionally restrict this range to avoid id conflicts when copying 6 | entities from one registry to another. 7 | 8 | The most common example of this is replicating server entities into a client 9 | registry, while also being able to create client entities. The partitions can be 10 | set up like so: 11 | 12 | ```lua 13 | local server_registry = ecr.registry(1, 1000) 14 | local client_registry = ecr.registry(1001, 2000) 15 | ``` 16 | 17 | This ensures that ids returned by `registry:create()` will never overlap. You 18 | can still create entities outside of the range with an explicit 19 | `registry:create(id)`. 20 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/tut/tags.md: -------------------------------------------------------------------------------- 1 | # Tags 2 | 3 | Tags are special component types that store no value. 4 | 5 | A tag can be created with: 6 | 7 | ```lua 8 | local Tag = ecr.tag() 9 | ``` 10 | 11 | Tags types are used in the same way as any other component type. They are useful 12 | for marking entities in some state, and are more efficient than something like 13 | `ecr.component() :: true`. 14 | 15 | Example usage: 16 | 17 | ```lua 18 | local Selected = ecr.tag() 19 | 20 | registry:add(id, Selected) 21 | registry:has(id, Selected) -- true 22 | 23 | registry:remove(id, Selected) 24 | registry:has(id, Selected) -- false 25 | ``` 26 | 27 | To check if an entity has a tag, favor `Registry:has()` over `Registry:get()`, 28 | since tags have no value and will return `nil`. 29 | 30 | You can check if a component type is a tag type or not with `ecr.is_tag(ctype)`. 31 | -------------------------------------------------------------------------------- /docs/api/Group.md: -------------------------------------------------------------------------------- 1 | # Group 2 | 3 | Fast iterator for viewing entities and components in a registry. 4 | 5 | ```lua 6 | type ecr.Group 7 | ``` 8 | 9 | ## Iteration 10 | 11 | Iterates over all entities in the group. 12 | 13 | - **Type** 14 | 15 | ```lua 16 | for id: Entity, ...: T... in Group do 17 | ``` 18 | 19 | - **Details** 20 | 21 | The entity followed by the component values are returned. 22 | 23 | Components can be added, changed and removed during iteration. 24 | Components added during iteration are not returned for that iteration. 25 | 26 | ::: warning 27 | During iteration, adding or removing components from entities not currently 28 | being iterated can invalidate the iterator. 29 | ::: 30 | 31 | ## Length 32 | 33 | Returns the amount of entities in the group. 34 | 35 | - **Type** 36 | 37 | ```lua 38 | #Group: number 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/tut/context.md: -------------------------------------------------------------------------------- 1 | # Context 2 | 3 | All registries have a special entity, using a special id `ecr.context`, called 4 | the *context entity*. 5 | 6 | If you ever need to store general data about the world (like a round timer, 7 | chosen map, etc) the context entity is a place to do this. Being an entity, 8 | systems can still act on it saving you from having separate logic for normal 9 | entity data and context data (like automatic replication). 10 | 11 | This entity does not exist by default, it is automatically created the first 12 | time [`Registry:context()`](../api/Registry#context) is called, subsequent calls 13 | return the same entity. 14 | 15 | ```lua 16 | local Round = ecr.component() :: number 17 | 18 | registry:context():set(Round, 1) 19 | 20 | -- or, if you prefer 21 | 22 | registry:create(ecr.context) 23 | registry:set(ecr.context, Round, 1) 24 | ``` 25 | 26 | This entity is still like any other, can show up in views, can still be 27 | destroyed (and later recreated), and is affected by 28 | [`Registry:clear()`](../api/Registry#clear). 29 | -------------------------------------------------------------------------------- /docs/api/Handle.md: -------------------------------------------------------------------------------- 1 | # Handle 2 | 3 | Thin wrapper around an entity and its registry. 4 | 5 | ```lua 6 | type ecr.Handle 7 | ``` 8 | 9 | ## Properties 10 | 11 | ### registry 12 | 13 | The registry the entity belongs to. 14 | 15 | - **Type** 16 | 17 | ```lua 18 | Handle.registry: Registry 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- 22 | 23 | ### entity 24 | 25 | The entity the handle refers to. 26 | 27 | - **Type** 28 | 29 | ```lua 30 | Handle.entity: entity 31 | ``` 32 | 33 | ## Methods 34 | 35 | The `Handle` class wraps the following registry methods: 36 | 37 | - destroy() 38 | - has_none() 39 | - add() 40 | - set() 41 | - insert() 42 | - patch() 43 | - has() 44 | - get() 45 | - try_get() 46 | - remove() 47 | 48 | The `set()` and `insert()` method will also return the handle it is called 49 | on. 50 | 51 | - **Example** 52 | 53 | ```lua 54 | local e = registry:handle() 55 | 56 | e:set(A, 1) 57 | :set(B, 2) 58 | 59 | print(e:get(A, B)) --> 1, 2 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/tut/entity-type.md: -------------------------------------------------------------------------------- 1 | # Entity Type 2 | 3 | Entities are represented by a special component type, `ecr.entity`. 4 | 5 | This can be used in the same way as other components types, except instead of 6 | representing components, represents entities. 7 | 8 | This type cannot be used to modify the registry, methods like `add()`, `set()`, 9 | `remove()` do not work with this type. 10 | 11 | ## Get all entities 12 | 13 | You can get all entities that currently exist in the registry with: 14 | 15 | ```lua 16 | for id in registry:view(ecr.entity) do 17 | print(id) 18 | end 19 | 20 | -- or 21 | 22 | local entities = registry:storage(ecr.entity).entities 23 | ``` 24 | 25 | ## Exclude-only views 26 | 27 | You can get all entities without specific components with: 28 | 29 | ```lua 30 | for entity in registry:view(ecr.entity):exclude(...) do end 31 | ``` 32 | 33 | ## Listeners 34 | 35 | You can listen to when an entity is created with: 36 | 37 | ```lua 38 | registry:on_add(ecr.entity):connect(function) 39 | ``` 40 | 41 | And when an entity is destroyed with: 42 | 43 | ```lua 44 | registry:on_remove(ecr.entity):connect(function) 45 | ``` 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 centau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/api/Pool.md: -------------------------------------------------------------------------------- 1 | # Pool 2 | 3 | The container used by the registry internally to store entities and component 4 | values for each component type. 5 | 6 | ## Properties 7 | 8 | ### size 9 | 10 | The amount of entities contained in the pool. 11 | 12 | - **Type** 13 | 14 | ```lua 15 | Pool.size: number 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- 19 | 20 | ### entities 21 | 22 | A buffer of all entities with the given component type. 23 | 24 | - **Type** 25 | 26 | ```lua 27 | Pool.entities: buffer 28 | ``` 29 | 30 | - **Details** 31 | 32 | 0-indexed. 33 | 34 | Sorted in the same order as [`Pool.values`](Pool#values). 35 | 36 | - i.e. `entities[n]`'s component value is located at `values[n + 1]`. 37 | 38 | -------------------------------------------------------------------------------- 39 | 40 | ### values 41 | 42 | An array of all values for the given component type. 43 | 44 | - **Type** 45 | 46 | ```lua 47 | Pool.values: Array 48 | ``` 49 | 50 | - **Details** 51 | 52 | 1-indexed. 53 | 54 | Sorted in the same order as [`Pool.entities`](Pool#entities.md). 55 | -------------------------------------------------------------------------------- /docs/api/Signal.md: -------------------------------------------------------------------------------- 1 | # Signal 2 | 3 | Manage listeners to an event. 4 | 5 | ```lua 6 | type ecr.Signal 7 | ``` 8 | 9 | ## Methods 10 | 11 | ### connect() 12 | 13 | Connects a given function to the signal to be called whenever the signal is 14 | fired. 15 | 16 | - **Type** 17 | 18 | ```lua 19 | function Signal:connect((T...) -> ()): Connection 20 | ``` 21 | 22 | - **Details** 23 | 24 | New connections made within a listener will not be ran until the next time 25 | the signal is fired. 26 | 27 | ## Connection 28 | 29 | ```lua 30 | type ecr.Connection 31 | ``` 32 | 33 | ### disconnect() 34 | 35 | Disconnects a listener from a signal. 36 | 37 | - **Type** 38 | 39 | ```lua 40 | function Connection:disconnect() 41 | ``` 42 | 43 | - **Details** 44 | 45 | ::: warning 46 | Disconnecting other listeners from within a listener is not allowed. 47 | Disconnecting a listenener from within itself is allowed. 48 | ::: 49 | 50 | -------------------------------------------------------------------------------- 51 | 52 | ### Reconnect() 53 | 54 | Reconnects a listener to a signal. 55 | 56 | - **Type** 57 | 58 | ```lua 59 | function Connection:reconnect() 60 | ``` 61 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: site-deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | 11 | concurrency: 12 | group: pages 13 | cancel-in-progress: false 14 | 15 | defaults: 16 | run: 17 | working-directory: docs 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | - name: Setup Node 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 18 31 | - name: Setup Pages 32 | uses: actions/configure-pages@v3 33 | - name: Install dependencies 34 | run: npm install 35 | - name: Build with VitePress 36 | run: npm run docs:build 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v2 39 | with: 40 | path: docs/.vitepress/dist 41 | 42 | deploy: 43 | environment: 44 | name: github-pages 45 | url: ${{ steps.deployment.outputs.page_url }} 46 | needs: build 47 | runs-on: ubuntu-latest 48 | name: Deploy 49 | steps: 50 | - name: Deploy to GitHub Pages 51 | id: deployment 52 | uses: actions/deploy-pages@v2 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 | # 10 | 11 |
12 | 13 | ### ⚠️ This library is in early stages of development with breaking changes being made often. 14 | 15 | ECR is a Luau ECS library. 16 | 17 | - A library and not a framework 18 | - Uses typechecking. 19 | - Sparse-set based storage that can support perfect SoA. 20 | - Carefully optimized memory usage and performance. 21 | - Signals for detecting changes to components. 22 | 23 | ## Getting started 24 | 25 | Read the [crash course](https://centau.github.io/ecr/tut/crash-course) for a 26 | brief introduction to the library. 27 | 28 | ## Code sample 29 | 30 | ```lua 31 | local ecr = require(ecr) 32 | 33 | -- define components 34 | local Position = ecr.component() :: Vector3 35 | local Velocity = ecr.component() :: Vector3 36 | 37 | -- define a system 38 | local function update_physics(world: ecr.Registry, dt: number) 39 | for id, pos, vel in world:view(Position, Velocity) do 40 | world:set(id, Position, pos + vel*dt) 41 | end 42 | end 43 | 44 | -- instantiate the world 45 | local world = ecr.registry() 46 | 47 | -- create entities and assign components 48 | for i = 1, 10 do 49 | local id = world:create() 50 | world:set(id, Position, Vector3.new(i, 1, 1)) 51 | world:set(id, Velocity, Vector3.new(10, 0, 0)) 52 | end 53 | 54 | -- run system 55 | update_physics(world, 1/60) 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/api/Queue.md: -------------------------------------------------------------------------------- 1 | # Queue 2 | 3 | Queues values to be processed later. 4 | 5 | ```lua 6 | type ecr.Queue 7 | ``` 8 | 9 | ## Methods 10 | 11 | ### add() 12 | 13 | Adds a set of values to a queue. 14 | 15 | - **Type** 16 | 17 | ```lua 18 | function Queue:add(...: T...) 19 | ``` 20 | 21 | - **Details** 22 | 23 | Each time this method is called, the size of the queue increases by one. 24 | 25 | All arguments given are later returned together in the same iteration. 26 | 27 | Queues are FIFO. 28 | 29 | ::: warning 30 | The first value in the argument list cannot be `nil` since that will cause 31 | iteration to stop early. 32 | ::: 33 | 34 | -------------------------------------------------------------------------------- 35 | 36 | ### clear() 37 | 38 | Clears the queue. 39 | 40 | - **Type** 41 | 42 | ```lua 43 | function Queue:clear() 44 | ``` 45 | 46 | ## Iteration 47 | 48 | Iterates all values added to the queue. 49 | 50 | - **Type** 51 | 52 | ```lua 53 | for ...: T... in Queue do 54 | ``` 55 | 56 | - **Details** 57 | 58 | The queue returns in sets of values passed to `Queue:add()` in the same 59 | order it was called in. 60 | 61 | The queue automatically clears itself after iteration. `Queue:iter()` 62 | returns an iterator that will not automatically clear on completion. 63 | 64 | ::: warning 65 | Adding values during iteration will cause them to be cleared when iteration 66 | completes and they will never be iterated. 67 | ::: 68 | -------------------------------------------------------------------------------- /docs/api/View.md: -------------------------------------------------------------------------------- 1 | # View 2 | 3 | Iterator for viewing entities and components in a registry. 4 | 5 | ```lua 6 | type ecr.View 7 | ``` 8 | 9 | ## Methods 10 | 11 | ### exclude() 12 | 13 | Excludes entities with the given components from the view. 14 | 15 | - **Type** 16 | 17 | ```lua 18 | function View:exclude(components: ...unknown): View 19 | ``` 20 | 21 | - **Details** 22 | 23 | Entities with *any* of the excluded components, will not be returned during 24 | iteration. 25 | 26 | -------------------------------------------------------------------------------- 27 | 28 | ## Iteration 29 | 30 | Iterates all entities in the view. 31 | 32 | - **Type** 33 | 34 | ```lua 35 | for id: Entity, ...: T... in View do 36 | ``` 37 | 38 | - **Details** 39 | 40 | The entity followed by the component values are returned. 41 | 42 | Components can be added, changed and removed during iteration. 43 | Components added during iteration are not returned for that iteration. 44 | 45 | ::: warning 46 | During iteration, adding or removing components from entities not currently 47 | being iterated can invalidate the iterator. 48 | ::: 49 | 50 | ## Length 51 | 52 | Returns the amount of entities in the view. 53 | 54 | - **Type** 55 | 56 | ```lua 57 | #View: number 58 | ``` 59 | 60 | - **Details** 61 | 62 | For single component views, this returns the exact amount of entities in the 63 | view. 64 | 65 | For multiple component views, this returns an estimated amount of entities. 66 | This estimate will not be less than the actual amount of entities. 67 | -------------------------------------------------------------------------------- /docs/tut/storage.md: -------------------------------------------------------------------------------- 1 | # Storage 2 | 3 | Each component type in the registry has its own [pool](../api/Pool.md). A pool 4 | stores every entity that has that component type, and their corresponding value 5 | for that component type. Pools are the underlying containers that the registry 6 | directly modifies. 7 | 8 | ECR was designed with transparent access to data in mind, and so, you can access 9 | these pools directly if needed. 10 | 11 | ```lua 12 | type Pool = { 13 | -- amount of entities in the pool (so amount that have the component type) 14 | size: number, 15 | 16 | -- buffer (used as an array) of all entities in the pool 17 | entities: buffer, 18 | 19 | -- array of all component values (value for entities[i] is values[i]) 20 | values: Array 21 | } 22 | ``` 23 | 24 | A pool for a type is retrieved like so: 25 | 26 | ```lua 27 | local pool = registry:storage(ctype) 28 | ``` 29 | 30 | You can read from pools, but you cannot write to pools, with the exception 31 | of writing to values of `Pool.values`. The registry maintains the size and 32 | values of `Pool.entities`, so those should not be changed. 33 | 34 | Buffers aren't nice to work with, so you can use 35 | [`ecr.buffer_to_array()`](../api/ecr#buffer_to_array) to access entities easier. 36 | 37 | ```lua 38 | local entities = ecr.buffer_to_array(pool.entities, pool.size) 39 | ``` 40 | 41 | This can be useful for e.g: 42 | 43 | - Getting all entities and components to replicate a registry from server to 44 | client for the first time they join. 45 | 46 | - Modifying values directly for performance systems. 47 | 48 | If needed you can also get all pools inside the registry via an iterator. 49 | 50 | ```lua 51 | for ctype, pool in registry:storage() do 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/api/restrictions.md: -------------------------------------------------------------------------------- 1 | # Restrictions 2 | 3 | The API omits sanity checks in areas that would be costly to do so, allowing you 4 | to run into problems like iterator invalidation and undefined behavior. This 5 | means that there are certain restrictions on what you can do. All restrictions 6 | are documented here and at the relevant API references. 7 | 8 | ## Signals 9 | 10 | - Listeners cannot disconnect other listeners. Listeners can disconnect 11 | themselves. 12 | 13 | - The registry cannot be modified within a listener, you cannot add or remove 14 | components, and create or destroy entities. 15 | 16 | ## Modifying During Iteration 17 | 18 | This applies to the iteration of views, observers and groups. 19 | 20 | - During iteration, adding or removing components from entities not currently 21 | being iterated can *invalidate the iterator*. 22 | 23 | ## Pools 24 | 25 | [`storage()`](Registry#storage.md) returns the underlying storages used 26 | by the registry to store entities and components. You can use these for more 27 | direct access than views and also modify the values of `Pool.values` directly. 28 | 29 | - Changing `Pool.entities` or `Pool.size` is *undefined behavior*. 30 | 31 | ## Releasing Ids 32 | 33 | - Destroying an entity that still has components with 34 | [`release()`](Registry#release.md) is *undefined behavior*. 35 | 36 | ## Multithreading 37 | 38 | - Components can be added or removed during iteration as long as it is the only 39 | thread doing so for that set of components. 40 | 41 | This does not apply if a component in that set belongs to a group that has 42 | other group components being used in another thread, e.g One thread can 43 | add/remove component `X` and another thread can add/remove `Y` and `Z` as long 44 | as `X` is not grouped with `Y` or `Z`. 45 | 46 | - The same set of components can be iterated by multiple threads at the same 47 | time if they are reading only. 48 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress" 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: "ECR", 6 | titleTemplate: ":title - A Luau ECS library", 7 | description: "A Luau ECS library.", 8 | base: "/ecr/", 9 | head: [["link", { rel: "icon", href: "/ecr/favicon.svg" }]], 10 | ignoreDeadLinks: true, 11 | 12 | markdown: { 13 | theme: "poimandres" 14 | }, 15 | 16 | themeConfig: { 17 | logo: "/logo.svg", 18 | siteTitle: false, 19 | 20 | // https://vitepress.dev/reference/default-theme-config 21 | nav: [ 22 | { text: "Home", link: "/" }, 23 | { text: "Tutorials", link: "/tut/crash-course" }, 24 | { text: "API", link: "/api/ecr"}, 25 | { text: "GitHub", link: "https://github.com/centau/ecr" } 26 | ], 27 | 28 | outline: "deep", 29 | 30 | sidebar: { 31 | "/api/": [ 32 | { 33 | text: "API", 34 | items: [ 35 | { text: "ecr", link: "/api/ecr" }, 36 | { text: "Registry", link: "/api/Registry" }, 37 | { text: "View", link: "/api/View" }, 38 | { text: "Observer", link: "/api/Observer" }, 39 | { text: "Group", link: "/api/Group" }, 40 | { text: "Handle", link: "/api/Handle" }, 41 | { text: "Signal", link: "/api/Signal" }, 42 | { text: "Queue", link: "/api/Queue" }, 43 | { text: "Pool", link: "/api/Pool" }, 44 | { text: "Restrictions", link: "/api/restrictions" }, 45 | ] 46 | } 47 | ], 48 | 49 | "/tut/": [ 50 | { 51 | text: "Tutorials", 52 | items: [ 53 | { text: "Crash Course", link: "/tut/crash-course" }, 54 | { text: "Tags", link: "/tut/tags" }, 55 | { text: "Entity Type", link: "/tut/entity-type" }, 56 | { text: "Context", link: "/tut/context" }, 57 | { text: "Pools", link: "/tut/storage" }, 58 | { text: "Groups", link: "/tut/groups" }, 59 | { text: "Partitions", link: "/tut/partition" } 60 | ] 61 | } 62 | ], 63 | } 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /docs/api/Observer.md: -------------------------------------------------------------------------------- 1 | # Observer 2 | 3 | Tracks component changes, and can be cleared at will. 4 | 5 | ```lua 6 | type ecr.Observer 7 | ``` 8 | 9 | ## Methods 10 | 11 | ### exclude() 12 | 13 | Excludes entities with the given components from the observer. 14 | 15 | - **Type** 16 | 17 | ```lua 18 | function Observer:exclude(components: ...unknown): Observer 19 | ``` 20 | 21 | - **Details** 22 | 23 | Entities with *any* of the excluded components, will not be returned during 24 | iteration. 25 | 26 | -------------------------------------------------------------------------------- 27 | 28 | ### disconnect() 29 | 30 | Disconnects the observer, stopping any new changes from being tracked 31 | 32 | - **Type** 33 | 34 | ```lua 35 | function Observer:disconnect(): Observer 36 | ``` 37 | 38 | The observer must be empty before it is disconnected. 39 | 40 | ::: warning 41 | This must be called for the observer to be garbage collected. 42 | ::: 43 | 44 | -------------------------------------------------------------------------------- 45 | 46 | ### reconnect() 47 | 48 | Reconnects the Observer and allows it to track changes again. 49 | 50 | - **Type** 51 | 52 | ```lua 53 | function Observer:reconnect(): Observer 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- 57 | 58 | ### clear() 59 | 60 | Clears all stored entities. 61 | 62 | - **Type** 63 | 64 | ```lua 65 | function Observer:clear(): Observer 66 | ``` 67 | 68 | ## Iteration 69 | 70 | Iterates all entities in the observer. 71 | 72 | - **Type** 73 | 74 | ```lua 75 | for id: Entity, ...: T... in Observer do 76 | ``` 77 | 78 | - **Details** 79 | 80 | The observer will return entities that had any of the given components added 81 | or changed since it was last cleared and still have all given components at 82 | the time of iteration. 83 | 84 | The entity followed by the latest component values are returned. 85 | 86 | Components can be added, changed and removed during iteration. 87 | Components added during iteration are not returned for that iteration. 88 | 89 | Will automatically clear the observer on completion. `Observer:iter()` 90 | returns an iterator that will not automatically clear on completion. 91 | 92 | ::: warning 93 | Adding values during iteration can cause them to be cleared when iteration 94 | ends and they will never be iterated. 95 | ::: 96 | 97 | ::: warning 98 | During iteration, adding or removing components from entities not currently 99 | being iterated can invalidate the iterator. 100 | ::: 101 | 102 | ## Length 103 | 104 | Returns the amount of entities in the observer. 105 | 106 | - **Type** 107 | 108 | ```lua 109 | #Observer: number 110 | ``` 111 | -------------------------------------------------------------------------------- /docs/tut/groups.md: -------------------------------------------------------------------------------- 1 | # Groups 2 | 3 | Groups are a technique used to optimize iteration over a set of component types. 4 | Groups achieve perfect [SoA](https://en.wikipedia.org/wiki/AoS_and_SoA) for the 5 | best iteration performance at the cost of more expensive addition and removal of 6 | group-owned components. 7 | 8 | ## Creation 9 | 10 | Groups can be created as followed: 11 | 12 | ```lua 13 | registry:group(A, B, C) 14 | ``` 15 | 16 | This creates a new group with components `A`, `B` and `C`. 17 | These 3 components are now said to be *owned* by the group. 18 | 19 | Each component type can only be owned by a single group. The following code 20 | would result in an error: 21 | 22 | ```lua 23 | registry:group(A, B) 24 | registry:group(B, D) 25 | ``` 26 | 27 | This errors because 2 different groups cannot claim ownership of `B`. 28 | 29 | Groups do not have to be stored aside when they are created, the first time a 30 | group is created it is stored permanently inside the registry, future 31 | `registry:group()` calls will just return the same group for the same set of 32 | components. 33 | 34 | Once a group is created, it will ensure that its owned components are aligned 35 | with each other in memory whenever a component is added or removed from an 36 | entity. This means the only performance cost grouping imposes is the addition 37 | and removal of group-owned components on entities. Changing the values 38 | of already added group-owned components is unaffected. 39 | 40 | ## Usage 41 | 42 | A group is iterated in the same way as a view. 43 | 44 | ```lua 45 | for id, position, velocity in registry:group(Position, Velocity) do 46 | registry:set(id, Position, position + velocity*dt) 47 | end 48 | ``` 49 | 50 | Components can be added, changed and removed during iteration. 51 | 52 | The exact size of a group can also be read: 53 | 54 | ```lua 55 | local size = #registry:group(A, B) 56 | ``` 57 | 58 | ## Perfect SoA 59 | 60 | Since groups guarantee alignment of its owned components, they can be modified 61 | together directly. 62 | 63 | ```lua 64 | local Position = ecr.component(Vector3.new) 65 | local Velocity = ecr.component(Vector3.new) 66 | 67 | local function update_positions(dt: number) 68 | local n = #registry:group(Position, Velocity) 69 | local positions = registry:storage(Position).values 70 | local velocities = registry:storage(Velocity).values 71 | 72 | for i = 1, n do 73 | positions[i] += velocities[i] * dt 74 | end 75 | ``` 76 | 77 | It is important not to exceed the size of the group `n` or you will act on 78 | entities outside of the group, where the `i`th component values may no longer 79 | correspond with each other. 80 | 81 | ## Limitations 82 | 83 | - As mentioned before, each component type can only be owned by one group, 84 | groups cannot share components so you need to profile to determine where the 85 | most benefit is gained. 86 | 87 | - In a rare case, during the iteration of a view including a group-owned 88 | component, an entity joining the group because a group-owned component was 89 | added will invalidate the view iterator. 90 | 91 | This is due to how groups organise their components in memory. This can be 92 | avoided if you: 93 | 1. defer adding components to after iteration instead of during iteration. 94 | 2. know that adding those group-owned components will not cause the entity 95 | to enter the group. 96 | 97 | Views can detect and will error if the above rules are broken so you do not need 98 | to worry about it until it happens. 99 | 100 | ## When to group 101 | 102 | While views are fast, in certain situations like where a view contains many 103 | components, iteration of a view may be the bottleneck of a system. 104 | In such cases, by grouping components together, iteration becomes as fast as 105 | possible, removing the bottleneck. 106 | 107 | You should only use grouping when you have profiled and identified that the 108 | iteration of a view is a system bottleneck. 109 | -------------------------------------------------------------------------------- /docs/api/ecr.md: -------------------------------------------------------------------------------- 1 | # ECR 2 | 3 | ## Functions 4 | 5 | ### registry() 6 | 7 | Creates a new registry. 8 | 9 | - **Type** 10 | 11 | ```lua 12 | function ecr.registry(): Registry 13 | function ecr.registry(start: number, end: number): Registry 14 | ``` 15 | 16 | - **Details** 17 | 18 | If specified, the entity id space can be restricted. This is useful for 19 | ensuring ids are not in conflict when copying entities from one registry to 20 | another. 21 | 22 | -------------------------------------------------------------------------------- 23 | 24 | ### component() 25 | 26 | Creates a new component type. 27 | 28 | - **Type** 29 | 30 | ```lua 31 | function ecr.component(): unknown 32 | function ecr.component(constructor: () -> T): T 33 | ``` 34 | 35 | - **Details** 36 | 37 | Returns a unique id representing a new component type. 38 | 39 | Can be given a constructor which can be invoked when 40 | [`registry:add()`](Registry#add.md) or 41 | [`registry:patch()`](Registry#patch.md) is used. 42 | 43 | - **Example** 44 | 45 | No constructor. 46 | 47 | ```lua 48 | local Position = ecr.component() :: Vector3 49 | local Health = ecr.component() :: number 50 | ``` 51 | 52 | With constructor. 53 | 54 | ```lua 55 | local Position = ecr.component(Vector3.new) 56 | 57 | local Health = ecr.component(function() 58 | return { 59 | Current = 100, 60 | Max = 100 61 | } 62 | end) 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- 66 | 67 | ### tag() 68 | 69 | Creates a new tag component type. 70 | 71 | - **Type** 72 | 73 | ```lua 74 | function ecr.tag(): nil 75 | ``` 76 | 77 | - **Details** 78 | 79 | Returns a unique id representing a new component type. 80 | 81 | Tag components are a special type of component that have no value. 82 | 83 | -------------------------------------------------------------------------------- 84 | 85 | ### is_tag() 86 | 87 | Checks if a component type is a tag. 88 | 89 | - **Type** 90 | 91 | ```lua 92 | function ecr.is_tag(ctype: T): boolean 93 | ``` 94 | 95 | -------------------------------------------------------------------------------- 96 | 97 | ### queue() 98 | 99 | Creates a new queue. 100 | 101 | - **Type** 102 | 103 | ```lua 104 | function ecr.queue(): Queue<...unknown> 105 | function ecr.queue(signal: ISignal) -> Queue 106 | 107 | type ISignal = 108 | { connect: (ISignal, (T...) -> ()) -> () } | 109 | { Connect: (ISignal, (T...) -> ()) -> () } | 110 | ((T...) -> ()) -> () 111 | ``` 112 | 113 | - **Details** 114 | 115 | Can accept any signal object that matches the interface to 116 | automatically connect a callback where any arguments it is called with are 117 | added to the queue. 118 | 119 | -------------------------------------------------------------------------------- 120 | 121 | ### name() 122 | 123 | Associates names with components for debugging. 124 | 125 | - **Type** 126 | 127 | ```lua 128 | function ecr.name(names: T & Map) -> T 129 | ``` 130 | 131 | - **Details** 132 | 133 | Allows for errors raised to display the component name instead of its 134 | argument position. 135 | 136 | -------------------------------------------------------------------------------- 137 | 138 | ### buffer_to_array() 139 | 140 | Converts a buffer of entities into an array of entities. 141 | 142 | - **Type** 143 | 144 | ```lua 145 | function ecr.buffer_to_array(buf: buffer, size: number, arr: Array?) -> Array 146 | ``` 147 | 148 | - **Details** 149 | 150 | Copies the first `size` ids from the buffer to a target array. 151 | 152 | If no target array is given, one will be created. 153 | 154 | -------------------------------------------------------------------------------- 155 | 156 | ### array_to_buffer() 157 | 158 | Converts an array of entities into a buffer of entities. 159 | 160 | - **Type** 161 | 162 | ```lua 163 | function ecr.buffer_to_array(arr: Array, size: number, buf: buffer?) -> buffer 164 | ``` 165 | 166 | - **Details** 167 | 168 | Copies the first `size` ids from an array to a target buffer. 169 | 170 | If no target buffer is given, one will be created. 171 | 172 | -------------------------------------------------------------------------------- 173 | 174 | ### buffer_to_buffer() 175 | 176 | Copies a buffer of entities into a buffer of entities. 177 | 178 | - **Type** 179 | 180 | ```lua 181 | function ecr.buffer_to_buffer(source: buffer, size: number, target: buffer?) -> buffer 182 | ``` 183 | 184 | - **Details** 185 | 186 | Copies the first `size` ids from a buffer to a target buffer. 187 | 188 | If no target buffer is given, one will be created. 189 | 190 | ## Constants 191 | 192 | ### entity 193 | 194 | Special component type that represents entities in a registry. 195 | 196 | - **Type** 197 | 198 | ```lua 199 | ecr.entity: entity 200 | ``` 201 | 202 | -------------------------------------------------------------------------------- 203 | 204 | ### context 205 | 206 | The context entity id. 207 | 208 | - **Type** 209 | 210 | ```lua 211 | ecr.context: entity 212 | ``` 213 | 214 | -------------------------------------------------------------------------------- 215 | 216 | ### null 217 | 218 | The null entity id. 219 | 220 | - **Type** 221 | 222 | ```lua 223 | ecr.null: entity 224 | ``` 225 | 226 | - **Details** 227 | 228 | Attempting to use this entity with a registry will error. 229 | 230 | The following expression will always return `false`: 231 | 232 | ```lua 233 | registry:contains(ecr.null) 234 | ``` 235 | 236 | -------------------------------------------------------------------------------- 237 | 238 | ### id_size 239 | 240 | The size of the entity id in bytes. 241 | 242 | - **Type** 243 | 244 | ```lua 245 | ecr.id_size: number 246 | ``` 247 | -------------------------------------------------------------------------------- /docs/wiki/entity-identifier.md: -------------------------------------------------------------------------------- 1 | # Entity Identifiers 2 | 3 | In ECS, an "entity" is just an abstract thing to mentally associate components 4 | with. In implementation, an entity identifier is just a key used to access 5 | internal storage of components. 6 | 7 | They can be thought of as pointers but with extra safety checks. 8 | 9 | Ids must be able to uniquely identify entities, that is no two entities can 10 | exist with the same id at the same time. 11 | 12 | ## Simple Approach 13 | 14 | A simple implementation would be to keep track of a counter; each time a new 15 | entity is created, this counter is incremented and its value returned as a new 16 | id. There would be no concern for the counter overflowing either. Numbers in 17 | Luau are represented using 64-bit floats which can represent `2^64`, i.e. 18 | `1.8446744e+19` unique values, making it very difficult (impossible?) to 19 | overflow. 20 | 21 | This approach has drawbacks, however. Using these ids as an array index will 22 | result in steady-growth of memory usage as you create more and more ids as the 23 | arrays must grow to fit the largest id. You can solve this by using a hashmap 24 | instead which does not need to grow with the largest id, but this sacrifices 25 | speed to save memory usage, as hashing is much slower than array indexing. 26 | 27 | > todo: add benchmark 28 | 29 | ## ECR's Approach 30 | 31 | ECR splits ids into 2 parts using bit manipulation. 32 | The lower part is called the *key* and the upper part is called the *version*. 33 | 34 | The key part is used as an array index and can be reused when other entities are 35 | destroyed and their keys become free. 36 | 37 | The version part is used to keep the *overall* id unique. If we only used the 38 | key part as the entire id, we run into issues with storing ids of dead entities, 39 | and having new entities reusing the exact same id, resulting in being unable to 40 | differentiate between a dead entity and an alive entity. So each time a new 41 | entity reuses a key, the version part of the id is incremented so that the 42 | overall id remains unique. 43 | 44 | Example using 8-bit ids with 4 bits for the key and 4 bit for the version: 45 | 46 | ```lua 47 | 0001 0001 = 17 -- id with key = 1, version = 1 48 | 0010 0001 = 33 -- id with key = 1, version = 2 49 | 0001 0010 = 18 -- id with key = 2, version = 1 50 | ``` 51 | 52 | Notice how despite reusing a key, we can keep overall ids unique by separating 53 | the id into key and version parts. 54 | 55 | ### Tracking Unused Keys 56 | 57 | There are a few ways to keep track of unused keys for reuse. One way is to use a 58 | stack to track unused keys, pushing onto the stack when an entity is destroyed 59 | and popping from the stack when a new entity is created. ECR instead uses an 60 | array with a linked-list built into it. 61 | 62 | Each index of the array corresponds to a key `k` (`array[k]` corresponds to key 63 | `k`). 64 | 65 |
66 | Implementation 67 | 68 | --- 69 | 70 | Say we have a function `ID(key, version)` which creates an id using the given 71 | key and version. 72 | 73 | When key `k` is in use, `array[k] = ID(k, v)` where 74 | `v` is how many times that key has been used. 75 | 76 | When key `k` is not in use, `array[k] = ID(k_next, v)` where 77 | `k_next` is the *next key not in use* or `0`, that is, a pointer to the next 78 | free key or nothing, 79 | and `v` is still how many times key `k` has been used. 80 | 81 | We keep track of the head of the list using a variable `head` that holds the 82 | first unused key. If an unused key `k` is at the tail of the list, then 83 | `array[k] = ID(0, v)`. A key can never be `0` (arrays in Luau start at 1) so 84 | we use `0` to signify nothing. If the list is empty then `head` is set to `0`. 85 | 86 | When we want to create a new id, we check if `head ~= 0`. If `head == 0` then 87 | we just create a brand new key `#array + 1`. Otherwise, we know that we have an 88 | unused key `k` where `head = k`. We know that `array[k] = ID(next_k, v)`. 89 | So we set `head = next_k` and set `array[k] = ID(k, v)` and return `ID(k, v)`. 90 | 91 | When we want to destroy an id with key `k` we do `array[k] = ID(head, v+1)` 92 | and set `head = k`. 93 | 94 | Difficult to grasp at first but the advantage of this approach is that we can 95 | quickly see if a given key is in use or not instead of searching through a stack 96 | of unused keys, as well as only needing a single array index for each entity, 97 | since both key and version are stored at the one index. 98 | 99 | --- 100 | 101 |
102 | 103 | ### Bit Manipulation and Floats 104 | 105 | In Luau, numbers are 64-bit floating points. The format for these floats are 106 | specified in the IEEE-754 standard. Due to how these floats work, they can 107 | perfectly represent any 53-bit integer with no rounding issues. ECR splits the 108 | 53 bits `[19:0]` for the key and `[52:20]` for the version. 109 | 110 | Extracting the lower 20 bits to get the key is done with a single bitwise `AND` 111 | operation (`bit32.band(id, mask)`) with a 20-bit bitmask 112 | (a very fast operation). As the `bit32` library only works with the lower 32 113 | bits, we cannot access bits `[52:20]` directly. We must instead shift the upper 114 | bits down by 20 places via division by a power of 2. 115 | 116 | Creating an id and getting the key and version of an id looks like: 117 | 118 | ```lua 119 | local KEY_MASK = 2^20 - 1 120 | local LSHIFT = 2^20 121 | local RSHIFT = 1 / LSHIFT 122 | 123 | local function ID_CREATE(key, version) 124 | return version * LSHIFT + key 125 | end 126 | 127 | local function ID_KEY(id) 128 | return bit32.band(id, KEY_MASK) 129 | end 130 | 131 | local function ID_VERSION(id) 132 | return (id - ID_KEY(id)) * RSHIFT 133 | end 134 | ``` 135 | 136 | Some things to note are: 137 | 138 | - instead of diving by a power of 2, we multiply by the reciprocal of a power of 139 | 2 as multiplication is faster than division. 140 | 141 | - to extract the version we first do `id - ID_KEY(id)` before shifting, so that 142 | the lower bits become all `0`s. If we skip this step, then the key part would 143 | become a fraction which introduces floating point rounding errors, making it 144 | so we can no longer recover the version accurately. 145 | 146 | Using 20 bits for the key part gives us `2^20 - 1 = 1,048,575` keys (we can't 147 | use 0) which is why that is the limit of the amount of entities you can create 148 | at once. 149 | 150 | Using 33 bits for the version part gives us `2^33 - 1 = 8,589,934,592` versions 151 | (we start versions at 1) so each key can be reused over 8 billion times. 152 | 153 | In the unlikely case that a key has been reused that many times, the key is 154 | permanently deprecated and will not be reused again (as simple as not adding 155 | the key back to the head of the list when it is removed). 156 | -------------------------------------------------------------------------------- /docs/wiki/component-storage.md: -------------------------------------------------------------------------------- 1 | # Component Storage 2 | 3 | The choice of how components are stored in memory is what defines the 4 | performance of an ECS. There are many different ways to do this, with two 5 | popular approaches being: 6 | 7 | - Archetype based storage 8 | - Sparse set based storage 9 | 10 | Archetype based storages are generally known to have fast iteration in 11 | combinations of components while Sparse set based ECSs are known for fast adding 12 | and removing of components. 13 | 14 | ECR uses a sparse set approach which will now be covered in detail. 15 | 16 | ## The Sparse set 17 | 18 | Firstly, ignore the ECS side of things and just try to understand the sparse set 19 | on its own. 20 | 21 | The sparse set is a datastructure used to store a set of integers. 22 | 23 | The time complexities of a sparse set are as such: 24 | 25 | - Insertion `O(1)` 26 | - Removal `O(1)` 27 | - Lookup `O(1)` 28 | - Iteration `O(n)` where `n` is the amount of elements in the set. 29 | 30 | A sparse set is composed of 2 arrays: 31 | 32 | - Sparse arrray. 33 | 34 | Maps an integer to a dense array index. 35 | 36 | - Dense array (contrary to the name). 37 | 38 | Stores all integers in the set without any spaces. 39 | 40 | An implementation in Luau looks like so: 41 | 42 | ```lua 43 | type Set = { 44 | size: number 45 | sparse: Array 46 | dense: Array 47 | } 48 | 49 | local function has(self: Set, k: number): boolean 50 | return self.sparse[k] ~= nil 51 | end 52 | 53 | local function insert(self: Set, k: number) 54 | if not self.sparse[k] then -- do nothing if k is already in set 55 | local n = self.size + 1; self.size = n 56 | self.sparse[k] = n 57 | self.dense[n] = k 58 | end 59 | end 60 | 61 | local function remove(self: Set, k: number) 62 | local i = self.sparse[k] 63 | if i then -- do nothing if k is not in set 64 | local n = self.size; self.size = n - 1 65 | 66 | -- to remove from a sparse set, 67 | -- we move the last element in the dense array 68 | -- to the position of k in the dense array 69 | -- and update the sparse array to show the new 70 | -- position of the moved element and remove k 71 | 72 | local last = self.dense[n] 73 | 74 | self.dense[i] = last 75 | self.dense[n] = nil 76 | 77 | self.sparse[last] = i 78 | self.sparse[k] = nil 79 | end 80 | end 81 | ``` 82 | 83 | The main drawback of a sparse set is that the sparse array must grow to 84 | accomodate the largest key in the set. This can cause wasted memory usage 85 | when the set contains a small amount of integers with a large value. 86 | 87 | This can be mitigated using techniques such as paging, but Luau automatically 88 | handles sparse arrays for us by converting them into a hashmap when it is 89 | sufficiently sparse to save memory. While a hashmap normally isn't the best 90 | solution for this, it is better than any custom Luau solution since this is 91 | built into Luau and is executed natively. 92 | 93 | ## Pools 94 | 95 | Now, back to the ECS. The pool datastructure in ECR is a modified sparse set. 96 | 97 | ```lua 98 | type Pool = { 99 | size: number, 100 | map: Array, -- sparse array 101 | entities: Array, -- dense array 102 | values: Array -- second dense array 103 | } 104 | ``` 105 | 106 | This structure uses a second dense array that is kept sorted the same as 107 | the first. The `map` array maps an id to an internal index used to access 108 | a dense array of ids and a dense array of corresponding component values. 109 | 110 | When adding and removing ids, whatever operation done to the `entities` array is 111 | also done to the `values` array so that `entities[i]` corresponds to `values[i]`. 112 | 113 | The way in which `map` maps an id to an internal index is by extracting a key 114 | from the id and using that key as an index for the `map` array. 115 | Refer to [here](entity-identifier.md) for specifics on keys and ids. 116 | 117 | In an ECS, each component type has its own pool. Given the properties of a 118 | sparse set, this makes adding, removing, changing and checking for components 119 | very fast operations. We have the best possible random-access and 120 | sequential-access speeds because of the sparse and dense arrays. However, 121 | because component pools are independent from each other, this approach suffers 122 | when querying for multiple component types. 123 | 124 | ### Single component query 125 | 126 | A query for a single component type is the best case scenario. To do so, you 127 | simply iterate over the `entities` and `values` array of a pool as a straight 128 | array with no cache misses. 129 | 130 | ### Multiple component query 131 | 132 | Multiple component queries, while still fast, become slower the more components 133 | that are involved in the query. To do so, you pick the smallest pool to iterate 134 | along its entities (as an entity must contain every component in the query, we 135 | know that every entity we need still exists in this pool so we iterate that to 136 | reduce the amount of random checking we must do), while doing lookups in all 137 | other pools to check if the entity has all other components, skipping that 138 | entity if it does not. While this does introduce cache misses, it isn't as bad 139 | as it sounds because of the very fast random-access property of sparse sets. 140 | 141 | This drawback can also be mitigated using a technique called *grouping* which I 142 | won't get into now. 143 | 144 | Another thing to note is that while iteration of multiple components is 145 | a large argument against the sparse-set ECS, more complex queries naturally 146 | run more complex code in the loop body, making the overhead of iterating 147 | multiple components less significant in many cases. In the less likely cases 148 | where the iteration itself is the bottleneck, grouping can be used to eliminate 149 | this entirely. 150 | 151 | ## Stale references 152 | 153 | This section assumes familiarity with [entity ids](entity-identifier.md). 154 | 155 | Consider the case where we have ids `ID(k=1, v=1)` and `ID(k=1, v=2)`. Both 156 | of these ids would refer to the same internal index in the pool because the 157 | pool only uses the key part and completely disregards the version. This leads to 158 | issues such as destroyed ids being able to access data for new ids. 159 | 160 | This can be prevented by checking the `entities` array to get the version when 161 | performing a lookup. This however turns a lookup operation from a single array 162 | index to two array indexes, which increases the chance of cache miss and harms 163 | performance. 164 | 165 | ECR instead stores the id version inside the `map` array as well. 166 | Since the `map` array just stores the internal index for the dense arrays, 167 | it is known that this internal index will never exceed the largest id key. This 168 | is taken advantage of by also using the upper bits to store the id version. 169 | This way we can get the internal index *and* the version using a single array 170 | index. Then it is just a matter of checking if the version is equal to the 171 | version of the id we are performing a lookup for. 172 | -------------------------------------------------------------------------------- /docs/tut/crash-course.md: -------------------------------------------------------------------------------- 1 | # Crash Course 2 | 3 | ECR is a sparse-set based ECS library heavily inspired by 4 | [EnTT](https://github.com/skypjack/entt). 5 | 6 | This is a brief introduction on how to use the library and some of the concepts 7 | involved with it. 8 | 9 | ## Registry 10 | 11 | The registry, or often called a *world*, is a container for entities and their 12 | components. 13 | 14 | ```lua 15 | local registry = ecr.registry() 16 | ``` 17 | 18 | ## Entities 19 | 20 | An entity represents an object in the world and is referenced using a unique id. 21 | 22 | ```lua 23 | local id: ecr.entity = registry:create() 24 | registry:contains(id) -- true 25 | 26 | registry:destroy(id) 27 | registry:contains(id) -- false 28 | ``` 29 | 30 | Entities can be freely created and destroyed. 31 | 32 | ## Components 33 | 34 | A component is data that can be added to entities. 35 | 36 | There is the *component type*, which represents a type of data, and there is 37 | the *component value*, which is a value for a type added to an entity. 38 | 39 | Component types are created by `ecr.component()`, which returns an id 40 | representing that type. This can be typecasted to the type of value it 41 | represents. 42 | 43 | ```lua 44 | local Name = ecr.component() :: string 45 | local Health = ecr.component() :: number 46 | ``` 47 | 48 | Entities can have any amount of components added to or removed from them, 49 | whenever. Like tables, they can have any amount of key-value pairs, where the 50 | component type is the key, and the component value is the value. Entities 51 | initially have no components when created, and will have all their components 52 | removed when destroyed. 53 | 54 | ```lua 55 | registry:set(id, Health, 100) -- adds a new component with a value of 100 56 | registry:get(id, Health) -- 100 57 | 58 | registry:remove(id, Health) -- removes the component 59 | registry:has(id, Health) -- false 60 | ``` 61 | 62 | Component values cannot be `nil`, components should be removed instead. 63 | 64 | ## Views 65 | 66 | You can get all entities that have a specific set of components by using a view. 67 | 68 | Views can include any amounts of components. A view only returns entities that 69 | have *at least* all the components included. 70 | 71 | ```lua 72 | for id, position, velocity in registry:view(Position, Velocity) do 73 | world:set(id, Position, position + velocity * 1/60) 74 | end 75 | ``` 76 | 77 | You can add or remove components and create or destroy entities during 78 | iteration. 79 | 80 | Components added or entities created during iteration will not be returned 81 | during that iteration. 82 | 83 | You can also exclude component types from views. Any entities that have an 84 | excluded type will not be included in the view. 85 | 86 | ```lua 87 | local view = registry:view(A):exclude(B) 88 | ``` 89 | 90 | Views are cheap to create and do not store their own state, so they do not need 91 | to be stored aside, and can be created on the fly as needed. 92 | 93 | ## Signals 94 | 95 | The registry contains three different signals for when a component type is 96 | added, changed or removed for any entity. 97 | 98 | ```lua 99 | registry:on_add(type):connect(listener) 100 | registry:on_change(type):connect(listener) 101 | registry:on_remove(type):connect(listener) 102 | ``` 103 | 104 | All three listeners are called with: 105 | 106 | 1. The entity being acted on. 107 | 2. The new component value (always `nil` in the case of `on_remove`). 108 | 109 | `on_add` is fired *after* the component is added. 110 | 111 | `on_change` and `on_remove` is fired *before* the component is changed or 112 | removed, so you can still retrieve the old value if needed. 113 | 114 | You cannot modify the registry within a listener, they should only be used to 115 | help track entities or clean up values. 116 | 117 | ## Observers 118 | 119 | An observer is similar to a view, except it only returns entities whose 120 | components have been added or changed and still have those components at the 121 | time of iteration. 122 | 123 | An observer can be created using `Registry:track()`. 124 | 125 | ```lua 126 | local observer = registry:track(Position, Model) 127 | 128 | return function() 129 | for entity, position, model in observer do 130 | print("changed: ", position, model) 131 | end 132 | end 133 | ``` 134 | 135 | After iterating, the observer automatically clears so that only fresh changes 136 | are iterated. 137 | 138 | Observers provide a concise way to act only on a subset of entities that had 139 | components updated since the last time a system ran. 140 | 141 | Unlike a view, observers do store their own state, and must be stored aside to 142 | keep track over time. 143 | 144 | ## Example Usage 145 | 146 | All component types are defined in a single file to keep things organised. All 147 | component types must also be defined before the registry using them is created. 148 | 149 | ::: code-group 150 | 151 | ```lua [cts.luau] 152 | local ecr = require(ecr) 153 | 154 | local cts = { 155 | Health = ecr.component() :: number, 156 | Poisoned = ecr.component() :: number 157 | } 158 | 159 | ecr.name(cts) 160 | 161 | return cts 162 | ``` 163 | 164 | ::: 165 | 166 | `ecr.name()` can be used to associate names with components, for clearer error 167 | messages when debugging. 168 | 169 | The library doesn't have any bult-in support for systems, the user is free to 170 | do this however they please. 171 | 172 | Examples using plain functions: 173 | 174 | ::: code-group 175 | 176 | ```lua [deal_poison_damage.luau] 177 | local cts = require(cts) 178 | 179 | return function(world: ecr.Registry, dt: number) 180 | for id, health in world:view(cts.Health, cts.Poisoned) do 181 | world:set(id, health, health - 10 * dt) 182 | end 183 | end 184 | ``` 185 | 186 | ```lua [reduce_poison_timer.luau] 187 | local cts = require(cts) 188 | 189 | return function(world: ecr.Registry, dt: number) 190 | for id, time in world:view(cts.Poisoned) do 191 | local new_time = time - dt 192 | 193 | if new_time <= 0 then 194 | world:remove(id, cts.Poisoned) 195 | else 196 | world:set(id, cts.Poisoned, new_time) 197 | end 198 | end 199 | end 200 | ``` 201 | 202 | ```lua [destroy_dead.luau] 203 | local cts = require(cts) 204 | 205 | return function(world: ecr.Registry) 206 | for id, health in world:view(cts.Health) do 207 | if health <= 0 then 208 | world:destroy(id) 209 | end 210 | end 211 | end 212 | ``` 213 | 214 | ::: 215 | 216 | ::: code-group 217 | 218 | ```lua [main.luau] 219 | local ecr = require(ecr) 220 | local cts = require(cts) 221 | 222 | local function loop(systems: {(ecr.Registry, number) -> ()}, world: ecr.Registry, dt: number) 223 | for _, system in systems do 224 | system(world, dt) 225 | end 226 | end 227 | 228 | local systems = { 229 | require(deal_poison_damage), 230 | require(reduce_poison_timer), 231 | require(destroy_dead) 232 | } 233 | 234 | local world = ecr.registry() 235 | 236 | for i = 1, 10 do 237 | local id = world:create() 238 | world:set(id, cts.Health, 100) 239 | world:set(id, cts.Poisoned, math.random(3, 5)) -- poison randomly for 3-5 seconds 240 | end 241 | 242 | while true do 243 | loop(systems, world, 1/60) 244 | end 245 | ``` 246 | 247 | ::: 248 | 249 | ## End 250 | 251 | At this point, the main concepts and features of ECR have been covered. 252 | You can read other guides on more advanced usage or view the API for more 253 | details. 254 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | --- 8 | 9 | ## Unreleased 10 | 11 | ### Improved 12 | 13 | - Iteration of 3 and 4 component views 3x faster. 14 | 15 | ### Fixed 16 | 17 | - Method `Registry:create(id)` causing an out of bounds buffer access if key was already in use with a different version. 18 | 19 | --- 20 | 21 | ## [0.9.0] - 2025-03-02 22 | 23 | ### Added 24 | 25 | - Method `Registry:find()`. 26 | - Method `Registry:insert()`. 27 | - Method `Registry:copy()`. 28 | - Overload `ecr.queue()` to accept a function connector. 29 | - Overload `ecr.registry()` to restrict entity range. 30 | - Function `ecr.buffer_to_buffer()`. 31 | 32 | ### Changed 33 | 34 | - Method `Registry:patch()` will return the new value. 35 | 36 | ### Removed 37 | 38 | - Method `Observer:persist()`. 39 | - Method `View:use()`. 40 | 41 | ### Fixed 42 | 43 | - Single type view not excluding properly with multiple exclude types. 44 | - `Connection:disconnect()` disconnecting the wrong listener. 45 | 46 | --- 47 | 48 | ## [0.8.0] - 2023-12-05 49 | 50 | ### Added 51 | 52 | - Constant `ecr.context`. 53 | - Constant `id_size`. 54 | - Function `ecr.array_to_buffer()`. 55 | - Function `ecr.buffer_to_array()`. 56 | - Overload for `Registry:storage()` to get an iterator for all storages. 57 | 58 | ### Changed 59 | 60 | - All components must be defined before the registry using them is created. 61 | - The context entity does not exist until `Registry:context()` is first called. 62 | - The context entity can now be destroyed. 63 | - Observers are now empty when first created. 64 | - Entity id size reduced from 8 bytes to 4 bytes. 65 | - Max entities that can exist at once is now `2^16-1` (`65,535`). 66 | 67 | - Renamed `ecr.Entity` type alias to `ecr.entity`. 68 | - Renamed `Registry:added()` to `Registry:on_add()`. 69 | - Renamed `Registry:changed()` to `Registry:on_change()` 70 | - Renamed `Registry:removing()` to `Registry:on_remove()`. 71 | - Renamed `Registry:orphaned()` to `Registry:has_none()`. 72 | 73 | - Property `Pool.entities` type is now a `buffer`. 74 | 75 | - Method `Registry:set()` can now also add tags. 76 | - Method `Observer:disconnect()` can only be called on empty observers. 77 | 78 | - Signal `Registry:on_change()` now fires *before* the component is changed. 79 | - New value still given as argument, but now can retrieve old value with `Registry:get()`. 80 | 81 | ### Removed 82 | 83 | - Method `Registry:version()`. 84 | - Method `Registry:current()`. 85 | 86 | ### Improved 87 | 88 | - Large refactor to use the new Luau `buffer` datatype. Up to 2x faster and 2-4x 89 | less memory usage across the board. 90 | 91 | --- 92 | 93 | ## [0.7.0] - 2023-07-20 94 | 95 | ### Added 96 | 97 | - `Connection:reconnect()`. 98 | - `ecr.entity` which is a built-in component that can be used to access a 99 | dedicated entity pool: 100 | - Exclude-only views `registry:view(ecr.entity):exclude(...)`. 101 | - Signals for creation and destruction of entities `registry:on_add(ecr.entity):connect()`. 102 | - Direct access to array of all entities in the registry `registry:storage(ecr.entity)`. 103 | - `ecr.is_tag()` to check if a given component is a tag type. 104 | - `Registry:context()` to store components not specific to an entity. 105 | - Methods for lower level access to pools. 106 | 107 | ### Changed 108 | 109 | - Using `Registry:set()` to set `nil` values will now error. 110 | - Use `Registry:remove()` instead. 111 | - `Registry:handle()` calls are now memoized, passing the same ids will return 112 | the same handle objects. 113 | 114 | ### Removed 115 | 116 | - Operations `#Registry` and `for id in Registry do`. 117 | - Replaced by entity storage. 118 | 119 | --- 120 | 121 | ## [0.6.1] - 2023-07-04 122 | 123 | ### Changed 124 | 125 | - `Registry:patch()` will invoke the component constructor if the entity does 126 | not have the given component. 127 | 128 | ### Fixed 129 | 130 | - `Registry:orphaned()` not erroring with destroyed id. 131 | - `ecr.queue()` clearing early. 132 | 133 | --- 134 | 135 | ## [0.6.0] - 2023-06-27 136 | 137 | ### Added 138 | 139 | - Check for iterator invalidation occuring with group misusage. 140 | - Function `ecr.tag()` to create valueless components. 141 | - Function `ecr.queue()` and the `Queue` class. 142 | - Function `ecr.name()` for associating names with component types. 143 | - Method `Registry:handle()` and the `Handle` class. 144 | - Method `Registry:try_get()`. 145 | - Method `Observer:persist()`. 146 | - Length operator for registry `#Registry`. 147 | - Iteration over registry `for id in Registry do`. 148 | 149 | ### Changed 150 | 151 | - Observers will automatically clear themselves after iteration by default. 152 | - Call `Observer:persist()` to stop this. 153 | - `Registry:get()` will now error if the entity does not have every component. 154 | 155 | ### Removed 156 | 157 | - Method `Registry:version()`. 158 | - Method `Registry:current()`. 159 | - Method `Registry:entities()`. 160 | - Method `Registry:size()`. 161 | 162 | --- 163 | 164 | ## [0.5.0] - 2023-05-14 165 | 166 | ### Changed 167 | 168 | - Connecting or disconnecting an already connected or disconnected observer will no longer error. 169 | - Excluding an already excluded component with `View:exclude()` and `Observer:exclude()` no longer errors. 170 | - Method `Registry:add()` will do nothing if the entity already has the component. 171 | - Method `Registry:orphan()` renamed to `Registry:orphaned()`. 172 | - Method `Registry:valid()` renamed to `Registry:contains()`. 173 | - Method `Registry:create()` is now guaranteed to always return unique identifiers. 174 | - Using invalid entities no longer causes undefined behavior in any method and instead errors. 175 | - `Registry:track()` will now track all components passed, not just the first one. 176 | 177 | ### Removed 178 | 179 | - Methods: 180 | - `View:each()` 181 | - `Observer:each()` 182 | - `Group:each()` 183 | 184 | ## Fixed 185 | 186 | - Undefined behavior sometimes occuring when removing grouped components. 187 | - Observers not returning up-to-date values when changing a component while it is disconnected. 188 | 189 | --- 190 | 191 | ## [0.4.0] - 2023-01-26 192 | 193 | ### Added 194 | 195 | - Component grouping and `Registry:group()`. 196 | - Method `View:use()`. 197 | - Constant `ecr.null`. 198 | 199 | ### Removed 200 | 201 | - Method `View:include()` and `Observer:include()`. 202 | 203 | ### Fixed 204 | 205 | - `Registry:add()` not firing `Registry:on_add()` signals. 206 | - Mismatch between argument list and values returned in multi-typed observers. 207 | - Observers not garbage collecting after calling `Observer:disconnect()`. 208 | 209 | ### Improved 210 | 211 | - Connection firing speed by ~70%. 212 | 213 | --- 214 | 215 | ## [0.3.0] - 2023-01-09 216 | 217 | ### Added 218 | 219 | - Overload for `Registry:create()` to create an entity with a given identifier. 220 | 221 | ### Changed 222 | 223 | - Registry signals no longer pass the registry as the first argument to listeners. 224 | - Observers no longer track entities with removed components. 225 | - Method `Registry:entities()` now creates and returns an array of only valid entities. 226 | - Function `ecr.registry()` can no longer pre-allocate memory. 227 | 228 | ### Removed 229 | 230 | - Method `Registry:capacity()`. 231 | 232 | ### Improved 233 | 234 | - Double-type view iteration speed by ~100%. 235 | 236 | --- 237 | 238 | ## [0.2.0] - 2022-12-08 239 | 240 | ### Added 241 | 242 | - Method `View:include()` and `Observer:include()`. 243 | - Method `Registry:patch()`. 244 | - Method `Registry:add()` and optional default parameter for `ecr.component()`. 245 | 246 | ### Changed 247 | 248 | - Behavior `for ... in View do` now behaves the same as `for ... in View:each() do`. 249 | - Signal diconnect API (Signal now returns a connection object to call disconnect on). 250 | 251 | ### Improved 252 | 253 | - Entity creation and release speed by ~100%. 254 | - Multi-type view iteration speed by ~60%. 255 | 256 | ## [0.1.0] - 2022-11-16 257 | 258 | - Initial release. 259 | -------------------------------------------------------------------------------- /docs/api/Registry.md: -------------------------------------------------------------------------------- 1 | # Registry 2 | 3 | Container for entities and components. 4 | 5 | ```lua 6 | type ecr.Registry 7 | ``` 8 | 9 | ## Methods 10 | 11 | ::: warning 12 | There are certain [restrictions](restrictions) with what you can do with the 13 | registry that you should be aware of. 14 | ::: 15 | 16 | ### create() 17 | 18 | Creates a new entity and returns the entity id. 19 | 20 | - **Type** 21 | 22 | ```lua 23 | function Registry:create(): entity 24 | function Registry:create(id: entity): entity 25 | 26 | type entity = ecr.entity 27 | ``` 28 | 29 | - **Details** 30 | 31 | An entity can be created using a specific id that was created by another 32 | registry or previously by the same registry. 33 | 34 | A previously used but now unused id may be reused every `32,000` creations. 35 | 36 | ::: warning 37 | Be wary of storing ids of destroyed entities for long periods of time or 38 | they may eventually refer to a newly created entity. 39 | ::: 40 | 41 | ::: warning 42 | The total amount of entities in a registry at any given time *cannot* 43 | exceed `65,535`. Attempting to exceed this limit will throw an error. 44 | ::: 45 | 46 | -------------------------------------------------------------------------------- 47 | 48 | ### destroy() 49 | 50 | Removes the entity from the registry and removes all of its components. 51 | 52 | - **Type** 53 | 54 | ```lua 55 | function Registry:destroy(id: entity) 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- 59 | 60 | ### contains() 61 | 62 | Checks if the given entity exists in the registry. 63 | 64 | - **Type** 65 | 66 | ```lua 67 | function Registry:contains(id: entity): boolean 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- 71 | 72 | ### add() 73 | 74 | Adds all given components to an entity. 75 | 76 | - **Type** 77 | 78 | ```lua 79 | function Registry:add(id: entity, components: T...) 80 | ``` 81 | 82 | - **Details** 83 | 84 | Adds the given components to the entity by using each component 85 | constructor or no value at all if the component is a tag type. 86 | 87 | Adding a component to an entity that already has the component will do 88 | nothing. 89 | 90 | -------------------------------------------------------------------------------- 91 | 92 | ### set() 93 | 94 | Adds or changes an entity's component. 95 | 96 | - **Type** 97 | 98 | ```lua 99 | function Registry:set(id: entity, component: T, value: T) 100 | ``` 101 | 102 | - **Details** 103 | 104 | Adds the component to the entity with the given value if the entity does not 105 | already have the component. 106 | 107 | Changes the component value for the given entity if the entity already has 108 | the component. 109 | 110 | -------------------------------------------------------------------------------- 111 | 112 | ### patch() 113 | 114 | Updates an entity's component. 115 | 116 | - **Type** 117 | 118 | ```lua 119 | function Registry:patch(id: entity, component: T, patcher: (T) -> T): T 120 | ``` 121 | 122 | - **Details** 123 | 124 | Takes a callback which is given the current component value as the only 125 | argument. The value returned by the callback is then set as the new value. 126 | 127 | If there is a constructor defined for the given component and the entity 128 | does not have the component, the constructor will be called and the returned 129 | value passed into the callback. 130 | 131 | - **Example** 132 | 133 | ```lua 134 | registry:patch(entity, Health, function(health) 135 | return health - 10 136 | end) 137 | ``` 138 | 139 | -------------------------------------------------------------------------------- 140 | 141 | ### has() 142 | 143 | Checks if an entity has all of the given components. 144 | 145 | - **Type** 146 | 147 | ```lua 148 | function Registry:has(id: entity, components: T...): boolean 149 | ``` 150 | 151 | - **Details** 152 | 153 | Will return `true` only if the entity has *every* component given. 154 | 155 | -------------------------------------------------------------------------------- 156 | 157 | ### get() 158 | 159 | Gets an entity's component values. 160 | 161 | - **Type** 162 | 163 | ```lua 164 | function Registry:get(id: entity, components: T...): T... 165 | ``` 166 | 167 | - **Details** 168 | 169 | Will error if the entity does not have a component. 170 | 171 | -------------------------------------------------------------------------------- 172 | 173 | ### try_get() 174 | 175 | Gets an entity's component value. 176 | 177 | - **Type** 178 | 179 | ```lua 180 | function Registry:try_get(id: entity, components: T): T? 181 | ``` 182 | 183 | - **Details** 184 | 185 | Will return `nil` if the entity does not have a component. 186 | 187 | -------------------------------------------------------------------------------- 188 | 189 | ### remove() 190 | 191 | Removes the given components from an entity. 192 | 193 | - **Type** 194 | 195 | ```lua 196 | function Registry:remove(id: entity, components: T...) 197 | ``` 198 | 199 | - **Details** 200 | 201 | Will do nothing if the entity does not have a component. 202 | 203 | -------------------------------------------------------------------------------- 204 | 205 | ### clear() 206 | 207 | Removes all entities and components from the registry. 208 | 209 | - **Type** 210 | 211 | ```lua 212 | function Registry:clear(components: T...) 213 | ``` 214 | 215 | - **Details** 216 | 217 | If components are given, removes all given components from all entities 218 | without destroying the entities. 219 | 220 | If no components are given, then all entities in the registry will be 221 | destroyed. 222 | 223 | -------------------------------------------------------------------------------- 224 | 225 | ### find() 226 | 227 | Returns the first entity found that has a component matching the given value. 228 | 229 | - **Type** 230 | 231 | ```lua 232 | function Registry:find(component: T, value: T): entity? 233 | ``` 234 | 235 | - **Details** 236 | 237 | This is a linear search. 238 | 239 | -------------------------------------------------------------------------------- 240 | 241 | ### copy() 242 | 243 | Copies the values of a component and pastes it into another component 244 | 245 | - **Type** 246 | 247 | ```lua 248 | function Registry:copy(copy: T, paste: T) 249 | ``` 250 | 251 | - **Details** 252 | 253 | This removes any entities that don't have a value in the copied component 254 | from the pasted component. It does not fire signals when called. 255 | 256 | -------------------------------------------------------------------------------- 257 | 258 | ### view() 259 | 260 | Creates a [`view`](View) with the given component types. 261 | 262 | - **Type** 263 | 264 | ```lua 265 | function Registry:view(components: T...): View 266 | ``` 267 | 268 | -------------------------------------------------------------------------------- 269 | 270 | ### track() 271 | 272 | Creates an [`observer`](Observer) with the given component types. 273 | 274 | - **Type** 275 | 276 | ```lua 277 | function Registry:track(...: T...): Observer 278 | ``` 279 | 280 | -------------------------------------------------------------------------------- 281 | 282 | ### group() 283 | 284 | Creates a [`group`](Group.md) with the given component types. 285 | 286 | - **Type** 287 | 288 | ```lua 289 | function Registry:group(...: T...): Group 290 | ``` 291 | 292 | - **Details** 293 | 294 | Rearranges the internal storage of components for better iteration 295 | performance when iterated together. 296 | 297 | Groups must be mutually exclusive, i.e. each component type can only belong 298 | to a single group. 299 | 300 | ::: warning 301 | This method introduces [restrictions](../tuts/groups.md#limitations) on 302 | adding components during views. 303 | ::: 304 | 305 | -------------------------------------------------------------------------------- 306 | 307 | ### on_add() 308 | 309 | Returns a [signal](Signal) which is fired whenever the given component type is 310 | added to an entity. 311 | 312 | - **Type** 313 | 314 | ```lua 315 | function Registry:on_add(component: T): Signal 316 | ``` 317 | 318 | The signal is fired *after* the component is changed. 319 | 320 | The listener is called with the entity and new component value. 321 | 322 | ::: warning 323 | The registry cannot be modified within a listener. 324 | ::: 325 | 326 | -------------------------------------------------------------------------------- 327 | 328 | ### on_change() 329 | 330 | Returns a [signal](Signal) which is fired whenever the given component type is 331 | changed for an entity. 332 | 333 | - **Type** 334 | 335 | ```lua 336 | function Registry:on_change(component: T): Signal 337 | ``` 338 | 339 | The signal is fired *before* the component is changed. 340 | 341 | The listener is called with the entity and new component value. 342 | 343 | ::: warning 344 | The registry cannot be modified within a listener. 345 | ::: 346 | 347 | -------------------------------------------------------------------------------- 348 | 349 | ### on_remove() 350 | 351 | Returns a [signal](Signal) which is fired whenever the given component is 352 | removed from an entity. 353 | 354 | - **Type** 355 | 356 | ```lua 357 | function Registry:on_remove(component: T): Signal 358 | ``` 359 | 360 | - **Details** 361 | 362 | The signal is fired *before* the component is removed. 363 | 364 | The listener is called with the entity. 365 | 366 | ::: warning 367 | The registry cannot be modified within a listener. 368 | ::: 369 | 370 | -------------------------------------------------------------------------------- 371 | 372 | ### handle() 373 | 374 | Returns a [handle](Handle) to an entity. 375 | 376 | - **Type** 377 | 378 | ```lua 379 | function Registry:handle(id: entity?): Handle 380 | ``` 381 | 382 | - **Details** 383 | 384 | If no entity is given then a new one is created. 385 | 386 | Handles are cached so that `registry:handle(id) == registry:handle(id)` is 387 | always true. 388 | 389 | -------------------------------------------------------------------------------- 390 | 391 | ### context() 392 | 393 | Returns a [handle](Handle) to the context entity. 394 | 395 | - **Type** 396 | 397 | ```lua 398 | function Registry:context(): Handle 399 | ``` 400 | 401 | - **Details** 402 | 403 | Will automatically create the context entity if it does not already exist. 404 | 405 | -------------------------------------------------------------------------------- 406 | 407 | ### storage() 408 | 409 | Returns the [pool](Pool) for a given component type. 410 | 411 | - **Type** 412 | 413 | ```lua 414 | function Registry:storage(component: T): Pool 415 | function Registry:storage(): () -> (unknown, Pool) 416 | ``` 417 | 418 | - **Details** 419 | 420 | If called with no arguments, returns an iterator to get all component types 421 | and their corresponding pool in the registry. 422 | 423 | -------------------------------------------------------------------------------- 424 | 425 | ### has_none() 426 | 427 | Checks if the given entity has no components. 428 | 429 | - **Type** 430 | 431 | ```lua 432 | function Registry:has_none(id: entity): boolean 433 | ``` 434 | 435 | -------------------------------------------------------------------------------- 436 | 437 | ### release() 438 | 439 | Removes the entity from the registry. 440 | 441 | - **Type** 442 | 443 | ```lua 444 | function Registry:release(id: entity) 445 | ``` 446 | 447 | - **Details** 448 | 449 | ::: danger 450 | This method does not remove any of the entity's components. Using this 451 | method on an entity that still has components is *undefined behavior*. 452 | ::: 453 | -------------------------------------------------------------------------------- /test/testkit.luau: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- testkit.luau 3 | -- v0.7.3 4 | -------------------------------------------------------------------------------- 5 | 6 | local color = { 7 | white_underline = function(s: string) 8 | return `\27[1;4m{s}\27[0m` 9 | end, 10 | 11 | white = function(s: string) 12 | return `\27[37;1m{s}\27[0m` 13 | end, 14 | 15 | green = function(s: string) 16 | return `\27[32;1m{s}\27[0m` 17 | end, 18 | 19 | red = function(s: string) 20 | return `\27[31;1m{s}\27[0m` 21 | end, 22 | 23 | yellow = function(s: string) 24 | return `\27[33;1m{s}\27[0m` 25 | end, 26 | 27 | red_highlight = function(s: string) 28 | return `\27[41;1;30m{s}\27[0m` 29 | end, 30 | 31 | green_highlight = function(s: string) 32 | return `\27[42;1;30m{s}\27[0m` 33 | end, 34 | 35 | gray = function(s: string) 36 | return `\27[30;1m{s}\27[0m` 37 | end, 38 | } 39 | 40 | local function convert_units(unit: string, value: number): (number, string) 41 | local prefix_colors = { 42 | [4] = color.red, 43 | [3] = color.red, 44 | [2] = color.yellow, 45 | [1] = color.yellow, 46 | [0] = color.green, 47 | [-1] = color.red, 48 | [-2] = color.yellow, 49 | [-3] = color.green, 50 | [-4] = color.red 51 | } 52 | 53 | local prefixes = { 54 | [4] = "T", 55 | [3] ="G", 56 | [2] ="M", 57 | [1] = "k", 58 | [0] = " ", 59 | [-1] = "m", 60 | [-2] = "u", 61 | [-3] = "n", 62 | [-4] = "p" 63 | } 64 | 65 | local order = 0 66 | 67 | while value >= 1000 do 68 | order += 1 69 | value /= 1000 70 | end 71 | 72 | while value ~= 0 and value < 1 do 73 | order -= 1 74 | value *= 1000 75 | end 76 | 77 | if value >= 100 then 78 | value = math.floor(value) 79 | elseif value >= 10 then 80 | value = math.floor(value * 1e1) / 1e1 81 | elseif value >= 1 then 82 | value = math.floor(value * 1e2) / 1e2 83 | end 84 | 85 | return value, prefix_colors[order](prefixes[order] .. unit) 86 | end 87 | 88 | local WALL = color.gray "│" 89 | 90 | -------------------------------------------------------------------------------- 91 | -- Testing 92 | -------------------------------------------------------------------------------- 93 | 94 | type Test = { 95 | name: string, 96 | case: Case?, 97 | cases: { Case }, 98 | duration: number, 99 | error: { 100 | message: string, 101 | trace: string 102 | }? 103 | } 104 | 105 | type Case = { 106 | name: string, 107 | result: number, 108 | line: number? 109 | } 110 | 111 | local PASS, FAIL, NONE, ERROR = 1, 2, 3, 4 112 | 113 | local skip: string? 114 | local test: Test? 115 | local tests: { Test } = {} 116 | 117 | local function output_test_result(test: Test) 118 | print(color.white(test.name)) 119 | 120 | for _, case in test.cases do 121 | local status = ({ 122 | [PASS] = color.green "PASS", 123 | [FAIL] = color.red "FAIL", 124 | [NONE] = color.yellow "NONE", 125 | [ERROR] = color.red "FAIL" 126 | })[case.result] 127 | 128 | local line = case.result == FAIL and color.red(`{case.line}:`) or "" 129 | 130 | print(`{status}{WALL} {line}{color.gray(case.name)}`) 131 | end 132 | 133 | if test.error then 134 | print(color.gray "error: " .. color.red(test.error.message)) 135 | print(color.gray "trace: " .. color.red(test.error.trace)) 136 | else 137 | print() 138 | end 139 | end 140 | 141 | local function CASE(name: string) 142 | assert(test, "no active test") 143 | 144 | local case = { 145 | name = name, 146 | result = NONE 147 | } 148 | 149 | test.case = case 150 | table.insert(test.cases, case) 151 | end 152 | 153 | local function CHECK(value: T, stack: number?): T 154 | assert(test, "no active test") 155 | local case = test.case 156 | 157 | if not case then 158 | CASE "" 159 | case = test.case 160 | end 161 | 162 | assert(case, "no active case") 163 | 164 | if case.result ~= FAIL then 165 | case.result = value and PASS or FAIL 166 | case.line = debug.info(stack and stack + 1 or 2, "l") 167 | end 168 | 169 | return value 170 | end 171 | 172 | local function TEST(name: string, fn: () -> ()) 173 | if skip and name ~= skip then return end 174 | 175 | local active = test 176 | assert(not active, "cannot start test while another test is in progress") 177 | 178 | test = { 179 | name = name, 180 | cases = {}, 181 | duration = 0 182 | }; assert(test) 183 | 184 | table.insert(tests, test) 185 | 186 | local start = os.clock() 187 | local err 188 | local success = xpcall(fn, function(m: string) 189 | err = { message = m, trace = debug.traceback(nil, 2) } 190 | end) 191 | test.duration = os.clock() - start 192 | 193 | if not test.case then CASE "" end 194 | assert(test.case, "no active case") 195 | 196 | if not success then 197 | test.case.result = ERROR 198 | test.error = err 199 | end 200 | 201 | test = nil 202 | end 203 | 204 | local function FINISH(): boolean 205 | local success = true 206 | local total_cases = 0 207 | local passed_cases = 0 208 | local duration = 0 209 | 210 | for _, test in tests do 211 | duration += test.duration 212 | for _, case in test.cases do 213 | total_cases += 1 214 | if case.result == PASS or case.result == NONE then 215 | passed_cases += 1 216 | else 217 | success = false 218 | end 219 | end 220 | 221 | output_test_result(test) 222 | end 223 | 224 | print(color.gray(string.format( 225 | `{passed_cases}/{total_cases} test cases passed in %.3f ms.`, 226 | duration*1e3 227 | ))) 228 | 229 | local fails = total_cases - passed_cases 230 | 231 | print( 232 | ( 233 | fails > 0 234 | and color.red 235 | or color.green 236 | )(`{fails} {fails == 1 and "fail" or "fails"}`) 237 | ) 238 | 239 | return success, table.clear(tests) 240 | end 241 | 242 | local function SKIP(name: string) 243 | assert(not test, "cannot skip during test") 244 | skip = name 245 | end 246 | 247 | -------------------------------------------------------------------------------- 248 | -- Benchmarking 249 | -------------------------------------------------------------------------------- 250 | 251 | type Bench = { 252 | time_start: number?, 253 | memory_start: number?, 254 | iterations: number? 255 | } 256 | 257 | local bench: Bench? 258 | 259 | function START(iter: number?): number 260 | local n = iter or 1 261 | assert(n > 0, "iterations must be greater than 0") 262 | assert(bench, "no active benchmark") 263 | assert(not bench.time_start, "clock was already started") 264 | 265 | bench.iterations = n 266 | bench.memory_start = gcinfo() 267 | bench.time_start = os.clock() 268 | return n 269 | end 270 | 271 | local function BENCH(name: string, fn: () -> ()) 272 | local active = bench 273 | assert(not active, "a benchmark is already in progress") 274 | 275 | bench = {}; assert(bench) 276 | 277 | ;(collectgarbage :: any)("collect") 278 | 279 | local mem_start = gcinfo() 280 | local time_start = os.clock() 281 | local err_msg: string? 282 | 283 | local success = xpcall(fn, function(m: string) 284 | err_msg = m .. debug.traceback(nil, 2) 285 | end) 286 | 287 | local time_stop = os.clock() 288 | local mem_stop = gcinfo() 289 | 290 | if not success then 291 | print(`{WALL}{color.red("ERROR")}{WALL} {name}`) 292 | print(color.gray(err_msg :: string)) 293 | else 294 | time_start = bench.time_start or time_start 295 | mem_start = bench.memory_start or mem_start 296 | 297 | local n = bench.iterations or 1 298 | local d, d_unit = convert_units("s", (time_stop - time_start) / n) 299 | local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3)) 300 | 301 | local function round(x: number): string 302 | return x > 0 and x < 10 and (x - math.floor(x)) > 0 303 | and string.format("%2.1f", x) 304 | or string.format("%3.f", x) 305 | end 306 | 307 | print(string.format( 308 | `%s %s %s %s{WALL} %s`, 309 | color.gray(round(d)), 310 | d_unit, 311 | color.gray(round(a)), 312 | a_unit, 313 | color.gray(name) 314 | )) 315 | end 316 | 317 | bench = nil 318 | end 319 | 320 | -------------------------------------------------------------------------------- 321 | -- Printing 322 | -------------------------------------------------------------------------------- 323 | 324 | local function print2(v: unknown) 325 | type Buffer = { n: number, [number]: string } 326 | type Cyclic = { n: number, [{}]: number } 327 | 328 | -- overkill concatenationless string buffer 329 | local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic) 330 | local TAB = " " 331 | local indent = table.concat(table.create(stack, TAB)) 332 | 333 | if type(value) == "string" then 334 | local n = str.n 335 | str[n + 1] = "\"" 336 | str[n + 2] = value 337 | str[n + 3] = "\"" 338 | str.n = n + 3 339 | elseif type(value) ~= "table" then 340 | local n = str.n 341 | str[n + 1] = value == nil and "nil" or tostring(value) 342 | str.n = n + 1 343 | elseif next(value) == nil then 344 | local n = str.n 345 | str[n + 1] = "{}" 346 | str.n = n + 1 347 | else -- is table 348 | local tabbed_indent = indent .. TAB 349 | 350 | if cyclic[value] then 351 | str.n += 1 352 | str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`) 353 | return 354 | else 355 | cyclic.n += 1 356 | cyclic[value] = cyclic.n 357 | end 358 | 359 | str.n += 3 360 | str[str.n - 2] = "{ " 361 | str[str.n - 1] = color.gray(tostring(cyclic[value])) 362 | str[str.n - 0] = "\n" 363 | 364 | local i, v = next(value, nil) 365 | while v ~= nil do 366 | local n = str.n 367 | str[n + 1] = tabbed_indent 368 | 369 | if type(i) ~= "string" then 370 | str[n + 2] = "[" 371 | str[n + 3] = tostring(i) 372 | str[n + 4] = "]" 373 | n += 4 374 | else 375 | str[n + 2] = tostring(i) 376 | n += 2 377 | end 378 | 379 | str[n + 1] = " = " 380 | str.n = n + 1 381 | 382 | tos(v, stack + 1, str, cyclic) 383 | 384 | i, v = next(value, i) 385 | 386 | n = str.n 387 | str[n + 1] = v ~= nil and ",\n" or "\n" 388 | str.n = n + 1 389 | end 390 | 391 | local n = str.n 392 | str[n + 1] = indent 393 | str[n + 2] = "}" 394 | str.n = n + 2 395 | end 396 | end 397 | 398 | local str = { n = 0 } 399 | local cyclic = { n = 0 } 400 | tos(v, 0, str, cyclic) 401 | print(table.concat(str)) 402 | end 403 | 404 | -------------------------------------------------------------------------------- 405 | -- Equality 406 | -------------------------------------------------------------------------------- 407 | 408 | local function shallow_eq(a: {}, b: {}): boolean 409 | if #a ~= #b then return false end 410 | 411 | for i, v in next, a do 412 | if b[i] ~= v then 413 | return false 414 | end 415 | end 416 | 417 | for i, v in next, b do 418 | if a[i] ~= v then 419 | return false 420 | end 421 | end 422 | 423 | return true 424 | end 425 | 426 | local function deep_eq(a: {}, b: {}): boolean 427 | if #a ~= #b then return false end 428 | 429 | for i, v in next, a do 430 | if type(b[i]) == "table" and type(v) == "table" then 431 | if deep_eq(b[i], v) == false then return false end 432 | elseif b[i] ~= v then 433 | return false 434 | end 435 | end 436 | 437 | for i, v in next, b do 438 | if type(a[i]) == "table" and type(v) == "table" then 439 | if deep_eq(a[i], v) == false then return false end 440 | elseif a[i] ~= v then 441 | return false 442 | end 443 | end 444 | 445 | return true 446 | end 447 | 448 | -------------------------------------------------------------------------------- 449 | -- Return 450 | -------------------------------------------------------------------------------- 451 | 452 | return { 453 | test = function() 454 | return TEST, CASE, CHECK, FINISH, SKIP 455 | end, 456 | 457 | benchmark = function() 458 | return BENCH, START 459 | end, 460 | 461 | print = print2, 462 | 463 | seq = shallow_eq, 464 | deq = deep_eq, 465 | 466 | color = color 467 | } 468 | -------------------------------------------------------------------------------- /test/benchmarks.luau: -------------------------------------------------------------------------------- 1 | local testkit = require("./testkit") 2 | local BENCH, START = testkit.benchmark() 3 | 4 | local function TITLE(name: string) 5 | print() 6 | print(testkit.color.white(name)) 7 | end 8 | 9 | local ecr = require "../src/ecr" 10 | 11 | local function rtrue() return true :: any end 12 | local A, B, C, D = ecr.component(rtrue), ecr.component(rtrue), ecr.component(rtrue), ecr.component(rtrue) 13 | local E, F, G, H = ecr.component(), ecr.component(), ecr.component(), ecr.component() 14 | 15 | local TA, TB, TC, TD = ecr.tag(), ecr.tag(), ecr.tag(), ecr.tag() 16 | 17 | local function BULK_CREATE_IDS(reg: ecr.Registry, n: number): { ecr.entity } 18 | local ids = table.create(n) 19 | for i = 1, n do 20 | ids[i] = reg:create() 21 | end 22 | return ids 23 | end 24 | 25 | local N = 2^16 - 2 -- 65,534 26 | 27 | local function REG_INIT(size: number) 28 | local reg = ecr.registry() 29 | 30 | for _, ctype in { ecr.entity, A, B, C, D, E, F, G, H, TA, TB, TC, TD } do 31 | reg:storage(ctype):reserve(size) 32 | end 33 | 34 | return reg 35 | end 36 | 37 | do TITLE "entities" 38 | BENCH("create (init)", function() 39 | local reg = REG_INIT(N/2) 40 | 41 | for i = 1, START(N) do 42 | reg:create() 43 | end 44 | end) 45 | 46 | BENCH("create", function() 47 | local reg = REG_INIT(N) 48 | 49 | for i = 1, START(N) do 50 | reg:create() 51 | end 52 | end) 53 | 54 | BENCH("release", function() 55 | local reg = REG_INIT(N) 56 | 57 | local id = table.create(N) 58 | 59 | for i = 1, N do 60 | id[i] = reg:create() 61 | end 62 | 63 | for i = 1, START(N) do 64 | reg:release(id[i]) 65 | end 66 | end) 67 | 68 | BENCH("create with id (init)", function() 69 | local sreg = REG_INIT(N) 70 | 71 | local id = table.create(N) 72 | 73 | for i = 1, N do 74 | id[i] = sreg:create() 75 | end 76 | 77 | local reg = REG_INIT(N) 78 | 79 | for i = 1, START(N) do 80 | reg:create(id[i]) 81 | end 82 | end) 83 | 84 | BENCH("create with id", function() 85 | local sreg = REG_INIT(N) 86 | 87 | local id = table.create(N) 88 | 89 | for i = 1, N do 90 | id[i] = sreg:create() 91 | end 92 | 93 | local reg = REG_INIT(N) 94 | 95 | START(N) 96 | reg:create(id[N]) 97 | for i = 1, N - 1 do 98 | reg:create(id[i]) 99 | end 100 | end) 101 | end 102 | 103 | do TITLE "add" 104 | local function setup(n: number) 105 | -- todo: why is (N + 2) / 2 + 1 so slow - something to do with array idx set to nil 106 | local reg = REG_INIT(n) 107 | 108 | local ids = table.create(N) 109 | for i = 1, N do 110 | ids[i] = reg:create() 111 | end 112 | return reg, ids 113 | end 114 | 115 | do 116 | local reg, ids = setup(N/4) -- todo: inaccurate reading 117 | 118 | BENCH("1 component (init)", function() 119 | for i = 1, START(N) do 120 | reg:add(ids[i], A) 121 | end 122 | end) 123 | end 124 | 125 | do 126 | local reg, ids = setup(N) 127 | 128 | BENCH("1 component", function() 129 | for i = 1, START(N) do 130 | reg:add(ids[i], A) 131 | end 132 | end) 133 | end 134 | 135 | do 136 | local reg, ids = setup(N) 137 | 138 | BENCH("2 components", function() 139 | for i = 1, START(N) do 140 | reg:add(ids[i], A, B) 141 | end 142 | end) 143 | end 144 | 145 | do 146 | local reg, ids = setup(N) 147 | 148 | BENCH("4 components", function() 149 | for i = 1, START(N) do 150 | reg:add(ids[i], A, B, C, D) 151 | end 152 | end) 153 | end 154 | 155 | do 156 | local reg, ids = setup(N/2) 157 | 158 | BENCH("1 tag (init)", function() 159 | for i = 1, START(N) do 160 | reg:add(ids[i], TA) 161 | end 162 | end) 163 | end 164 | 165 | do 166 | local reg, ids = setup(N) 167 | 168 | -- todo: despite causing a reallocation why is this not recorded by gcinfo()? 169 | BENCH("1 tag", function() 170 | for i = 1, START(N) do 171 | reg:add(ids[i], TA) 172 | end 173 | end) 174 | end 175 | 176 | do 177 | local reg, ids = setup(N) 178 | 179 | BENCH("4 tags", function() 180 | for i = 1, START(N) do 181 | reg:add(ids[i], TA, TB, TC, TD) 182 | end 183 | end) 184 | end 185 | end 186 | 187 | do TITLE "set" 188 | local function setup() 189 | local reg = REG_INIT(N) 190 | local ids = table.create(N) 191 | for i = 1, N do 192 | ids[i] = reg:create() 193 | end 194 | return reg, ids 195 | end 196 | 197 | do 198 | local reg, ids = setup() 199 | 200 | BENCH("add 1", function() 201 | for i = 1, START(N) do 202 | reg:set(ids[i], A, true) 203 | end 204 | end) 205 | 206 | BENCH("change 1", function() 207 | for i = 1, START(N) do 208 | reg:set(ids[i], A, true) 209 | end 210 | end) 211 | end 212 | 213 | do 214 | local reg, ids = setup() 215 | 216 | BENCH("add 2", function() 217 | for i = 1, START(N) do 218 | local e = ids[i] 219 | reg:set(e, A, true) 220 | reg:set(e, B, true) 221 | end 222 | end) 223 | 224 | BENCH("change 2", function() 225 | for i = 1, START(N) do 226 | local e = ids[i] 227 | reg:set(e, A, true) 228 | reg:set(e, B, true) 229 | end 230 | end) 231 | end 232 | 233 | do 234 | local reg, ids = setup() 235 | BENCH("add 4", function() 236 | for i = 1, START(N) do 237 | local e = ids[i] 238 | reg:set(e, A, true) 239 | reg:set(e, B, true) 240 | reg:set(e, C, true) 241 | reg:set(e, D, true) 242 | end 243 | end) 244 | 245 | BENCH("change 4", function() 246 | for i = 1, START(N) do 247 | local e = ids[i] 248 | reg:set(e, A, true) 249 | reg:set(e, B, true) 250 | reg:set(e, C, true) 251 | reg:set(e, D, true) 252 | end 253 | end) 254 | end 255 | 256 | do 257 | local reg, ids = setup() 258 | BENCH("add tag", function() 259 | for i = 1, START(N) do 260 | reg:set(ids[i], TA) 261 | end 262 | end) 263 | end 264 | end 265 | 266 | do TITLE "insert" 267 | 268 | BENCH("insert new array", function() 269 | local reg = REG_INIT(N) 270 | local ids = table.create(N) 271 | 272 | for i = 1, N do 273 | local e = reg:create() 274 | ids[i] = e 275 | end 276 | 277 | for i = 1, START(N) do 278 | reg:insert(ids[i], A, true) 279 | end 280 | end) 281 | 282 | BENCH("insert new value", function() 283 | local reg = REG_INIT(N) 284 | local ids = table.create(N) 285 | 286 | for i = 1, N do 287 | local e = reg:create() 288 | reg:insert(e, A, true) 289 | ids[i] = e 290 | end 291 | 292 | for i = 1, START(N) do 293 | reg:insert(ids[i], A, true) 294 | end 295 | end) 296 | 297 | BENCH("insert new value to same entity", function() 298 | local reg = REG_INIT(N) 299 | local id = reg:create() 300 | 301 | for i = 1, START(N) do 302 | reg:insert(id, A, true) 303 | end 304 | end) 305 | 306 | end 307 | 308 | do TITLE "patch" 309 | local function setup() 310 | local reg = REG_INIT(N) 311 | local ids = table.create(N) 312 | for i = 1, N do 313 | local e = reg:create() 314 | ids[i] = e 315 | reg:set(e, A, 0) 316 | reg:set(e, B, 0) 317 | reg:set(e, C, 0) 318 | reg:set(e, D, 0) 319 | end 320 | return reg, ids 321 | end 322 | 323 | local function patcher(v: number) 324 | return v + 1 325 | end 326 | 327 | do 328 | local reg, ids = setup() 329 | 330 | BENCH("patch 1", function() 331 | for i = 1, START(N) do 332 | local e = ids[i] 333 | reg:patch(e, A, patcher) 334 | end 335 | end) 336 | end 337 | 338 | do 339 | local reg, ids = setup() 340 | 341 | BENCH("patch 2", function() 342 | for i = 1, START(N) do 343 | local e = ids[i] 344 | reg:patch(e, A, patcher) 345 | reg:patch(e, B, patcher) 346 | end 347 | end) 348 | end 349 | 350 | do 351 | local reg, ids = setup() 352 | 353 | BENCH("patch 4", function() 354 | for i = 1, START(N) do 355 | local e = ids[i] 356 | reg:patch(e, A, patcher) 357 | reg:patch(e, B, patcher) 358 | reg:patch(e, C, patcher) 359 | reg:patch(e, D, patcher) 360 | end 361 | end) 362 | end 363 | end 364 | 365 | do TITLE "has" 366 | local reg = REG_INIT(N) 367 | local ids = table.create(N) 368 | 369 | for i = 1, N do 370 | ids[i] = reg:create() 371 | reg:set(ids[i], A, true) 372 | reg:set(ids[i], B, true) 373 | reg:set(ids[i], C, true) 374 | reg:set(ids[i], D, true) 375 | end 376 | 377 | BENCH("1 component", function() 378 | for i = 1, START(N) do 379 | reg:has(ids[i], A) 380 | end 381 | end) 382 | 383 | BENCH("2 components", function() 384 | for i = 1, START(N) do 385 | local e = ids[i] 386 | reg:has(e, A, B) 387 | end 388 | end) 389 | 390 | BENCH("4 components", function() 391 | for i = 1, START(N) do 392 | local e = ids[i] 393 | reg:has(e, A, B, C, D) 394 | end 395 | end) 396 | end 397 | 398 | do TITLE("get") 399 | local reg = REG_INIT(N) 400 | local ids = table.create(N) 401 | 402 | for i = 1, N do 403 | local e = reg:create() 404 | ids[i] = e 405 | reg:set(e, A, true) 406 | reg:set(e, B, true) 407 | reg:set(e, C, true) 408 | reg:set(e, D, true) 409 | 410 | reg:set(e, E, true) 411 | reg:set(e, F, true) 412 | reg:set(e, G, true) 413 | reg:set(e, H, true) 414 | end 415 | 416 | BENCH("1 component", function() 417 | for i = 1, START(N) do 418 | reg:get(ids[i], A) 419 | end 420 | end) 421 | 422 | BENCH("2 components", function() 423 | for i = 1, START(N) do 424 | local e = ids[i] 425 | reg:get(e, A, B) 426 | end 427 | end) 428 | 429 | BENCH("4 components", function() 430 | for i = 1, START(N) do 431 | local e = ids[i] 432 | reg:get(e, A, B, C, D) 433 | end 434 | end) 435 | 436 | BENCH("try 1 component", function() 437 | for i = 1, START(N) do 438 | reg:try_get(ids[i], A) 439 | end 440 | end) 441 | end 442 | 443 | do TITLE("remove") 444 | local function setup() 445 | local reg = REG_INIT(N) 446 | local ids = table.create(N) 447 | for i = 1, N do 448 | local e = reg:create() 449 | ids[i] = e 450 | reg:set(e, A, true) 451 | reg:set(e, B, true) 452 | reg:set(e, C, true) 453 | reg:set(e, D, true) 454 | reg:set(e, E, true) 455 | reg:set(e, F, true) 456 | reg:set(e, G, true) 457 | reg:set(e, H, true) 458 | end 459 | return reg, ids 460 | end 461 | 462 | BENCH("1 unowned", function() 463 | local reg, ids = setup() 464 | 465 | for i = 1, N do 466 | reg:remove(ids[i], A) 467 | end 468 | 469 | for i = 1, START(N) do 470 | reg:remove(ids[i], A) 471 | end 472 | end) 473 | 474 | BENCH("1 component", function() 475 | local reg, ids = setup() 476 | 477 | for i = 1, START(N) do 478 | reg:remove(ids[i], A) 479 | end 480 | end) 481 | 482 | BENCH("2 components", function() 483 | local reg, ids = setup() 484 | 485 | for i = 1, START(N) do 486 | local e = ids[i] 487 | reg:remove(e, A, B) 488 | end 489 | end) 490 | 491 | BENCH("4 components", function() 492 | local reg, ids = setup() 493 | 494 | for i = 1, START(N) do 495 | local e = ids[i] 496 | reg:remove(e, A, B, C, D) 497 | end 498 | end) 499 | 500 | BENCH("8 components", function() 501 | local reg, ids = setup() 502 | 503 | for i = 1, START(N) do 504 | local e = ids[i] 505 | reg:remove(e, A, B, C, D, E, F, G, H) 506 | end 507 | end) 508 | end 509 | 510 | do TITLE "find" 511 | local reg = REG_INIT(N) 512 | for i = 1, N do 513 | local id = reg:create() 514 | reg:set(id, A, i) 515 | end 516 | 517 | BENCH("search through 1", function() 518 | for i = 1, START(N) do 519 | reg:find(A, 1) 520 | end 521 | end) 522 | 523 | BENCH("search through 10", function() 524 | for i = 1, START(N) do 525 | reg:find(A, 10) 526 | end 527 | end) 528 | 529 | assert(N > 1000) 530 | 531 | BENCH("search through 1000", function() 532 | for i = 1, START(100) do 533 | reg:find(A, 1000) 534 | end 535 | end) 536 | end 537 | 538 | do TITLE "clear" 539 | local function setup() 540 | local reg = REG_INIT(N) 541 | local ids = table.create(N) 542 | for i = 1, N do 543 | local e = reg:create() 544 | ids[i] = e 545 | reg:set(e, A, true) 546 | reg:set(e, B, true) 547 | reg:set(e, C, true) 548 | reg:set(e, D, true) 549 | reg:set(e, E, true) 550 | reg:set(e, F, true) 551 | reg:set(e, G, true) 552 | reg:set(e, H, true) 553 | end 554 | return reg, ids 555 | end 556 | 557 | BENCH("1 component", function() 558 | local reg = setup() 559 | 560 | START(N) 561 | reg:clear(A) 562 | end) 563 | 564 | BENCH("2 components", function() 565 | local reg = setup() 566 | 567 | START(N) 568 | reg:clear(A, B) 569 | end) 570 | 571 | BENCH("4 components", function() 572 | local reg = setup() 573 | 574 | START(N) 575 | reg:clear(A, B, C, D) 576 | end) 577 | 578 | BENCH("8 components", function() 579 | local reg = setup() 580 | 581 | START(N) 582 | reg:clear(A, B, C, D, E, F, G, H) 583 | end) 584 | 585 | BENCH("all entities (no components)", function() 586 | local reg = REG_INIT(N) 587 | for i = 1, N do reg:create() end 588 | 589 | START(N) 590 | reg:clear() 591 | end) 592 | 593 | BENCH("1 component (grouped)", function() 594 | local reg = setup() 595 | reg:group(A, B) 596 | 597 | START(N) 598 | reg:clear(A) 599 | end) 600 | end 601 | 602 | do TITLE("create view") 603 | local reg = REG_INIT(N) 604 | 605 | reg:view(A, B, C, D, E, F, G, H) -- register components 606 | 607 | BENCH("1 component", function() 608 | for i = 1, START(N) do 609 | reg:view(A) 610 | end 611 | end) 612 | 613 | BENCH("4 component", function() 614 | for i = 1, START(N) do 615 | reg:view(A, B, C, D) 616 | end 617 | end) 618 | 619 | BENCH("1 component and iterate", function() 620 | for i = 1, START(N) do 621 | for _ in reg:view(A) do end 622 | end 623 | end) 624 | 625 | BENCH("4 component and iterate", function() 626 | for i = 1, START(N) do 627 | for _ in reg:view(A, B, C, D) do end 628 | end 629 | end) 630 | end 631 | 632 | do 633 | local function view_bench(reg: ecr.Registry) 634 | local function exact_size(view: ecr.View<...any>): number 635 | local i = 0 636 | for _ in view do 637 | i += 1 638 | end 639 | return i 640 | end 641 | 642 | BENCH("1 component", function() 643 | local view = reg:view(A) 644 | 645 | START(exact_size(view)) 646 | for entity, a in view do end 647 | end) 648 | 649 | BENCH("2 components", function() 650 | local view = reg:view(A, B) 651 | 652 | START(exact_size(view)) 653 | for entity, a, b in view do end 654 | end) 655 | 656 | BENCH("3 components", function() 657 | local view = reg:view(A, B, C) 658 | 659 | START(exact_size(view)) 660 | for entity, a, b, c in view do end 661 | end) 662 | 663 | BENCH("4 components", function() 664 | local view = reg:view(A, B, C, D) 665 | 666 | START(exact_size(view)) 667 | for entity, a, b, c, d in view do end 668 | end) 669 | 670 | BENCH("8 components", function() 671 | local view = reg:view(A, B, C, D, E, F, G, H) 672 | 673 | START(exact_size(view)) 674 | for entity, a, b, c, d, e, f, g, h in view do end 675 | end) 676 | end 677 | 678 | do TITLE("iter view (ordered)") 679 | local reg = REG_INIT(N) 680 | 681 | for i = 1, N do 682 | local entity = reg:create() 683 | reg:set(entity, A, true) 684 | reg:set(entity, B, true) 685 | reg:set(entity, C, true) 686 | reg:set(entity, D, true) 687 | reg:set(entity, E, true) 688 | reg:set(entity, F, true) 689 | reg:set(entity, G, true) 690 | reg:set(entity, H, true) 691 | end 692 | 693 | view_bench(reg) 694 | end 695 | 696 | do TITLE("iter view (random)") 697 | local reg = REG_INIT(N) 698 | 699 | local function flip() return math.random() > 0.5 end 700 | 701 | for i = 1, N do 702 | local entity = reg:create() 703 | if flip() then reg:set(entity, A, true) end 704 | if flip() then reg:set(entity, B, true) end 705 | if flip() then reg:set(entity, C, true) end 706 | if flip() then reg:set(entity, D, true) end 707 | if flip() then reg:set(entity, E, true) end 708 | if flip() then reg:set(entity, F, true) end 709 | if flip() then reg:set(entity, G, true) end 710 | if flip() then reg:set(entity, H, true) end 711 | end 712 | 713 | view_bench(reg) 714 | end 715 | 716 | do TITLE("iter view (random + common)") 717 | -- 7 components are randomly assigned to entities, 718 | -- any entities that receive all 7 will be tagged with an 8th component. 719 | -- This guarantees that when iterating over this 8th component, all 720 | -- entities iterated will also contain the other 7. 721 | 722 | local reg = REG_INIT(N) 723 | 724 | local function flip() return math.random() > 0.5 end 725 | 726 | for i = 1, N do 727 | local entity = reg:create() 728 | local b, c, d, e, f, g, h 729 | if flip() then b=true; reg:set(entity, B, true) end 730 | if flip() then c=true; reg:set(entity, C, true) end 731 | if flip() then d=true; reg:set(entity, D, true) end 732 | if flip() then e=true; reg:set(entity, E, true) end 733 | if flip() then f=true; reg:set(entity, F, true) end 734 | if flip() then g=true; reg:set(entity, G, true) end 735 | if flip() then h=true; reg:set(entity, H, true) end 736 | if b and c and d and e and f and g and h then reg:set(entity, A, true) end 737 | end 738 | 739 | view_bench(reg) 740 | end 741 | 742 | do TITLE("iter view (tags)") 743 | local reg = REG_INIT(N) 744 | 745 | for i = 1, N do 746 | local e = reg:handle() 747 | e:add(TA, TB, TC, TD) 748 | end 749 | 750 | BENCH("1 tag", function() 751 | START(N) 752 | for id, a in reg:view(TA) do end 753 | end) 754 | 755 | BENCH("4 tags", function() 756 | START(N) 757 | for id, a, b, c, d in reg:view(TA, TB, TC, TD) do end 758 | end) 759 | end 760 | 761 | do TITLE("patch view (ordered)") 762 | local reg = REG_INIT(N) 763 | 764 | for i = 1, N do 765 | local entity = reg:create() 766 | reg:set(entity, A, true) 767 | reg:set(entity, B, true) 768 | reg:set(entity, C, true) 769 | reg:set(entity, D, true) 770 | end 771 | 772 | BENCH("1 component", function() 773 | START(N) 774 | reg:view(A):patch(function(a) return a end) 775 | end) 776 | 777 | BENCH("2 components", function() 778 | START(N) 779 | reg:view(A, B):patch(function(a, b) return a, b end) 780 | end) 781 | 782 | BENCH("4 components", function() 783 | START(N) 784 | reg:view(A, B, C, D):patch(function(a, b, c, d) return a, b, c, d end) 785 | end) 786 | end 787 | end 788 | 789 | do TITLE("handle") 790 | local N2 = N/32 791 | 792 | local reg = REG_INIT(N) 793 | local ids = table.create(N2) 794 | 795 | for i = 1, N2 do 796 | ids[i] = reg:create() 797 | end 798 | 799 | BENCH("create new", function() 800 | for i = 1, START(N2) do 801 | reg:handle(ids[i]) 802 | end 803 | end) 804 | 805 | -- cache to prevent gc when starting next benchmark 806 | local cache = table.create(N2) 807 | for i = 1, N2 do 808 | cache[i] = reg:handle(ids[i]) 809 | end 810 | 811 | BENCH("create cached", function() 812 | for i = 1, START(N2) do 813 | reg:handle(ids[i]) 814 | end 815 | end) 816 | end 817 | 818 | do TITLE "group" 819 | do 820 | local reg = REG_INIT(N) 821 | 822 | local group = reg:group(A, B) 823 | 824 | local ids = {} 825 | 826 | for i = 1, N do 827 | ids[i] = reg:create() 828 | end 829 | 830 | BENCH("add 2 components", function() 831 | for i = 1, START(N) do 832 | local e = ids[i] 833 | reg:set(e, A, 1) 834 | reg:set(e, B, 2) 835 | end 836 | end) 837 | 838 | BENCH("iter 2 components", function() 839 | START(N) 840 | for entity, a, b in group do end 841 | end) 842 | 843 | BENCH("remove 2 components", function() 844 | for i = 1, START(N) do 845 | local e = ids[i] 846 | reg:remove(e, A, B) 847 | end 848 | end) 849 | 850 | BENCH("add 1 component", function() 851 | for i = 1, START(N) do 852 | local e = ids[i] 853 | reg:set(e, A, 1) 854 | end 855 | end) 856 | end 857 | 858 | do 859 | local reg = REG_INIT(N) 860 | 861 | local group2 = reg:group(C, D, E, F) 862 | 863 | local ids = {} 864 | 865 | for i = 1, N do 866 | ids[i] = reg:create() 867 | end 868 | 869 | BENCH("add 4 components", function() 870 | for i = 1, START(N) do 871 | local e = ids[i] 872 | reg:set(e, C, 1) 873 | reg:set(e, D, 2) 874 | reg:set(e, E, 3) 875 | reg:set(e, F, 4) 876 | end 877 | end) 878 | 879 | BENCH("iter 4 components", function() 880 | START(N) 881 | for entity, a, b, c, d in group2 do end 882 | end) 883 | 884 | BENCH("remove 4 components", function() 885 | for i = 1, START(N) do 886 | local e = ids[i] 887 | reg:remove(e, C, D, E, F) 888 | end 889 | end) 890 | 891 | BENCH("add 1 component", function() 892 | for i = 1, START(N) do 893 | local e = ids[i] 894 | reg:set(e, C, 1) 895 | end 896 | end) 897 | end 898 | end 899 | 900 | do TITLE("destroy") 901 | 902 | BENCH("0 components", function() 903 | local reg = ecr.registry() 904 | 905 | for p in reg:storage() do 906 | print"p" 907 | end 908 | 909 | local ids = table.create(N) 910 | 911 | for i = 1, N do 912 | ids[i] = reg:create() 913 | end 914 | 915 | for i = 1, START(N) do 916 | reg:destroy(ids[i]) 917 | end 918 | end) 919 | 920 | BENCH("1 component", function() 921 | local reg = ecr.registry() 922 | local ids = table.create(N) 923 | 924 | for i = 1, N do 925 | local e = reg:create() 926 | reg:set(e, A, true) 927 | ids[i] = e 928 | end 929 | 930 | for i = 1, START(N) do 931 | reg:destroy(ids[i]) 932 | end 933 | end) 934 | 935 | BENCH("2 components", function() 936 | local reg = ecr.registry() 937 | local ids = table.create(N) 938 | 939 | for i = 1, N do 940 | local e = reg:create() 941 | reg:set(e, A, true) 942 | reg:set(e, B, true) 943 | ids[i] = e 944 | end 945 | 946 | for i = 1, START(N) do 947 | reg:destroy(ids[i]) 948 | end 949 | end) 950 | 951 | BENCH("4 components", function() 952 | local reg = ecr.registry() 953 | local ids = table.create(N) 954 | 955 | for i = 1, N do 956 | local e = reg:create() 957 | reg:set(e, A, true) 958 | reg:set(e, B, true) 959 | reg:set(e, C, true) 960 | reg:set(e, D, true) 961 | ids[i] = e 962 | end 963 | 964 | for i = 1, START(N) do 965 | reg:destroy(ids[i]) 966 | end 967 | end) 968 | 969 | BENCH("8 components", function() 970 | local reg = ecr.registry() 971 | local ids = table.create(N) 972 | 973 | for i = 1, N do 974 | local e = reg:create() 975 | reg:set(e, A, true) 976 | reg:set(e, B, true) 977 | reg:set(e, C, true) 978 | reg:set(e, D, true) 979 | reg:set(e, E, true) 980 | reg:set(e, F, true) 981 | reg:set(e, G, true) 982 | reg:set(e, H, true) 983 | ids[i] = e 984 | end 985 | 986 | for i = 1, START(N) do 987 | reg:destroy(ids[i]) 988 | end 989 | end) 990 | 991 | BENCH("0 components (8 registered)", function() 992 | local reg = ecr.registry() 993 | local ids = table.create(N) 994 | 995 | for i = 1, N - 1 do 996 | local e = reg:create() 997 | ids[i] = e 998 | end 999 | 1000 | do -- register all components 1001 | local e = reg:create() 1002 | reg:set(e, A, true) 1003 | reg:set(e, B, true) 1004 | reg:set(e, C, true) 1005 | reg:set(e, D, true) 1006 | reg:set(e, E, true) 1007 | reg:set(e, F, true) 1008 | reg:set(e, G, true) 1009 | reg:set(e, H, true) 1010 | end 1011 | 1012 | for i = 1, START(N - 1) do 1013 | reg:destroy(ids[i]) 1014 | end 1015 | end) 1016 | 1017 | BENCH("0 components (128 registered)", function() 1018 | local ctypes = {} 1019 | for i = 1, 128 do 1020 | ctypes[i] = ecr.component() 1021 | end 1022 | 1023 | local reg = ecr.registry() 1024 | 1025 | do 1026 | local id = reg:create() 1027 | for i = 1, 128 do 1028 | reg:set(id, ctypes[i], true) 1029 | end 1030 | end 1031 | 1032 | local ids = table.create(N - 1) 1033 | 1034 | for i = 1, N - 1 do 1035 | ids[i] = reg:create() 1036 | end 1037 | 1038 | for i = 1, START(N - 1) do 1039 | reg:destroy(ids[i]) 1040 | end 1041 | end) 1042 | 1043 | BENCH("0 components (1024 registered)", function() 1044 | local ctypes = {} 1045 | for i = 1, 1024 do 1046 | ctypes[i] = ecr.component() 1047 | end 1048 | 1049 | local reg = ecr.registry() 1050 | 1051 | do 1052 | local id = reg:create() 1053 | for i = 1, 1024 do 1054 | reg:set(id, ctypes[i], true) 1055 | end 1056 | end 1057 | 1058 | local ids = table.create(N) 1059 | 1060 | for i = 1, N/10 do 1061 | ids[i] = reg:create() 1062 | end 1063 | 1064 | for i = 1, START(N/10) do 1065 | reg:destroy(ids[i]) 1066 | end 1067 | end) 1068 | 1069 | BENCH("32 components (1024 registered)", function() 1070 | local ctypes = {} 1071 | for i = 1, 1024 do 1072 | ctypes[i] = ecr.component() 1073 | end 1074 | 1075 | local reg = ecr.registry() 1076 | 1077 | do 1078 | local id = reg:create() 1079 | for i = 1, 1024 do 1080 | reg:set(id, ctypes[i], true) 1081 | end 1082 | end 1083 | 1084 | local ids = table.create(N) 1085 | 1086 | for i = 1, N/10 do 1087 | ids[i] = reg:create() 1088 | for j = 1, 32 do 1089 | reg:set(ids[i], ctypes[j], true) 1090 | end 1091 | end 1092 | 1093 | for i = 1, START(N/10) do 1094 | reg:destroy(ids[i]) 1095 | end 1096 | end) 1097 | end 1098 | 1099 | do TITLE("contains") 1100 | BENCH("", function() 1101 | local reg = REG_INIT(N) 1102 | local ids = table.create(N) 1103 | for i = 1, N do 1104 | ids[i] = reg:create() 1105 | end 1106 | 1107 | START(N) 1108 | 1109 | for i = 1, N do 1110 | reg:contains(ids[i]) 1111 | end 1112 | end) 1113 | end 1114 | 1115 | do TITLE("copy") 1116 | local reg = REG_INIT(N) 1117 | local ids = table.create(N) 1118 | 1119 | for i = 1, N do 1120 | ids[i] = reg:create() 1121 | end 1122 | 1123 | BENCH("values no pre-alloc", function() 1124 | for i = 1, N do 1125 | reg:set(ids[i], A, "1") 1126 | end 1127 | 1128 | START(N) 1129 | 1130 | reg:copy(A, B) 1131 | end) 1132 | 1133 | BENCH("values pre-alloc", function() 1134 | for i = 1, N do 1135 | reg:set(ids[i], A, "2") 1136 | end 1137 | 1138 | START(N) 1139 | 1140 | reg:copy(A, B) 1141 | end) 1142 | 1143 | BENCH("tag no pre-alloc", function() 1144 | for i = 1, N do 1145 | reg:set(ids[i], TA) 1146 | end 1147 | 1148 | START(N) 1149 | 1150 | reg:copy(TA, TB) 1151 | end) 1152 | 1153 | BENCH("tag pre-alloc", function() 1154 | START(N) 1155 | 1156 | reg:copy(TA, TB) 1157 | end) 1158 | end 1159 | 1160 | do TITLE "signal" 1161 | local reg = REG_INIT(N) 1162 | local ids = table.create(N) 1163 | 1164 | for i = 1, N do 1165 | ids[i] = reg:create() 1166 | end 1167 | 1168 | BENCH("added", function() 1169 | reg:on_add(A):connect(function() end) 1170 | 1171 | for i = 1, START(N) do 1172 | reg:set(ids[i], A, true) 1173 | end 1174 | end) 1175 | 1176 | BENCH("changed", function() 1177 | reg:on_change(A):connect(function() end) 1178 | 1179 | for i = 1, START(N) do 1180 | reg:set(ids[i], A, false) 1181 | end 1182 | end) 1183 | 1184 | BENCH("removed", function() 1185 | reg:on_remove(A):connect(function() end) 1186 | 1187 | for i = 1, START(N) do 1188 | reg:remove(ids[i], A) 1189 | end 1190 | end) 1191 | end 1192 | 1193 | do TITLE "observer" 1194 | local reg = REG_INIT(N) 1195 | local observer = reg:track(A) 1196 | 1197 | local ids = table.create(N) 1198 | for i = 1, N do 1199 | ids[i] = reg:create() 1200 | end 1201 | 1202 | (observer :: any).pool:reserve(N/2) 1203 | 1204 | BENCH("add (init)", function() 1205 | for i = 1, START(N) do 1206 | reg:set(ids[i], A, true) 1207 | end 1208 | end) 1209 | 1210 | reg:clear(A) 1211 | assert(#observer == 0) 1212 | 1213 | BENCH("add", function() 1214 | for i = 1, START(N) do 1215 | reg:set(ids[i], A, true) 1216 | end 1217 | end) 1218 | 1219 | BENCH("change", function() 1220 | for i = 1, START(N) do 1221 | reg:set(ids[i], A, false) 1222 | end 1223 | end) 1224 | 1225 | BENCH("iterate", function() 1226 | START(N) 1227 | for id, v in observer do end 1228 | end) 1229 | 1230 | BENCH("remove", function() 1231 | for i = 1, START(N) do 1232 | reg:remove(ids[i], A) 1233 | end 1234 | end) 1235 | 1236 | BENCH("clear", function() 1237 | START(N) 1238 | observer:clear() 1239 | end) 1240 | end 1241 | 1242 | do TITLE "queue" 1243 | do 1244 | local q = ecr.queue() 1245 | 1246 | BENCH("add 1", function() 1247 | for i = 1, START(N) do 1248 | q:add(i) 1249 | end 1250 | end) 1251 | 1252 | BENCH("iter 1", function() 1253 | START(N) 1254 | for i in q do end 1255 | end) 1256 | end 1257 | 1258 | do 1259 | local q = ecr.queue() 1260 | 1261 | BENCH("add 4", function() 1262 | for i = 1, START(N) do 1263 | q:add(i, i, i, i) 1264 | end 1265 | end) 1266 | 1267 | BENCH("iter 4", function() 1268 | START(N) 1269 | for a, b, c, d in q do end 1270 | end) 1271 | end 1272 | end 1273 | 1274 | do TITLE "buffer" 1275 | local reg = ecr.registry() 1276 | 1277 | local ids = BULK_CREATE_IDS(reg, N) 1278 | local buf = buffer.create(N * ecr.id_size) 1279 | 1280 | BENCH("array to buffer", function() 1281 | ecr.array_to_buffer(ids, START(N), buf) 1282 | end) 1283 | 1284 | local arr = table.create(N) 1285 | 1286 | BENCH("buffer to array", function() 1287 | ecr.buffer_to_array(buf, START(N), arr) 1288 | end) 1289 | 1290 | local pool = reg:storage(ecr.entity) 1291 | 1292 | BENCH("buffer to buffer", function() 1293 | ecr.buffer_to_buffer(pool.entities, START(N), buf) 1294 | end) 1295 | end 1296 | 1297 | local PN = 2^11 -- 2048 1298 | 1299 | do TITLE(`practical test ECS ({PN} entities)`) 1300 | local Position = ecr.component() :: number 1301 | local Velocity = ecr.component() :: number 1302 | local Health = ecr.component() :: number 1303 | local Dead = ecr.tag() 1304 | 1305 | local world = ecr.registry() 1306 | 1307 | world:group(Position, Velocity) 1308 | 1309 | local function init() 1310 | for i = 1, PN do 1311 | local id = world:create() 1312 | world:set(id, Position, i) 1313 | world:set(id, Velocity, i) 1314 | world:set(id, Health, 0) 1315 | 1316 | end 1317 | end 1318 | 1319 | do -- pre-alloc 1320 | init() 1321 | 1322 | for id in world:view(ecr.entity) do 1323 | world:add(id, Dead) 1324 | end 1325 | 1326 | world:clear() 1327 | end 1328 | 1329 | BENCH("create entities with 3 components", function() 1330 | init() 1331 | end) 1332 | 1333 | BENCH("update positions", function() 1334 | for id, pos, vel in world:group(Position, Velocity) do 1335 | world:set(id, Position, pos + vel*1/60) 1336 | end 1337 | end) 1338 | 1339 | BENCH("update positions (direct)", function() 1340 | local n = #world:group(Position, Velocity) 1341 | local positions = world:storage(Position).values 1342 | local velocities = world:storage(Velocity).values 1343 | 1344 | for i = 1, n do 1345 | positions[i] += velocities[i] * 1/60 1346 | end 1347 | 1348 | end) 1349 | 1350 | BENCH("add tags", function() 1351 | for id, health in world:view(Health) do 1352 | if health <= 0 then 1353 | world:set(id, Dead) 1354 | end 1355 | end 1356 | end) 1357 | 1358 | BENCH("destroy", function() 1359 | for id in world:view(Dead) do 1360 | world:destroy(id) 1361 | end 1362 | end) 1363 | end 1364 | 1365 | -- attempt to repeat the same test above but OOP equivalent 1366 | do TITLE(`practical test OOP ({PN} entities)`) 1367 | local function gc() 1368 | (collectgarbage :: any)("collect") 1369 | end 1370 | 1371 | 1372 | type Obj = { 1373 | Position: number, 1374 | Velocity: number, 1375 | Health: number, 1376 | Dead: boolean? 1377 | } 1378 | 1379 | local world = {} :: { Obj } 1380 | 1381 | --[[ 1382 | 1383 | One advantage of ECS is the dependence on interfaces and not complete object 1384 | structure. 1385 | 1386 | Luau has many optimizations for accessing a table index of known shape, so 1387 | we use multiple table shapes with a common interface to try simulate what 1388 | we would do in ECS. This should degrade OOP performance as the table index 1389 | predicter will not be correct every time as table shapes differ. 1390 | 1391 | Note: In OOP, composition is usually done instead of below, but that needs 1392 | manual separation of data and behavior by the programmer, which needs more 1393 | work than what ECS can do. 1394 | 1395 | ]] 1396 | 1397 | local ctors = { 1398 | function(i: number): Obj 1399 | return { 1400 | Position = i, Velocity = i, Health = 0, 1401 | } 1402 | end, 1403 | function(i: number): Obj 1404 | return { 1405 | Position = i, Velocity = i, Health = 0, 1406 | A = 0, B = 0, C = 0, D = 0 -- dummy fields 1407 | } 1408 | end, 1409 | function(i: number): Obj 1410 | return { 1411 | Position = i, Velocity = i, Health = 0, 1412 | A = 0, B = 0 -- dummy fields 1413 | } 1414 | end, 1415 | function(i: number): Obj 1416 | return { 1417 | Position = i, Velocity = i, Health = 0, 1418 | A = 0, B = 0, C = 0, D = 0, E = 0, F = 0, G = 0 -- dummy fields 1419 | } 1420 | end, 1421 | } 1422 | 1423 | local ctor_n = #ctors 1424 | 1425 | local function create(i): Obj 1426 | return ctors[math.random(1, ctor_n)](i) 1427 | end 1428 | 1429 | --[[ 1430 | 1431 | We garbage collect random tables to make the heap more fragmented, making it 1432 | more difficult to allocate new table objects, as would happen in a practical 1433 | scenario when as a game runs for a long period of time. 1434 | 1435 | ]] 1436 | 1437 | local junk = {} 1438 | 1439 | do 1440 | 1441 | for i = 1, PN*4 do 1442 | junk[i] = create(i) 1443 | end 1444 | 1445 | for i = 1, PN*4 do 1446 | if math.random() > 0.6 then 1447 | junk[i] = nil 1448 | end 1449 | end 1450 | 1451 | gc() 1452 | end 1453 | 1454 | --[[ 1455 | 1456 | Luau allocates same-shape tables in linear pages which will be more cache 1457 | friendly. However, in a practical game program, tables will be constantly 1458 | garbage collected and recreated, which will cause a mismatch and randomness 1459 | between an array of tables vs how those tables are allocated on the heap. 1460 | We try artifically cause this randomness by allocating in a random pattern. 1461 | 1462 | ]] 1463 | 1464 | local permutation = {} 1465 | 1466 | do 1467 | for i = 1, PN do 1468 | permutation[i] = i 1469 | end 1470 | 1471 | for _ = 1, PN do 1472 | local a, b = math.random(1, PN), math.random(1, PN) 1473 | permutation[a], permutation[b] = permutation[b], permutation[a] 1474 | end 1475 | end 1476 | 1477 | BENCH("create entities with 3 components", function() 1478 | for i = 1, PN do 1479 | local idx = permutation[i] 1480 | world[idx] = create(i) 1481 | end 1482 | end) 1483 | 1484 | BENCH("update positions", function() 1485 | for _, obj in world do 1486 | obj.Position += obj.Velocity * 1/60 1487 | end 1488 | end) 1489 | 1490 | BENCH("add tags", function() 1491 | for _, obj in world do 1492 | if obj.Health <= 0 then 1493 | obj.Dead = true 1494 | end 1495 | end 1496 | end) 1497 | 1498 | BENCH("destroy", function() 1499 | --table.clear(world) -- this hangs the program for some reason 1500 | -- forcing a full gc cycle isn't fair \o/ 1501 | gc() 1502 | end) 1503 | end 1504 | -------------------------------------------------------------------------------- /test/tests.luau: -------------------------------------------------------------------------------- 1 | local testkit = require("./testkit") 2 | 3 | local TEST, CASE, CHECK, FINISH = testkit.test() 4 | 5 | local function CHECK_ERR(s: string, fn: (T...) -> (), ...: T...) 6 | local ok, err: string? = pcall(fn, ...) 7 | if CHECK(not ok, 2) then 8 | local i = string.find(err :: string, " ") 9 | assert(i) 10 | local msg = string.sub(err :: string, i+1) 11 | CHECK(msg == s, 2) 12 | end 13 | end 14 | 15 | type Map = { [T]: U } 16 | type Array = { T } 17 | 18 | local ecr = require "../src/ecr" 19 | 20 | local A, B, C, D = ecr.component(), ecr.component(), ecr.component(), ecr.component() 21 | local E, F, G, H = ecr.component(), ecr.component(), ecr.component(), ecr.component(function() return true end) 22 | 23 | local TA, TB, TC, TD = ecr.tag(), ecr.tag(), ecr.tag(), ecr.tag() 24 | 25 | local MAX_VER = ecr._test.max_ver 26 | 27 | local function KEY(id: ecr.entity): number 28 | local key = ecr.inspect(id) 29 | return key 30 | end 31 | 32 | local function VER(id: ecr.entity): number 33 | local _, ver = ecr.inspect(id) 34 | return ver 35 | end 36 | 37 | local function GET_KEY_VERSION(reg: ecr.Registry, key: number): number 38 | return ecr._test.get_key_version(reg, key) 39 | end 40 | 41 | local function CREATE_ID(key: number, ver: number) 42 | return ecr._test.create_id(key, ver) 43 | end 44 | 45 | local function SET_KEY_VERSION_AND_ENSURE_EXISTS(reg: ecr.Registry, key: number, ver: number) 46 | reg:release(reg:create(CREATE_ID(key, 1))) 47 | ecr._test.set_key_version(reg, key, ver) 48 | end 49 | 50 | local function SET_KEY_VERSION(reg: ecr.Registry, key: number, ver: number) 51 | ecr._test.set_key_version(reg, key, ver) 52 | end 53 | 54 | local function BULK_CREATE_IDS(reg: ecr.Registry, n: number): { ecr.entity } 55 | local ids = table.create(n) 56 | for i = 1, n do 57 | ids[i] = reg:create() 58 | end 59 | return ids 60 | end 61 | 62 | local function DEBUG_ID(key_ver: ecr.entity): string 63 | local key = bit32.band(key_ver, 2^16 - 1) 64 | local ver = bit32.rshift(bit32.band(key_ver, bit32.lshift(2^16 - 1, 16)), 16) 65 | return `{key}:{ver}` 66 | end 67 | 68 | local function DEBUG_ENTITY_LIST(reg: ecr.Registry) 69 | local list = reg:storage("list") :: any 70 | local s = {} 71 | table.insert(s, "------------------") 72 | table.insert(s, `free: {list.free}`) 73 | for i = 0, list.capacity - 1 do 74 | local key_ver = buffer.readu32(list.data, i*4) 75 | table.insert(s, `{i}: {DEBUG_ID(key_ver)} {key_ver}`) 76 | end 77 | table.insert(s, "------------------") 78 | print(table.concat(s, "\n")) 79 | end 80 | 81 | local N = 1e3 82 | 83 | TEST("registry:create()", function() 84 | do CASE "new ids unique" 85 | local reg = ecr.registry() 86 | local cache = {} 87 | 88 | for i = 1, N do 89 | local id = reg:create() 90 | CHECK(not cache[id]) 91 | cache[id] = true 92 | end 93 | end 94 | 95 | do CASE "reusing keys produce unique ids" 96 | local reg = ecr.registry() 97 | 98 | local cache = {} 99 | 100 | for i = 1, N do 101 | local id = reg:create() 102 | cache[id] = true 103 | end 104 | 105 | for id in cache do 106 | reg:release(id) 107 | end 108 | 109 | for i = 1, N do 110 | local id = reg:create() 111 | CHECK(not cache[id]) 112 | cache[id] = true 113 | end 114 | end 115 | 116 | do CASE "use specific id" 117 | do -- copy ids from a server registry to a new registry in random order 118 | local sreg = ecr.registry() 119 | 120 | local ids = BULK_CREATE_IDS(sreg, N) 121 | 122 | -- randomize id array order 123 | local mixed = table.clone(ids) 124 | for i = 1, N do 125 | local a = math.random(1, N) 126 | local b = math.random(1, N) 127 | mixed[a], mixed[b] = mixed[b], mixed[a] 128 | end 129 | 130 | local reg = ecr.registry() 131 | 132 | for i = 1, N do 133 | reg:create(mixed[i]) 134 | end 135 | 136 | for i = 1, N do 137 | CHECK(reg:contains(ids[i])) 138 | end 139 | end 140 | 141 | do -- test internal linked list order 142 | local reg = ecr.registry() 143 | reg:create(CREATE_ID(3, 2)) 144 | CHECK(reg:create() == CREATE_ID(1, 1)) 145 | CHECK(reg:create() == CREATE_ID(2, 1)) 146 | CHECK(reg:create() == CREATE_ID(4, 1)) 147 | end 148 | end 149 | 150 | do CASE "reuse previously used id" 151 | do 152 | local reg = ecr.registry() 153 | local id = reg:create() 154 | reg:release(id) 155 | reg:create(id) 156 | CHECK(reg:contains(id)) 157 | end 158 | 159 | do -- test internal linked list order and version tracking 160 | local reg = ecr.registry() 161 | local id1, id2, id3 = reg:create(), reg:create(), reg:create() 162 | local id4 = reg:create() 163 | 164 | reg:release(id4) 165 | reg:release(id3) 166 | reg:release(id2) 167 | reg:release(id1) 168 | 169 | -- 1 -> 2 -> 3 -> 4 170 | reg:create(id4) 171 | -- 1 -> 2 -> 3 172 | reg:create(id2) 173 | -- 1 -> 3 174 | reg:create(id1) 175 | -- 3 176 | local id5 = reg:create(CREATE_ID(5, 1)) 177 | reg:release(id5) 178 | -- 3 -> 5 179 | reg:create(id5) 180 | -- 3 181 | reg:create(id3) 182 | -- [empty] 183 | 184 | CHECK(reg:contains(id1)) 185 | CHECK(reg:contains(id2)) 186 | CHECK(reg:contains(id3)) 187 | CHECK(reg:contains(id4)) 188 | CHECK(reg:contains(id5)) 189 | 190 | CHECK(KEY(id1) == 1) 191 | CHECK(KEY(id2) == 2) 192 | CHECK(KEY(id3) == 3) 193 | CHECK(KEY(id4) == 4) 194 | CHECK(KEY(id5) == 5) 195 | end 196 | end 197 | 198 | do CASE "error if key is already in use" 199 | do 200 | local sreg = ecr.registry() 201 | local id = sreg:create() 202 | 203 | local reg = ecr.registry() 204 | 205 | CHECK(id == reg:create()) 206 | 207 | CHECK_ERR("unable to create entity; key is already in use", function() 208 | reg:create(id) 209 | end) 210 | end 211 | 212 | do -- try force create an id with same key as one in use 213 | local ok, err = pcall(function() 214 | local reg = ecr.registry() 215 | local id1 = reg:create() -- ID(1,1) 216 | reg:destroy(id1) 217 | reg:create() -- ID(1,2) 218 | reg:create(id1) 219 | end) 220 | CHECK(not ok and string.find(err, "already in use")) 221 | end 222 | end 223 | 224 | do CASE "error if null is used" 225 | local reg = ecr.registry() 226 | CHECK_ERR("malformed id", function() 227 | reg:create(ecr.null) 228 | end) 229 | end 230 | 231 | do CASE "entity limit" 232 | -- 2 ids reserved: ctx and null 233 | local LIMIT = 2^16 - 2 234 | 235 | local reg = ecr.registry() 236 | 237 | for i = 1, LIMIT do 238 | reg:create() 239 | end 240 | 241 | CHECK(not reg:contains(ecr.null)) 242 | 243 | CHECK_ERR("cannot create entity; registry is at max entities", function() 244 | reg:create() 245 | end) 246 | end 247 | 248 | do CASE "partitions respected" 249 | local sreg = ecr.registry(1, 100) 250 | local reg = ecr.registry(101, 200) 251 | 252 | local ids = {} 253 | 254 | for i = 1, 100 do 255 | ids[i] = sreg:create() 256 | end 257 | CHECK_ERR("cannot create entity; registry is at max entities", function() 258 | sreg:create() 259 | end) 260 | 261 | for i = 1, 100 do 262 | reg:create() 263 | end 264 | CHECK_ERR("cannot create entity; registry is at max entities", function() 265 | reg:create() 266 | end) 267 | 268 | for _, id in ids do 269 | reg:create(id) 270 | end 271 | 272 | for _, id in ids do 273 | CHECK(reg:contains(id)) 274 | end 275 | 276 | -- check if entity outside partition is not readded to list 277 | reg:destroy(ids[1]) 278 | CHECK_ERR("cannot create entity; registry is at max entities", function() 279 | reg:create() 280 | end) 281 | 282 | end 283 | 284 | do CASE "partition edge cases" 285 | CHECK(not pcall(function() ecr.registry(0, 1) end)) 286 | CHECK(not pcall(function() ecr.registry(2, 1) end)) 287 | CHECK(not pcall(function() ecr.registry(1, 2^16-1) end)) 288 | CHECK(pcall(function() ecr.registry(1, 2^16-2) end)) 289 | 290 | do 291 | local reg = ecr.registry(1000, 1000) 292 | reg:create() 293 | CHECK_ERR("cannot create entity; registry is at max entities", function() 294 | reg:create() 295 | end) 296 | end 297 | 298 | do -- test linked list traversal branch 299 | local reg = ecr.registry(901, 1000) 300 | reg:create(CREATE_ID(1000, 1)) -- create last entity to initialize linked list of entire partition 301 | SET_KEY_VERSION(reg, 950, 10) 302 | reg:destroy(reg:create(CREATE_ID(950, 2))) -- create entity in middle of list then destroy 303 | CHECK(reg:create() == CREATE_ID(950, 3)) -- pop head which was previously destroyed entity 304 | for i = 1, 49 do -- check linked list is maintained 305 | CHECK(reg:create() == CREATE_ID(900 + i, 1)) 306 | end 307 | end 308 | end 309 | end) 310 | 311 | TEST("registry:release()", function() 312 | local reg = ecr.registry() 313 | 314 | do CASE "released entities invalid" 315 | local id = reg:create() 316 | reg:release(id) 317 | CHECK(not reg:contains(id)) 318 | end 319 | 320 | do CASE "release invalid id" 321 | local id = reg:create() 322 | reg:release(id) 323 | CHECK_ERR("invalid entity", function() 324 | reg:release(id) 325 | end) 326 | end 327 | 328 | do CASE "release null id" 329 | CHECK_ERR("invalid entity", function() 330 | reg:release(ecr.null) 331 | end) 332 | end 333 | end) 334 | 335 | TEST("registry:contains()", function() 336 | local reg = ecr.registry() 337 | 338 | do 339 | local id = reg:create() 340 | 341 | do CASE "new entity valid" 342 | CHECK(reg:contains(id)) 343 | end 344 | 345 | reg:release(id) 346 | 347 | do CASE "released entity invalid" 348 | CHECK(not reg:contains(id)) 349 | end 350 | 351 | local newid = reg:create() 352 | 353 | do CASE "id with reused key valid" 354 | CHECK(reg:contains(newid)) 355 | end 356 | 357 | do CASE "released entity still invalid despite key reuse" 358 | CHECK(not reg:contains(id)) 359 | end 360 | 361 | do CASE "id that has not been created yet" 362 | local sreg = ecr.registry() 363 | for i = 1, 10 do sreg:create() end 364 | local id2 = sreg:create() 365 | CHECK(not reg:contains(id2)) 366 | end 367 | end 368 | 369 | do CASE "integer invalid" 370 | local id = reg:create() 371 | assert(KEY(id) == 2) 372 | CHECK(not reg:contains(2)) 373 | end 374 | 375 | do CASE "null entity invalid" 376 | CHECK(not reg:contains(ecr.null)) 377 | end 378 | end) 379 | 380 | TEST("registry:version()", function() 381 | local reg = ecr.registry() 382 | 383 | do CASE "get version" 384 | local id = reg:create() 385 | CHECK(VER(id) == 1) 386 | end 387 | 388 | do CASE "version increments on reuse" 389 | local id_old = reg:create() 390 | CHECK(VER(id_old) == 1) 391 | reg:destroy(id_old) 392 | local id_new = reg:create() 393 | CHECK(VER(id_old) == 1) 394 | CHECK(VER(id_new) == 2) 395 | end 396 | end) 397 | 398 | TEST("registry:current()", function() 399 | local reg = ecr.registry() 400 | local id_old = reg:create() 401 | reg:release(id_old) 402 | local id_new = reg:create() 403 | 404 | do CASE "get current version" 405 | CHECK(VER(id_old) == 1) 406 | CHECK(GET_KEY_VERSION(reg, KEY(id_old)) == 2) 407 | end 408 | 409 | do CASE "version increments on release" 410 | reg:release(id_new) 411 | CHECK(GET_KEY_VERSION(reg, KEY(id_new)) == 3) 412 | end 413 | end) 414 | 415 | TEST("registry:add()", function() 416 | local ADD_A = ecr.component(function() return true end) 417 | local ADD_B = ecr.component(function() return false end) 418 | local ADD_C = ecr.component(function() return nil end) 419 | 420 | local reg = ecr.registry() 421 | 422 | do CASE "add components" 423 | local id = reg:create() 424 | reg:add(id, ADD_A, ADD_B) 425 | CHECK(reg:has(id, ADD_A)) 426 | CHECK(reg:has(id, ADD_B)) 427 | end 428 | 429 | do CASE "add an already added component does nothing" 430 | local id = reg:create() 431 | reg:set(id, ADD_A, false) 432 | reg:add(id, ADD_A) 433 | CHECK(reg:get(id, ADD_A) == false) 434 | end 435 | 436 | do CASE "add a component with no constructor errors" 437 | CHECK_ERR("no constructor defined for component (arg #1)", function() 438 | local id = reg:create() 439 | reg:add(id, B) 440 | end) 441 | end 442 | 443 | do CASE "constructor returning nil errors" 444 | CHECK_ERR("component (arg #1) constructor did not return a value", function() 445 | local id = reg:create() 446 | reg:add(id, ADD_C) 447 | end) 448 | end 449 | 450 | do CASE "add component to invalid id with unused key" 451 | CHECK_ERR("invalid entity", function() 452 | local id = reg:create() 453 | reg:release(id) 454 | reg:add(id, ADD_A) 455 | end) 456 | end 457 | 458 | do CASE "add component to invalid id with used key" 459 | CHECK_ERR("invalid entity", function() 460 | local id = reg:create() 461 | reg:release(id) 462 | reg:create() -- reuse key 463 | reg:add(id, ADD_A) 464 | end) 465 | end 466 | 467 | do CASE "add component to invalid id with used key with component" 468 | CHECK_ERR("invalid entity", function() 469 | local id = reg:create() 470 | reg:release(id) 471 | local newid = reg:create() -- reuse key 472 | reg:add(newid, ADD_A) 473 | reg:add(id, A) 474 | end) 475 | end 476 | 477 | do CASE "add component to null entity" 478 | CHECK_ERR("invalid entity", function() 479 | reg:add(ecr.null, ADD_A) 480 | end) 481 | end 482 | end) 483 | 484 | TEST("registry:set()", function() 485 | local reg = ecr.registry() 486 | 487 | do 488 | local id = reg:create() 489 | 490 | do CASE "add component" 491 | reg:set(id, A, 1) 492 | CHECK(reg:get(id, A) == 1) 493 | end 494 | 495 | do CASE "change component" 496 | reg:set(id, A, 2) 497 | CHECK(reg:get(id, A) == 2) 498 | end 499 | 500 | do CASE "remove component" 501 | reg:remove(id, A) 502 | CHECK(reg:try_get(id, A) == nil) 503 | CHECK(not reg:has(id, A)) 504 | end 505 | end 506 | 507 | do CASE "add component to invalid id with unused key" 508 | CHECK_ERR("invalid entity", function() 509 | local id = reg:create() 510 | reg:release(id) 511 | reg:set(id, A, 1) 512 | end) 513 | end 514 | 515 | do CASE "add component to invalid id with used key" 516 | CHECK_ERR("invalid entity", function() 517 | local id = reg:create() 518 | reg:release(id) 519 | reg:create() -- reuse key 520 | reg:set(id, A, 1) 521 | end) 522 | end 523 | 524 | do CASE "add component to invalid id with used key that has component" 525 | CHECK_ERR("invalid entity", function() 526 | local id = reg:create() 527 | reg:release(id) 528 | 529 | local newid = reg:create() -- reuse key 530 | reg:set(newid, A, 0) 531 | 532 | reg:set(id, A, 1) 533 | end) 534 | end 535 | 536 | do CASE "add component to null entity" 537 | CHECK_ERR("invalid entity", function() 538 | reg:set(ecr.null, A, true) 539 | end) 540 | end 541 | 542 | do CASE "change component of invalid entity with used key with component" 543 | CHECK_ERR("invalid entity", function() 544 | local id = reg:create() 545 | reg:release(id) 546 | 547 | local newid = reg:create() -- reuse key 548 | reg:set(newid, A, 1) 549 | 550 | reg:set(id, A, 2) 551 | end) 552 | end 553 | 554 | do CASE "attempt to set nil value" 555 | CHECK_ERR("cannot set component value to nil", function() 556 | local id = reg:create() 557 | reg:set(id, A, nil) 558 | end) 559 | end 560 | 561 | do CASE "set tag" 562 | local id = reg:create() 563 | reg:set(id, TA, nil) 564 | CHECK(reg:has(id, TA)) 565 | end 566 | end) 567 | 568 | TEST("registry:get()", function() 569 | local reg = ecr.registry() 570 | 571 | do 572 | local id = reg:create() 573 | 574 | do CASE "get component" 575 | reg:set(id, A, 1) 576 | CHECK(reg:get(id, A) == 1) 577 | end 578 | 579 | do CASE "get multiple components" 580 | reg:set(id, B, 2) 581 | reg:set(id, C, 3) 582 | reg:set(id, D, 4) 583 | 584 | local values = { reg:get(id, A, B, C, D) } 585 | 586 | for i, value in values do 587 | CHECK(value == i) 588 | end 589 | end 590 | 591 | do CASE "get nil component" 592 | reg:remove(id, A) 593 | CHECK_ERR("entity does not have component (arg #1)", function() 594 | reg:get(id, A) 595 | end) 596 | end 597 | end 598 | 599 | do CASE "get component of invalid id" 600 | local id = reg:create() 601 | reg:release(id) 602 | CHECK_ERR("entity does not have component (arg #1)", function() 603 | reg:get(id, A) 604 | end) 605 | end 606 | 607 | do CASE "get component of invalid id with used key that has component" 608 | local id = reg:create() 609 | reg:release(id) 610 | 611 | local newid = reg:create() 612 | reg:set(newid, A, 1) 613 | 614 | CHECK_ERR("invalid entity", function() 615 | reg:get(id, A) 616 | end) 617 | end 618 | end) 619 | 620 | TEST("registry:try_get()", function() 621 | local reg = ecr.registry() 622 | 623 | do CASE "get nil component" 624 | local id = reg:create() 625 | reg:remove(id, A) 626 | CHECK(reg:try_get(id, A) == nil) 627 | end 628 | 629 | do CASE "get component of invalid id" 630 | local id = reg:create() 631 | reg:release(id) 632 | CHECK(reg:try_get(id, A) == nil) 633 | end 634 | 635 | do CASE "get component of invalid id with used key that has component" 636 | local id = reg:create() 637 | reg:release(id) 638 | 639 | local newid = reg:create() 640 | reg:set(newid, A, 1) 641 | 642 | CHECK(reg:try_get(id, A) == nil) 643 | end 644 | end) 645 | 646 | TEST("registry:has()", function() 647 | local reg = ecr.registry() 648 | 649 | do 650 | local id = reg:create() 651 | 652 | do CASE "has component" 653 | reg:set(id, A, true) 654 | CHECK(reg:has(id, A) == true) 655 | 656 | reg:set(id, B, true) 657 | CHECK(reg:has(id, A, B)) 658 | end 659 | 660 | do CASE "does not have component" 661 | reg:remove(id, A) 662 | CHECK(reg:has(id, A) == false) 663 | CHECK(reg:has(id, B, A) == false) 664 | end 665 | end 666 | 667 | do CASE "invalid entity" 668 | local id = reg:create() 669 | reg:release(id) 670 | CHECK(reg:has(id, A) == false) 671 | end 672 | 673 | do CASE "invalid entity with used key that has component" 674 | local id = reg:create() 675 | reg:release(id) 676 | 677 | local newid = reg:create() 678 | reg:set(newid, A, 1) 679 | 680 | CHECK(reg:has(id, A) == false) 681 | end 682 | end) 683 | 684 | TEST("registry:insert()", function() 685 | local ARRAY = ecr.component() :: {number} 686 | local reg = ecr.registry() 687 | local id = reg:create() 688 | 689 | do CASE "insert adds array if no value" 690 | reg:insert(id, ARRAY, 1) 691 | 692 | CHECK(reg:has(id, ARRAY)) 693 | CHECK(type(reg:get(id, ARRAY)) == "table") 694 | CHECK(reg:get(id, ARRAY)[1] == 1) 695 | end 696 | 697 | do CASE "insert adds values to the end" 698 | reg:insert(id, ARRAY, 2) 699 | 700 | CHECK(reg:has(id, ARRAY)) 701 | CHECK(type(reg:get(id, ARRAY)) == "table") 702 | CHECK(reg:get(id, ARRAY)[1] == 1) 703 | CHECK(reg:get(id, ARRAY)[2] == 2) 704 | end 705 | 706 | end) 707 | 708 | TEST("registry:patch()", function() 709 | local reg = ecr.registry() 710 | 711 | do CASE "change component" 712 | local id = reg:create() 713 | reg:set(id, A, 0) 714 | 715 | local ran = false 716 | reg:on_change(A):connect(function() 717 | ran = true 718 | end) 719 | 720 | reg:patch(id, A :: number, function(v) 721 | CHECK(v == 0) 722 | return v + 1 723 | end) 724 | CHECK(reg:get(id, A) == 1) 725 | CHECK(ran) 726 | end 727 | 728 | do CASE "patch returns nil" 729 | local id = reg:create() 730 | reg:set(id, A, true) 731 | 732 | CHECK_ERR("function cannot return nil", function() 733 | reg:patch(id, A :: number, function(v) 734 | return nil :: any 735 | end) 736 | end) 737 | end 738 | 739 | do CASE "change non-existent component" 740 | local id = reg:create() 741 | 742 | CHECK_ERR("entity does not have component and no constructor for component (unknown)", function() 743 | reg:patch(id, A :: number, function(v) 744 | CHECK(false) 745 | return v + 1 746 | end) 747 | end) 748 | end 749 | 750 | do CASE "add non-existent component without constructor" 751 | local id = reg:create() 752 | 753 | local ran = false 754 | reg:on_add(H):connect(function() 755 | ran = true 756 | end) 757 | 758 | reg:patch(id, H) 759 | 760 | CHECK(ran) 761 | CHECK(reg:get(id, H) == true) 762 | end 763 | 764 | do CASE "change non-existent component with constructor" 765 | local id = reg:create() 766 | 767 | local ran = false 768 | reg:on_add(H):connect(function() 769 | ran = true 770 | end) 771 | 772 | reg:patch(id, H, function(v) 773 | CHECK(v == true) 774 | return not v 775 | end) 776 | 777 | CHECK(ran) 778 | CHECK(reg:get(id, H) == false) 779 | 780 | CHECK_ERR("function cannot return nil", function() 781 | reg:patch(id, H, function(v) 782 | return nil :: any 783 | end) 784 | end) 785 | end 786 | 787 | do CASE "invalid id" 788 | local id = reg:create() 789 | reg:release(id) 790 | 791 | CHECK_ERR("invalid entity", function() 792 | reg:patch(id, A :: number, function(v) 793 | CHECK(false) 794 | return v + 1 795 | end) 796 | end) 797 | end 798 | 799 | do CASE "invalid id that shares same key with id that has component" 800 | local id = reg:create() 801 | reg:release(id) 802 | 803 | local newid = reg:create() 804 | reg:set(newid, A, 1) 805 | 806 | CHECK_ERR("invalid entity", function() 807 | reg:patch(id, A :: number, function(v) 808 | CHECK(false) 809 | return v + 1 810 | end) 811 | end) 812 | end 813 | end) 814 | 815 | TEST("registry:remove()", function() 816 | local reg = ecr.registry() 817 | 818 | do 819 | local id = reg:create() 820 | reg:set(id, A, 1) 821 | reg:set(id, B, 1) 822 | reg:set(id, C, 1) 823 | 824 | local id2 = reg:create() 825 | reg:set(id2, A, 2) 826 | 827 | do CASE "remove single component" 828 | reg:remove(id, A) 829 | CHECK(reg:has(id, A) == false) 830 | CHECK(reg:get(id2, A) == 2) -- check removal maintains associativity 831 | end 832 | 833 | do CASE "remove multiple components" 834 | reg:remove(id, B, C) 835 | CHECK(reg:has(id, B) == false) 836 | CHECK(reg:has(id, C) == false) 837 | end 838 | end 839 | 840 | do CASE "remove component from invalid entity" 841 | local id = reg:create() 842 | reg:release(id) 843 | 844 | local ok = pcall(function() 845 | reg:remove(id, A) 846 | end) 847 | 848 | CHECK(ok) 849 | end 850 | 851 | do CASE "remove component from invalid id that shares key with entity that has component" 852 | local id = reg:create() 853 | reg:release(id) 854 | 855 | local newid = reg:create() 856 | reg:set(newid, A, 1) 857 | 858 | --CHECK_ERR("invalid entity", function() 859 | --reg:remove(id, A) 860 | --end) 861 | 862 | reg:remove(id, A) 863 | 864 | CHECK(reg:has(newid, A)) 865 | end 866 | end) 867 | 868 | TEST("registry:find()", function() 869 | local reg = ecr.registry() 870 | 871 | local ids = BULK_CREATE_IDS(reg, 5) 872 | 873 | for i, id in ids do 874 | reg:set(id, A, i) 875 | reg:set(id, TA) 876 | end 877 | 878 | CHECK(reg:find(A, 1) == ids[1]) 879 | CHECK(reg:find(A, 3) == ids[3]) 880 | CHECK(reg:find(A, 5) == ids[5]) 881 | 882 | CHECK(reg:find(A, 6) == nil) 883 | reg:clear(A) 884 | CHECK(reg:find(A, 1) == nil) 885 | 886 | CHECK(reg:find(TA) == ids[1]) 887 | reg:clear(TA) 888 | CHECK(reg:find(TA) == nil) 889 | end) 890 | 891 | TEST("registry:clear()", function() 892 | do 893 | local reg = ecr.registry() 894 | local id1 = reg:create() 895 | local id2 = reg:create() 896 | 897 | reg:set(id1, A, 1) 898 | reg:set(id2, A, 2) 899 | 900 | reg:set(id1, B, 1) 901 | reg:set(id2, B, 2) 902 | 903 | reg:set(id1, C, 1) 904 | reg:set(id2, C, 2) 905 | 906 | do CASE "clear A" 907 | reg:clear(A) 908 | CHECK(reg:has(id1, A) == false) 909 | CHECK(reg:has(id2, A) == false) 910 | end 911 | 912 | do CASE "clear B and C" 913 | reg:clear(B, C) 914 | CHECK(reg:has(id1, B) == false) 915 | CHECK(reg:has(id2, B) == false) 916 | CHECK(reg:has(id1, C) == false) 917 | CHECK(reg:has(id2, C) == false) 918 | end 919 | end 920 | 921 | do CASE "clear entities" 922 | local reg = ecr.registry() 923 | 924 | local ids = table.create(N) 925 | 926 | -- set key = 1 version to max 927 | SET_KEY_VERSION_AND_ENSURE_EXISTS(reg, 1, MAX_VER) 928 | 929 | for i = 1, N do 930 | ids[i] = reg:create() 931 | end 932 | 933 | reg:clear() 934 | 935 | local ids_after = BULK_CREATE_IDS(reg, N) 936 | 937 | for i = 1, N do 938 | if i == 1 then 939 | CHECK(ids_after[i] == CREATE_ID(1, 1)) 940 | else 941 | CHECK(VER(ids[i]) + 1 == VER(ids_after[i])) 942 | end 943 | end 944 | end 945 | 946 | do CASE "clear with groups" 947 | local reg = ecr.registry() 948 | reg:group(A, B) 949 | 950 | local listener = { count = 0 } :: { id: number?, count: number } 951 | 952 | reg:on_remove(A):connect(function(id) 953 | listener.id = id 954 | listener.count += 1 955 | end) 956 | 957 | local eB = reg:handle():set(B, 2) 958 | local eAB = reg:handle():set(A, 1):set(B, 1) 959 | 960 | reg:clear(A) 961 | 962 | CHECK(reg:storage(A).size == 0) 963 | CHECK(listener.id == eAB.entity and listener.count == 1) 964 | 965 | eAB:set(A, 1) -- 1 966 | eB:set(A, 2) -- 2 967 | 968 | eAB:remove(A) 969 | 970 | CHECK(reg:storage(A).size == 1) 971 | local pool = reg:storage(A) 972 | local entities = ecr.buffer_to_array(pool.entities, pool.size) 973 | CHECK(entities[1] == eB.entity) 974 | end 975 | end) 976 | 977 | TEST("registry:has_none()", function() 978 | local reg = ecr.registry() 979 | local id = reg:create() 980 | 981 | do CASE "new entity is has none" 982 | CHECK(reg:has_none(id)) 983 | end 984 | 985 | reg:set(id, B, 1) 986 | 987 | do CASE "no longer has none after assigning component" 988 | CHECK(not reg:has_none(id)) 989 | end 990 | 991 | reg:remove(id, B) 992 | 993 | do CASE "has none after removing last component" 994 | CHECK(reg:has_none(id)) 995 | end 996 | 997 | reg:release(id) -- UB 998 | 999 | do CASE "rejects invalid entity" 1000 | CHECK_ERR("invalid entity", function() 1001 | reg:has_none(id) 1002 | end) 1003 | end 1004 | end) 1005 | 1006 | TEST("registry:destroy()", function() 1007 | local reg = ecr.registry() 1008 | 1009 | do CASE "component was removed" 1010 | local id = reg:create() 1011 | 1012 | reg:set(id, A, 1) 1013 | reg:set(id, B, 1) 1014 | 1015 | reg:destroy(id) 1016 | 1017 | -- do not rely on this behavior 1018 | CHECK(not reg:contains(id)) 1019 | CHECK(not reg:has(id, A)) 1020 | CHECK(not reg:has(id, B)) 1021 | end 1022 | 1023 | do CASE "destroy invalid id" 1024 | local id = reg:create() 1025 | reg:release(id) 1026 | 1027 | CHECK_ERR("invalid entity", function() 1028 | reg:destroy(id) 1029 | end) 1030 | end 1031 | end) 1032 | 1033 | TEST("#registry", function() 1034 | local reg = ecr.registry() 1035 | 1036 | do CASE "initial registry size is 0" 1037 | CHECK(#reg:view(ecr.entity) == 0) 1038 | end 1039 | 1040 | local id = reg:create() 1041 | 1042 | do CASE "size after entity creation" 1043 | CHECK(#reg:view(ecr.entity) == 1) 1044 | end 1045 | 1046 | reg:release(id) 1047 | 1048 | do CASE "size after entity release" 1049 | CHECK(#reg:view(ecr.entity) == 0) 1050 | end 1051 | 1052 | for i = 1, 100 do reg:create() end 1053 | 1054 | do CASE "mass creation" 1055 | CHECK(#reg:view(ecr.entity) == 100) 1056 | end 1057 | 1058 | reg:clear() 1059 | 1060 | do CASE "size after clear" 1061 | CHECK(#reg:view(ecr.entity) == 0) 1062 | end 1063 | 1064 | reg:create(id) 1065 | 1066 | do CASE "size after specific entity creation" 1067 | CHECK(#reg:view(ecr.entity) == 1) 1068 | end 1069 | 1070 | do CASE "size after context entity creation" 1071 | reg:context() 1072 | CHECK(#reg:view(ecr.entity) == 2) 1073 | end 1074 | end) 1075 | 1076 | TEST("registry:view(ecr.entity)", function() 1077 | do 1078 | local reg = ecr.registry() 1079 | 1080 | local ids = BULK_CREATE_IDS(reg, N) 1081 | 1082 | -- destroy random entities 1083 | for i = 1, N do 1084 | if math.random() > 0.5 then 1085 | reg:destroy(ids[i]) 1086 | ids[i] = nil 1087 | end 1088 | end 1089 | 1090 | do CASE "all entities are valid" 1091 | local cache = {} 1092 | 1093 | for entity in reg:view(ecr.entity) do 1094 | cache[entity] = true 1095 | end 1096 | 1097 | for _, entity in ids do 1098 | CHECK(cache[entity]) 1099 | end 1100 | end 1101 | end 1102 | end) 1103 | 1104 | TEST("registry:view()", function() 1105 | local function flip() 1106 | return math.random() > 0.3 1107 | end 1108 | 1109 | local function empty(t) 1110 | return next(t) == nil 1111 | end 1112 | 1113 | local function getids(view: ecr.View<...unknown>): { [ecr.entity]: { unknown } } 1114 | local cache = {} 1115 | 1116 | for id, a, b, c, d in view do 1117 | cache[id] = { a, b, c, d } 1118 | end 1119 | 1120 | return cache 1121 | end 1122 | 1123 | local reg = ecr.registry() 1124 | 1125 | local ids = {} 1126 | local As = {} 1127 | local Bs = {} 1128 | local Cs = {} 1129 | local ABs = {} 1130 | local ABCs = {} 1131 | local ABCDs = {} 1132 | local AnotCs = {} 1133 | local ABnotCs = {} 1134 | local AnotBCs = {} 1135 | local notABs = {} 1136 | 1137 | for i = 1, N do 1138 | local id = reg:create() 1139 | ids[i] = id 1140 | 1141 | local has = {} 1142 | for _, component in { A, B, C, D, E, F, G, H } do 1143 | if flip() then 1144 | reg:set(id, component, id * component) 1145 | has[component] = true 1146 | end 1147 | end 1148 | 1149 | if has[A] then table.insert(As, id) end 1150 | if has[B] then table.insert(Bs, id) end 1151 | if has[C] then table.insert(Cs, id) end 1152 | if has[A] and not has[C] then table.insert(AnotCs, id) end 1153 | if has[A] and has[B] then table.insert(ABs, id) end 1154 | if has[A] and has[B] and has[C] then table.insert(ABCs, id) end 1155 | if has[A] and has[B] and not has[C] then table.insert(ABnotCs, id) end 1156 | if has[A] and not has[B] and not has[C] then table.insert(AnotBCs, id) end 1157 | if has[A] and has[B] and has[C] and has[D] then table.insert(ABCDs, id) end 1158 | if not has[A] and not has[B] then table.insert(notABs, id) end 1159 | end 1160 | 1161 | do CASE "view size" 1162 | CHECK(#reg:view(A) == #As) 1163 | CHECK(#reg:view(B) == #Bs) 1164 | CHECK(#reg:view(A, B, C) >= #ABCs) 1165 | CHECK(#reg:view(A, B):exclude(C) >= #ABnotCs) 1166 | end 1167 | 1168 | do CASE "view all entities with A" 1169 | local viewed = getids(reg:view(A)) 1170 | CHECK(not empty(viewed)) 1171 | for _, id in As do 1172 | local v = viewed[id] 1173 | if not CHECK(v) then continue end 1174 | CHECK(v[1] == id * A) 1175 | end 1176 | end 1177 | 1178 | do CASE "view all entities with B" 1179 | local viewed = getids(reg:view(B)) 1180 | CHECK(not empty(viewed)) 1181 | for _, id in Bs do 1182 | local v = viewed[id] 1183 | if not CHECK(v) then continue end 1184 | CHECK(v[1] == id * B) 1185 | end 1186 | end 1187 | 1188 | do CASE "view all entities with AB" 1189 | local viewed = getids(reg:view(A, B)) 1190 | CHECK(not empty(viewed)) 1191 | for _, id in ABs do 1192 | local v = viewed[id] 1193 | if not CHECK(v) then continue end 1194 | CHECK(v[1] == id * A) 1195 | CHECK(v[2] == id * B) 1196 | end 1197 | end 1198 | 1199 | do CASE "view all entities with ABC" 1200 | local viewed = getids(reg:view(A, B, C)) 1201 | CHECK(not empty(viewed)) 1202 | for _, id in ABCs do 1203 | local v = viewed[id] 1204 | if not CHECK(v) then continue end 1205 | CHECK(v[1] == id * A) 1206 | CHECK(v[2] == id * B) 1207 | CHECK(v[3] == id * C) 1208 | end 1209 | end 1210 | 1211 | do CASE "view all entities with ABCD" 1212 | local viewed = getids(reg:view(A, B, C, D)) 1213 | CHECK(not empty(viewed)) 1214 | for _, id in ABCDs do 1215 | local v = viewed[id] 1216 | if not CHECK(v) then continue end 1217 | CHECK(v[1] == id * A) 1218 | CHECK(v[2] == id * B) 1219 | CHECK(v[3] == id * C) 1220 | CHECK(v[4] == id * D) 1221 | end 1222 | end 1223 | 1224 | -- do CASE "view all entities with ABC using C" 1225 | -- local viewed = getids(reg:view(A, B, C):use(C)) 1226 | -- CHECK(not empty(viewed)) 1227 | -- for _, id in ABCs do 1228 | -- local v = viewed[id] 1229 | -- if not CHECK(v) then continue end 1230 | -- CHECK(v[1] == id) 1231 | -- CHECK(v[2] == id) 1232 | -- CHECK(v[3] == id) 1233 | -- end 1234 | -- end 1235 | 1236 | do CASE "view all entities with A and not C" 1237 | local viewed = getids(reg:view(A):exclude(C)) 1238 | CHECK(not empty(viewed)) 1239 | for _, id in AnotCs do 1240 | local v = viewed[id] 1241 | if not CHECK(v) then continue end 1242 | CHECK(v[1] == id * A) 1243 | end 1244 | for id in viewed do 1245 | CHECK(not reg:has(id, C)) 1246 | end 1247 | end 1248 | 1249 | do CASE "view all entities with AB and not C" 1250 | local viewed = getids(reg:view(A, B):exclude(C)) 1251 | CHECK(not empty(viewed)) 1252 | for _, id in ABnotCs do 1253 | local v = viewed[id] 1254 | if not CHECK(v) then continue end 1255 | CHECK(v[1] == id * A) 1256 | CHECK(v[2] == id * B) 1257 | end 1258 | for id in viewed do 1259 | CHECK(not reg:has(id, C)) 1260 | end 1261 | end 1262 | 1263 | do CASE "view all entities with A and not BC" 1264 | 1265 | local viewed = getids(reg:view(A):exclude(B, C)) 1266 | CHECK(not empty(viewed)) 1267 | for _, id in AnotBCs do 1268 | local v = viewed[id] 1269 | if not CHECK(v) then continue end 1270 | CHECK(v[1] == id * A) 1271 | end 1272 | for id in viewed do 1273 | CHECK(not reg:has(id, B)) 1274 | CHECK(not reg:has(id, C)) 1275 | end 1276 | end 1277 | 1278 | do CASE "view all entities without AB" 1279 | local viewed = getids(reg:view(ecr.entity):exclude(A, B)) 1280 | CHECK(not empty(viewed)) 1281 | for _, id in notABs do 1282 | local v = viewed[id] 1283 | if not CHECK(v) then continue end 1284 | end 1285 | for id in viewed do 1286 | CHECK(not reg:has(id, A)) 1287 | CHECK(not reg:has(id, B)) 1288 | end 1289 | end 1290 | 1291 | do CASE "addition during view iteration" 1292 | local cache = {} 1293 | for id, c in reg:view(C) do 1294 | -- check that newly added entities+components are not included in iterations 1295 | CHECK(not cache[id]) 1296 | if flip() then -- create new entity+component 1297 | local new = reg:create() 1298 | reg:set(new, C, true) 1299 | table.insert(Cs, id) 1300 | cache[new] = true 1301 | end 1302 | end 1303 | end 1304 | 1305 | do CASE "component removal during view iteration" 1306 | local viewed = {} 1307 | for id in reg:view(A) do 1308 | if flip() then 1309 | reg:remove(id, A) 1310 | table.remove(As, table.find(As, id)) 1311 | end 1312 | viewed[id] = true 1313 | end 1314 | CHECK(not empty(viewed)) 1315 | for _, entity in As do -- check that an entity isn't skipped due to reordering interally 1316 | CHECK(viewed[entity]) 1317 | end 1318 | end 1319 | 1320 | -- do CASE "duplicate include errors" 1321 | -- local ok = pcall(function() 1322 | -- reg:view(A, B, A) 1323 | -- end) 1324 | -- CHECK(not ok) 1325 | -- end 1326 | 1327 | do CASE "exclude an include errors" 1328 | local ok = pcall(function() 1329 | reg:view(A, B):exclude(C, A) 1330 | end) 1331 | CHECK(not ok) 1332 | end 1333 | 1334 | do CASE "view patch single" 1335 | local expected_values = {} 1336 | 1337 | for id, v in reg:view(A) do 1338 | expected_values[id] = v + 1 1339 | end 1340 | 1341 | reg:view(A :: number):patch(function(v) return v + 1 end) 1342 | 1343 | for id, v in reg:view(A) do 1344 | CHECK(v == expected_values[id]) 1345 | end 1346 | end 1347 | 1348 | do CASE "view patch double" 1349 | local expected_values = {} 1350 | 1351 | for id, a, b in reg:view(A, B) do 1352 | expected_values[id] = { a + 1, b + 2 } 1353 | end 1354 | 1355 | reg:view(A :: number, B :: number):patch(function(a, b) return a + 1, b + 2 end) 1356 | 1357 | for id, a, b in reg:view(A, B) do 1358 | CHECK(a == expected_values[id][1]) 1359 | CHECK(b == expected_values[id][2]) 1360 | end 1361 | end 1362 | 1363 | do CASE "view patch double (reverse query order)" 1364 | local expected_values = {} 1365 | 1366 | for id, a, b in reg:view(B, A) do 1367 | expected_values[id] = { a + 1, b + 2 } 1368 | end 1369 | 1370 | reg:view(B :: number, A :: number):patch(function(a, b) return a + 1, b + 2 end) 1371 | 1372 | for id, a, b in reg:view(B, A) do 1373 | CHECK(a == expected_values[id][1]) 1374 | CHECK(b == expected_values[id][2]) 1375 | end 1376 | end 1377 | 1378 | do CASE "view patch multi" 1379 | local expected_values = {} 1380 | 1381 | for id, a, b, c in reg:view(A, B, C) do 1382 | expected_values[id] = { a + 1, b + 2, c + 3 } 1383 | end 1384 | 1385 | reg:view(A :: number, B :: number, C :: number):patch(function(a, b, c) 1386 | return a + 1, b + 2, c + 3 1387 | end) 1388 | 1389 | for id, a, b, c in reg:view(A, B, C) do 1390 | CHECK(a == expected_values[id][1]) 1391 | CHECK(b == expected_values[id][2]) 1392 | CHECK(c == expected_values[id][3]) 1393 | end 1394 | end 1395 | end) 1396 | 1397 | TEST("registry:on_add()", function() 1398 | local reg = ecr.registry() 1399 | 1400 | -- runcount, reg, entity, value 1401 | local cc, ce, cv = 0, nil, nil 1402 | 1403 | local function fn(...) 1404 | cc += 1 1405 | ce, cv = ... 1406 | end 1407 | 1408 | do 1409 | local con = reg:on_add(H):connect(fn) 1410 | local id = reg:create() 1411 | 1412 | CASE "adding triggers" 1413 | reg:add(id, H) 1414 | CHECK(cc == 1) 1415 | CHECK(ce == id) 1416 | CHECK(cv == true) 1417 | 1418 | CASE "changing does not trigger" 1419 | reg:set(id, H, false) 1420 | CHECK(cc == 1) 1421 | 1422 | CASE "removing then adding triggers" 1423 | reg:remove(id, H) 1424 | reg:set(id, H, true) 1425 | CHECK(cc == 2) 1426 | 1427 | con:disconnect() 1428 | 1429 | CASE "adding after disconnect does not trigger" 1430 | reg:remove(id, H) 1431 | reg:set(id, H, true) 1432 | CHECK(cc == 2) 1433 | end 1434 | 1435 | do CASE "entity creation" 1436 | reg:on_add(ecr.entity):connect(fn) 1437 | 1438 | local id = reg:create() 1439 | 1440 | CHECK(cc == 3) 1441 | CHECK(ce == id) 1442 | 1443 | reg:release(id) 1444 | reg:create(id) 1445 | 1446 | CHECK(cc == 4) 1447 | CHECK(ce == id) 1448 | end 1449 | end) 1450 | 1451 | TEST("registry:on_change()", function() 1452 | local reg = ecr.registry() 1453 | local id = reg:create() 1454 | 1455 | local cc, ce, cv, ov = 0, nil, nil, nil 1456 | 1457 | local function fn(...) 1458 | cc += 1 1459 | ce, cv = ... 1460 | ov = reg:get(..., A) 1461 | end 1462 | 1463 | local con = reg:on_change(A):connect(fn) 1464 | 1465 | CASE "adding does not trigger" 1466 | reg:set(id, A, true) 1467 | CHECK(cc == 0) 1468 | 1469 | CASE "changing triggers" 1470 | reg:set(id, A, false) 1471 | CHECK(cc == 1) 1472 | CHECK(ce == id) 1473 | CHECK(cv == false) 1474 | CHECK(ov == true) 1475 | 1476 | reg:patch(id, A, function(v) 1477 | return not v 1478 | end) 1479 | CHECK(cc == 2) 1480 | CHECK(ce == id) 1481 | CHECK(cv == true) 1482 | 1483 | CASE "removing does not trigger" 1484 | reg:remove(id, A) 1485 | CHECK(cc == 2) 1486 | 1487 | con:disconnect() 1488 | 1489 | CASE "changing after disconnect does not trigger" 1490 | reg:set(id, A, true) -- add 1491 | reg:set(id, A, false) -- change 1492 | CHECK(cc == 2) 1493 | 1494 | con:reconnect() 1495 | 1496 | CASE "changing after reconnect fires again" 1497 | reg:set(id, A, true) 1498 | CHECK(cc == 3) 1499 | end) 1500 | 1501 | TEST("registry:on_remove()", function() 1502 | do 1503 | local reg = ecr.registry() 1504 | local id = reg:create() 1505 | 1506 | local cc, ce, cv, lv = 0, nil, nil, nil 1507 | 1508 | local function fn(...) 1509 | cc += 1 1510 | lv = reg:get(id, A) -- get current value 1511 | ce, cv = ... 1512 | end 1513 | 1514 | local con = reg:on_remove(A):connect(fn) 1515 | 1516 | do CASE "removing triggers" 1517 | reg:set(id, A, true) 1518 | reg:remove(id, A) 1519 | CHECK(cc == 1) 1520 | CHECK(ce == id) 1521 | CHECK(cv == nil) 1522 | CHECK(lv == true) 1523 | 1524 | reg:set(id, A, true) 1525 | reg:remove(id, A) 1526 | CHECK(cc == 2) 1527 | CHECK(ce == id) 1528 | CHECK(cv == nil) 1529 | CHECK(lv == true) 1530 | end 1531 | 1532 | do CASE "removing via destroy" 1533 | reg:set(id, A, false) 1534 | reg:destroy(id) 1535 | 1536 | CHECK(cc == 3) 1537 | CHECK(ce == id) 1538 | CHECK(cv == nil) 1539 | CHECK(lv == false) 1540 | end 1541 | 1542 | con:disconnect() 1543 | 1544 | do CASE "removing after disconnect does not trigger" 1545 | reg:create(id) 1546 | reg:set(id, A, true) 1547 | reg:remove(id, A) 1548 | CHECK(cc == 3) 1549 | end 1550 | end 1551 | 1552 | do CASE "clearing components fires removing event" 1553 | local reg = ecr.registry() 1554 | 1555 | local entities = {} 1556 | 1557 | for i = 1, 1e3 do 1558 | entities[i] = reg:create() 1559 | reg:set(entities[i], A, true) 1560 | end 1561 | 1562 | local cache = {} 1563 | 1564 | reg:on_remove(A):connect(function(entity) 1565 | cache[entity] = true 1566 | end) 1567 | 1568 | reg:clear(A) 1569 | 1570 | for _, entity in entities do 1571 | CHECK(cache[entity]) 1572 | end 1573 | end 1574 | 1575 | do CASE "entity destruction" 1576 | local reg = ecr.registry() 1577 | 1578 | local cid 1579 | reg:on_remove(ecr.entity):connect(function(id_) 1580 | cid = id_ 1581 | end) 1582 | 1583 | local id1 = reg:create() 1584 | local id2 = reg:create() 1585 | reg:release(id1) 1586 | 1587 | CHECK(cid ~= nil) 1588 | CHECK(cid == id1) 1589 | 1590 | reg:clear() 1591 | 1592 | CHECK(cid == id2) 1593 | end 1594 | end) 1595 | 1596 | TEST("signals", function() 1597 | local reg = ecr.registry() 1598 | 1599 | do CASE "firing after disconnecting" 1600 | local run = false 1601 | local dont_run = true 1602 | local a = reg:on_add(A):connect(function() run = true end) -- idx 1 1603 | local b = reg:on_add(A):connect(function() dont_run = false end) -- idx 2 1604 | -- {A, B} 1605 | a:disconnect() 1606 | -- {B} 1607 | a:reconnect() 1608 | -- {B, A} 1609 | local _c = reg:on_add(A):connect(function() end) -- idx 3 1610 | -- {B, A, C} 1611 | b:disconnect() 1612 | -- should be {A, C}, not {B, C} 1613 | reg:handle():set(A, true) 1614 | CHECK(run) 1615 | CHECK(dont_run) 1616 | end 1617 | 1618 | end) 1619 | 1620 | TEST("registry:track()", function() 1621 | local function flip() 1622 | return math.random() > 0.3 1623 | end 1624 | 1625 | local function empty(t) 1626 | return next(t) == nil 1627 | end 1628 | 1629 | local function cache_ids(observer: ecr.Observer): { [ecr.entity]: { unknown } } 1630 | local cache = {} 1631 | for id, a, b, c in observer do 1632 | cache[id] = { a, b, c } 1633 | end 1634 | return cache 1635 | end 1636 | 1637 | local reg = ecr.registry() 1638 | local observerA = reg:track(A) 1639 | local observerB = reg:track(B) 1640 | local observerABC = reg:track(A, B, C) 1641 | local observerAnotC = reg:track(A):exclude(C) 1642 | 1643 | local ids = {} 1644 | local As = {} 1645 | local Bs = {} 1646 | local ABCs = {} 1647 | local AnotCs = {} 1648 | 1649 | for i = 1, N do 1650 | local id = reg:create() 1651 | ids[i] = id 1652 | 1653 | local has = {} 1654 | for _, component in { A, B, C, D, E, F, G, H } do 1655 | if flip() then 1656 | reg:set(id, component, id) 1657 | has[component] = true 1658 | end 1659 | end 1660 | 1661 | if has[A] then table.insert(As, id) end 1662 | if has[B] then table.insert(Bs, id) end 1663 | if has[A] and has[B] and has[C] then table.insert(ABCs, id) end 1664 | if has[A] and not has[C] then table.insert(AnotCs, id) end 1665 | end 1666 | 1667 | do CASE "observer size" 1668 | CHECK(#observerA == #As) 1669 | CHECK(#observerB == #Bs) 1670 | CHECK(#observerABC >= #ABCs) 1671 | CHECK(#observerAnotC >= #AnotCs) 1672 | end 1673 | 1674 | -- do CASE "initial components treated as changed" 1675 | -- do 1676 | -- local observer = reg:track(A) 1677 | 1678 | -- local cache = cache_ids(observer) 1679 | -- CHECK(not empty(cache)) 1680 | 1681 | -- for _, id in As do 1682 | -- local v = cache[id] 1683 | -- if not CHECK(v) then continue end 1684 | -- CHECK(v[1] == id) 1685 | -- end 1686 | -- end 1687 | -- do 1688 | -- local observer = reg:track(A, B, C) 1689 | -- local cache = cache_ids(observer) 1690 | -- CHECK(not empty(cache)) 1691 | 1692 | -- for _, id in ABCs do 1693 | -- local v = cache[id] 1694 | -- if not CHECK(v) then continue end 1695 | -- CHECK(v[1] == id) 1696 | -- CHECK(v[2] == id) 1697 | -- CHECK(v[3] == id) 1698 | -- end 1699 | -- end 1700 | -- end 1701 | 1702 | do CASE "A tracked" 1703 | local cache = cache_ids(observerA) 1704 | CHECK(not empty(cache)) 1705 | for _, id in As do 1706 | local v = cache[id] 1707 | if not CHECK(v) then continue end 1708 | CHECK(v[1] == id) 1709 | end 1710 | CHECK(#observerA == 0) 1711 | end 1712 | 1713 | do CASE "AnotC tracked" 1714 | local cache = cache_ids(observerAnotC) 1715 | CHECK(not empty(cache)) 1716 | for _, id in AnotCs do 1717 | local v = cache[id] 1718 | if not CHECK(v) then continue end 1719 | CHECK(v[1] == id) 1720 | end 1721 | CHECK(#observerAnotC == 0) 1722 | end 1723 | 1724 | do CASE "ABC tracked" 1725 | local cache = cache_ids(observerABC) 1726 | CHECK(not empty(cache)) 1727 | for _, id in ABCs do 1728 | local v = cache[id] 1729 | if not CHECK(v) then continue end 1730 | CHECK(v[1] == id) 1731 | CHECK(v[2] == id) 1732 | CHECK(v[3] == id) 1733 | end 1734 | CHECK(#observerABC == 0) 1735 | end 1736 | 1737 | observerA:clear() 1738 | 1739 | do CASE "changed components tracked after clear" 1740 | CHECK(#observerA == 0) 1741 | 1742 | reg:set(As[1], A, As[1]) 1743 | 1744 | local ran = false 1745 | for id, v in observerA do 1746 | ran = true 1747 | CHECK(id == As[1]) 1748 | CHECK(v == id) 1749 | end 1750 | CHECK(ran) 1751 | CHECK(#observerA == 0) 1752 | end 1753 | 1754 | do CASE "changed components updated" 1755 | for _, id in Bs do 1756 | reg:patch(id, B :: number, function(cur) 1757 | return cur + 1 1758 | end) 1759 | end 1760 | 1761 | for id, v in observerB:iter() do 1762 | CHECK(v == id + 1) 1763 | end 1764 | 1765 | CHECK(#observerB == 0) 1766 | end 1767 | 1768 | do CASE "removed components are not returned" 1769 | local removed = {} 1770 | 1771 | for _, id in Bs do 1772 | if flip() then 1773 | reg:remove(id, B) 1774 | removed[id] = true 1775 | end 1776 | end 1777 | 1778 | CHECK(not empty(removed)) 1779 | 1780 | for id in observerB do 1781 | CHECK(not removed[id]) 1782 | end 1783 | end 1784 | 1785 | observerA:disconnect() 1786 | 1787 | do CASE "changes not recorded if disconnected" 1788 | local size = #observerA 1789 | local id = reg:create() 1790 | reg:set(id, A, 1) 1791 | CHECK(#observerA == size) 1792 | end 1793 | 1794 | observerA:reconnect():clear() 1795 | 1796 | do CASE "changes recorded again after reconnect" 1797 | local cache = {} 1798 | for i = 1, 1e2 do 1799 | local id = reg:create() 1800 | reg:set(id, A, id) 1801 | cache[id] = true 1802 | table.insert(As, id) 1803 | end 1804 | 1805 | for id, v in observerA do 1806 | CHECK(cache[id]) 1807 | CHECK(v == id) 1808 | end 1809 | end 1810 | 1811 | observerA:clear() 1812 | table.clear(As) 1813 | 1814 | -- do CASE "observer returns up-to-date values despite being changed while disconnected" 1815 | -- local observer = reg:track(A):clear() 1816 | -- local id1 = reg:create() 1817 | -- local id2 = reg:create() 1818 | -- local id3 = reg:create() 1819 | 1820 | -- reg:set(id1, A, 1) 1821 | -- reg:set(id2, A, 2) 1822 | -- reg:set(id3, A, 3) 1823 | 1824 | -- observer:disconnect() 1825 | 1826 | -- reg:remove(id1, A) -- remove id1 1827 | -- reg:set(id2, A, -2) -- change id2 1828 | 1829 | -- local values = {} 1830 | 1831 | -- for id, v in observer do 1832 | -- values[id] = v 1833 | -- end 1834 | 1835 | -- CHECK(#observer == 2) 1836 | -- CHECK(not values[id1]) 1837 | -- CHECK(values[id2] == -2) 1838 | -- CHECK(values[id3] == 3) 1839 | -- end 1840 | 1841 | do CASE "disconnect non-empty observer" 1842 | local observer = reg:track(A) 1843 | 1844 | reg:handle():set(A, true) 1845 | 1846 | CHECK_ERR("attempt to disconnect a non-empty observer", function() 1847 | observer:disconnect() 1848 | end) 1849 | 1850 | observer:clear():disconnect() 1851 | end 1852 | 1853 | do CASE "observer garbage collection" 1854 | local wref = setmetatable({}, { __mode = "v" }) 1855 | wref.observer = observerA 1856 | wref.pool = (observerA :: any).pool 1857 | observerA:clear():disconnect() 1858 | observerA = nil :: any 1859 | 1860 | if not game then -- check if running in roblox (collect is sandboxed) 1861 | collectgarbage("collect" :: any) 1862 | collectgarbage("collect" :: any) 1863 | collectgarbage("collect" :: any) 1864 | CHECK(not wref.observer) 1865 | 1866 | local id = reg:create() 1867 | reg:set(id, A, 1) 1868 | reg:remove(id, A) -- internal pool only cleared after event fired again 1869 | 1870 | collectgarbage("collect" :: any) 1871 | collectgarbage("collect" :: any) 1872 | collectgarbage("collect" :: any) 1873 | CHECK(not wref.pool) 1874 | end 1875 | end 1876 | 1877 | do CASE "track multiple" 1878 | local observer = reg:track(A, B, C) 1879 | observer:clear() 1880 | 1881 | local id1 = reg:create() 1882 | reg:set(id1, C, true) 1883 | 1884 | local id2 = reg:create() 1885 | reg:set(id2, A, true) 1886 | 1887 | local id3 = reg:create() 1888 | reg:set(id3, A, 1) 1889 | reg:set(id3, B, 2) 1890 | reg:set(id3, C, 3) 1891 | 1892 | local runcount = 0 1893 | for id, a, b, c in observer do 1894 | runcount += 1 1895 | CHECK(id == id3) 1896 | CHECK(a == 1 and b == 2 and c == 3) 1897 | end 1898 | CHECK(runcount == 1) 1899 | end 1900 | end) 1901 | 1902 | TEST("registry:group()", function() 1903 | local function flip() return math.random() > 0.3 end 1904 | local function empty(t) return next(t) == nil end 1905 | local function getids(group: ecr.Group<...unknown>): { [ecr.entity]: { unknown } } 1906 | local cache = {} 1907 | 1908 | for id, a, b, c, d, e in group do 1909 | cache[id] = { a, b, c, d, e } 1910 | end 1911 | 1912 | return cache 1913 | end 1914 | 1915 | local Z, X = ecr.component(), ecr.component() 1916 | 1917 | local reg = ecr.registry() 1918 | local groupAB = reg:group(A, B) 1919 | reg:group(C, D, E) 1920 | 1921 | local ids = {} 1922 | local Bs = {} 1923 | local ABs = {} 1924 | local CDEs = {} 1925 | local FGs = {} 1926 | 1927 | for i = 1, N do 1928 | local id = reg:create() 1929 | ids[i] = id 1930 | 1931 | local has = {} 1932 | for _, component in { A, B, C, D, E, F, G, H } do 1933 | if flip() then 1934 | reg:set(id, component, id) 1935 | has[component] = true 1936 | end 1937 | end 1938 | 1939 | if has[B] then table.insert(Bs, id) end 1940 | if has[A] and has[B] then table.insert(ABs, id) end 1941 | if has[C] and has[D] and has[E] then table.insert(CDEs, id) end 1942 | if has[F] and has[G] then table.insert(FGs, id) end 1943 | end 1944 | 1945 | do CASE "group size" 1946 | CHECK(#reg:group(A, B) == #ABs) 1947 | CHECK(#reg:group(C, D, E) == #CDEs) 1948 | end 1949 | 1950 | do CASE "group AB cached" 1951 | local viewed = getids(groupAB) 1952 | CHECK(not empty(viewed)) 1953 | for _, id in ABs do 1954 | local v = viewed[id] 1955 | if not CHECK(v) then continue end 1956 | CHECK(v[1] == id) 1957 | CHECK(v[2] == id) 1958 | CHECK(v[3] == nil) 1959 | end 1960 | end 1961 | 1962 | do CASE "group AB" 1963 | local viewed = getids(reg:group(A, B)) 1964 | CHECK(not empty(viewed)) 1965 | for _, id in ABs do 1966 | local v = viewed[id] 1967 | if not CHECK(v) then continue end 1968 | CHECK(v[1] == id) 1969 | CHECK(v[2] == id) 1970 | CHECK(v[3] == nil) 1971 | end 1972 | end 1973 | 1974 | do CASE "group CDE" 1975 | local viewed = getids(reg:group(C, D, E)) 1976 | CHECK(not empty(viewed)) 1977 | for _, id in CDEs do 1978 | local v = viewed[id] 1979 | if not CHECK(v) then continue end 1980 | CHECK(v[1] == id) 1981 | CHECK(v[2] == id) 1982 | CHECK(v[3] == id) 1983 | CHECK(v[4] == nil) 1984 | end 1985 | end 1986 | 1987 | do CASE "late initialized group FG" 1988 | CHECK(#reg:group(F, G) == #FGs) 1989 | local viewed = getids(reg:group(F, G)) 1990 | CHECK(not empty(viewed)) 1991 | for _, id in FGs do 1992 | local v = viewed[id] 1993 | if not CHECK(v) then continue end 1994 | CHECK(v[1] == id) 1995 | CHECK(v[2] == id) 1996 | CHECK(v[3] == nil) 1997 | end 1998 | end 1999 | 2000 | do CASE "removing components during group iteration" 2001 | local cache = {} 2002 | local removed = {} 2003 | 2004 | for id, a, b in reg:group(A, B) do 2005 | CHECK(not cache[id]) -- ensure iterators arent invalidated 2006 | cache[id] = { a, b } 2007 | if flip() then 2008 | removed[id] = true 2009 | reg:remove(id, A) 2010 | end 2011 | end 2012 | 2013 | CHECK(#reg:group(A, B) > 0) 2014 | 2015 | for id, a, b in reg:group(A, B) do 2016 | local v = cache[id] 2017 | CHECK(v) 2018 | CHECK(v[1] == a) -- check association is kept after remove 2019 | CHECK(v[2] == b) 2020 | cache[id] = nil 2021 | end 2022 | 2023 | CHECK(not empty(cache)) 2024 | 2025 | for id in cache do 2026 | local v = removed[id] 2027 | if not v then 2028 | print(id, KEY(id)) 2029 | end 2030 | CHECK(removed[id]) -- check only removed ids are left 2031 | end 2032 | end 2033 | 2034 | do CASE "clearing with groups" 2035 | reg:clear(A) 2036 | CHECK(#reg:group(A, B) == 0) 2037 | reg:set(ABs[1], A, 1) 2038 | CHECK(#reg:group(A, B) == 1) 2039 | end 2040 | 2041 | do CASE "removing owned component from entity not in group" 2042 | local id = reg:create() 2043 | reg:set(id, A, 1) 2044 | reg:set(id, C, 3) 2045 | reg:remove(id, A) 2046 | CHECK(reg:try_get(id, A) == nil and reg:get(id, C) == 3) 2047 | end 2048 | 2049 | do CASE "using owned component for new group errors" 2050 | CHECK_ERR( 2051 | "cannot create group; component (arg #2) is not owned by the same group as previous args", 2052 | function() 2053 | reg:group(F, H) 2054 | end 2055 | ) 2056 | end 2057 | 2058 | do CASE "single component group errors" 2059 | local ok = pcall(function() 2060 | reg:group(H) 2061 | end) 2062 | CHECK(not ok) 2063 | end 2064 | 2065 | do CASE "group ABCDE" 2066 | local reg2 = ecr.registry() 2067 | local ids2 = {} 2068 | for i = 1, 1e2 do 2069 | local id = reg2:create() 2070 | reg2:set(id, A, id) 2071 | reg2:set(id, B, id) 2072 | reg2:set(id, C, id) 2073 | reg2:set(id, D, id) 2074 | reg2:set(id, E, id) 2075 | ids2[i] = id 2076 | end 2077 | 2078 | reg2:group(A, B, C, D, E) 2079 | 2080 | local viewed = getids(reg2:group(A, B, C, D, E)) 2081 | 2082 | CHECK(not empty(viewed)) 2083 | for _, id in ids2 do 2084 | local v = viewed[id] 2085 | if not CHECK(v) then break end 2086 | CHECK(v[1] == id) 2087 | CHECK(v[2] == id) 2088 | CHECK(v[3] == id) 2089 | CHECK(v[4] == id) 2090 | CHECK(v[5] == id) 2091 | end 2092 | end 2093 | 2094 | do CASE "invalidation check" 2095 | reg:group(Z, X) 2096 | 2097 | local e1 = reg:handle() 2098 | e1:set(Z, true) 2099 | 2100 | local e2 = reg:handle() 2101 | e2:set(Z, true) 2102 | 2103 | CHECK_ERR("group reordered during iteration", function() 2104 | for id in reg:view(Z) do 2105 | reg:set(id, X, true) 2106 | end 2107 | end) 2108 | 2109 | CHECK(not e1:has(X)) -- confirm invalidation occured 2110 | end 2111 | end) 2112 | 2113 | TEST("registry:handle()", function() 2114 | local reg = ecr.registry() 2115 | 2116 | do CASE "call with nil" 2117 | local e = reg:handle() 2118 | 2119 | CHECK(reg:contains(e.entity)) 2120 | 2121 | e:set(A, 1) 2122 | :set(B, 2) 2123 | 2124 | CHECK(e:has(A, B)) 2125 | end 2126 | 2127 | do CASE "call with existing" 2128 | local id = reg:create() 2129 | local e = reg:handle(id) 2130 | CHECK(e.entity == id) 2131 | end 2132 | 2133 | do CASE "caching" 2134 | local e = reg:handle() 2135 | local e2 = reg:handle(e.entity) 2136 | CHECK(e == e2) 2137 | 2138 | local id = reg:create() 2139 | local e_b = reg:handle(id) 2140 | local e_b2 = reg:handle(id) 2141 | CHECK(e_b == e_b2) 2142 | end 2143 | 2144 | do CASE "garbage collection" 2145 | local weak = setmetatable({}, { __mode = "v" }) 2146 | 2147 | local id = reg:create() 2148 | 2149 | do 2150 | weak.v = reg:handle(id) 2151 | end 2152 | 2153 | (collectgarbage :: any)("collect") 2154 | 2155 | CHECK(weak.v == nil :: any) 2156 | end 2157 | end) 2158 | 2159 | TEST("registry:context()", function() 2160 | local reg = ecr.registry() 2161 | 2162 | do CASE "initially invalid" 2163 | CHECK(not reg:contains(ecr.context)) 2164 | CHECK(#reg:view(ecr.entity) == 0) 2165 | end 2166 | 2167 | local ctx = reg:context() 2168 | 2169 | do CASE "valid" 2170 | CHECK(reg:contains(ctx.entity)) 2171 | end 2172 | 2173 | ctx:set(A, 1):set(B, 2) 2174 | 2175 | do CASE "components" 2176 | CHECK(ctx:get(A) == 1) 2177 | CHECK(ctx:get(B) == 2) 2178 | end 2179 | 2180 | do CASE "sizes" 2181 | CHECK(#reg:view(ecr.entity) == 1) 2182 | CHECK(#reg:view(A) == 1) 2183 | end 2184 | 2185 | do CASE "destroy" 2186 | ctx:destroy() 2187 | CHECK(not reg:contains(ctx.entity)) 2188 | reg:context() 2189 | CHECK(not ctx:has(A) and not ctx:has(B)) 2190 | end 2191 | 2192 | reg:clear() 2193 | 2194 | do CASE "clearing affects" 2195 | CHECK(not reg:contains(ctx.entity)) 2196 | end 2197 | end) 2198 | 2199 | TEST("registry:storage()", function() 2200 | do CASE "get storage" 2201 | local reg = ecr.registry() 2202 | 2203 | local ids = BULK_CREATE_IDS(reg, N) 2204 | 2205 | for i = 1, N do 2206 | reg:set(ids[i], A, i) 2207 | end 2208 | 2209 | local pool = reg:storage(A) 2210 | 2211 | local entities = ecr.buffer_to_array(pool.entities, pool.size) 2212 | 2213 | for i, id in entities do 2214 | CHECK(ids[i] == id) 2215 | CHECK(pool.values[i] == i) 2216 | end 2217 | end 2218 | 2219 | do CASE "get all storages" 2220 | local reg = ecr.registry() 2221 | 2222 | local pool_A = reg:storage(A) 2223 | local pool_B = reg:storage(B) 2224 | 2225 | local cache = {} 2226 | 2227 | for ctype, pool in reg:storage() do 2228 | cache[ctype] = pool 2229 | end 2230 | 2231 | CHECK(cache[A] == pool_A) 2232 | CHECK(cache[B] == pool_B) 2233 | end 2234 | end) 2235 | 2236 | TEST("registry:copy()", function() 2237 | local reg = ecr.registry() 2238 | 2239 | do CASE "copy" 2240 | local entities = { 2241 | reg:handle():set(A, "value").entity, 2242 | reg:handle():set(A, "value").entity, 2243 | reg:handle():set(A, "value").entity, 2244 | reg:handle():set(A, "value").entity, 2245 | } 2246 | 2247 | for id, b in reg:view(B) do 2248 | CHECK(false) 2249 | end 2250 | 2251 | reg:copy(A, B) 2252 | 2253 | for id, b in reg:view(B) do 2254 | CHECK(b == "value") 2255 | end 2256 | 2257 | for _, id in entities do 2258 | reg:destroy(id) 2259 | end 2260 | end 2261 | 2262 | do CASE "copies nil" 2263 | local e = reg:handle():set(B, "value") 2264 | 2265 | for id, b in reg:view(B) do 2266 | CHECK(b == "value") 2267 | end 2268 | 2269 | reg:copy(A, B) 2270 | 2271 | for id, b in reg:view(B) do 2272 | CHECK(false) 2273 | end 2274 | e:destroy() 2275 | end 2276 | 2277 | do CASE "copies tags" 2278 | local e = reg:handle():set(TA) 2279 | 2280 | for id, b in reg:view(TB) do 2281 | CHECK(false) 2282 | end 2283 | 2284 | reg:copy(TA, TB) 2285 | 2286 | local pass = false 2287 | for id, b in reg:view(TB) do 2288 | CHECK(reg:has(id, TB, TA)) 2289 | pass = true 2290 | end 2291 | CHECK(pass) 2292 | e:destroy() 2293 | end 2294 | 2295 | do CASE "throws error if signal" 2296 | reg:handle():set(TA) 2297 | reg:on_add(TB):connect(function(a0: number, a1: nil) end) 2298 | 2299 | for id, b in reg:view(TB) do 2300 | CHECK(false) 2301 | end 2302 | 2303 | local ok = pcall(reg.copy, reg, TA, TB) 2304 | CHECK(not ok) 2305 | end 2306 | end) 2307 | 2308 | TEST("ecr.component()", function() 2309 | do CASE "components have unique ids" 2310 | local cache = {} 2311 | for i = 1, 1000 do 2312 | local c = ecr.component() 2313 | CHECK(cache[c] == nil) 2314 | cache[c] = true 2315 | end 2316 | end 2317 | end) 2318 | 2319 | TEST("ecr.queue()", function() 2320 | do 2321 | local queue = ecr.queue() 2322 | 2323 | do CASE "iterate single value" 2324 | for i = 1, 10 do 2325 | queue:add(i) 2326 | end 2327 | 2328 | CHECK(#queue == 10) 2329 | 2330 | local i = 0 2331 | for v in queue do 2332 | i += 1 2333 | CHECK(v == i) 2334 | end 2335 | 2336 | CHECK(#queue == 0) 2337 | CHECK(i == 10) 2338 | end 2339 | 2340 | do CASE "iterate multiple values" 2341 | for i = 1, 10 do 2342 | (queue :: any):add(i+1, i+2, i+3) 2343 | end 2344 | 2345 | CHECK(#queue == 10) 2346 | 2347 | local i = 0 2348 | for a, b, c in queue do 2349 | i += 1 2350 | CHECK(a == i+1) 2351 | CHECK(b == i+2) 2352 | CHECK(c == i+3) 2353 | end 2354 | 2355 | CHECK(#queue == 0) 2356 | CHECK(i == 10) 2357 | end 2358 | end 2359 | 2360 | do CASE "new iter clear" 2361 | local queue = ecr.queue() 2362 | queue:add(1) 2363 | for _ in queue:iter() do end 2364 | CHECK(#queue == 0) 2365 | end 2366 | 2367 | do CASE "manual clear" 2368 | local queue = ecr.queue() 2369 | for i = 1, 10 do 2370 | queue:add(i, i+1, i+2) 2371 | end 2372 | 2373 | CHECK(#queue == 10) 2374 | 2375 | queue:clear() 2376 | 2377 | CHECK(#queue == 0) 2378 | 2379 | for _ in queue do 2380 | CHECK(false) 2381 | end 2382 | end 2383 | 2384 | do CASE "uneven values" 2385 | local queue = ecr.queue() 2386 | queue:add(1) 2387 | ;(queue :: any):add(1, 2, nil) 2388 | ;(queue :: any):add(1, nil, 3) 2389 | 2390 | local i = 0 2391 | for a, b, c in queue do 2392 | i += 1 2393 | if i == 1 then CHECK(a == 1 and b == nil and c == nil) 2394 | elseif i == 2 then CHECK(a == 1 and b == 2 and c == nil) 2395 | elseif i == 3 then CHECK(a == 1 and b == nil and c == 3) 2396 | else CHECK(false) end 2397 | end 2398 | end 2399 | 2400 | do CASE "nil value first" 2401 | local queue = ecr.queue() 2402 | CHECK_ERR("first argument cannot be nil", function() 2403 | queue:add(nil, 1, 2, 3) 2404 | end) 2405 | end 2406 | 2407 | do CASE "automatic signal connection" 2408 | local listener: (...any) -> () 2409 | local signal = { 2410 | connect = function(_, fn) 2411 | listener = fn 2412 | end 2413 | } 2414 | 2415 | local queue = ecr.queue(signal) 2416 | 2417 | listener(1, 2, 3) 2418 | 2419 | CHECK(#queue == 1) 2420 | for a, b, c in queue do 2421 | CHECK(a == 1 and b == 2 and c == 3) 2422 | end 2423 | end 2424 | end) 2425 | 2426 | TEST("ecr.name()", function() 2427 | local cts = ecr.name { 2428 | A = ecr.component() :: number, 2429 | B = ecr.component() :: string 2430 | } 2431 | 2432 | local reg = ecr.registry() 2433 | 2434 | local id = reg:create() 2435 | 2436 | CHECK_ERR(`entity does not have component "A"`, function() 2437 | reg:get(id, cts.A) 2438 | end) 2439 | 2440 | CHECK_ERR(`no constructor defined for component "B"`, function() 2441 | reg:add(id, cts.B) 2442 | end) 2443 | 2444 | CHECK(ecr.name(cts.A) == "A") 2445 | end) 2446 | 2447 | TEST("ecr.tag()", function() 2448 | do CASE "is tag" 2449 | CHECK(ecr.is_tag(TA)) 2450 | CHECK(not ecr.is_tag(A)) 2451 | end 2452 | 2453 | local reg = ecr.registry() 2454 | 2455 | local observer = reg:track(TA, TB, TC, TD, A) 2456 | 2457 | local e = reg:handle() 2458 | 2459 | do CASE "add" 2460 | local ran = false 2461 | reg:on_add(TA):connect(function(id, v) 2462 | ran = true 2463 | CHECK(id == e.entity) 2464 | CHECK(v == nil) 2465 | end) 2466 | 2467 | e:add(TA) 2468 | 2469 | CHECK(e:has(TA)) 2470 | CHECK(e:get(TA) == nil) 2471 | CHECK(ran) 2472 | end 2473 | 2474 | do CASE "view" 2475 | local i = 0 2476 | for id, tag in reg:view(TA) do 2477 | i += 1 2478 | CHECK(i == 1) 2479 | CHECK(tag == nil) 2480 | end 2481 | end 2482 | 2483 | -- multi-type views return values by unpacking a table 2484 | -- the concern here is since tag values are nil, there is potential for 2485 | -- undefined behavior with unpacking an array with holes 2486 | -- we pre-allocate the arrays so the above should not be an issue 2487 | do CASE "view unpack" 2488 | e:add(TB, TC, TD) 2489 | e:set(A, true) 2490 | 2491 | local ran = 0 2492 | 2493 | for id, ta, tb, tc, td, a in reg:view(TA, TB, TC, TD, A) do 2494 | ran += 1 2495 | CHECK(a == true) 2496 | end 2497 | 2498 | for id, ta, tb, tc, td, a in observer do 2499 | ran += 1 2500 | CHECK(a == true) 2501 | end 2502 | 2503 | for id, ta, tb, tc, td, a in reg:group(TA, TB, TC, TD, A) do 2504 | ran += 1 2505 | CHECK(a == true) 2506 | end 2507 | 2508 | CHECK(ran == 3) 2509 | end 2510 | 2511 | do CASE "remove" 2512 | e:remove(TA) 2513 | CHECK(not e:has(TA)) 2514 | end 2515 | 2516 | -- tags are implemented as normal components internally 2517 | -- further testing should not be necessary 2518 | end) 2519 | 2520 | TEST("buffer helpers", function() 2521 | local reg = ecr.registry() 2522 | 2523 | local ids = BULK_CREATE_IDS(reg, 10) 2524 | local buf = buffer.create(10 * ecr.id_size) 2525 | 2526 | do CASE "array to buffer" 2527 | ecr.array_to_buffer(ids, 10, buf) 2528 | CHECK(true) 2529 | end 2530 | 2531 | do CASE "buffer to array" 2532 | local arr = table.create(10) 2533 | ecr.buffer_to_array(buf, 10, arr) 2534 | for i = 1, 10 do 2535 | CHECK(arr[i] == ids[i]) 2536 | end 2537 | end 2538 | 2539 | do CASE "buffer to buffer" 2540 | buffer.fill(buf, 0, 0) 2541 | local pool = reg:storage(ecr.entity) 2542 | ecr.buffer_to_buffer(pool.entities, pool.size, buf) 2543 | local arr = ecr.buffer_to_array(buf, pool.size) 2544 | for i = 1, 10 do 2545 | CHECK(arr[i] == ids[i]) 2546 | end 2547 | end 2548 | end) 2549 | 2550 | if DEFER_ID_REUSE then 2551 | 2552 | TEST("deferred id reuse", function() 2553 | do CASE "version overflow" 2554 | local reg2 = ecr.registry() 2555 | 2556 | reg2:release( reg2:create() ); -- ensure id internally exists 2557 | 2558 | SET_KEY_VERSION_AND_ENSURE_EXISTS(reg2, 1, MAX_VER - 1) 2559 | 2560 | -- verify version was set to MAX-1 correctly 2561 | local id = reg2:create() 2562 | CHECK(VER(id) == MAX_VER - 1) 2563 | CHECK(KEY(id) == 1) 2564 | -- trigger version increment 2565 | reg2:release(id) 2566 | 2567 | -- verify version is now MAX 2568 | id = reg2:create() 2569 | CHECK(VER(id) == MAX_VER) 2570 | CHECK(KEY(id) == 1) 2571 | 2572 | -- id deprecated 2573 | reg2:release(id) 2574 | CHECK(VER(id) == MAX_VER) 2575 | 2576 | -- should produce a new key and not recycle as the previous key is at MAX version 2577 | id = reg2:create() 2578 | CHECK(VER(id) == 1) 2579 | CHECK(KEY(id) == 2) 2580 | end 2581 | 2582 | do CASE "deprecated key" 2583 | local reg = ecr.registry() 2584 | CHECK(GET_KEY_VERSION(reg, KEY(CREATE_DEPRECATED_ID(reg))) == 0) 2585 | end 2586 | 2587 | do CASE "add component to deprecated id" 2588 | local reg = ecr.registry() 2589 | 2590 | CHECK_ERR("invalid entity", function() 2591 | local id = CREATE_DEPRECATED_ID(reg) 2592 | reg:add(id, A) 2593 | end) 2594 | end 2595 | 2596 | do CASE "add component to deprecated id" 2597 | local reg = ecr.registry() 2598 | 2599 | CHECK_ERR("invalid entity", function() 2600 | local id = CREATE_DEPRECATED_ID(reg) 2601 | reg:set(id, A, 1) 2602 | end) 2603 | end 2604 | 2605 | do CASE "reuse" 2606 | local reg = ecr.registry() 2607 | 2608 | local ids = table.create(ecr._test.max_creatable) 2609 | 2610 | for i = 1, ecr._test.max_creatable do 2611 | ids[i] = reg:create(CREATE_ID(i, MAX_VER)) 2612 | end 2613 | 2614 | for i = 1, ecr._test.max_creatable do 2615 | reg:release(ids[i]) 2616 | end 2617 | 2618 | local id = reg:create() 2619 | CHECK(KEY(id) == 1) 2620 | CHECK(VER(id) == 1) 2621 | end 2622 | end) 2623 | 2624 | else 2625 | 2626 | TEST("immediate id reuse", function() 2627 | do CASE "reuse 1" 2628 | local reg = ecr.registry() 2629 | 2630 | for i = 1, MAX_VER do 2631 | reg:release(reg:create()) 2632 | end 2633 | 2634 | local id = reg:create() 2635 | CHECK(KEY(id) == 1) 2636 | CHECK(VER(id) == 1) 2637 | end 2638 | 2639 | do CASE "clear" 2640 | local reg = ecr.registry() 2641 | local ids = BULK_CREATE_IDS(reg, ecr._test.max_creatable) 2642 | 2643 | local chosen = ecr._test.max_creatable // 2 2644 | local chosen_key = KEY(ids[chosen]) 2645 | 2646 | reg:release(ids[chosen]) 2647 | SET_KEY_VERSION_AND_ENSURE_EXISTS(reg, KEY(ids[chosen]), ecr._test.max_ver) 2648 | CHECK(reg:create() == ecr._test.create_id(chosen_key, ecr._test.max_ver)) 2649 | 2650 | reg:clear() 2651 | 2652 | local ids_new = BULK_CREATE_IDS(reg, ecr._test.max_creatable) 2653 | 2654 | for i = 1, ecr._test.max_creatable do 2655 | local id = ids[i] 2656 | local id_new = ids_new[i] 2657 | 2658 | if KEY(id_new) == chosen_key then 2659 | CHECK(VER(id_new) == VER(id)) 2660 | else 2661 | CHECK(VER(id) + 1 == VER(id_new)) 2662 | end 2663 | end 2664 | end 2665 | end) 2666 | 2667 | end 2668 | 2669 | TEST("random test", function() 2670 | --[[ 2671 | 2672 | This test simulates modifying a registry through a sequence of systems 2673 | by randomly creating, destroying, adding, updating, removing entities 2674 | and their components. 2675 | 2676 | Each component value contains the id of the entity that owns it as well 2677 | as a counter that is maintained separately from the registry. 2678 | 2679 | To check if the case passes we check if the two independent counters 2680 | match up and that the id in the component corresponds to the entity that 2681 | owns it. 2682 | 2683 | ]] 2684 | 2685 | -- map component to entity to value 2686 | local incs = {} :: Map> 2687 | 2688 | local cts = {} :: Array<{ id: ecr.entity, v: number }> 2689 | 2690 | for i = 1, 5 do 2691 | local ctype = ecr.component() 2692 | cts[i] = ctype :: any 2693 | incs[ctype] = {} 2694 | end 2695 | 2696 | local reg = ecr.registry() 2697 | 2698 | local function flip(p: number?) 2699 | return math.random() > (p and (1 - p) or 0.5) 2700 | end 2701 | 2702 | -- set of all ids in use 2703 | local idcount = 0 2704 | local ids = {} :: { [ecr.entity]: true } 2705 | 2706 | -- array of all destroyed ids 2707 | local frees = {} :: { ecr.entity } 2708 | 2709 | 2710 | local group1 = reg:group(cts[1], cts[3]) 2711 | local group2 = reg:group(cts[2], cts[4], cts[5]) 2712 | 2713 | for i = 1, N do 2714 | if flip(0.6) then 2715 | local id = reg:create() 2716 | ids[id] = true 2717 | idcount += 1 2718 | end 2719 | 2720 | do 2721 | local id = table.remove(frees) 2722 | if flip(0.2) and id then 2723 | local ok = pcall(reg.create, reg, id) 2724 | ids[id] = ok or nil :: any 2725 | idcount += 1 2726 | end 2727 | end 2728 | 2729 | for _, ctype in cts do 2730 | local inc = incs[ctype] 2731 | 2732 | do -- test iterators 2733 | local ctype2 2734 | local ctype3 2735 | 2736 | repeat 2737 | ctype2 = cts[math.random(1, #cts)] 2738 | until ctype2 ~= ctype 2739 | 2740 | if flip(0.3) then 2741 | repeat 2742 | ctype3 = cts[math.random(1, #cts)] 2743 | until ctype3 ~= ctype2 and ctype3 ~= ctype 2744 | end 2745 | 2746 | if ctype3 then 2747 | for _ in reg:view(ctype, ctype2, ctype3) do end 2748 | else 2749 | for _ in reg:view(ctype, ctype2) do end 2750 | end 2751 | end 2752 | 2753 | for id in ids do 2754 | -- add component 2755 | if flip() and not inc[id] then 2756 | inc[id] = 0 2757 | -- 2758 | if not reg:try_get(id, ctype) then 2759 | reg:set(id, ctype, { id = id, v = 0 }) 2760 | end 2761 | end 2762 | 2763 | -- increment component 2764 | if flip() and inc[id] then 2765 | inc[id] += 1 2766 | -- 2767 | reg:patch(id, ctype, function(old) 2768 | return { id = old.id, v = old.v + 1 } 2769 | end) 2770 | end 2771 | 2772 | -- decrement component 2773 | if flip(0.2) and inc[id] then 2774 | inc[id] -= 1 2775 | -- 2776 | local old = reg:get(id, ctype) 2777 | reg:set(id, ctype, { id = old.id, v = old.v - 1 }) 2778 | end 2779 | 2780 | -- remove component 2781 | if flip(0.1) and inc[id] then 2782 | inc[id] = nil 2783 | -- 2784 | reg:remove(id, ctype) 2785 | end 2786 | end 2787 | end 2788 | 2789 | for id in ids do 2790 | -- destroy id 2791 | if flip(0.005) then 2792 | ids[id] = nil 2793 | idcount -= 1 2794 | table.insert(frees, id) 2795 | for _, inc in incs do 2796 | inc[id] = nil 2797 | end 2798 | -- 2799 | reg:destroy(id) 2800 | end 2801 | end 2802 | end 2803 | 2804 | local ran = 0 2805 | 2806 | for _, ctype in cts do 2807 | local inc = incs[ctype] 2808 | 2809 | for id in ids do 2810 | ran += 1 2811 | local component = reg:try_get(id, ctype) :: any 2812 | if inc[id] then 2813 | CHECK(component) 2814 | CHECK(inc[id] == component.v) 2815 | CHECK(component.id == id) 2816 | else 2817 | CHECK(not component) 2818 | end 2819 | end 2820 | end 2821 | 2822 | for id in reg:view(ecr.entity) do 2823 | CHECK(ids[id]) 2824 | end 2825 | 2826 | CHECK(#group1 > 0 and #group2 > 0) 2827 | CHECK(ran > 0) 2828 | CHECK(idcount > 50) 2829 | end) 2830 | 2831 | local success = FINISH() 2832 | if not success then error(nil, 0) end 2833 | --------------------------------------------------------------------------------