Skip to content

Commit 8276a69

Browse files
authored
🐛 core: Changed modifier always works when entity generations overflow (#213)
1 parent b508220 commit 8276a69

File tree

2 files changed

+45
-10
lines changed

2 files changed

+45
-10
lines changed

packages/core/src/query/query.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -377,16 +377,17 @@ export function createQueryInstance<T extends QueryParameter[]>(
377377
traitMatches =
378378
(oldMask & bitflag) === 0 && (currentMask & bitflag) === bitflag;
379379
break;
380-
case 'remove':
381-
traitMatches =
382-
((oldMask & bitflag) === bitflag && (currentMask & bitflag) === 0) ||
383-
((oldMask & bitflag) === 0 &&
384-
(currentMask & bitflag) === 0 &&
385-
(dirtyMask[generationId][eid] & bitflag) === bitflag);
386-
break;
387-
case 'change':
388-
traitMatches = (changedMask[generationId][eid] & bitflag) === bitflag;
389-
break;
380+
case 'remove':
381+
traitMatches =
382+
((oldMask & bitflag) === bitflag && (currentMask & bitflag) === 0) ||
383+
((oldMask & bitflag) === 0 &&
384+
(currentMask & bitflag) === 0 &&
385+
((dirtyMask[generationId]?.[eid] ?? 0) & bitflag) === bitflag);
386+
break;
387+
case 'change':
388+
traitMatches =
389+
((changedMask[generationId]?.[eid] ?? 0) & bitflag) === bitflag;
390+
break;
390391
}
391392

392393
if (!traitMatches) {

packages/core/tests/query-modifiers.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,4 +574,38 @@ describe('Query modifiers', () => {
574574

575575
expect(entity.get(Name)!.name).toBe('modified');
576576
});
577+
578+
// @internal Tests internal implementation edge case with generation overflow
579+
it('[internal] should handle Changed modifier when trait registration causes generation overflow', () => {
580+
// Create a fresh world to control trait registration count
581+
const testWorld = createWorld();
582+
testWorld.init();
583+
584+
// IsExcluded is already registered (bitflag=2 after), register 29 more to get bitflag=2^30
585+
const fillerTraits = Array.from({ length: 29 }, () => trait());
586+
for (const t of fillerTraits) {
587+
testWorld.spawn(t);
588+
}
589+
590+
// Create Changed modifier - snapshots entityMasks with 1 generation
591+
const Changed = createChanged();
592+
593+
// Spawn an entity so entityIndex.dense is not empty (required to trigger the bug)
594+
const entity = testWorld.spawn();
595+
596+
// Register the 31st trait to trigger overflow (bitflag 2^30 -> 2^31 -> overflow)
597+
const Trait31 = trait();
598+
entity.add(Trait31);
599+
600+
// Now entityMasks has 2 generations, but changedMask only has 1
601+
602+
// Create the 32nd trait - will be in generation 1
603+
const NewTrait = trait();
604+
605+
// This should not throw "cannot read properties of undefined (reading '0')"
606+
// when accessing changedMask[generationId][eid] where generationId is 1 but changedMask only has index 0
607+
expect(() => {
608+
testWorld.query(Changed(NewTrait));
609+
}).not.toThrow();
610+
});
577611
});

0 commit comments

Comments
 (0)