Compare commits

...

12 commits

Author SHA1 Message Date
Clown
071fb62ec4
Merge 96bed9bd7e into 41ebde415f 2025-03-30 17:03:09 -04:00
Ukendio
41ebde415f OnChange should invoke after data has been set
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-03-30 22:49:11 +02:00
Ukendio
7bcd6bd220 Update docs 2025-03-30 22:29:43 +02:00
Ukendio
2b90fabec5 Add tests for addons 2025-03-30 22:14:22 +02:00
Ukendio
9c68218d5d Update changelog 2025-03-30 21:47:25 +02:00
Ukendio
cf88c259f8 Replace OnSet hook with OnChange 2025-03-30 21:31:18 +02:00
Ukendio
a466ab151b Move coverage under docs 2025-03-30 18:55:14 +02:00
Ukendio
3050ea1560 Add coverage to Nav 2025-03-30 18:42:16 +02:00
Ukendio
5a1424ee48 Merge branch 'main' of https://github.com/Ukendio/jecs 2025-03-30 18:41:59 +02:00
Ukendio
0455a55625 Remove duplicate .github folder 2025-03-30 18:38:14 +02:00
EncodedVenom
27b58e9745
Update jecs.md
Makes the sentence clearer
2025-03-30 12:24:24 -04:00
YetAnotherClown
96bed9bd7e Empty Commit 2025-02-25 11:28:04 -05:00
24 changed files with 403 additions and 469 deletions

View file

@ -4,6 +4,7 @@
"testkit": "tools/testkit",
"mirror": "mirror",
"tools": "tools",
"addons": "addons",
},
"languageMode": "strict"
}

View file

@ -11,29 +11,46 @@ The format is based on [Keep a Changelog][kac], and this project adheres to
## [Unreleased]
- `[world]`:
- 16% faster `world:get`
- `world:has` no longer typechecks components after the 8th one.
- `[typescript]`
- Changed `world:clear` to also look through the component record for the cleared `ID`
- Removes the cleared ID from every entity that has it
- Changed entity ID layouts by putting the index in the lower bits, which should make every world function 1-5 nanoseconds faster
- Fixed `world:delete` not removing every pair with an unalive target
- Specifically happened when you had at least two pairs of different relations with multiple targets each
- `[hooks]`:
- Replaced `OnSet` with `OnChange`
- The former was used to detect emplace/move actions. Now the behaviour for `OnChange` is that it will run only when the value has changed
- Changed `OnAdd` to specifically run after the data has been set for non-zero-sized components. Also returns the value that the component was set to
- This should allow a more lenient window for modifying data
- Changed `OnRemove` to lazily lookup which archetype the entity will move to
- Can now have interior structural changes within `OnRemove` hooks
- Fixed Entity type to default to `undefined | unknown` instead of just `undefined`
## [0.5.0] - 2024-12-26
- `[world]`:
- Fixed `world:target` not giving adjacent pairs
- Added `world:each` to find entities with a specific Tag
- Added `world:children` to find children of entity
- `[query]`:
- Fixed bug where `world:clear` did not invoke `jecs.OnRemove` hooks
- Changed `query.__iter` to drain on iteration
- It will initialize once wherever you left iteration off at last time
- Changed `query:iter` to restart the iterator
- Removed `query:drain` and `query:next`
- If you want to get individual results outside of a for-loop, you need to call `query:iter` to initialize the iterator and then call the iterator function manually
```lua
local it = world:query(A, B, C):iter()
local entity, a, b, c = it()
entity, a, b, c = it() -- get next results
```
- `[world`
- Fixed a bug with `world:clear` not invoking `jecs.OnRemove` hooks
- `[typescript]`:
- Changed pair to accept generics
- Improved handling of Tags
- Added `query:cached`
- Adds query cache that updates itself when an archetype matching the query gets created or deleted.
- `[luau]`:
- Changed how entities' types are inferred with user-defined type functions
- Changed `Pair<First, Second>` to return `Second` if `First` is a `Tag`; otherwise, returns `First`.
## [0.4.0] - 2024-11-17
- `[world]`:
- Added recycling to `world:entity`
- If you see much larger entity ids, that is because its generation has been incremented
- `[query]`:
- Removed `query:drain`
- The default behaviour is simply to drain the iterator
- Removed `query:next`
- Just call the iterator function returned by `query:iter` directly if you want to get the next results
- Removed `query:replace`
- `[luau]`:
- Fixed `query:archetypes` not taking `self`
- Changed so that the `jecs.Pair` type now returns the first element's type so you won't need to typecast anymore.
## [0.3.2] - 2024-10-01

