Skip to content

Commit b508220

Browse files
authored
🐛 core: fix modifier trait types in updateEach (#212)
1 parent 7e415c6 commit b508220

File tree

5 files changed

+634
-557
lines changed

5 files changed

+634
-557
lines changed
Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import { $internal } from '../../common';
22
import { isRelation } from '../../relation/utils/is-relation';
3-
import type { Trait, TraitOrRelation } from '../../trait/types';
3+
import type { ExtractTraits, TraitOrRelation } from '../../trait/types';
44
import { universe } from '../../universe/universe';
55
import { createModifier } from '../modifier';
66
import type { Modifier } from '../types';
77
import { createTrackingId, setTrackingMasks } from '../utils/tracking-cursor';
88

99
export function createAdded() {
10-
const id = createTrackingId();
10+
const id = createTrackingId();
1111

12-
for (const world of universe.worlds) {
13-
if (!world) continue;
14-
setTrackingMasks(world, id);
15-
}
12+
for (const world of universe.worlds) {
13+
if (!world) continue;
14+
setTrackingMasks(world, id);
15+
}
1616

17-
return <T extends TraitOrRelation[] = TraitOrRelation[]>(
18-
...inputs: T
19-
): Modifier<Trait[], `added-${number}`> => {
20-
const traits = inputs.map((input) => (isRelation(input) ? input[$internal].trait : input));
21-
return createModifier(`added-${id}`, id, traits);
22-
};
17+
return <T extends TraitOrRelation[]>(
18+
...inputs: T
19+
): Modifier<ExtractTraits<T>, `added-${number}`> => {
20+
const traits = inputs.map((input) =>
21+
isRelation(input) ? input[$internal].trait : input
22+
) as ExtractTraits<T>;
23+
return createModifier(`added-${id}`, id, traits);
24+
};
2325
}

packages/core/src/query/modifiers/changed.ts

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getEntityId } from '../../entity/utils/pack-entity';
44
import { isRelation } from '../../relation/utils/is-relation';
55
import { hasTrait, registerTrait } from '../../trait/trait';
66
import { getTraitInstance, hasTraitInstance } from '../../trait/trait-instance';
7-
import type { Trait, TraitOrRelation } from '../../trait/types';
7+
import type { ExtractTraits, Trait, TraitOrRelation } from '../../trait/types';
88
import { universe } from '../../universe/universe';
99
import type { World } from '../../world';
1010
import { createModifier } from '../modifier';
@@ -13,73 +13,75 @@ import { checkQueryTrackingWithRelations } from '../utils/check-query-tracking-w
1313
import { createTrackingId, setTrackingMasks } from '../utils/tracking-cursor';
1414

1515
export function createChanged() {
16-
const id = createTrackingId();
16+
const id = createTrackingId();
1717

18-
for (const world of universe.worlds) {
19-
if (!world) continue;
20-
setTrackingMasks(world, id);
21-
}
18+
for (const world of universe.worlds) {
19+
if (!world) continue;
20+
setTrackingMasks(world, id);
21+
}
2222

23-
return <T extends TraitOrRelation[] = TraitOrRelation[]>(
24-
...inputs: T
25-
): Modifier<Trait[], `changed-${number}`> => {
26-
const traits = inputs.map((input) => (isRelation(input) ? input[$internal].trait : input));
27-
return createModifier(`changed-${id}`, id, traits);
28-
};
23+
return <T extends TraitOrRelation[]>(
24+
...inputs: T
25+
): Modifier<ExtractTraits<T>, `changed-${number}`> => {
26+
const traits = inputs.map((input) =>
27+
isRelation(input) ? input[$internal].trait : input
28+
) as ExtractTraits<T>;
29+
return createModifier(`changed-${id}`, id, traits);
30+
};
2931
}
3032

