This commit is contained in:
EncodedVenom 2025-05-16 18:57:55 -04:00
commit cbb4b84dc6
9 changed files with 227 additions and 254 deletions

View file

@ -1,226 +1,169 @@
# Jecs Changelog # Changelog
All notable changes to this project will be documented in this file. ## Unreleased
The format is based on [Keep a Changelog][kac], and this project adheres to ## 0.6.0
[Semantic Versioning][semver].
[kac]: https://keepachangelog.com/en/1.1.0/
[semver]: https://semver.org/spec/v2.0.0.html
## [Unreleased]
- `[world]`:
- Added `world:range` to restrict entity range
- Changed `world:entity` to accept the overload to create an entity at the desired index
- 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
- Optimized `world:has` for both single component and multiple component presence.
- This comes at the cost that it cannot check the component presence for more than 4 components at a time. If this is important, consider calling to this function multiple times.
## [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]`:
- 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
- `[world]`:
- Changed `world:cleanup` to traverse a header type for graph edges. (Edit)
- Fixed a regression that occurred when you call `world:set` following a `world:remove` using the same component
- Remove explicit error in JECS_DEBUG for `world:target` when not applying an index parameter
- `[typescript]` :
- Fixed `world.set` with NoInfer<T>
## [0.3.1] - 2024-10-01
- `[world]`:
- Added an index parameter to `world:target`
- Added a way to change the components limit via `_G.JECS_HI_COMPONENT_ID`
- Set it to whatever number you want but try to make it as close to the number of components you will use as possible
- Make sure to set this before importing jecs or else it will not work
- Added debug mode, enable via setting `_G.JECS_DEBUG` to true
- Make sure to set this before importing jecs or else it will not work
- Added `world:cleanup` which is called to cleanup empty archetypes manually
- Changed `world:delete` to delete archetypes that are dependent on the passed entity
- Changed `world:delete` to delete entity's children before the entity to prevent cycles
- `[query]`:
- Fixed the iterator to not drain by default
- `[typescript]`
- Fixed entry point of the package.json file to be `src` rather than `src/init`
- Fixed `query.next` returning a query object whereas it would be expected to return a tuple containing the entity and the corresponding component values
- Exported `query.archetypes`
- Changed `pair` to return a number instead of an entity
- Preventing direct usage of a pair as an entity while still allowing it to be used as a component
- Exported built-in components `ChildOf` and `Name`
- Exported `world.parent`
## [0.2.10] - 2024-09-07
- `[world]`:
- Improved performance for hooks
- Changed `world:set` to be idempotent when setting tags
- `[traits]`:
- Added cleanup condition `jecs.OnDelete` for when the entity or component is deleted
- Added cleanup action `jecs.Remove` which removes instances of the specified (component) id from all entities
- This is the default cleanup action
- Added component trait `jecs.Tag` which allows for zero-cost components used as tags
- Setting data to a component with this trait will do nothing
- `[luau]`:
- Exported `world:contains()`
- Exported `query:drain()`
- Exported `Query`
- Improved types for the hook `OnAdd`, `OnSet`, `OnRemove`
- Changed functions to accept any ID including pairs in type parameters
- Applies to `world:add()`, `world:set()`, `world:remove()`, `world:get()`, `world:has()` and `world:query()`
- New exported type `Id<T = nil> = Entity<T> | Pair`
- Changed `world:contains()` to return a `boolean` instead of an entity which may or may not exist
- Fixed `world:has()` to take the correct parameters
## [0.2.2] - 2024-07-07
### Added ### Added
- `World:range` to restrict entity range to allow for e.g. reserving ids `1000..5000` for clients and everything above that (5000+) for entities from the server. This makes it possible to receive ids from a server that don't have to be mapped to local ids.
- Added `query:replace(function(...T) return ...U end)` for replacing components in place - `jecs.component`, `jecs.tag` and `jecs.meta` for preregistering ids and their metadata before a world
- Method is fast pathed to replace the data to the components for each corresponding entity - Overload to `World:entity` to create an entity at the desired id.
### Changed ### Changed
- `World:clear` to remove the `ID` from every entity instead of the previous behaviour of removing all of the components on the entity. You should prefer deleting the entity instead for the previous behaviour.
- Iterator now goes backwards instead to prevent common cases of iterator invalidation - Entity ID layouts by putting the index in the lower bits, which should make every world function 15 nanoseconds faster.
- Hooks now pass the full component ID which is useful for pairs when you need both the relation and the target.
## [0.2.1] - 2024-07-06 - Replaced `OnSet` with `OnChange`, which now only runs when the component ID was previously present on the entity.
- `OnAdd` now runs after the value has been set and also passes the component ID and the value.
### Added - `OnRemove` now lazily looks up which archetype the entity will move to. This is meant to support interior structural changes within every hook.
- Optimized `world:has` for both single and multiple component presence. This comes at the cost that it cannot check the component presence for more than 4 components at a time. If this is important, consider calling this function multiple times.
- Added `jecs.Component` built-in component which will be added to ids created with `world:component()`.
- Used to find every component id with `query(jecs.Component)
## [0.2.0] - 2024-07-03
### Added
- Added `world:parent(entity)` and `jecs.ChildOf` respectively as first class citizen for building parent-child relationships.
- Give a parent to an entity with `world:add($source, pair(ChildOf, $target))`
- Use `world:parent(entity)` to find the target of the relationship
- Added user-facing Luau types
### Changed
- Improved iteration speeds 20-40% by manually indexing rather than using `next()` :scream:
## [0.1.1] - 2024-05-19
### Added
- Added `world:clear(entity)` for removing the components to the corresponding entity
- Added Typescript Types
## [0.1.0] - 2024-05-13
### Changed
- Optimized iterator
## [0.1.0-rc.6] - 2024-05-13
### Added
- Added a `jecs.Wildcard` term
- it lets you query any partially matched pairs
## [0.1.0-rc.5] - 2024-05-10
### Added
- Added Entity relationships for creating logical connections between entities
- Added `world:__iter method` which allows for iteration over the whole world to get every entity
- used for reconciling whole worlds such as via replication, saving/loading, etc
- Added `world:add(entity, component)` which adds a component to the entity
- it is an idempotent function, so calling it twice and in any order should be fine
### Fixed ### Fixed
- `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.
- Fixed component overriding when in disorder ## 0.5.0
- Previously setting the components in different order results in it overriding component data because it incorrectly mapped the index of the column. So it took the index from the source archetype rather than the destination archetype
## [0.0.0-prototype.rc.3] - 2024-05-01
### Added ### Added
- `World:each` to find entities with a specific Tag.
- Added observers - `World:children` to find children of an entity.
- Added an arm to query `query:without()` for chaining invariants. - `Query:cached` to add a query cache that updates itself when an archetype matching the query gets created or deleted.
### Changed ### Changed
- Inference of entities' types using user-defined type functions.
- `Pair<First, Second>` to return `Second` if `First` is a `Tag`; otherwise, returns `First`.
- Separates ranges for components and entity IDs. ### Fixed
- `World:target` not giving adjacent pairs.
- IDs created with `world:component()` will promote array lookups rather than map lookups in the `component_index` which is a significant boost ## 0.4.0
- No longer caches the column pointers directly and instead the column indices which stay persistent even when data is reallocated during swap-removals ### Added
- This was an issue with the iterator being invalidated when you move an entity to a different archetype. - Recycling support to `world:entity` so reused entity IDs now increment generation.
### Fixedhttps://github.com/Ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3 ### Removed
- `Query:drain`
- Fixed a bug where changing an existing component would be slow because it was always appending changing the row of the entity record - `Query:next`
- The fix dramatically improves times where it is basically down to just the speed of setting a field in a table - `Query:replace`
## [0.0.0-prototype.rc.2] - 2024-04-26
### Changed ### Changed
- `jecs.Pair` type in Luau now returns the first element's type to avoid manual typecasting.
- Optimized the creation of the query ### Fixed
- It will now finds the smallest archetype map to iterate over - `Query:archetypes` now correctly takes `self`.
- Optimized the query iterator
- It will now populates iterator with columns for faster indexing ## 0.3.2 - 2024-10-01
- Renamed the insertion method from world:add to world:set to better reflect what it does. ### Changed
- `World:cleanup` to traverse a header type for graph edges.
## [0.0.0-prototype.rc.2] - 2024-04-23 ### Fixed
- Regression when calling `World:set` after `World:remove` on the same component.
- Removed explicit error in `JECS_DEBUG` for `World:target` missing index.
- `World.set` type inference with `NoInfer<T>` in TypeScript.
- Initial release ## 0.3.1 - 2024-10-01
[unreleased]: https://github.com/ukendio/jecs/compare/v0.0.0.0-prototype.rc.2...HEAD ### Added
[0.2.2]: https://github.com/ukendio/jecs/releases/tag/v0.2.2 - Index parameter to `World:target`.
[0.2.1]: https://github.com/ukendio/jecs/releases/tag/v0.2.1 - Global config `_G.JECS_HI_COMPONENT_ID` to change component ID limit (must be set before importing JECS).
[0.2.0]: https://github.com/ukendio/jecs/releases/tag/v0.2.0 - Debug mode via `_G.JECS_DEBUG` (must be set before importing JECS).
[0.1.1]: https://github.com/ukendio/jecs/releases/tag/v0.1.1 - `world:cleanup` to manually clean up empty archetypes.
[0.1.0]: https://github.com/ukendio/jecs/releases/tag/v0.1.0
[0.1.0-rc.6]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.6 ### Changed
[0.1.0-rc.5]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.5 - `world:delete` now also deletes dependent archetypes and child entities.
[0.0.0-prototype-rc.3]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3
[0.0.0-prototype.rc.2]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.2 ### Fixed
[0.0.0-prototype-rc.1]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.1 - `Query` iterator to not drain by default.
- TypeScript package entry to `src` instead of `src/init`.
- `Query.next` now returns expected result tuple in typescript.
- `pair` returns a number instead of entity to prevent misuse.
- Exported built-in components `ChildOf`, `Name`, and `world.parent`.
## 0.2.10
### Added
- Trait `jecs.Tag` for zero-cost tag components.
- Cleanup conditions: `jecs.OnDelete`, `jecs.Remove`.
### Changed
- `world:set` is now idempotent when setting tags.
### Fixed
- Improved performance for hooks.
- Exported types and functions: `world:contains()`, `query:drain()`, `Query`.
- Hook types: `OnAdd`, `OnSet`, `OnRemove`.
- ID flexibility for `add`, `set`, `remove`, `get`, `has`, `query`.
- `world:contains()` now returns `boolean`.
- `world:has()` parameters now correct.
## 0.2.2
### Added
- `query:replace(fn)` for in-place replacement of component values.
### Changed
- Iterator now goes backwards to avoid invalidation during iteration.
## 0.2.1
### Added
- Built-in `jecs.Component` used to find all component IDs.
## 0.2.0
### Added
- `world:parent(entity)` and `jecs.ChildOf` for parent-child relationships.
### Changed
- Iteration performance improved by 2040% through manual indexing.
## 0.1.1
### Added
- `world:clear(entity)` for removing all components from an entity.
- TypeScript types.
## 0.1.0
### Changed
- Optimized iterator.
## 0.1.0-rc.6
### Added
- `jecs.Wildcard` term to query partially matched pairs.
## 0.1.0-rc.5
### Added
- Entity relationships.
- `world:__iter()` for full world iteration.
- `world:add(entity, component)` (idempotent).
### Fixed
- Component overriding when set out of order.
## 0.0.0-prototype.rc.3
### Added
- Observers.
- `query:without()` for invariant queries.
### Changed
- Separate ID ranges for entities and components.
- Avoid caching pointers; cache stable column indices instead.
### Fixed
- Slow component updates due to unnecessary row changes.
## 0.0.0-prototype.rc.2 - 2024-04-26
### Changed
- Query now uses smallest archetype map.
- Optimized query iterator.
- Renamed `world:add` to `world:set`.
## 0.0.0-prototype.rc.1
### Added
- Initial release.