149
addons/observers.luau Normal file
View file

@ -0,0 +1,149 @@
local jecs = require("@jecs")
local testkit = require("@testkit")
local function observers_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index
local function emplaced(entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity)
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity)
end
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
end
local function world_track(world, ...)
local entity_index = world.entity_index
local terms = { ... }
local q_shim = { filter_with = terms }
local n = 0
local dense_array = {}
local sparse_array = {}
local function emplaced(entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity)
local archetype = r.archetype
if jecs.query_match(q_shim, archetype) then
n += 1
dense_array[n] = entity
sparse_array[entity] = n
end
end
local function removed(entity)
local i = sparse_array[entity]
if i ~= n then
dense_array[i] = dense_array[n]
end
dense_array[n] = nil
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
local function iter()
local i = n
return function()
local row = i
if row == 0 then
return nil
end
i -= 1
return dense_array[row]
end
end
local it = {
__iter = iter,
without = function(self, ...)
q_shim.filter_without = { ... }
return self
end
}
return setmetatable(it, it)
end
local function observers_add(world)
local signals = {
added = {},
emplaced = {},
removed = {}
}
world.added = function(_, component, fn)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local idr = jecs.id_record_ensure(world, component)
idr.hooks.on_add = function(entity)
for _, listener in listeners do
listener(entity)
end
end
end
table.insert(listeners, fn)
end
world.changed = function(_, component, fn)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local idr = jecs.id_record_ensure(world, component)
idr.hooks.on_change = function(entity, value)
for _, listener in listeners do
listener(entity, value)
end
end
end
table.insert(listeners, fn)
end
world.removed = function(_, component, fn)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local idr = jecs.id_record_ensure(world, component)
idr.hooks.on_remove = function(entity)
for _, listener in listeners do
listener(entity)
end
end
end
table.insert(listeners, fn)
end
world.signals = signals
world.track = world_track
world.observer = observers_new
return world
end
return observers_add

View file

@ -1,22 +0,0 @@
---
name: Bug report
about: File a bug report for any behavior that you believe is unintentional or problematic
title: "[BUG]"
labels: bug
assignees: ''
---
## Describe the bug
Put a clear and concise description of what the bug is. This should be short and to the point, not to exceed more than a paragraph. Put the details inside your reproduction steps.
## Reproduction
Make an easy-to-follow guide on how to reproduce it. Does it happen all the time? Will specific features affect reproduction? All these questions should be answered for a good issue.
This is a good place to put rbxl files or scripts that help explain your reproduction steps.
## Expected Behavior
What you expect to happen
## Actual Behavior
What actually happens

View file

@ -1,14 +0,0 @@
---
name: Documentation
about: Open an issue to add, change, or otherwise modify any part of the documentation.
title: "[DOCS]"
labels: documentation
assignees: ''
---
## Which Sections Does This Issue Cover?
[Put sections (e.g. Query Concepts), page links, etc as necessary]
## What Needs To Change?
What specifically needs to change and what suggestions do you have to change it?

View file