3133
/** @inline */
3234
function markChanged(world: World, entity: Entity, trait: Trait) {
33-
const ctx = world[$internal];
35+
const ctx = world[$internal];
3436

35-
// Early exit if the trait is not on the entity.
36-
if (!hasTrait(world, entity, trait)) return;
37+
// Early exit if the trait is not on the entity.
38+
if (!hasTrait(world, entity, trait)) return;
3739

38-
// Register the trait if it's not already registered.
39-
if (!hasTraitInstance(ctx.traitInstances, trait)) registerTrait(world, trait);
40-
const data = getTraitInstance(ctx.traitInstances, trait)!;
40+
// Register the trait if it's not already registered.
41+
if (!hasTraitInstance(ctx.traitInstances, trait)) registerTrait(world, trait);
42+
const data = getTraitInstance(ctx.traitInstances, trait)!;
4143

42-
// Mark the trait as changed in bitmasks for Changed modifiers.
43-
const eid = getEntityId(entity);
44-
const { generationId, bitflag } = data;
44+
// Mark the trait as changed in bitmasks for Changed modifiers.
45+
const eid = getEntityId(entity);
46+
const { generationId, bitflag } = data;
4547

46-
for (const changedMask of ctx.changedMasks.values()) {
47-
if (!changedMask[generationId]) changedMask[generationId] = [];
48-
if (!changedMask[generationId][eid]) changedMask[generationId][eid] = 0;
49-
changedMask[generationId][eid] |= bitflag;
50-
}
48+
for (const changedMask of ctx.changedMasks.values()) {
49+
if (!changedMask[generationId]) changedMask[generationId] = [];
50+
if (!changedMask[generationId][eid]) changedMask[generationId][eid] = 0;
51+
changedMask[generationId][eid] |= bitflag;
52+
}
5153

52-
// Update tracking queries with change event
53-
for (const query of data.trackingQueries) {
54-
if (!query.hasChangedModifiers) continue;
55-
if (!query.changedTraits.has(trait)) continue;
54+
// Update tracking queries with change event
55+
for (const query of data.trackingQueries) {
56+
if (!query.hasChangedModifiers) continue;
57+
if (!query.changedTraits.has(trait)) continue;
5658

57-
const match =
58-
query.relationFilters && query.relationFilters.length > 0
59-
? checkQueryTrackingWithRelations(
60-
world,
61-
query,
62-
entity,
63-
'change',
64-
generationId,
65-
bitflag
66-
)
67-
: query.checkTracking(world, entity, 'change', generationId, bitflag);
68-
if (match) query.add(entity);
69-
else query.remove(world, entity);
70-
}
59+
const match =
60+
query.relationFilters && query.relationFilters.length > 0
61+
? checkQueryTrackingWithRelations(
62+
world,
63+
query,
64+
entity,
65+
'change',
66+
generationId,
67+
bitflag
68+
)
69+
: query.checkTracking(world, entity, 'change', generationId, bitflag);
70+
if (match) query.add(entity);
71+
else query.remove(world, entity);
72+
}
7173

72-
return data;
74+
return data;
7375
}
7476

7577
export function setChanged(world: World, entity: Entity, trait: Trait) {
76-
const data = markChanged(world, entity, trait);
77-
if (!data) return;
78-
for (const sub of data.changeSubscriptions) sub(entity);
78+
const data = markChanged(world, entity, trait);
79+
if (!data) return;
80+
for (const sub of data.changeSubscriptions) sub(entity);
7981
}
8082

8183
export function setPairChanged(world: World, entity: Entity, trait: Trait, target: Entity) {
82-
const data = markChanged(world, entity, trait);
83-
if (!data) return;
84-
for (const sub of data.changeSubscriptions) sub(entity, target);
84+
const data = markChanged(world, entity, trait);
85+
if (!data) return;
86+
for (const sub of data.changeSubscriptions) sub(entity, target);
8587
}
Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import { $internal } from '../../common';
22
import { isRelation } from '../../relation/utils/is-relation';
3-
import type { Trait, TraitOrRelation } from '../../trait/types';
3+
import type { ExtractTraits, TraitOrRelation } from '../../trait/types';
44
import { universe } from '../../universe/universe';
55
import { createModifier } from '../modifier';
66
import type { Modifier } from '../types';
77
import { createTrackingId, setTrackingMasks } from '../utils/tracking-cursor';
88

