@codehz/ecs 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder.d.mts +15 -12
- package/dist/index.mjs +1 -2
- package/dist/testing.d.mts +0 -1
- package/dist/testing.mjs +63 -2
- package/dist/testing.mjs.map +1 -1
- package/dist/world.mjs +65 -81
- package/dist/world.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +41 -0
- package/src/world/hooks.ts +5 -0
- package/src/world/world.ts +13 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codehz/ecs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"README.en.md"
|
|
28
28
|
],
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@codehz/pipeline": "^0.3
|
|
31
|
-
"@types/bun": "1.3.
|
|
30
|
+
"@codehz/pipeline": "^0.4.3",
|
|
31
|
+
"@types/bun": "1.3.13",
|
|
32
32
|
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
|
33
33
|
"@typescript-eslint/parser": "^8.48.1",
|
|
34
34
|
"eslint": "^9.39.1",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"lint-staged": "^16.2.6",
|
|
38
38
|
"prettier": "^3.6.2",
|
|
39
39
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
40
|
-
"tsdown": "^0.
|
|
40
|
+
"tsdown": "^0.22.0"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"url": "https://github.com/codehz/ecs"
|
|
@@ -329,4 +329,45 @@ describe("Wildcard-Relation Hooks", () => {
|
|
|
329
329
|
expect(world.exists(entity1)).toBe(true);
|
|
330
330
|
expect(world.exists(entity2)).toBe(true);
|
|
331
331
|
});
|
|
332
|
+
|
|
333
|
+
// Regression: wildcard remove (relation(Comp, "*")) should NOT produce target=0
|
|
334
|
+
// in on_remove. The wildcard marker (WILDCARD_TARGET_ID=0) leaks into
|
|
335
|
+
// removedComponents and gets decoded as target=0 by reconstructWildcardWithRemoved.
|
|
336
|
+
it("should NOT report target=0 in on_remove when using wildcard remove (relation(Comp, '*'))", () => {
|
|
337
|
+
const world = new World();
|
|
338
|
+
|
|
339
|
+
// Use dontFragment:true so that removeWildcardRelations adds the wildcard
|
|
340
|
+
// marker to removedComponents, triggering the bug path.
|
|
341
|
+
const RelData = component<{ value: string }>({ dontFragment: true });
|
|
342
|
+
const target = world.new();
|
|
343
|
+
const wildcardRel = relation(RelData, "*");
|
|
344
|
+
const concreteRel = relation(RelData, target);
|
|
345
|
+
|
|
346
|
+
const removeCalls: { relations: [EntityId, { value: string }][] }[] = [];
|
|
347
|
+
|
|
348
|
+
world.hook([wildcardRel], {
|
|
349
|
+
on_remove: (_entityId, relations) => {
|
|
350
|
+
removeCalls.push({ relations });
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const entity = world.spawn().with(concreteRel, { value: "hello" }).build();
|
|
355
|
+
world.sync();
|
|
356
|
+
|
|
357
|
+
// Remove ALL relations via wildcard — this triggers the bug
|
|
358
|
+
world.remove(entity, wildcardRel);
|
|
359
|
+
world.sync();
|
|
360
|
+
|
|
361
|
+
expect(removeCalls.length).toBe(1);
|
|
362
|
+
const reportedRelations = removeCalls[0]!.relations;
|
|
363
|
+
|
|
364
|
+
// The hook should report the removed relation(s) WITHOUT the wildcard
|
|
365
|
+
// marker leaking in as target=0.
|
|
366
|
+
for (const [reportedTarget] of reportedRelations) {
|
|
367
|
+
expect(reportedTarget).not.toBe(0);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// It should still correctly report the actual removed relation
|
|
371
|
+
expect(reportedRelations).toContainEqual([target, { value: "hello" }]);
|
|
372
|
+
});
|
|
332
373
|
});
|
package/src/world/hooks.ts
CHANGED
|
@@ -263,6 +263,9 @@ function reconstructWildcardWithRemoved(
|
|
|
263
263
|
// Re-inject matching relations that were just removed, so the hook callback
|
|
264
264
|
// sees the complete snapshot as it existed before the removal.
|
|
265
265
|
for (const [removedCompId, removedValue] of removedComponents.entries()) {
|
|
266
|
+
// Skip wildcard markers themselves — they encode WILDCARD_TARGET_ID=0 and
|
|
267
|
+
// would produce spurious [0, undefined] entries in the hook callback.
|
|
268
|
+
if (isWildcardRelationId(removedCompId)) continue;
|
|
266
269
|
if (componentMatchesHookType(removedCompId, wildcardId)) {
|
|
267
270
|
const targetId = getTargetIdFromRelationId(removedCompId);
|
|
268
271
|
if (targetId !== undefined) {
|
|
@@ -346,6 +349,8 @@ function collectWildcardFromRemoved(
|
|
|
346
349
|
const result: [EntityId, any][] = [];
|
|
347
350
|
|
|
348
351
|
for (const [removedCompId, removedValue] of removedComponents.entries()) {
|
|
352
|
+
// Skip wildcard markers themselves — they encode WILDCARD_TARGET_ID=0.
|
|
353
|
+
if (isWildcardRelationId(removedCompId)) continue;
|
|
349
354
|
if (componentMatchesHookType(removedCompId, wildcardId)) {
|
|
350
355
|
const targetId = getTargetIdFromRelationId(removedCompId);
|
|
351
356
|
if (targetId !== undefined) {
|
package/src/world/world.ts
CHANGED
|
@@ -698,25 +698,28 @@ export class World {
|
|
|
698
698
|
* **Important:** Store the query reference and reuse it across frames for optimal performance.
|
|
699
699
|
* Creating a new query each frame defeats the caching mechanism.
|
|
700
700
|
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
701
|
+
* **Note on optional components:** Only **required** (non-optional) component types should be
|
|
702
|
+
* passed to `createQuery`. Optional components (wrapped with `{ optional: ... }`) must be
|
|
703
|
+
* specified at **iteration time** via {@link Query.forEach}, {@link Query.getEntitiesWithComponents},
|
|
704
|
+
* or {@link Query.iterate} — NOT here. Including optional wrappers in `createQuery` will cause
|
|
705
|
+
* undefined behavior because the internal normalization relies on numeric sorting of component IDs.
|
|
706
|
+
*
|
|
707
|
+
* @param componentTypes - Array of **required** component types to match (do not include optional wrappers)
|
|
708
|
+
* @param filter - Optional filter for additional constraints (e.g., exclude entities with certain components)
|
|
703
709
|
* @returns A Query instance that can be used to iterate matching entities
|
|
704
710
|
*
|
|
705
711
|
* @example
|
|
706
|
-
* // Create once, reuse many times
|
|
712
|
+
* // Create once, reuse many times (required components only)
|
|
707
713
|
* const movementQuery = world.createQuery([Position, Velocity]);
|
|
708
714
|
*
|
|
709
|
-
* //
|
|
710
|
-
* movementQuery.forEach((entity) => {
|
|
711
|
-
*
|
|
712
|
-
* const vel = world.get(entity, Velocity);
|
|
713
|
-
* pos.x += vel.x;
|
|
714
|
-
* pos.y += vel.y;
|
|
715
|
+
* // Optional components are passed at iteration time, not creation time:
|
|
716
|
+
* movementQuery.forEach([Position, { optional: Velocity }], (entity, pos, vel) => {
|
|
717
|
+
* pos.x += vel?.value?.x ?? 0;
|
|
715
718
|
* });
|
|
716
719
|
*
|
|
717
720
|
* // With filter
|
|
718
721
|
* const activeQuery = world.createQuery([Position], {
|
|
719
|
-
*
|
|
722
|
+
* negativeComponentTypes: [Disabled]
|
|
720
723
|
* });
|
|
721
724
|
*/
|
|
722
725
|
createQuery(componentTypes: EntityId<any>[], filter: QueryFilter = {}): Query {
|