@ -1,27 +0,0 @@
---
name: Feature Request
about: File a feature request for something you believe should be added to Jecs
title: "[FEATURE]"
labels: enhancement
assignees: ''
---
## Describe your Feature
You should explain your feature here, and the motivation for why you want it.
## Implementation
Explain how you would implement your feature here. Provide relevant API examples and such here (if applicable).
## Alternatives
What other alternative implementations or otherwise relevant information is important to why you decided to go with this specific implementation?
## Considerations
Some questions that need to be answered include the following:
- Will old code break in response to this feature?
- What are the performance impacts with this feature (if any)?
- How is it useful to include?

View file

@ -1,15 +0,0 @@
## Brief Description of your Changes.
Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here.
## Impact of your Changes
What implications will this have on the project? Will there be altered behavior or performance with this change?
## Tests Performed
What have you done to ensure this change has the least possible impact on the project?
## Additional Comments
Anything else you feel is relevant.

View file

@ -1,19 +0,0 @@
name: analysis
on: [push, pull_request, workflow_dispatch]
jobs:
run:
name: Run Luau Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Luau
uses: encodedvenom/install-luau@v2.1
- name: Analyze
run: |
output=$(luau-analyze src || true) # Suppress errors for now.

View file

@ -1,11 +0,0 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: npm
directory: "/"
schedule:
interval: "daily"

View file

@ -1,64 +0,0 @@
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: deploy-docs
on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci # or pnpm install / yarn install / bun install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -1,17 +0,0 @@
name: publish-npm
on:
push:
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: "20"
- uses: JS-DevTools/npm-publish@v3
with:
token: ${{ secrets.NPM_AUTH_TOKEN }}

View file

@ -1,71 +0,0 @@
name: release
on:
push:
tags: ["v*"]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Rokit
uses: CompeyDev/setup-rokit@v0.1.2
- name: Install Dependencies
run: wally install
- name: Build
run: rojo build --output build.rbxm default.project.json
- name: Upload Build Artifact
uses: actions/upload-artifact@v3
with:
name: build
path: build.rbxm
release:
name: Release
needs: [build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Download Jecs Build
uses: actions/download-artifact@v3
with:
name: build
path: build
- name: Rename Build
run: mv build/build.rbxm jecs.rbxm
- name: Create Release
uses: softprops/action-gh-release@v1
with:
name: Jecs ${{ github.ref_name }}
files: |
jecs.rbxm
publish:
name: Publish
needs: [release]
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Rokit
uses: CompeyDev/setup-rokit@v0.1.2
- name: Wally Login
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
- name: Publish
run: wally publish

View file

@ -1,31 +0,0 @@
name: unit-testing
on: [push, pull_request, workflow_dispatch]
jobs:
run:
name: Run Luau Tests
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Luau
uses: encodedvenom/install-luau@v4.3
with:
version: "latest"
verbose: "true"
- name: Run Unit Tests
id: run_tests
run: |
output=$(luau test/tests.luau)
echo "$output"
if [[ "$output" == *"0 fails"* ]]; then
echo "Unit Tests Passed"
else
echo "Error: One or More Unit Tests Failed."
exit 1
fi

View file

@ -1,69 +1,64 @@
import { defineConfig } from 'vitepress'
import { defineConfig } from "vitepress";
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Jecs",
base: "/jecs/",
description: "A VitePress Site",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Learn', link: '/' },
{ text: 'API', link: '/api/jecs.md' },
{ text: 'Examples', link: 'https://github.com/Ukendio/jecs/tree/main/examples' },
],
title: "Jecs",
base: "/jecs/",
description: "A VitePress Site",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Learn", link: "/" },
{ text: "API", link: "/api/jecs.md" },
{ text: "Examples", link: "https://github.com/Ukendio/jecs/tree/main/examples" },
],
sidebar: {
"/api/": [
{
text: "API reference",
items: [
{ text: "jecs", link: "/api/jecs" },
{ text: "World", link: "/api/world" },
{ text: "Query", link: "/api/query" }
]
}
],
"/learn/": [
{
text: "Introduction",
items: [
{ text: 'Getting Started', link: '/learn/overview/get-started' },
{ text: 'First Jecs Project', link: '/learn/overview/first-jecs-project' }
]
},
{
text: 'Concepts',
items: [
{ text: 'Entities and Components', link: '/learn/concepts/entities-and-components' },
{ text: 'Queries', link: '/learn/concepts/queries' },
{ text: 'Relationships', link: '/learn/concepts/relationships' },
{ text: 'Component Traits', link: 'learn/concepts/component-traits' },
{ text: 'Addons', link: '/learn/concepts/addons' }
]
},
{
text: "FAQ",
items: [
{ text: 'How can I contribute?', link: '/learn/faq/contributing' }
]
},
sidebar: {
"/api/": [
{
text: "API reference",
items: [
{ text: "jecs", link: "/api/jecs" },
{ text: "World", link: "/api/world" },
{ text: "Query", link: "/api/query" },
],
},
],
"/learn/": [
{
text: "Introduction",
items: [
{ text: "Getting Started", link: "/learn/overview/get-started" },
{ text: "First Jecs Project", link: "/learn/overview/first-jecs-project" },
],
},
{
text: "Concepts",
items: [
{ text: "Entities and Components", link: "/learn/concepts/entities-and-components" },
{ text: "Queries", link: "/learn/concepts/queries" },
{ text: "Relationships", link: "/learn/concepts/relationships" },
{ text: "Component Traits", link: "learn/concepts/component-traits" },
{ text: "Addons", link: "/learn/concepts/addons" },
],
},
{
text: "FAQ",
items: [{ text: "How can I contribute?", link: "/learn/faq/contributing" }],
},
],
"/contributing/": [
{
text: "Contributing",
items: [
{ text: "Contribution Guidelines", link: "/learn/contributing/guidelines" },
{ text: "Submitting Issues", link: "/learn/contributing/issues" },
{ text: "Submitting Pull Requests", link: "/learn/contributing/pull-requests" },
],
},
],
},
],
"/contributing/": [
{
text: 'Contributing',
items: [
{ text: 'Contribution Guidelines', link: '/learn/contributing/guidelines' },
{ text: 'Submitting Issues', link: '/learn/contributing/issues' },
{ text: 'Submitting Pull Requests', link: '/learn/contributing/pull-requests' },
]
}
]
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/ukendio/jecs' }
]
}
})
socialLinks: [{ icon: "github", link: "https://github.com/ukendio/jecs" }],
},
});