99
export function createRemoved() {
10-
const id = createTrackingId();
10+
const id = createTrackingId();
1111

12-
for (const world of universe.worlds) {
13-
if (!world) continue;
14-
setTrackingMasks(world, id);
15-
}
12+
for (const world of universe.worlds) {
13+
if (!world) continue;
14+
setTrackingMasks(world, id);
15+
}
1616

17-
return <T extends TraitOrRelation[] = TraitOrRelation[]>(
18-
...inputs: T
19-
): Modifier<Trait[], `removed-${number}`> => {
20-
const traits = inputs.map((input) => (isRelation(input) ? input[$internal].trait : input));
21-
return createModifier(`removed-${id}`, id, traits);
22-
};
17+
return <T extends TraitOrRelation[]>(
18+
...inputs: T
19+
): Modifier<ExtractTraits<T>, `removed-${number}`> => {
20+
const traits = inputs.map((input) =>
21+
isRelation(input) ? input[$internal].trait : input
22+
) as ExtractTraits<T>;
23+
return createModifier(`removed-${id}`, id, traits);
24+
};
2325
}

packages/core/src/trait/types.ts

Lines changed: 71 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,102 +8,110 @@ import type { AoSFactory, Schema, Store, StoreType } from '../storage';
88
export type TraitType = StoreType;
99

1010
export type TraitValue<TSchema extends Schema> = TSchema extends AoSFactory
11-
? ReturnType<TSchema>
12-
: Partial<TraitRecord<TSchema>>;
11+
? ReturnType<TSchema>
12+
: Partial<TraitRecord<TSchema>>;
1313

1414
export type Trait<TSchema extends Schema = any> = {
15-
/** Public read-only ID for fast array lookups */
16-
readonly id: number;
17-
readonly schema: TSchema;
18-
[$internal]: {
19-
set: (index: number, store: any, value: TraitValue<TSchema>) => void;
20-
fastSet: (index: number, store: any, value: TraitValue<TSchema>) => boolean;
21-
fastSetWithChangeDetection: (
22-
index: number,
23-
store: any,
24-
value: TraitValue<TSchema>
25-
) => boolean;
26-
get: (index: number, store: any) => TraitRecord<TSchema>;
27-
id: number;
28-
createStore: () => Store<TSchema>;
29-
/** Reference to parent relation if this trait is owned by a relation */
30-
relation: Relation<any> | null;
31-
type: StoreType;
32-
};
15+
/** Public read-only ID for fast array lookups */
16+
readonly id: number;
17+
readonly schema: TSchema;
18+
[$internal]: {
19+
set: (index: number, store: any, value: TraitValue<TSchema>) => void;
20+
fastSet: (index: number, store: any, value: TraitValue<TSchema>) => boolean;
21+
fastSetWithChangeDetection: (
22+
index: number,
23+
store: any,
24+
value: TraitValue<TSchema>
25+
) => boolean;
26+
get: (index: number, store: any) => TraitRecord<TSchema>;
27+
id: number;
28+
createStore: () => Store<TSchema>;
29+
/** Reference to parent relation if this trait is owned by a relation */
30+
relation: Relation<any> | null;
31+
type: StoreType;
32+
};
3333
} & ((params?: TraitValue<TSchema>) => [Trait<TSchema>, TraitValue<TSchema>]);
3434

3535
export type TagTrait = Trait<Record<string, never>> & { [$internal]: { type: 'tag' } };
3636

3737
export type TraitTuple<T extends Trait = Trait> = [
38-
T,
39-
T extends Trait<infer S>
40-
? S extends AoSFactory
41-
? ReturnType<S>
42-
: Partial<TraitRecord<S>>
43-
: never
38+
T,
39+
T extends Trait<infer S>
40+
? S extends AoSFactory
41+
? ReturnType<S>
42+
: Partial<TraitRecord<S>>
43+
: never
4444
];
4545

4646
export type ConfigurableTrait<T extends Trait = Trait> = T | TraitTuple<T> | RelationPair<T>;
4747

4848
export type SetTraitCallback<T extends Trait | RelationPair> = (
49-
prev: TraitRecord<ExtractSchema<T>>
49+
prev: TraitRecord<ExtractSchema<T>>
5050
) => TraitValue<ExtractSchema<T>>;
5151

5252
type TraitRecordFromSchema<T extends Schema> = T extends AoSFactory
53-
? ReturnType<T>
54-
: {
55-
[P in keyof T]: T[P] extends (...args: never[]) => unknown ? ReturnType<T[P]> : T[P];
56-
};
53+
? ReturnType<T>
54+
: {
55+
[P in keyof T]: T[P] extends (...args: never[]) => unknown ? ReturnType<T[P]> : T[P];
56+
};
5757

