@codehz/ecs 0.7.1 → 0.7.3
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/{builder.d.mts → dist/builder.d.mts} +4 -2
- package/{world.mjs → dist/world.mjs} +9 -30
- package/dist/world.mjs.map +1 -0
- package/examples/advanced-scheduling.ts +96 -0
- package/examples/collision-detection.ts +229 -0
- package/examples/inventory-system-relations.ts +108 -0
- package/examples/parent-child-hierarchy.ts +206 -0
- package/examples/serialization.ts +337 -0
- package/examples/simple.ts +96 -0
- package/examples/spatial-grid.ts +276 -0
- package/examples/state-machine.ts +273 -0
- package/examples/tag-filtering.ts +266 -0
- package/package.json +58 -12
- package/src/__tests__/commands/buffer-limits.test.ts +72 -0
- package/src/__tests__/commands/buffer.test.ts +195 -0
- package/src/__tests__/component/singleton.test.ts +148 -0
- package/src/__tests__/core/archetype.test.ts +247 -0
- package/src/__tests__/core/bitset.test.ts +171 -0
- package/src/__tests__/core/changeset.test.ts +254 -0
- package/src/__tests__/core/multi-map.test.ts +74 -0
- package/src/__tests__/entity/component-registry.test.ts +66 -0
- package/src/__tests__/entity/entity.test.ts +520 -0
- package/src/__tests__/entity/id-manager.test.ts +157 -0
- package/src/__tests__/entity/id-system.test.ts +260 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
- package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
- package/src/__tests__/query/basic.test.ts +341 -0
- package/src/__tests__/query/caching.test.ts +112 -0
- package/src/__tests__/query/filter.test.ts +111 -0
- package/src/__tests__/query/optional.test.ts +231 -0
- package/src/__tests__/query/perf.test.ts +99 -0
- package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
- package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
- package/src/__tests__/relations/wildcard.test.ts +179 -0
- package/src/__tests__/serialization/bounds.test.ts +237 -0
- package/src/__tests__/testing/assertions.test.ts +224 -0
- package/src/__tests__/testing/entity-builder.test.ts +84 -0
- package/src/__tests__/testing/snapshot.test.ts +150 -0
- package/src/__tests__/testing/world-fixture.test.ts +73 -0
- package/src/__tests__/world/component-hooks.test.ts +185 -0
- package/src/__tests__/world/component-management.test.ts +447 -0
- package/src/__tests__/world/entity-management.test.ts +86 -0
- package/src/__tests__/world/get-optional.test.ts +96 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
- package/src/__tests__/world/perf.test.ts +93 -0
- package/src/__tests__/world/query.test.ts +223 -0
- package/src/__tests__/world/serialize.test.ts +83 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
- package/src/archetype/archetype.ts +472 -0
- package/src/archetype/helpers.ts +186 -0
- package/src/archetype/store.ts +33 -0
- package/src/commands/buffer.ts +110 -0
- package/src/commands/changeset.ts +104 -0
- package/src/component/entity-store.ts +223 -0
- package/src/component/registry.ts +657 -0
- package/src/component/type-utils.ts +9 -0
- package/src/entity/index.ts +63 -0
- package/src/entity/manager.ts +115 -0
- package/src/entity/relation.ts +319 -0
- package/src/entity/types.ts +135 -0
- package/src/index.ts +41 -0
- package/src/query/filter.ts +75 -0
- package/src/query/query.ts +313 -0
- package/src/query/registry.ts +101 -0
- package/src/storage/serialization.ts +130 -0
- package/src/testing/index.ts +634 -0
- package/src/types/index.ts +99 -0
- package/src/utils/bit-set.ts +133 -0
- package/src/utils/multi-map.ts +96 -0
- package/src/utils/utils.ts +19 -0
- package/src/world/builder.ts +100 -0
- package/src/world/commands.ts +378 -0
- package/src/world/hooks.ts +358 -0
- package/src/world/references.ts +38 -0
- package/src/world/serialization.ts +122 -0
- package/src/world/world.ts +1201 -0
- package/world.mjs.map +0 -1
- /package/{index.d.mts → dist/index.d.mts} +0 -0
- /package/{index.mjs → dist/index.mjs} +0 -0
- /package/{testing.d.mts → dist/testing.d.mts} +0 -0
- /package/{testing.mjs → dist/testing.mjs} +0 -0
- /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { EntityId } from "../entity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maximum number of command buffer execution iterations to prevent infinite loops
|
|
5
|
+
*/
|
|
6
|
+
const MAX_COMMAND_ITERATIONS = 100;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Command for deferred execution
|
|
10
|
+
* Uses discriminated union for type safety
|
|
11
|
+
*/
|
|
12
|
+
export type Command =
|
|
13
|
+
| { type: "set"; entityId: EntityId; componentType: EntityId<any>; component: any }
|
|
14
|
+
| { type: "delete"; entityId: EntityId; componentType: EntityId<any> }
|
|
15
|
+
| { type: "destroy"; entityId: EntityId };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Command buffer for deferred structural changes
|
|
19
|
+
*/
|
|
20
|
+
export class CommandBuffer {
|
|
21
|
+
private commands: Command[] = [];
|
|
22
|
+
private swapBuffer: Command[] = [];
|
|
23
|
+
/** Reusable map to group commands by entity, avoids per-sync allocations */
|
|
24
|
+
private entityCommands: Map<EntityId, Command[]> = new Map();
|
|
25
|
+
private executeEntityCommands: (entityId: EntityId, commands: Command[]) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a command buffer with an executor function
|
|
29
|
+
*/
|
|
30
|
+
constructor(executeEntityCommands: (entityId: EntityId, commands: Command[]) => void) {
|
|
31
|
+
this.executeEntityCommands = executeEntityCommands;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Add a component to an entity (deferred)
|
|
36
|
+
*/
|
|
37
|
+
set(entityId: EntityId, componentType: EntityId<void>): void;
|
|
38
|
+
set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
|
|
39
|
+
set(entityId: EntityId, componentType: EntityId, component?: any): void {
|
|
40
|
+
this.commands.push({ type: "set", entityId, componentType, component });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Remove a component from an entity (deferred)
|
|
45
|
+
*/
|
|
46
|
+
remove<T>(entityId: EntityId, componentType: EntityId<T>): void {
|
|
47
|
+
this.commands.push({ type: "delete", entityId, componentType });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Destroy an entity (deferred)
|
|
52
|
+
*/
|
|
53
|
+
delete(entityId: EntityId): void {
|
|
54
|
+
this.commands.push({ type: "destroy", entityId });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Execute all commands and clear the buffer
|
|
59
|
+
*/
|
|
60
|
+
execute(): void {
|
|
61
|
+
let iterations = 0;
|
|
62
|
+
|
|
63
|
+
while (this.commands.length > 0) {
|
|
64
|
+
if (iterations >= MAX_COMMAND_ITERATIONS) {
|
|
65
|
+
throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
|
|
66
|
+
}
|
|
67
|
+
iterations++;
|
|
68
|
+
|
|
69
|
+
// Swap buffers to avoid allocation
|
|
70
|
+
const currentCommands = this.commands;
|
|
71
|
+
this.commands = this.swapBuffer;
|
|
72
|
+
|
|
73
|
+
// Group commands by entity, reusing the persistent Map
|
|
74
|
+
const entityCommands = this.entityCommands;
|
|
75
|
+
for (const cmd of currentCommands) {
|
|
76
|
+
const existing = entityCommands.get(cmd.entityId);
|
|
77
|
+
if (existing !== undefined) {
|
|
78
|
+
existing.push(cmd);
|
|
79
|
+
} else {
|
|
80
|
+
entityCommands.set(cmd.entityId, [cmd]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clear the consumed buffer for reuse
|
|
85
|
+
currentCommands.length = 0;
|
|
86
|
+
this.swapBuffer = currentCommands;
|
|
87
|
+
|
|
88
|
+
// Process each entity's commands and clear the map (but not the arrays,
|
|
89
|
+
// as callers may hold references to them after the executor returns)
|
|
90
|
+
for (const [entityId, commands] of entityCommands) {
|
|
91
|
+
this.executeEntityCommands(entityId, commands);
|
|
92
|
+
}
|
|
93
|
+
entityCommands.clear();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get current commands (for testing)
|
|
99
|
+
*/
|
|
100
|
+
getCommands(): Command[] {
|
|
101
|
+
return [...this.commands];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Clear all commands
|
|
106
|
+
*/
|
|
107
|
+
clear(): void {
|
|
108
|
+
this.commands = [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { EntityId } from "../entity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @internal Represents a set of component changes to be applied to an entity
|
|
5
|
+
*/
|
|
6
|
+
export class ComponentChangeset {
|
|
7
|
+
readonly adds = new Map<EntityId<any>, any>();
|
|
8
|
+
readonly removes = new Set<EntityId<any>>();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Add a component to the changeset
|
|
12
|
+
*/
|
|
13
|
+
set<T>(componentType: EntityId<T>, component: T): void {
|
|
14
|
+
this.adds.set(componentType, component);
|
|
15
|
+
this.removes.delete(componentType); // Remove from removes if it was going to be removed
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Remove a component from the changeset
|
|
20
|
+
*/
|
|
21
|
+
delete<T>(componentType: EntityId<T>): void {
|
|
22
|
+
this.removes.add(componentType);
|
|
23
|
+
this.adds.delete(componentType); // Remove from adds if it was going to be added
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if the changeset has any changes
|
|
28
|
+
*/
|
|
29
|
+
hasChanges(): boolean {
|
|
30
|
+
return this.adds.size > 0 || this.removes.size > 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clear all changes
|
|
35
|
+
*/
|
|
36
|
+
clear(): void {
|
|
37
|
+
this.adds.clear();
|
|
38
|
+
this.removes.clear();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Merge another changeset into this one
|
|
43
|
+
*/
|
|
44
|
+
merge(other: ComponentChangeset): void {
|
|
45
|
+
// Merge additions
|
|
46
|
+
for (const [componentType, component] of other.adds) {
|
|
47
|
+
this.adds.set(componentType, component);
|
|
48
|
+
this.removes.delete(componentType);
|
|
49
|
+
}
|
|
50
|
+
// Merge removals
|
|
51
|
+
for (const componentType of other.removes) {
|
|
52
|
+
this.removes.add(componentType);
|
|
53
|
+
this.adds.delete(componentType);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply the changeset to existing components and return the final state
|
|
59
|
+
*/
|
|
60
|
+
applyTo(existingComponents: Map<EntityId<any>, any>): Map<EntityId<any>, any> {
|
|
61
|
+
// Apply removals
|
|
62
|
+
for (const componentType of this.removes) {
|
|
63
|
+
existingComponents.delete(componentType);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Apply additions/updates
|
|
67
|
+
for (const [componentType, component] of this.adds) {
|
|
68
|
+
existingComponents.set(componentType, component);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return existingComponents;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the final component types after applying the changeset
|
|
76
|
+
* @param existingComponentTypes - The current component types on the entity
|
|
77
|
+
* @returns The final component types or undefined if no changes
|
|
78
|
+
*/
|
|
79
|
+
getFinalComponentTypes(existingComponentTypes: EntityId<any>[]): EntityId<any>[] | undefined {
|
|
80
|
+
const finalComponentTypes = new Set<EntityId<any>>(existingComponentTypes);
|
|
81
|
+
let changed = false;
|
|
82
|
+
|
|
83
|
+
// Apply removals
|
|
84
|
+
for (const componentType of this.removes) {
|
|
85
|
+
if (!finalComponentTypes.has(componentType)) {
|
|
86
|
+
this.removes.delete(componentType);
|
|
87
|
+
continue; // Component not present, skip
|
|
88
|
+
}
|
|
89
|
+
changed = true;
|
|
90
|
+
finalComponentTypes.delete(componentType);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Apply additions
|
|
94
|
+
for (const componentType of this.adds.keys()) {
|
|
95
|
+
if (finalComponentTypes.has(componentType)) {
|
|
96
|
+
continue; // Component already present, skip
|
|
97
|
+
}
|
|
98
|
+
changed = true;
|
|
99
|
+
finalComponentTypes.add(componentType);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return changed ? Array.from(finalComponentTypes) : undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { hasWildcardRelation } from "../archetype/helpers";
|
|
2
|
+
import type { Command } from "../commands/buffer";
|
|
3
|
+
import {
|
|
4
|
+
getComponentIdFromRelationId,
|
|
5
|
+
getComponentMerge,
|
|
6
|
+
getDetailedIdType,
|
|
7
|
+
isWildcardRelationId,
|
|
8
|
+
type ComponentId,
|
|
9
|
+
type EntityId,
|
|
10
|
+
type WildcardRelationId,
|
|
11
|
+
} from "../entity";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Manages component entity (singleton) storage.
|
|
15
|
+
*
|
|
16
|
+
* Component entities use a flat Map-based storage rather than the Archetype-based
|
|
17
|
+
* storage used for regular entities. Their IDs are in the component ID range
|
|
18
|
+
* (or are relation IDs), distinguishing them from regular entity IDs.
|
|
19
|
+
*/
|
|
20
|
+
export class ComponentEntityStore {
|
|
21
|
+
private readonly componentEntityComponents: Map<EntityId, Map<EntityId<any>, any>> = new Map();
|
|
22
|
+
private readonly relationEntityIdsByTarget: Map<EntityId, Set<EntityId>> = new Map();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if an entity ID is a component entity type.
|
|
26
|
+
* Returns true for component IDs, component-relation IDs, and entity-relation IDs —
|
|
27
|
+
* i.e. anything that is NOT a plain entity or an invalid ID.
|
|
28
|
+
*/
|
|
29
|
+
exists(entityId: EntityId): boolean {
|
|
30
|
+
const detailed = getDetailedIdType(entityId);
|
|
31
|
+
return detailed.type !== "entity" && detailed.type !== "invalid";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a component entity has a specific component.
|
|
36
|
+
*/
|
|
37
|
+
has(entityId: EntityId, componentType: EntityId<any>): boolean {
|
|
38
|
+
return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if a singleton component has data — the has(componentId) overload.
|
|
43
|
+
* In singleton usage the entity ID and the component type are the same value.
|
|
44
|
+
*/
|
|
45
|
+
hasSingleton(componentId: EntityId<any>): boolean {
|
|
46
|
+
return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if a component entity has any wildcard relations matching a component ID.
|
|
51
|
+
*/
|
|
52
|
+
hasWildcard(entityId: EntityId, componentId: ComponentId<any>): boolean {
|
|
53
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
54
|
+
if (!data) return false;
|
|
55
|
+
return hasWildcardRelation(data, componentId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a component value from a component entity.
|
|
60
|
+
* Throws if the component does not exist.
|
|
61
|
+
*/
|
|
62
|
+
get<T>(entityId: EntityId, componentType: EntityId<T>): T {
|
|
63
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
64
|
+
if (!data || !data.has(componentType)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return data.get(componentType) as T;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get an optional component value from a component entity.
|
|
74
|
+
* Returns undefined if the component does not exist.
|
|
75
|
+
*/
|
|
76
|
+
getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined {
|
|
77
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
78
|
+
if (!data || !data.has(componentType)) return undefined;
|
|
79
|
+
return { value: data.get(componentType) as T };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get all wildcard relations of a given type from a component entity.
|
|
84
|
+
*/
|
|
85
|
+
getWildcard<T>(entityId: EntityId, wildcardComponentType: WildcardRelationId<T>): [EntityId<unknown>, T][] {
|
|
86
|
+
const componentId = getComponentIdFromRelationId(wildcardComponentType);
|
|
87
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
88
|
+
if (componentId === undefined || !data) return [];
|
|
89
|
+
|
|
90
|
+
const relations: [EntityId<unknown>, T][] = [];
|
|
91
|
+
for (const [key, value] of data.entries()) {
|
|
92
|
+
if (getComponentIdFromRelationId(key) !== componentId) continue;
|
|
93
|
+
const detailed = getDetailedIdType(key);
|
|
94
|
+
if (detailed.type === "entity-relation" || detailed.type === "component-relation") {
|
|
95
|
+
relations.push([detailed.targetId, value as T]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return relations;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Clear all data for a component entity.
|
|
103
|
+
*/
|
|
104
|
+
clear(entityId: EntityId): void {
|
|
105
|
+
if (this.componentEntityComponents.delete(entityId)) {
|
|
106
|
+
this.unregisterRelationEntityId(entityId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Cleanup all component entities that reference a given target entity.
|
|
112
|
+
* Called when a target entity is destroyed.
|
|
113
|
+
*/
|
|
114
|
+
cleanupReferencesTo(targetId: EntityId): void {
|
|
115
|
+
const relationEntities = this.relationEntityIdsByTarget.get(targetId);
|
|
116
|
+
if (!relationEntities) return;
|
|
117
|
+
for (const relationEntityId of relationEntities) {
|
|
118
|
+
this.componentEntityComponents.delete(relationEntityId);
|
|
119
|
+
}
|
|
120
|
+
this.relationEntityIdsByTarget.delete(targetId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Execute a batch of commands for a component entity.
|
|
125
|
+
*/
|
|
126
|
+
executeCommands(entityId: EntityId, commands: Command[]): void {
|
|
127
|
+
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
128
|
+
this.clear(entityId);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const pendingSetValues = new Map<EntityId<any>, any>();
|
|
133
|
+
|
|
134
|
+
for (const command of commands) {
|
|
135
|
+
if (command.type === "set" && command.componentType) {
|
|
136
|
+
const merge = getComponentMerge(command.componentType);
|
|
137
|
+
let nextValue = command.component;
|
|
138
|
+
if (merge !== undefined && pendingSetValues.has(command.componentType)) {
|
|
139
|
+
const prevValue = pendingSetValues.get(command.componentType);
|
|
140
|
+
nextValue = merge(prevValue, command.component);
|
|
141
|
+
}
|
|
142
|
+
pendingSetValues.set(command.componentType, nextValue);
|
|
143
|
+
|
|
144
|
+
let data = this.componentEntityComponents.get(entityId);
|
|
145
|
+
if (!data) {
|
|
146
|
+
data = new Map();
|
|
147
|
+
this.componentEntityComponents.set(entityId, data);
|
|
148
|
+
this.registerRelationEntityId(entityId);
|
|
149
|
+
}
|
|
150
|
+
data.set(command.componentType, nextValue);
|
|
151
|
+
} else if (command.type === "delete" && command.componentType) {
|
|
152
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
153
|
+
|
|
154
|
+
if (isWildcardRelationId(command.componentType)) {
|
|
155
|
+
const componentId = getComponentIdFromRelationId(command.componentType);
|
|
156
|
+
if (componentId !== undefined) {
|
|
157
|
+
if (data) {
|
|
158
|
+
for (const key of Array.from(data.keys())) {
|
|
159
|
+
if (getComponentIdFromRelationId(key) === componentId) {
|
|
160
|
+
data.delete(key);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const key of Array.from(pendingSetValues.keys())) {
|
|
165
|
+
if (getComponentIdFromRelationId(key) === componentId) {
|
|
166
|
+
pendingSetValues.delete(key);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
data?.delete(command.componentType);
|
|
172
|
+
pendingSetValues.delete(command.componentType);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (data?.size === 0) {
|
|
176
|
+
this.clear(entityId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Initialize a component entity from a deserialization snapshot.
|
|
184
|
+
*/
|
|
185
|
+
initFromSnapshot(entityId: EntityId, componentMap: Map<EntityId<any>, any>): void {
|
|
186
|
+
this.componentEntityComponents.set(entityId, componentMap);
|
|
187
|
+
this.registerRelationEntityId(entityId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Iterate over all component entity entries.
|
|
192
|
+
* Used for serialization.
|
|
193
|
+
*/
|
|
194
|
+
entries(): IterableIterator<[EntityId, Map<EntityId<any>, any>]> {
|
|
195
|
+
return this.componentEntityComponents.entries();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private registerRelationEntityId(entityId: EntityId): void {
|
|
199
|
+
const detailed = getDetailedIdType(entityId);
|
|
200
|
+
if (detailed.type !== "entity-relation") return;
|
|
201
|
+
const targetId = detailed.targetId;
|
|
202
|
+
if (targetId === undefined) return;
|
|
203
|
+
const existing = this.relationEntityIdsByTarget.get(targetId);
|
|
204
|
+
if (existing) {
|
|
205
|
+
existing.add(entityId);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private unregisterRelationEntityId(entityId: EntityId): void {
|
|
212
|
+
const detailed = getDetailedIdType(entityId);
|
|
213
|
+
if (detailed.type !== "entity-relation") return;
|
|
214
|
+
const targetId = detailed.targetId;
|
|
215
|
+
if (targetId === undefined) return;
|
|
216
|
+
const existing = this.relationEntityIdsByTarget.get(targetId);
|
|
217
|
+
if (!existing) return;
|
|
218
|
+
existing.delete(entityId);
|
|
219
|
+
if (existing.size === 0) {
|
|
220
|
+
this.relationEntityIdsByTarget.delete(targetId);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|