View file

@ -45,6 +45,6 @@ function jecs.pair(
```
::: info
Note that while relationship pairs can be used as components, meaning you can add data with it as an ID, however they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
:::

View file

@ -2,12 +2,26 @@
A collection of third-party jecs addons made by the community. If you would like to share what you're working on, [submit a pull request](https://github.com/Ukendio/jecs)!
# Debuggers
# Development tools
## [jabby](https://github.com/alicesaidhi/jabby)
A jecs debugger with a string-based query language and entity editing capabilities.
## [jecs_entity_visualiser](https://github.com/Ukendio/jecs/tree/main/addons/entity_visualiser)
A simple entity and component visualiser in the output
## [jecs_lifetime_tracker](https://github.com/Ukendio/jecs/tree/main/addons/lifetime_tracker)
A tool for inspecting entity lifetimes
# Helpers
## [jecs_observers](https://github.com/Ukendio/jecs/tree/main/addons/observers)
Observers for queries and signals for components
# Schedulers
## [lockstep scheduler](https://gist.github.com/1Axen/6d4f78b3454cf455e93794505588354b)
@ -26,3 +40,5 @@ Provides hooks and a scheduler that implements jabby and a topographical runtime
An agnostic scheduler inspired by Bevy and Flecs, with core features including phases, pipelines, run conditions, and startup systems.
Planck also provides plugins for Jabby, Matter Hooks, and more.
# Observers

View file

@ -61,8 +61,8 @@ type ecs_id_record_t = {
flags: number,
size: number,
hooks: {
on_add: ((entity: i53) -> ())?,
on_set: ((entity: i53, data: any) -> ())?,
on_add: ((entity: i53, data: any?) -> ())?,
on_change: ((entity: i53, data: any) -> ())?,
on_remove: ((entity: i53) -> ())?,
},
}
@ -111,7 +111,7 @@ local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256
-- stylua: ignore start
local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2
local EcsOnSet = HI_COMPONENT_ID + 3
local EcsOnChange = HI_COMPONENT_ID + 3
local EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5
local EcsComponent = HI_COMPONENT_ID + 6
@ -124,12 +124,9 @@ local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12
local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13
local EcsRest = HI_COMPONENT_ID + 14
local ECS_ID_DELETE = 0b0000_0001
local ECS_ID_IS_TAG = 0b0000_0010
local ECS_ID_HAS_ON_ADD = 0b0000_0100
local ECS_ID_HAS_ON_SET = 0b0000_1000
local ECS_ID_HAS_ON_REMOVE = 0b0001_0000
local ECS_ID_MASK = 0b0000_0000
local ECS_ID_DELETE = 0b01
local ECS_ID_IS_TAG = 0b10
local ECS_ID_MASK = 0b00
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
@ -572,9 +569,11 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t
has_delete = true
end
local on_add, on_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, EcsOnRemove)
local on_add, on_change, on_remove = world_get(world,
relation, EcsOnAdd, EcsOnChange, EcsOnRemove)
local is_tag = not world_has_one_inline(world, relation, EcsComponent)
local is_tag = not world_has_one_inline(world,
relation, EcsComponent)
if is_tag and is_pair then
is_tag = not world_has_one_inline(world, target, EcsComponent)
@ -582,9 +581,6 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t
flags = bit32.bor(
flags,
if on_add then ECS_ID_HAS_ON_ADD else 0,
if on_remove then ECS_ID_HAS_ON_REMOVE else 0,
if on_set then ECS_ID_HAS_ON_SET else 0,
if has_delete then ECS_ID_DELETE else 0,
if is_tag then ECS_ID_IS_TAG else 0
)
@ -596,7 +592,7 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t
flags = flags,
hooks = {
on_add = on_add,
on_set = on_set,
on_change = on_change,
on_remove = on_remove,
},
}
@ -737,13 +733,13 @@ local function find_archetype_with(world: ecs_world_t, node: ecs_archetype_t, id
-- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array.
local dst = table.clone(node.types) :: { i53 }
local at = find_insert(id_types, id)
if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it
-- directly instead of needing to hash types for a lookup to the archetype.
return node
end
local dst = table.clone(id_types) :: { i53 }
table.insert(dst, at, id)
return archetype_ensure(world, dst)
@ -931,14 +927,15 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown
local idr_hooks = idr.hooks
if from == to then
-- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly.
local tr = to.records[id]
local column = from.columns[tr]
column[record.row] = data
local on_set = idr_hooks.on_set
if on_set then
on_set(entity, data)
-- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly.
local on_change = idr_hooks.on_change
if on_change then
on_change(entity, data)
end
return
@ -961,12 +958,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown
local on_add = idr_hooks.on_add
if on_add then
on_add(entity)
end
local on_set = idr_hooks.on_set
if on_set then
on_set(entity, data)
on_add(entity, data)
end
end
@ -2471,7 +2463,7 @@ local function world_new()
end
world_add(self, EcsName, EcsComponent)
world_add(self, EcsOnSet, EcsComponent)
world_add(self, EcsOnChange, EcsComponent)
world_add(self, EcsOnAdd, EcsComponent)
world_add(self, EcsOnRemove, EcsComponent)
world_add(self, EcsWildcard, EcsComponent)
@ -2479,7 +2471,7 @@ local function world_new()
world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd")
world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove")
world_set(self, EcsOnSet, EcsName, "jecs.OnSet")
world_set(self, EcsOnChange, EcsName, "jecs.OnChange")
world_set(self, EcsWildcard, EcsName, "jecs.Wildcard")
world_set(self, EcsChildOf, EcsName, "jecs.ChildOf")
world_set(self, EcsComponent, EcsName, "jecs.Component")
@ -2612,7 +2604,7 @@ return {
OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>,
OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>,
OnSet = EcsOnSet :: Entity<(entity: Entity, data: any) -> ()>,
OnChange = EcsOnChange :: Entity<(entity: Entity, data: any) -> ()>,
ChildOf = EcsChildOf :: Entity,
Component = EcsComponent :: Entity,
Wildcard = EcsWildcard :: Entity,

View file

@ -0,0 +1,46 @@
local jecs = require("@jecs")
local observers_add = require("@addons/observers")
local world = jecs.world()
observers_add(world)
local A = world:component()
local B = world:component()
local C = world:component()
world:observer({
query = world:query(),
callback = function(entity)
buf ..= debug.info(2, "sl")
end
})
local i = 0
world:added(A, function(entity)
assert(i == 0)
i += 1
end)
world:added(A, function(entity)
assert(i == 1)
i += 1
end)
world:removed(A, function(entity)
assert(false)
end)
local observer = world:observer({
query = world:query(A, B),
callback = function(entity)
assert(i == 2)
i += 1
end
})
local e = world:entity()
world:add(e, A)
assert(i == 2)
world:add(e, B)
assert(i == 3)

View file

@ -133,6 +133,8 @@ TEST("#adding a recycled target", function()
end)
local entity_visualizer = require("@tools/entity_visualiser")
TEST("#repro2", function()
local world = world_new()
local Lifetime = world:component() :: jecs.Id<number>
@ -144,7 +146,6 @@ TEST("#repro2", function()
world:set(entity, pair(Lifetime, Beam), 2)
world:set(entity, pair(4, 5), 6) -- noise
local entity_visualizer = require("@tools/entity_visualiser")
-- entity_visualizer.components(world, entity)
for e in world:each(pair(Lifetime, __)) do
@ -1664,11 +1665,20 @@ TEST("Hooks", function()
local Number = world:component()
local e1 = world:entity()
world:set(Number, jecs.OnSet, function(entity, data)
local call = 0
world:set(Number, jecs.OnChange, function(entity, data)
CHECK(e1 == entity)
CHECK(data == world:get(entity, Number))
if call == 1 then
CHECK(false)
elseif call == 2 then
CHECK(world:get(entity, Number) == data)
end
CHECK(data == 1)
end)
call = 1
world:set(e1, Number, 1)
call = 2
world:set(e1, Number, 1)
end

View file

@ -2,7 +2,6 @@ local jecs = require("@jecs")
local pair = jecs.pair
local ChildOf = jecs.ChildOf
local lifetime_tracker_add = require("@tools/lifetime_tracker")
local pe = require("@tools/entity_visualiser").prettify
local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false})
local FriendsWith = world:component()
world:print_snapshot()

View file

@ -1,33 +0,0 @@
return {
white_underline = function(s: any)
return `\27[1;4m{s}\27[0m`
end,
white = function(s: any)
return `\27[37;1m{s}\27[0m`
end,
green = function(s: any)
return `\27[32;1m{s}\27[0m`
end,
red = function(s: any)
return `\27[31;1m{s}\27[0m`
end,
yellow = function(s: any)
return `\27[33;1m{s}\27[0m`
end,
red_highlight = function(s: any)
return `\27[41;1;30m{s}\27[0m`
end,
green_highlight = function(s: any)
return `\27[42;1;30m{s}\27[0m`
end,
gray = function(s: any)
return `\27[30;1m{s}\27[0m`
end,
}

View file

@ -1,7 +1,40 @@
local jecs = require("@jecs")
local ECS_GENERATION = jecs.ECS_GENERATION
local ECS_ID = jecs.ECS_ID
local ansi = require("@tools/ansi")
local ansi = {
white_underline = function(s: any)
return `\27[1;4m{s}\27[0m`
end,
white = function(s: any)
return `\27[37;1m{s}\27[0m`
end,
green = function(s: any)
return `\27[32;1m{s}\27[0m`
end,
red = function(s: any)
return `\27[31;1m{s}\27[0m`
end,
yellow = function(s: any)
return `\27[33;1m{s}\27[0m`
end,
red_highlight = function(s: any)
return `\27[41;1;30m{s}\27[0m`
end,
green_highlight = function(s: any)
return `\27[42;1;30m{s}\27[0m`
end,
gray = function(s: any)
return `\27[30;1m{s}\27[0m`
end,
}
local function pe(e: any)
local gen = ECS_GENERATION(e)

View file

@ -7,7 +7,6 @@ local pair = jecs.pair
local prettify = require("@tools/entity_visualiser").prettify
local pe = prettify
local ansi = require("@tools/ansi")
function print_centered_entity(entity, width: number)
local entity_str = tostring(entity)
@ -40,12 +39,10 @@ local function lifetime_tracker_add(world: jecs.World, opt)
local ENTITY_RANGE = (jecs.Rest :: any) + 1
local w = setmetatable({}, { __index = world })
padding_enabled = opt.padding_enabled
local world_entity = world.entity
w.entity = function(self, entity)
world.entity = function(_, entity)
if entity then
return world_entity(world, entity)
end
@ -59,7 +56,7 @@ local function lifetime_tracker_add(world: jecs.World, opt)
pad()
return e
end
w.print_entity_index = function(self)
world.print_entity_index = function()
local max_id = entity_index.max_id
local alive_count = entity_index.alive_count
local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {})
@ -86,7 +83,7 @@ local function lifetime_tracker_add(world: jecs.World, opt)
pad()
end
local timelines = {}
w.print_snapshot = function(self)
world.print_snapshot = function(_)
local timeline = #timelines + 1
local entity_column_width = 10
local status_column_width = 8
@ -161,7 +158,7 @@ local function lifetime_tracker_add(world: jecs.World, opt)
end
local world_add = world.add
local relations = {}
w.add = function(self, entity: any, component: any)
world.add = function(_, entity: any, component: any)
world_add(world, entity, component)
if jecs.IS_PAIR(component) then
local relation = jecs.pair_first(world, component)
@ -172,7 +169,7 @@ local function lifetime_tracker_add(world: jecs.World, opt)
end
local world_delete = world.delete
w.delete = function(self, e)
world.delete = function(world, e)
world_delete(world, e)
local idr_t = component_index[pair(__, e)]
@ -210,7 +207,7 @@ local function lifetime_tracker_add(world: jecs.World, opt)
print(`*deleted {pe(e)}`)
pad()
end
return w
return world
end
return lifetime_tracker_add

View file

@ -112,6 +112,7 @@ type Test = {
trace: string,
}?,
focus: boolean,
fn: () -> ()
}
type Case = {
@ -231,7 +232,9 @@ local function TEST(name: string, fn: () -> ())
fn = fn
}
table.insert(tests, test)
local t = test
table.insert(tests, t)
end
local function FOCUS()
@ -258,22 +261,22 @@ local function FINISH(): boolean
continue
end
test = t
fn = t.fn
local fn = t.fn
local start = os.clock()
local err
local success = xpcall(fn, function(m: string)
local ok = xpcall(fn, function(m: string)
err = { message = m, trace = debug.traceback(nil, 2) }
end)
test.duration = os.clock() - start
t.duration = os.clock() - start
if not test.case then
if not t.case then
CASE("")
end
assert(test.case, "no active case")
assert(t.case, "no active case")
if not success then
test.case.result = ERROR
test.error = err
if not ok then
t.case.result = ERROR
t.error = err
end
collectgarbage()
end