@codehz/ecs 0.7.2 → 0.7.4

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.
Files changed (81) hide show
  1. package/examples/advanced-scheduling.ts +96 -0
  2. package/examples/collision-detection.ts +229 -0
  3. package/examples/inventory-system-relations.ts +108 -0
  4. package/examples/parent-child-hierarchy.ts +206 -0
  5. package/examples/serialization.ts +337 -0
  6. package/examples/simple.ts +96 -0
  7. package/examples/spatial-grid.ts +276 -0
  8. package/examples/state-machine.ts +273 -0
  9. package/examples/tag-filtering.ts +266 -0
  10. package/package.json +60 -12
  11. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  12. package/src/__tests__/commands/buffer.test.ts +195 -0
  13. package/src/__tests__/component/singleton.test.ts +148 -0
  14. package/src/__tests__/core/archetype.test.ts +247 -0
  15. package/src/__tests__/core/bitset.test.ts +171 -0
  16. package/src/__tests__/core/changeset.test.ts +254 -0
  17. package/src/__tests__/core/multi-map.test.ts +74 -0
  18. package/src/__tests__/entity/component-registry.test.ts +66 -0
  19. package/src/__tests__/entity/entity.test.ts +520 -0
  20. package/src/__tests__/entity/id-manager.test.ts +157 -0
  21. package/src/__tests__/entity/id-system.test.ts +260 -0
  22. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  23. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  24. package/src/__tests__/query/basic.test.ts +341 -0
  25. package/src/__tests__/query/caching.test.ts +112 -0
  26. package/src/__tests__/query/filter.test.ts +111 -0
  27. package/src/__tests__/query/optional.test.ts +231 -0
  28. package/src/__tests__/query/perf.test.ts +99 -0
  29. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  30. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  31. package/src/__tests__/relations/wildcard.test.ts +179 -0
  32. package/src/__tests__/serialization/bounds.test.ts +237 -0
  33. package/src/__tests__/testing/assertions.test.ts +224 -0
  34. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  35. package/src/__tests__/testing/snapshot.test.ts +150 -0
  36. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  37. package/src/__tests__/world/component-hooks.test.ts +185 -0
  38. package/src/__tests__/world/component-management.test.ts +447 -0
  39. package/src/__tests__/world/entity-management.test.ts +86 -0
  40. package/src/__tests__/world/get-optional.test.ts +96 -0
  41. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  42. package/src/__tests__/world/perf.test.ts +93 -0
  43. package/src/__tests__/world/query.test.ts +223 -0
  44. package/src/__tests__/world/serialize.test.ts +83 -0
  45. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  46. package/src/archetype/archetype.ts +472 -0
  47. package/src/archetype/helpers.ts +186 -0
  48. package/src/archetype/store.ts +33 -0
  49. package/src/commands/buffer.ts +110 -0
  50. package/src/commands/changeset.ts +104 -0
  51. package/src/component/entity-store.ts +223 -0
  52. package/src/component/registry.ts +657 -0
  53. package/src/component/type-utils.ts +9 -0
  54. package/src/entity/index.ts +63 -0
  55. package/src/entity/manager.ts +115 -0
  56. package/src/entity/relation.ts +319 -0
  57. package/src/entity/types.ts +135 -0
  58. package/src/index.ts +41 -0
  59. package/src/query/filter.ts +75 -0
  60. package/src/query/query.ts +313 -0
  61. package/src/query/registry.ts +101 -0
  62. package/src/storage/serialization.ts +130 -0
  63. package/src/testing/index.ts +634 -0
  64. package/src/types/index.ts +99 -0
  65. package/src/utils/bit-set.ts +133 -0
  66. package/src/utils/multi-map.ts +96 -0
  67. package/src/utils/utils.ts +19 -0
  68. package/src/world/builder.ts +100 -0
  69. package/src/world/commands.ts +378 -0
  70. package/src/world/hooks.ts +358 -0
  71. package/src/world/references.ts +38 -0
  72. package/src/world/serialization.ts +122 -0
  73. package/src/world/world.ts +1201 -0
  74. /package/{builder.d.mts → dist/builder.d.mts} +0 -0
  75. /package/{index.d.mts → dist/index.d.mts} +0 -0
  76. /package/{index.mjs → dist/index.mjs} +0 -0
  77. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  78. /package/{testing.mjs → dist/testing.mjs} +0 -0
  79. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
  80. /package/{world.mjs → dist/world.mjs} +0 -0
  81. /package/{world.mjs.map → dist/world.mjs.map} +0 -0