View file

@ -1,16 +1,16 @@
local jecs = require("@jecs") local jecs = require("@jecs")
type Observer<T...> = { type Observer = {
callback: (jecs.Entity) -> (), callback: (jecs.Entity) -> (),
query: jecs.Query<T...>, query: jecs.Query<...jecs.Id>,
} }
export type PatchedWorld = jecs.World & { export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (), added: <T>(PatchedWorld, jecs.Id<T>, (jecs.Entity, jecs.Id, T) -> ()) -> () -> (),
removed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id) -> ()) -> () -> (), removed: <T>(PatchedWorld, jecs.Id<T>, (jecs.Entity, jecs.Id) -> ()) -> () -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (), changed: <T>(PatchedWorld, jecs.Id<T>, (jecs.Entity, jecs.Id, T) -> ()) -> () -> (),
observer: (PatchedWorld, Observer<any>) -> (), observer: (PatchedWorld, Observer) -> (),
monitor: (PatchedWorld, Observer<any>) -> (), monitor: (PatchedWorld, Observer) -> (),
} }
local function observers_new(world, description) local function observers_new(world, description)

View file

@ -5,15 +5,19 @@ local collect = require("../collect")
local client_ids = {} local client_ids = {}
local function ecs_map_get(world: types.World, id: types.Entity) local function ecs_map_get(world: types.World, id: types.Entity)
local deserialised_id = 0 local deserialised_id = client_ids[id]
if not deserialised_id then
if not world:exists(id) or not world:contains(id) then if world:has(id, jecs.Name) then
deserialised_id = world:entity(id) deserialised_id = world:entity(id)
else
if world:exists(id) then
deserialised_id = world:entity()
else
deserialised_id = world:entity(id)
end
end
client_ids[id] = deserialised_id client_ids[id] = deserialised_id
else
deserialised_id = client_ids[id]
end end
return deserialised_id return deserialised_id
end end

