From 0243efe4ba4d91d6143397f2d81a67b08fd4dec7 Mon Sep 17 00:00:00 2001 From: EncodedVenom <32179912+EncodedVenom@users.noreply.github.com> Date: Fri, 27 Dec 2024 14:05:10 -0500 Subject: [PATCH 1/8] Remove pin on luau version --- .github/workflows/unit-testing.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/unit-testing.yaml b/.github/workflows/unit-testing.yaml index 01a9981..9834d9d 100644 --- a/.github/workflows/unit-testing.yaml +++ b/.github/workflows/unit-testing.yaml @@ -15,7 +15,6 @@ jobs: - name: Install Luau uses: encodedvenom/install-luau@v4.2 with: - version: '0.651' verbose: 'true' - name: Run Unit Tests From 500c49481219a4b86d436c15a0758601e379e6e3 Mon Sep 17 00:00:00 2001 From: EncodedVenom <32179912+EncodedVenom@users.noreply.github.com> Date: Fri, 27 Dec 2024 14:09:39 -0500 Subject: [PATCH 2/8] Fix unit test --- .github/workflows/unit-testing.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit-testing.yaml b/.github/workflows/unit-testing.yaml index 9834d9d..984f50d 100644 --- a/.github/workflows/unit-testing.yaml +++ b/.github/workflows/unit-testing.yaml @@ -15,6 +15,7 @@ jobs: - name: Install Luau uses: encodedvenom/install-luau@v4.2 with: + version: 'latest' verbose: 'true' - name: Run Unit Tests From c8884c8eac932b3c41363b7c1960dca4d2190ec1 Mon Sep 17 00:00:00 2001 From: EncodedVenom <32179912+EncodedVenom@users.noreply.github.com> Date: Fri, 27 Dec 2024 14:20:06 -0500 Subject: [PATCH 3/8] bump ver --- .github/workflows/unit-testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-testing.yaml b/.github/workflows/unit-testing.yaml index 984f50d..43d2aa8 100644 --- a/.github/workflows/unit-testing.yaml +++ b/.github/workflows/unit-testing.yaml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v4 - name: Install Luau - uses: encodedvenom/install-luau@v4.2 + uses: encodedvenom/install-luau@v4.3 with: version: 'latest' verbose: 'true' From 489366bb8e5540b73c7638378ecf61f66820b534 Mon Sep 17 00:00:00 2001 From: juste <73649494+hautajoki@users.noreply.github.com> Date: Thu, 2 Jan 2025 11:39:29 +0200 Subject: [PATCH 4/8] Update ts declarations (#170) --- jecs.d.ts | 228 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 126 insertions(+), 102 deletions(-) diff --git a/jecs.d.ts b/jecs.d.ts index 8701f55..a343b4e 100644 --- a/jecs.d.ts +++ b/jecs.d.ts @@ -2,7 +2,10 @@ * A unique identifier in the world, entity. * The generic type T defines the data type when this entity is used as a component */ -export type Entity = number & { __jecs_value: T }; +export type Entity = number & { + readonly __nominal_Entity: unique symbol; + readonly __type_TData: TData; +}; /** * An entity with no associated data when used as a component @@ -10,172 +13,193 @@ export type Entity = number & { __jecs_value: T }; export type Tag = Entity; /** - * A pair of entities - * P is the type of the predicate, O is the type of the object, and V is the type of the value (defaults to P) + * A pair of entities: + * - `pred` is the type of the "predicate" entity. + * - `obj` is the type of the "object" entity. */ -export type Pair

= number & { - __jecs_pair_pred: P; - __jecs_pair_obj: O; - __jecs_pair_value: P extends Entity - ? O extends Entity - ? V - : never - : P extends Entity ? V : never +export type Pair

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

, obj: Entity): Pair, Entity>; +export function pair(pred: Entity

, obj: Entity): Pair; /** * Checks if the entity is a composite key (pair) @@ -198,24 +222,24 @@ export function IS_PAIR(value: Id): value is Pair; * @param pair The pair to get the first entity from * @returns The first entity (predicate) of the pair */ -export function pair_first(pair: Pair): Entity

; +export function pair_first(world: World, p: Pair): Entity

; /** * Gets the second entity (object) of a pair * @param pair The pair to get the second entity from * @returns The second entity (object) of the pair */ -export function pair_second(pair: Pair): Entity; +export function pair_second(world: World, p: Pair): Entity; -export const OnAdd: Entity<(e: Entity) => void>; -export const OnRemove: Entity<(e: Entity) => void>; -export const OnSet: Entity<(e: Entity, value: unknown) => void>; -export const ChildOf: Entity; -export const Wildcard: Entity; -export const w: Entity; -export const OnDelete: Entity; -export const OnDeleteTarget: Entity; -export const Delete: Entity; -export const Remove: Entity; -export const Name: Entity; -export const Rest: Entity; +export declare const OnAdd: Entity<(e: Entity) => void>; +export declare const OnRemove: Entity<(e: Entity) => void>; +export declare const OnSet: Entity<(e: Entity, value: unknown) => void>; +export declare const ChildOf: Entity; +export declare const Wildcard: Entity; +export declare const w: Entity; +export declare const OnDelete: Entity; +export declare const OnDeleteTarget: Entity; +export declare const Delete: Entity; +export declare const Remove: Entity; +export declare const Name: Entity; +export declare const Rest: Entity; From 08c0d24f182f9749e6c7130bb4c2f511454a894a Mon Sep 17 00:00:00 2001 From: EncodedVenom <32179912+EncodedVenom@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:26:07 -0500 Subject: [PATCH 5/8] Create publish-npm.yml (#172) --- .github/workflows/publish-npm.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/publish-npm.yml diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..d37982b --- /dev/null +++ b/.github/workflows/publish-npm.yml @@ -0,0 +1,15 @@ +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 }} From 302dedd53868d7950f640fa5f15c53a31ffa9b34 Mon Sep 17 00:00:00 2001 From: EncodedVenom <32179912+EncodedVenom@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:29:30 -0500 Subject: [PATCH 6/8] chore: give a name to the publish npm action (I forgor sorry) --- .github/workflows/publish-npm.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index d37982b..6888d13 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -1,3 +1,5 @@ +name: Publish to NPM + on: push: branches: main From aafafc90b27df9808862ba5281abe2dc98c09146 Mon Sep 17 00:00:00 2001 From: EncodedVenom <32179912+EncodedVenom@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:36:42 -0500 Subject: [PATCH 7/8] chore: bump (testing npm action) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d9e66b..01f50f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rbxts/jecs", - "version": "0.5.0", + "version": "0.5.1", "description": "Stupidly fast Entity Component System", "main": "jecs.luau", "repository": { From 509048b9df3db1a6f53b1aac885eb5c79f5987bc Mon Sep 17 00:00:00 2001 From: vnnh <90565423+vnnh@users.noreply.github.com> Date: Sat, 4 Jan 2025 23:41:52 -0600 Subject: [PATCH 8/8] Fix table.move usage in query_cached (#173) * Fix table.move usage in query_cached * Add test case --- jecs.luau | 2 +- test/tests.luau | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/jecs.luau b/jecs.luau index 473b620..332652e 100644 --- a/jecs.luau +++ b/jecs.luau @@ -1589,7 +1589,7 @@ local function query_cached(query: QueryInner) local with = query.filter_with local ids = query.ids if with then - table.move(ids, 1, #ids, #with, with) + table.move(ids, 1, #ids, #with + 1, with) else query.filter_with = ids end diff --git a/test/tests.luau b/test/tests.luau index 8da151f..a8d53b1 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1495,5 +1495,22 @@ TEST("repro", function() end CHECK(counter == 1) end + + do CASE "#3" -- ISSUE #171 + local world = world_new() + local component1 = world:component() + local tag1 = world:entity() + + local query = world:query(component1):with(tag1):cached() + + local entity = world:entity() + world:set(entity, component1, "some data") + + local counter = 0 + for x in query:iter() do + counter += 1 + end + CHECK(counter == 0) + end end) FINISH()