5858
/**
5959
* The record of a trait.
6060
* For SoA it is a snapshot of the state for a single entity.
6161
* For AoS it is the state instance for a single entity.
6262
*/
6363
export type TraitRecord<T extends Trait | Schema> = T extends Trait
64-
? TraitRecordFromSchema<T['schema']>
65-
: TraitRecordFromSchema<T>;
64+
? TraitRecordFromSchema<T['schema']>
65+
: TraitRecordFromSchema<T>;
6666

6767
// Type Utils
6868

6969
export type ExtractSchema<T extends Trait | Relation<Trait> | RelationPair> = T extends RelationPair<
70-
infer R
70+
infer R
7171
>
72-
? ExtractSchema<R>
73-
: T extends Relation<infer R>
74-
? ExtractSchema<R>
75-
: T extends Trait<infer S>
76-
? S
77-
: never;
72+
? ExtractSchema<R>
73+
: T extends Relation<infer R>
74+
? ExtractSchema<R>
75+
: T extends Trait<infer S>
76+
? S
77+
: never;
7878
export type ExtractStore<T extends Trait> = T extends { [$internal]: { createStore(): infer Store } }
79-
? Store
80-
: never;
79+
? Store
80+
: never;
8181
export type ExtractIsTag<T extends Trait> = T extends { [$internal]: { type: 'tag' } } ? true : false;
8282

8383
export type IsTag<T extends Trait> = ExtractIsTag<T>;
8484

8585
export interface TraitInstance<T extends Trait = Trait, S extends Schema = ExtractSchema<T>> {
86-
generationId: number;
87-
bitflag: number;
88-
trait: Trait;
89-
store: Store<S>;
90-
/** Non-tracking queries that include this trait */
91-
queries: Set<QueryInstance>;
92-
/** Tracking queries (Added/Removed/Changed) that include this trait */
93-
trackingQueries: Set<QueryInstance>;
94-
notQueries: Set<QueryInstance>;
95-
/** Queries that filter by this relation (only for relation traits) */
96-
relationQueries: Set<QueryInstance>;
97-
schema: S;
98-
changeSubscriptions: Set<(entity: Entity, target?: Entity) => void>;
99-
addSubscriptions: Set<(entity: Entity, target?: Entity) => void>;
100-
removeSubscriptions: Set<(entity: Entity, target?: Entity) => void>;
101-
/**
102-
* Only for relation traits.
103-
* For exclusive: relationTargets[eid] = targetId (number)
104-
* For non-exclusive: relationTargets[eid] = [targetId1, targetId2, ...] (number[])
105-
*/
106-
relationTargets?: number[] | number[][];
86+
generationId: number;
87+
bitflag: number;
88+
trait: Trait;
89+
store: Store<S>;
90+
/** Non-tracking queries that include this trait */
91+
queries: Set<QueryInstance>;
92+
/** Tracking queries (Added/Removed/Changed) that include this trait */
93+
trackingQueries: Set<QueryInstance>;
94+
notQueries: Set<QueryInstance>;
95+
/** Queries that filter by this relation (only for relation traits) */
96+
relationQueries: Set<QueryInstance>;
97+
schema: S;
98+
changeSubscriptions: Set<(entity: Entity, target?: Entity) => void>;
99+
addSubscriptions: Set<(entity: Entity, target?: Entity) => void>;
100+
removeSubscriptions: Set<(entity: Entity, target?: Entity) => void>;
101+
/**
102+
* Only for relation traits.
103+
* For exclusive: relationTargets[eid] = targetId (number)
104+
* For non-exclusive: relationTargets[eid] = [targetId1, targetId2, ...] (number[])
105+
*/
106+
relationTargets?: number[] | number[][];
107107
}
108108

109109
export type TraitOrRelation = Trait | Relation<Trait>;
110+
111+
/** Extracts the underlying Trait from a TraitOrRelation (Relations contain a Trait) */
112+
export type ExtractTrait<T> = T extends Relation<infer TTrait> ? TTrait : T;
113+
114+
/** Maps a tuple of TraitOrRelation to their underlying Traits */
115+
export type ExtractTraits<T extends TraitOrRelation[]> = {
116+
[K in keyof T]: ExtractTrait<T[K]>;
117+
};

0 commit comments

Comments
 (0)