@@ -0,0 +1,378 @@
1
+ import type { Archetype } from "../archetype/archetype";
2
+ import type { DontFragmentStore } from "../archetype/store";
3
+ import type { Command } from "../commands/buffer";
4
+ import type { ComponentChangeset } from "../commands/changeset";
5
+ import { normalizeComponentTypes } from "../component/type-utils";
6
+ import {
7
+ getComponentIdFromRelationId,
8
+ getComponentMerge,
9
+ isDontFragmentComponent,
10
+ isDontFragmentRelation,
11
+ isDontFragmentWildcard,
12
+ isWildcardRelationId,
13
+ relation,
14
+ type ComponentId,
15
+ type EntityId,
16
+ } from "../entity";
17
+
18
+ export interface CommandProcessorContext {
19
+ dontFragmentStore: DontFragmentStore;
20
+ ensureArchetype: (componentTypes: Iterable<EntityId<any>>) => Archetype;
21
+ }
22
+
23
+ export function processCommands(
24
+ entityId: EntityId,
25
+ currentArchetype: Archetype,
26
+ commands: Command[],
27
+ changeset: ComponentChangeset,
28
+ handleExclusiveRelation: (entityId: EntityId, archetype: Archetype, componentId: ComponentId<any>) => void,
29
+ ): void {
30
+ for (const command of commands) {
31
+ if (command.type === "set") {
32
+ // TypeScript knows command.componentType and command.component exist
33
+ processSetCommand(
34
+ entityId,
35
+ currentArchetype,
36
+ command.componentType,
37
+ command.component,
38
+ changeset,
39
+ handleExclusiveRelation,
40
+ );
41
+ } else if (command.type === "delete") {
42
+ // TypeScript knows command.componentType exists
43
+ processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
44
+ }
45
+ }
46
+ }
47
+
48
+ function processSetCommand(
49
+ entityId: EntityId,
50
+ currentArchetype: Archetype,
51
+ componentType: EntityId<any>,
52
+ component: any,
53
+ changeset: ComponentChangeset,
54
+ handleExclusiveRelation: (entityId: EntityId, archetype: Archetype, componentId: ComponentId<any>) => void,
55
+ ): void {
56
+ // Extract componentId if it's a relation (fast path)
57
+ const componentId = getComponentIdFromRelationId(componentType);
58
+ if (componentId !== undefined) {
59
+ // Handle exclusive relations by removing existing relations with the same base component
60
+ handleExclusiveRelation(entityId, currentArchetype, componentId);
61
+
62
+ // For dontFragment relations, ensure wildcard marker is in archetype signature
63
+ if (isDontFragmentComponent(componentId)) {
64
+ const wildcardMarker = relation(componentId, "*");
65
+ // Add wildcard marker to changeset if not already in archetype
66
+ if (!currentArchetype.componentTypeSet.has(wildcardMarker)) {
67
+ changeset.set(wildcardMarker, undefined);
68
+ }
69
+ }
70
+ }
71
+
72
+ const merge = getComponentMerge(componentType);
73
+ if (merge !== undefined && changeset.adds.has(componentType)) {
74
+ const prev = changeset.adds.get(componentType);
75
+ changeset.set(componentType, merge(prev, component));
76
+ return;
77
+ }
78
+
79
+ changeset.set(componentType, component);
80
+ }
81
+
82
+ function processDeleteCommand(
83
+ entityId: EntityId,
84
+ currentArchetype: Archetype,
85
+ componentType: EntityId<any>,
86
+ changeset: ComponentChangeset,
87
+ ): void {
88
+ const componentId = getComponentIdFromRelationId(componentType);
89
+
90
+ if (isWildcardRelationId(componentType) && componentId !== undefined) {
91
+ removeWildcardRelations(entityId, currentArchetype, componentId, changeset);
92
+ } else {
93
+ changeset.delete(componentType);
94
+ maybeRemoveWildcardMarker(entityId, currentArchetype, componentType, componentId, changeset);
95
+ }
96
+ }
97
+
98
+ export function removeMatchingRelations(
99
+ entityId: EntityId,
100
+ archetype: Archetype,
101
+ baseComponentId: ComponentId<any>,
102
+ changeset: ComponentChangeset,
103
+ ): void {
104
+ // Check archetype components
105
+ for (const componentType of archetype.componentTypes) {
106
+ // Skip wildcard markers - they should only be removed by maybeRemoveWildcardMarker
107
+ if (isWildcardRelationId(componentType)) continue;
108
+
109
+ if (getComponentIdFromRelationId(componentType) === baseComponentId) {
110
+ changeset.delete(componentType);
111
+ }
112
+ }
113
+
114
+ // Check dontFragment relations stored on entity
115
+ const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
116
+ if (dontFragmentData) {
117
+ for (const componentType of dontFragmentData.keys()) {
118
+ if (getComponentIdFromRelationId(componentType) === baseComponentId) {
119
+ changeset.delete(componentType);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ function removeWildcardRelations(
126
+ entityId: EntityId,
127
+ currentArchetype: Archetype,
128
+ baseComponentId: ComponentId<any>,
129
+ changeset: ComponentChangeset,
130
+ ): void {
131
+ removeMatchingRelations(entityId, currentArchetype, baseComponentId, changeset);
132
+
133
+ // If removing dontFragment relations, also remove the wildcard marker
134
+ if (isDontFragmentComponent(baseComponentId)) {
135
+ changeset.delete(relation(baseComponentId, "*"));
136
+ }
137
+ }
138
+
139
+ export function maybeRemoveWildcardMarker(
140
+ entityId: EntityId,
141
+ archetype: Archetype,
142
+ removedComponentType: EntityId<any>,
143
+ componentId: ComponentId<any> | undefined,
144
+ changeset: ComponentChangeset,
145
+ ): void {
146
+ if (componentId === undefined || !isDontFragmentComponent(componentId)) {
147
+ return;
148
+ }
149
+
150
+ const wildcardMarker = relation(componentId, "*");
151
+
152
+ // Check if there are any other relations with the same component ID
153
+ for (const otherComponentType of archetype.componentTypes) {
154
+ if (otherComponentType === removedComponentType) continue;
155
+ if (otherComponentType === wildcardMarker) continue;
156
+ if (changeset.removes.has(otherComponentType)) continue;
157
+
158
+ if (getComponentIdFromRelationId(otherComponentType) === componentId) {
159
+ return; // Found another relation, keep the marker
160
+ }
161
+ }
162
+
163
+ const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
164
+ if (dontFragmentData) {
165
+ for (const otherComponentType of dontFragmentData.keys()) {
166
+ if (otherComponentType === removedComponentType) continue;
167
+ if (changeset.removes.has(otherComponentType)) continue;
168
+
169
+ if (getComponentIdFromRelationId(otherComponentType) === componentId) {
170
+ return; // Found another relation, keep the marker
171
+ }
172
+ }
173
+ }
174
+
175
+ changeset.delete(wildcardMarker);
176
+ }
177
+
178
+ function hasEntityComponent(archetype: Archetype, entityId: EntityId, componentType: EntityId<any>): boolean {
179
+ if (archetype.componentTypeSet.has(componentType)) {
180
+ return true;
181
+ }
182
+
183
+ return archetype.getEntityDontFragmentRelations(entityId)?.has(componentType) ?? false;
184
+ }
185
+
186
+ function pruneMissingRemovals(changeset: ComponentChangeset, archetype: Archetype, entityId: EntityId): void {
187
+ // Collect to-prune entries first to avoid mutating the set during iteration
188
+ let toPrune: EntityId<any>[] | undefined;
189
+ for (const componentType of changeset.removes) {
190
+ if (!hasEntityComponent(archetype, entityId, componentType)) {
191
+ if (toPrune === undefined) toPrune = [];
192
+ toPrune.push(componentType);
193
+ }
194
+ }
195
+ if (toPrune !== undefined) {
196
+ for (const componentType of toPrune) {
197
+ changeset.removes.delete(componentType);
198
+ }
199
+ }
200
+ }
201
+
202
+ function hasArchetypeStructuralChange(changeset: ComponentChangeset, currentArchetype: Archetype): boolean {
203
+ for (const componentType of changeset.removes) {
204
+ if (!isDontFragmentRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) {
205
+ return true;
206
+ }
207
+ }
208
+
209
+ for (const componentType of changeset.adds.keys()) {
210
+ if (!isDontFragmentRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) {
211
+ return true;
212
+ }
213
+ }
214
+
215
+ return false;
216
+ }
217
+
218
+ function buildFinalRegularComponentTypes(currentArchetype: Archetype, changeset: ComponentChangeset): EntityId<any>[] {
219
+ const finalRegularTypes = new Set(currentArchetype.componentTypes);
220
+
221
+ for (const componentType of changeset.removes) {
222
+ if (!isDontFragmentRelation(componentType)) {
223
+ finalRegularTypes.delete(componentType);
224
+ }
225
+ }
226
+
227
+ for (const [componentType] of changeset.adds) {
228
+ if (!isDontFragmentRelation(componentType)) {
229
+ finalRegularTypes.add(componentType);
230
+ }
231
+ }
232
+
233
+ return Array.from(finalRegularTypes);
234
+ }
235
+
236
+ export function applyChangeset(
237
+ ctx: CommandProcessorContext,
238
+ entityId: EntityId,
239
+ currentArchetype: Archetype,
240
+ changeset: ComponentChangeset,
241
+ entityToArchetype: Map<EntityId, Archetype>,
242
+ removedComponents: Map<EntityId<any>, any> | null,
243
+ ): Archetype {
244
+ pruneMissingRemovals(changeset, currentArchetype, entityId);
245
+ const archetypeChanged = hasArchetypeStructuralChange(changeset, currentArchetype);
246
+
247
+ if (archetypeChanged) {
248
+ const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
249
+ const newArchetype = ctx.ensureArchetype(finalRegularTypes);
250
+ const currentComponents = currentArchetype.removeEntity(entityId)!;
251
+
252
+ if (removedComponents !== null) {
253
+ for (const componentType of changeset.removes) {
254
+ removedComponents.set(componentType, currentComponents.get(componentType));
255
+ }
256
+ }
257
+
258
+ newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
259
+ entityToArchetype.set(entityId, newArchetype);
260
+ return newArchetype;
261
+ }
262
+
263
+ // No archetype move needed: only component payload updates and/or dontFragment relation updates.
264
+ if (removedComponents !== null) {
265
+ applyDontFragmentChanges(ctx.dontFragmentStore, entityId, changeset, removedComponents);
266
+ } else {
267
+ applyDontFragmentChangesNoHooks(ctx.dontFragmentStore, entityId, changeset);
268
+ }
269
+
270
+ // Direct update for regular components in archetype
271
+ for (const [componentType, component] of changeset.adds) {
272
+ if (isDontFragmentRelation(componentType)) {
273
+ continue;
274
+ }
275
+ currentArchetype.set(entityId, componentType, component);
276
+ }
277
+
278
+ return currentArchetype;
279
+ }
280
+
281
+ /**
282
+ * No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
283
+ */
284
+ function applyDontFragmentChanges(
285
+ dontFragmentRelations: DontFragmentStore,
286
+ entityId: EntityId,
287
+ changeset: ComponentChangeset,
288
+ removedComponents: Map<EntityId<any>, any>,
289
+ ): void {
290
+ // Get or create the entity's dontFragment relations map
291
+ let entityRelations = dontFragmentRelations.get(entityId);
292
+
293
+ for (const componentType of changeset.removes) {
294
+ if (isDontFragmentRelation(componentType)) {
295
+ if (entityRelations) {
296
+ const removedValue = entityRelations.get(componentType);
297
+ if (removedValue !== undefined || entityRelations.has(componentType)) {
298
+ removedComponents.set(componentType, removedValue);
299
+ entityRelations.delete(componentType);
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ for (const [componentType, component] of changeset.adds) {
306
+ if (isDontFragmentRelation(componentType)) {
307
+ if (!entityRelations) {
308
+ entityRelations = new Map();
309
+ dontFragmentRelations.set(entityId, entityRelations);
310
+ }
311
+ entityRelations.set(componentType, component);
312
+ }
313
+ }
314
+
315
+ // Clean up empty map
316
+ if (entityRelations && entityRelations.size === 0) {
317
+ dontFragmentRelations.delete(entityId);
318
+ }
319
+ }
320
+
321
+ function applyDontFragmentChangesNoHooks(
322
+ dontFragmentRelations: DontFragmentStore,
323
+ entityId: EntityId,
324
+ changeset: ComponentChangeset,
325
+ ): void {
326
+ let entityRelations = dontFragmentRelations.get(entityId);
327
+
328
+ for (const componentType of changeset.removes) {
329
+ if (isDontFragmentRelation(componentType)) {
330
+ if (entityRelations) {
331
+ entityRelations.delete(componentType);
332
+ }
333
+ }
334
+ }
335
+
336
+ for (const [componentType, component] of changeset.adds) {
337
+ if (isDontFragmentRelation(componentType)) {
338
+ if (!entityRelations) {
339
+ entityRelations = new Map();
340
+ dontFragmentRelations.set(entityId, entityRelations);
341
+ }
342
+ entityRelations.set(componentType, component);
343
+ }
344
+ }
345
+
346
+ // Clean up empty map
347
+ if (entityRelations && entityRelations.size === 0) {
348
+ dontFragmentRelations.delete(entityId);
349
+ }
350
+ }
351
+
352
+ export function filterRegularComponentTypes(componentTypes: Iterable<EntityId<any>>): EntityId<any>[] {
353
+ const regularTypes: EntityId<any>[] = [];
354
+
355
+ for (const componentType of componentTypes) {
356
+ // Keep wildcard markers for dontFragment components (they mark the archetype)
357
+ if (isDontFragmentWildcard(componentType)) {
358
+ regularTypes.push(componentType);
359
+ continue;
360
+ }
361
+
362
+ // Skip specific dontFragment relations from archetype signature
363
+ if (isDontFragmentRelation(componentType)) {
364
+ continue;
365
+ }
366
+
367
+ regularTypes.push(componentType);
368
+ }
369
+
370
+ return regularTypes;
371
+ }
372
+
373
+ export function areComponentTypesEqual(types1: EntityId<any>[], types2: EntityId<any>[]): boolean {
374
+ if (types1.length !== types2.length) return false;
375
+ const sorted1 = normalizeComponentTypes(types1);
376
+ const sorted2 = normalizeComponentTypes(types2);
377
+ return sorted1.every((v, i) => v === sorted2[i]);
378
+ }