69
jecs.d.ts vendored
View file

@ -108,6 +108,13 @@ export class World {
*/ */
constructor(); constructor();
/**
* Enforces a check for entities to be created within a desired range.
* @param range_begin The starting point
* @param range_end The end point (optional)
*/
range(range_begin: number, range_end?: number): void;
/** /**
* Creates a new entity. * Creates a new entity.
* @returns An entity (Tag) with no data. * @returns An entity (Tag) with no data.
@ -130,19 +137,6 @@ export class World {
*/ */
target(entity: Entity, relation: Entity, index?: number): Entity | undefined; target(entity: Entity, relation: Entity, index?: number): Entity | undefined;
/**
* Cleans up the world by removing empty archetypes and rebuilding the archetype collections.
* This helps maintain memory efficiency by removing unused archetype definitions.
*/
cleanup(): void;
/**
* Clears all components and relationships from the given entity, but
* does not delete the entity from the world.
* @param entity The entity to clear.
*/
clear(entity: Entity): void;
/** /**
* Deletes an entity (and its components/relationships) from the world entirely. * Deletes an entity (and its components/relationships) from the world entirely.
* @param entity The entity to delete. * @param entity The entity to delete.
@ -164,6 +158,19 @@ export class World {
*/ */
set<E extends Id<unknown>>(entity: Entity, component: E, value: InferComponent<E>): void; set<E extends Id<unknown>>(entity: Entity, component: E, value: InferComponent<E>): void;
/**
* Cleans up the world by removing empty archetypes and rebuilding the archetype collections.
* This helps maintain memory efficiency by removing unused archetype definitions.
*/
cleanup(): void;
/**
* Clears all components and relationships from the given entity, but
* does not delete the entity from the world.
* @param entity The entity to clear.
*/
clear(entity: Entity): void;
/** /**
* Removes a component from the given entity. * Removes a component from the given entity.
* @param entity The target entity. * @param entity The target entity.
@ -191,12 +198,6 @@ export class World {
*/ */
has(entity: Entity, ...components: Id[]): boolean; has(entity: Entity, ...components: Id[]): boolean;
/**
* Checks if an entity exists in the world.
* @param entity The entity to verify.
*/
contains(entity: Entity): boolean;
/** /**
* Gets the parent (the target of a `ChildOf` relationship) for an entity, * Gets the parent (the target of a `ChildOf` relationship) for an entity,
* if such a relationship exists. * if such a relationship exists.
@ -205,11 +206,17 @@ export class World {
parent(entity: Entity): Entity | undefined; parent(entity: Entity): Entity | undefined;
/** /**
* Searches the world for entities that match specified components. * Checks if an entity exists in the world.
* @param components The list of components to query. * @param entity The entity to verify.
* @returns A Query object to iterate over results.
*/ */
query<T extends Id[]>(...components: T): Query<InferComponents<T>>; contains(entity: Entity): boolean;
/**
* Checks if an entity with the given ID is currently alive, ignoring its generation.
* @param entity The entity to verify.
* @returns boolean true if any entity with the given ID exists (ignoring generation), false otherwise
*/
exists(entity: Entity): boolean;
/** /**
* Returns an iterator that yields all entities that have the specified component or relationship. * Returns an iterator that yields all entities that have the specified component or relationship.
@ -225,8 +232,24 @@ export class World {
* @returns An iterator function that yields child entities * @returns An iterator function that yields child entities
*/ */
children(parent: Entity): IterableFunction<Entity>; children(parent: Entity): IterableFunction<Entity>;
/**
* Searches the world for entities that match specified components.
* @param components The list of components to query.
* @returns A Query object to iterate over results.
*/
query<T extends Id[]>(...components: T): Query<InferComponents<T>>;
} }
export function component<T>(): Entity<T>;
export function tag<T>(): Entity<T>;
// note: original types had id: Entity, id: Id<T>, which does not work with TS.
export function meta<T>(e: Entity, id: Id<T>, value?: T): Entity<T>
export function is_tag<T>(world: World, id: Id<T>): boolean;
/** /**
* Creates a composite key (pair) * Creates a composite key (pair)
* @param pred The first entity (predicate) * @param pred The first entity (predicate)

View file

@ -780,7 +780,11 @@ local function world_entity(world: ecs_world_t, entity: i53?): i53
local any = dense_array[dense] local any = dense_array[dense]
if dense <= alive_count then if dense <= alive_count then
return any if any ~= entity then
error("Entity ID is already in use with a different generation")
else
return entity
end
end end
local e_swap = dense_array[dense] local e_swap = dense_array[dense]
@ -790,9 +794,9 @@ local function world_entity(world: ecs_world_t, entity: i53?): i53
r_swap.dense = dense r_swap.dense = dense
r.dense = alive_count r.dense = alive_count
dense_array[dense] = e_swap dense_array[dense] = e_swap
dense_array[alive_count] = any dense_array[alive_count] = entity
return any return entity
else else
for i = max_id + 1, index do for i = max_id + 1, index do
sparse_array[i] = { dense = i } :: ecs_record_t sparse_array[i] = { dense = i } :: ecs_record_t
@ -2646,10 +2650,10 @@ return {
tag = (ECS_TAG :: any) :: <T>() -> Entity<T>, tag = (ECS_TAG :: any) :: <T>() -> Entity<T>,
meta = (ECS_META :: any) :: <T>(id: Entity, id: Id<T>, value: T) -> Entity<T>, meta = (ECS_META :: any) :: <T>(id: Entity, id: Id<T>, value: T) -> Entity<T>,
is_tag = (ecs_is_tag :: any) :: <T>(World, Id<T>) -> boolean, is_tag = (ecs_is_tag :: any) :: <T>(World, Id<T>) -> boolean,
OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, OnAdd = EcsOnAdd :: Entity<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, OnRemove = EcsOnRemove :: Entity<(entity: Entity, id: Id) -> ()>,
OnChange = EcsOnChange :: Entity<(entity: Entity, data: any) -> ()>, OnChange = EcsOnChange :: Entity<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
ChildOf = EcsChildOf :: Entity, ChildOf = EcsChildOf :: Entity,
Component = EcsComponent :: Entity, Component = EcsComponent :: Entity,
Wildcard = EcsWildcard :: Entity, Wildcard = EcsWildcard :: Entity,

View file

@ -1,6 +1,6 @@
{ {
"name": "@rbxts/jecs", "name": "@rbxts/jecs",
"version": "0.6.0-rc.1", "version": "0.6.0",
"description": "Stupidly fast Entity Component System", "description": "Stupidly fast Entity Component System",
"main": "jecs.luau", "main": "jecs.luau",
"repository": { "repository": {

View file

@ -23,7 +23,6 @@ TEST("addons/observers", function()
world:set(world:entity(), A, true) world:set(world:entity(), A, true)
CHECK(count == 3) CHECK(count == 3)
print(count)
end end
do CASE "Ensure ordering between signals and observers" do CASE "Ensure ordering between signals and observers"

View file

@ -672,7 +672,7 @@ TEST("world:range()", function()
local e2v1 = world:entity(399) local e2v1 = world:entity(399)
CHECK(world:contains(e2v1)) CHECK(world:contains(e2v1))
CHECK(ECS_ID(e2v1) == 399) CHECK(ECS_ID(e2v1) == 399)
CHECK(ECS_GENERATION(e2v1) == 1) CHECK(ECS_GENERATION(e2v1) == 0)
end end
do CASE "over range start" do CASE "over range start"
@ -685,12 +685,12 @@ TEST("world:range()", function()
local e2v1 = world:entity(405) local e2v1 = world:entity(405)
CHECK(world:contains(e2v1)) CHECK(world:contains(e2v1))
CHECK(ECS_ID(e2v1) == 405) CHECK(ECS_ID(e2v1) == 405)
CHECK(ECS_GENERATION(e2v1) == 1) CHECK(ECS_GENERATION(e2v1) == 0)
do world:delete(e2v1)
local _e2v1 = world:entity(405) local e2v2 = world:entity(e2v1)
CHECK(_e2v1 == e2v1) CHECK(ECS_ID(e2v2) == 405)
end CHECK(ECS_GENERATION(e2v2) == 0)
end end
end) end)

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ukendio/jecs" name = "ukendio/jecs"
version = "0.6.0-rc.1" version = "0.6.0"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
license = "MIT" license = "MIT"