@lagless/core 0.0.33

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 (147) hide show
  1. package/README.md +970 -0
  2. package/dist/index.d.ts +11 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +12 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/di/di-container.d.ts +7 -0
  7. package/dist/lib/di/di-container.d.ts.map +1 -0
  8. package/dist/lib/di/di-container.js +25 -0
  9. package/dist/lib/di/di-container.js.map +1 -0
  10. package/dist/lib/di/di-decorators.d.ts +4 -0
  11. package/dist/lib/di/di-decorators.d.ts.map +1 -0
  12. package/dist/lib/di/di-decorators.js +96 -0
  13. package/dist/lib/di/di-decorators.js.map +1 -0
  14. package/dist/lib/di/index.d.ts +3 -0
  15. package/dist/lib/di/index.d.ts.map +1 -0
  16. package/dist/lib/di/index.js +4 -0
  17. package/dist/lib/di/index.js.map +1 -0
  18. package/dist/lib/ecs-config.d.ts +16 -0
  19. package/dist/lib/ecs-config.d.ts.map +1 -0
  20. package/dist/lib/ecs-config.js +28 -0
  21. package/dist/lib/ecs-config.js.map +1 -0
  22. package/dist/lib/ecs-runner.d.ts +20 -0
  23. package/dist/lib/ecs-runner.d.ts.map +1 -0
  24. package/dist/lib/ecs-runner.js +60 -0
  25. package/dist/lib/ecs-runner.js.map +1 -0
  26. package/dist/lib/ecs-simulation.d.ts +44 -0
  27. package/dist/lib/ecs-simulation.d.ts.map +1 -0
  28. package/dist/lib/ecs-simulation.js +138 -0
  29. package/dist/lib/ecs-simulation.js.map +1 -0
  30. package/dist/lib/hash-verification/abstract-hash-verification.system.d.ts +32 -0
  31. package/dist/lib/hash-verification/abstract-hash-verification.system.d.ts.map +1 -0
  32. package/dist/lib/hash-verification/abstract-hash-verification.system.js +49 -0
  33. package/dist/lib/hash-verification/abstract-hash-verification.system.js.map +1 -0
  34. package/dist/lib/hash-verification/create-hash-reporter.d.ts +16 -0
  35. package/dist/lib/hash-verification/create-hash-reporter.d.ts.map +1 -0
  36. package/dist/lib/hash-verification/create-hash-reporter.js +21 -0
  37. package/dist/lib/hash-verification/create-hash-reporter.js.map +1 -0
  38. package/dist/lib/hash-verification/divergence.signal.d.ts +11 -0
  39. package/dist/lib/hash-verification/divergence.signal.d.ts.map +1 -0
  40. package/dist/lib/hash-verification/divergence.signal.js +5 -0
  41. package/dist/lib/hash-verification/divergence.signal.js.map +1 -0
  42. package/dist/lib/hash-verification/index.d.ts +4 -0
  43. package/dist/lib/hash-verification/index.d.ts.map +1 -0
  44. package/dist/lib/hash-verification/index.js +5 -0
  45. package/dist/lib/hash-verification/index.js.map +1 -0
  46. package/dist/lib/input/abstract-input-provider.d.ts +61 -0
  47. package/dist/lib/input/abstract-input-provider.d.ts.map +1 -0
  48. package/dist/lib/input/abstract-input-provider.js +110 -0
  49. package/dist/lib/input/abstract-input-provider.js.map +1 -0
  50. package/dist/lib/input/index.d.ts +8 -0
  51. package/dist/lib/input/index.d.ts.map +1 -0
  52. package/dist/lib/input/index.js +9 -0
  53. package/dist/lib/input/index.js.map +1 -0
  54. package/dist/lib/input/input-provider-di-token.d.ts +6 -0
  55. package/dist/lib/input/input-provider-di-token.d.ts.map +1 -0
  56. package/dist/lib/input/input-provider-di-token.js +12 -0
  57. package/dist/lib/input/input-provider-di-token.js.map +1 -0
  58. package/dist/lib/input/input-registry.d.ts +8 -0
  59. package/dist/lib/input/input-registry.d.ts.map +1 -0
  60. package/dist/lib/input/input-registry.js +21 -0
  61. package/dist/lib/input/input-registry.js.map +1 -0
  62. package/dist/lib/input/local-input-provider.d.ts +6 -0
  63. package/dist/lib/input/local-input-provider.d.ts.map +1 -0
  64. package/dist/lib/input/local-input-provider.js +12 -0
  65. package/dist/lib/input/local-input-provider.js.map +1 -0
  66. package/dist/lib/input/replay-input-provider.d.ts +12 -0
  67. package/dist/lib/input/replay-input-provider.d.ts.map +1 -0
  68. package/dist/lib/input/replay-input-provider.js +46 -0
  69. package/dist/lib/input/replay-input-provider.js.map +1 -0
  70. package/dist/lib/input/rpc-history.d.ts +40 -0
  71. package/dist/lib/input/rpc-history.d.ts.map +1 -0
  72. package/dist/lib/input/rpc-history.js +308 -0
  73. package/dist/lib/input/rpc-history.js.map +1 -0
  74. package/dist/lib/input/rpc.d.ts +8 -0
  75. package/dist/lib/input/rpc.d.ts.map +1 -0
  76. package/dist/lib/input/rpc.js +9 -0
  77. package/dist/lib/input/rpc.js.map +1 -0
  78. package/dist/lib/mem/abstract-memory.interface.d.ts +6 -0
  79. package/dist/lib/mem/abstract-memory.interface.d.ts.map +1 -0
  80. package/dist/lib/mem/abstract-memory.interface.js +3 -0
  81. package/dist/lib/mem/abstract-memory.interface.js.map +1 -0
  82. package/dist/lib/mem/index.d.ts +5 -0
  83. package/dist/lib/mem/index.d.ts.map +1 -0
  84. package/dist/lib/mem/index.js +6 -0
  85. package/dist/lib/mem/index.js.map +1 -0
  86. package/dist/lib/mem/managers/components-manager.d.ts +15 -0
  87. package/dist/lib/mem/managers/components-manager.d.ts.map +1 -0
  88. package/dist/lib/mem/managers/components-manager.js +30 -0
  89. package/dist/lib/mem/managers/components-manager.js.map +1 -0
  90. package/dist/lib/mem/managers/entities-manager.d.ts +31 -0
  91. package/dist/lib/mem/managers/entities-manager.d.ts.map +1 -0
  92. package/dist/lib/mem/managers/entities-manager.js +106 -0
  93. package/dist/lib/mem/managers/entities-manager.js.map +1 -0
  94. package/dist/lib/mem/managers/filters-manager.d.ts +17 -0
  95. package/dist/lib/mem/managers/filters-manager.d.ts.map +1 -0
  96. package/dist/lib/mem/managers/filters-manager.js +46 -0
  97. package/dist/lib/mem/managers/filters-manager.js.map +1 -0
  98. package/dist/lib/mem/managers/player-resources-manager.d.ts +21 -0
  99. package/dist/lib/mem/managers/player-resources-manager.d.ts.map +1 -0
  100. package/dist/lib/mem/managers/player-resources-manager.js +52 -0
  101. package/dist/lib/mem/managers/player-resources-manager.js.map +1 -0
  102. package/dist/lib/mem/managers/prng-manager.d.ts +36 -0
  103. package/dist/lib/mem/managers/prng-manager.d.ts.map +1 -0
  104. package/dist/lib/mem/managers/prng-manager.js +126 -0
  105. package/dist/lib/mem/managers/prng-manager.js.map +1 -0
  106. package/dist/lib/mem/managers/singletons-manager.d.ts +13 -0
  107. package/dist/lib/mem/managers/singletons-manager.d.ts.map +1 -0
  108. package/dist/lib/mem/managers/singletons-manager.js +29 -0
  109. package/dist/lib/mem/managers/singletons-manager.js.map +1 -0
  110. package/dist/lib/mem/managers/tick-manager.d.ts +10 -0
  111. package/dist/lib/mem/managers/tick-manager.d.ts.map +1 -0
  112. package/dist/lib/mem/managers/tick-manager.js +18 -0
  113. package/dist/lib/mem/managers/tick-manager.js.map +1 -0
  114. package/dist/lib/mem/mem.d.ts +28 -0
  115. package/dist/lib/mem/mem.d.ts.map +1 -0
  116. package/dist/lib/mem/mem.js +63 -0
  117. package/dist/lib/mem/mem.js.map +1 -0
  118. package/dist/lib/prefab.d.ts +16 -0
  119. package/dist/lib/prefab.d.ts.map +1 -0
  120. package/dist/lib/prefab.js +17 -0
  121. package/dist/lib/prefab.js.map +1 -0
  122. package/dist/lib/signals/event-emitter.d.ts +9 -0
  123. package/dist/lib/signals/event-emitter.d.ts.map +1 -0
  124. package/dist/lib/signals/event-emitter.js +22 -0
  125. package/dist/lib/signals/event-emitter.js.map +1 -0
  126. package/dist/lib/signals/signal.d.ts +40 -0
  127. package/dist/lib/signals/signal.d.ts.map +1 -0
  128. package/dist/lib/signals/signal.js +133 -0
  129. package/dist/lib/signals/signal.js.map +1 -0
  130. package/dist/lib/signals/signals.registry.d.ts +9 -0
  131. package/dist/lib/signals/signals.registry.d.ts.map +1 -0
  132. package/dist/lib/signals/signals.registry.js +31 -0
  133. package/dist/lib/signals/signals.registry.js.map +1 -0
  134. package/dist/lib/types/abstract-filter.d.ts +16 -0
  135. package/dist/lib/types/abstract-filter.d.ts.map +1 -0
  136. package/dist/lib/types/abstract-filter.js +53 -0
  137. package/dist/lib/types/abstract-filter.js.map +1 -0
  138. package/dist/lib/types/ecs-types.d.ts +109 -0
  139. package/dist/lib/types/ecs-types.d.ts.map +1 -0
  140. package/dist/lib/types/ecs-types.js +3 -0
  141. package/dist/lib/types/ecs-types.js.map +1 -0
  142. package/dist/lib/types/index.d.ts +3 -0
  143. package/dist/lib/types/index.d.ts.map +1 -0
  144. package/dist/lib/types/index.js +4 -0
  145. package/dist/lib/types/index.js.map +1 -0
  146. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  147. package/package.json +57 -0
package/README.md ADDED
@@ -0,0 +1,970 @@
1
+ # @lagless/core
2
+
3
+ ## 1. Responsibility & Context
4
+
5
+ Provides the core Entity-Component-System (ECS) engine for deterministic multiplayer games with rollback netcode. Manages all game state in a single ArrayBuffer, orchestrates simulation ticks with snapshot/rollback, implements a dependency injection container for systems and signals, and provides input handling with prediction/verification. This is the central library of the Lagless framework — all game logic is built on top of this ECS foundation.
6
+
7
+ ## 2. Architecture Role
8
+
9
+ **Foundation layer** — sits above `@lagless/binary`, `@lagless/math`, and `@lagless/misc`. The core ECS engine that all game simulations depend on.
10
+
11
+ **Downstream consumers:**
12
+ - `circle-sumo-simulation` — Implements game-specific components, systems, and logic using the core ECS abstractions
13
+ - Game-specific runners — Extend `ECSRunner` to wire up custom game logic
14
+
15
+ **Upstream dependencies:**
16
+ - `@lagless/binary` — Binary serialization and memory layout (MemoryTracker, TypedArray schemas)
17
+ - `@lagless/math` — Deterministic math operations (MathOps.clamp01 for interpolation)
18
+ - `@lagless/misc` — SimulationClock, SnapshotHistory, PRNG, UUID
19
+
20
+ ## 3. Public API
21
+
22
+ ### Core Classes
23
+
24
+ #### Mem
25
+
26
+ Single ArrayBuffer containing all game state. Manages 7 memory regions via specialized managers:
27
+
28
+ ```typescript
29
+ class Mem {
30
+ readonly tickManager: TickManager; // Current tick counter
31
+ readonly prngManager: PRNGManager; // Deterministic PRNG with seed
32
+ readonly componentsManager: ComponentsManager; // All ECS components (SoA layout)
33
+ readonly singletonsManager: SingletonsManager; // Global state singletons
34
+ readonly filtersManager: FiltersManager; // Entity filters (bitmask-based)
35
+ readonly entitiesManager: EntitiesManager; // Entity lifecycle (create/destroy)
36
+ readonly playerResourcesManager: PlayerResourcesManager; // Per-player resources
37
+
38
+ constructor(config: ECSConfig, deps: ECSDeps);
39
+
40
+ exportSnapshot(): ArrayBuffer; // Clone ArrayBuffer (for snapshot storage)
41
+ applySnapshot(arrayBuffer: ArrayBuffer): void; // Overwrite ArrayBuffer (for rollback)
42
+ getHash(): number; // Hash entire ArrayBuffer (for debugging desyncs)
43
+ }
44
+ ```
45
+
46
+ **Key behavior:**
47
+ - All managers write to the same ArrayBuffer in strict order (deterministic layout)
48
+ - Components stored as SoA (Struct of Arrays) — e.g., `component.unsafe.positionX[entityId]`
49
+ - Snapshot = `arrayBuffer.slice(0)`, Rollback = overwrite bytes
50
+
51
+ #### ECSSimulation
52
+
53
+ Manages the simulation loop: tick accumulation, snapshot storage, rollback, input processing, and signal orchestration.
54
+
55
+ ```typescript
56
+ class ECSSimulation {
57
+ readonly mem: Mem; // Game state
58
+ readonly clock: SimulationClock; // Time accumulation with PhaseNudger
59
+
60
+ get tick(): number; // Current simulation tick
61
+ get interpolationFactor(): number; // [0, 1] for smooth rendering between ticks
62
+
63
+ constructor(config: ECSConfig, deps: ECSDeps, inputProvider: AbstractInputProvider);
64
+
65
+ registerSystems(systems: IECSSystem[]): void; // Register systems (called once by ECSRunner)
66
+ addTickHandler(handler: (tick: number) => void): () => void; // Subscribe to tick events
67
+ start(): void; // Start simulation clock
68
+ update(dt: number): void; // Main loop: check rollback, simulate ticks, update interpolation
69
+ }
70
+ ```
71
+
72
+ **Simulation flow:**
73
+ 1. `update(dt)` — Advances `clock.accumulatedTime` by dt
74
+ 2. Check for rollback (if input provider invalidates past ticks)
75
+ 3. Simulate ticks from current tick to target tick
76
+ 4. Each tick: run all systems in order, handle signals, save snapshot (if snapshotRate)
77
+ 5. Update `interpolationFactor` for smooth rendering
78
+
79
+ #### ECSRunner
80
+
81
+ Abstract base class that wires together DI container, simulation, systems, and signals. Extend this to create game-specific runners.
82
+
83
+ ```typescript
84
+ abstract class ECSRunner {
85
+ readonly DIContainer: Container; // DI container for systems/signals
86
+ readonly Simulation: ECSSimulation; // Simulation instance
87
+ readonly Config: ECSConfig; // Configuration
88
+ readonly InputProviderInstance: AbstractInputProvider; // Input handling
89
+
90
+ protected constructor(
91
+ config: ECSConfig,
92
+ inputProvider: AbstractInputProvider,
93
+ systems: IECSSystemConstructor[],
94
+ signals?: ISignalConstructor[],
95
+ deps: ECSDeps,
96
+ );
97
+
98
+ start(): void; // Start simulation
99
+ update(dt: number): void; // Update simulation
100
+ dispose(): void; // Clean up resources
101
+ }
102
+ ```
103
+
104
+ **What it does:**
105
+ - Registers all components, singletons, filters, player resources with DI container
106
+ - Resolves all systems and signals via DI
107
+ - Registers systems with simulation
108
+ - Initializes signal registry
109
+
110
+ #### ECSConfig
111
+
112
+ Configuration for simulation parameters. All values have sensible defaults.
113
+
114
+ ```typescript
115
+ class ECSConfig {
116
+ readonly seed: RawSeed; // PRNG seed (16 bytes)
117
+ readonly maxEntities: number; // Max entities (default: 1000)
118
+ readonly maxPlayers: number; // Max players (default: 6)
119
+ readonly initialInputDelayTick: number; // Starting input delay (default: 2)
120
+ readonly minInputDelayTick: number; // Min delay (default: 1)
121
+ readonly maxInputDelayTick: number; // Max delay (default: 8)
122
+ readonly fps: number; // Target FPS (default: 60)
123
+ readonly frameLength: number; // Frame duration in ms (1000/fps)
124
+ readonly snapshotRate: number; // Save snapshot every N ticks (default: 1)
125
+ readonly snapshotHistorySize: number; // Max snapshots stored (default: 100)
126
+ readonly maxNudgePerFrame: number; // Max time correction per frame (default: frameLength/4)
127
+
128
+ constructor(options?: Partial<ECSConfig>);
129
+ }
130
+
131
+ type RawSeed = [number, ...number[]]; // 16-element array for 128-bit PRNG seed
132
+ ```
133
+
134
+ #### Prefab
135
+
136
+ Builder pattern for creating entities with initial component values.
137
+
138
+ ```typescript
139
+ class Prefab {
140
+ static create(): Prefab; // Create new prefab builder
141
+
142
+ with<T extends IComponentConstructor>(
143
+ Component: T,
144
+ values?: Partial<ComponentValues<T['schema']>>
145
+ ): Prefab; // Add component with optional initial values
146
+
147
+ [Symbol.iterator](): IterableIterator<...>; // Iterate component assignments
148
+ }
149
+ ```
150
+
151
+ **Usage:**
152
+ ```typescript
153
+ const playerPrefab = Prefab.create()
154
+ .with(Transform2d, { positionX: 0, positionY: 0, rotation: 0 })
155
+ .with(Velocity2d, { velocityX: 0, velocityY: 0 })
156
+ .with(CircleBody, { radius: 10 });
157
+
158
+ const entityId = entitiesManager.createEntity(playerPrefab);
159
+ ```
160
+
161
+ ### Dependency Injection
162
+
163
+ #### Container
164
+
165
+ DI container for automatic dependency resolution. Systems and signals use `@ECSSystem()` and `@ECSSignal()` decorators for dependency injection.
166
+
167
+ ```typescript
168
+ class Container {
169
+ resolve<T>(cls: Token<T>): T; // Resolve class and its dependencies (cached as singleton)
170
+ register<T>(cls: Token<T>, instance: T): void; // Register pre-created instance
171
+ }
172
+
173
+ type Token<T = any> = new (...args: any[]) => T; // Constructor type
174
+ ```
175
+
176
+ #### Decorators
177
+
178
+ ```typescript
179
+ function ECSSystem(...overrideDeps: Token[]): ClassDecorator;
180
+ function ECSSignal(...overrideDeps: Token[]): ClassDecorator;
181
+ ```
182
+
183
+ **How it works:**
184
+ - Decorators use TypeScript metadata (`reflect-metadata`) to infer constructor dependencies
185
+ - Override deps explicitly if needed: `@ECSSystem(ComponentA, FilterB)`
186
+ - Container resolves dependencies recursively and caches instances
187
+
188
+ ### Input System
189
+
190
+ #### AbstractInputProvider
191
+
192
+ Base class for input handling. Implementations: `LocalInputProvider` (client-side), `ReplayInputProvider` (for replays).
193
+
194
+ ```typescript
195
+ abstract class AbstractInputProvider {
196
+ abstract init(simulation: ECSSimulation): void;
197
+ abstract update(): void; // Called after simulation ticks
198
+ abstract getInvalidateRollbackTick(): number | undefined; // Return tick to rollback to if inputs changed
199
+ abstract dispose(): void;
200
+ }
201
+ ```
202
+
203
+ #### RPC (Remote Procedure Call)
204
+
205
+ Represents player input for a single tick.
206
+
207
+ ```typescript
208
+ class RPC {
209
+ tick: number; // Tick this input applies to
210
+ playerId: number; // Player who sent this input
211
+ ordinal: number; // Input type ID
212
+ data: ArrayBuffer; // Binary-encoded input data
213
+
214
+ constructor(tick: number, playerId: number, ordinal: number, data: ArrayBuffer);
215
+ }
216
+ ```
217
+
218
+ #### RPCHistory
219
+
220
+ Stores input history with efficient tick-based lookup.
221
+
222
+ ```typescript
223
+ class RPCHistory {
224
+ addRPC(rpc: RPC): void; // Add input to history
225
+ getRPCsByTick(tick: number): RPC[]; // Get all inputs for a tick
226
+ getRPCsByTickAndPlayer(tick: number, playerId: number): RPC[]; // Get player's inputs for tick
227
+ clear(): void; // Clear all history
228
+ rollback(tick: number): void; // Remove inputs >= tick
229
+ }
230
+ ```
231
+
232
+ #### InputRegistry
233
+
234
+ Maps input constructors to their ordinals and schemas.
235
+
236
+ ```typescript
237
+ class InputRegistry {
238
+ constructor(inputs: IInputConstructor[]);
239
+ getByOrdinal(ordinal: number): IInputConstructor;
240
+ getByConstructor(constructor: IInputConstructor): number; // Returns ordinal
241
+ }
242
+ ```
243
+
244
+ ### Signals
245
+
246
+ #### Signal<TData>
247
+
248
+ Event system with rollback-aware Predicted/Verified/Cancelled lifecycle. Systems emit signals, UI subscribes to Predicted/Verified/Cancelled events.
249
+
250
+ ```typescript
251
+ abstract class Signal<TData = unknown> {
252
+ readonly Predicted: EventEmitter<SignalEvent<TData>>; // Emitted when signal first occurs
253
+ readonly Verified: EventEmitter<SignalEvent<TData>>; // Emitted after input delay confirms signal
254
+ readonly Cancelled: EventEmitter<SignalEvent<TData>>; // Emitted if rollback invalidates signal
255
+
256
+ constructor(config: ECSConfig);
257
+
258
+ emit(tick: number, data: TData): void; // Emit signal from system
259
+ }
260
+
261
+ interface SignalEvent<TData> {
262
+ tick: number; // Tick when signal occurred
263
+ data: TData; // Signal payload
264
+ }
265
+ ```
266
+
267
+ **Lifecycle:**
268
+ 1. System emits signal at tick T → `Predicted` fires (UI shows immediate feedback)
269
+ 2. At tick T + maxInputDelayTick → Check if signal still exists after rollback/replay
270
+ - If yes → `Verified` fires (confirmed)
271
+ - If no → `Cancelled` fires (was misprediction)
272
+
273
+ ### Types
274
+
275
+ ```typescript
276
+ // ECS Schema (generated by codegen tool)
277
+ interface ECSSchema {
278
+ components: IComponentConstructor[];
279
+ singletons: ISingletonConstructor[];
280
+ filters: IFilterConstructor[];
281
+ inputs: IInputConstructor[];
282
+ playerResource: IPlayerResourceConstructor;
283
+ }
284
+
285
+ interface ECSDeps extends ECSSchema {}
286
+
287
+ // System interface
288
+ interface IECSSystem {
289
+ run(dt: number): void; // Called every tick
290
+ }
291
+
292
+ interface IECSSystemConstructor {
293
+ new (...args: any[]): IECSSystem;
294
+ deps: Token[]; // Injected by @ECSSystem() decorator
295
+ }
296
+
297
+ // Component (SoA layout)
298
+ interface IComponentConstructor {
299
+ name: string;
300
+ ID: number; // Power of 2 for bitmask filtering
301
+ schema: Record<string, TypedArrayConstructor>; // Field name -> TypedArray constructor
302
+
303
+ calculateSize(maxEntities: number, memTracker: MemoryTracker): void;
304
+ new (maxEntities: number, buffer: ArrayBuffer, memTracker: MemoryTracker): IComponentInstance;
305
+ }
306
+
307
+ interface IComponentInstance {
308
+ unsafe: Record<string, TypedArray>; // Field name -> TypedArray (e.g., positionX[entityId])
309
+ }
310
+
311
+ // Singleton (single instance, not per-entity)
312
+ interface ISingletonConstructor {
313
+ name: string;
314
+ schema: Record<string, TypedArrayConstructor>;
315
+
316
+ calculateSize(memTracker: MemoryTracker): void;
317
+ new (buffer: ArrayBuffer, memTracker: MemoryTracker): ISingletonInstance;
318
+ }
319
+
320
+ interface ISingletonInstance {
321
+ unsafe: Record<string, TypedArray>; // Field name -> TypedArray (single element)
322
+ }
323
+
324
+ // Filter (entity iteration)
325
+ abstract class AbstractFilter implements Iterable<number> {
326
+ [Symbol.iterator](): IterableIterator<number>; // Iterate entity IDs matching filter
327
+ }
328
+
329
+ interface IFilterConstructor {
330
+ new (...args: any[]): AbstractFilter;
331
+ }
332
+
333
+ // Input (player commands)
334
+ interface IInputConstructor {
335
+ ordinal: number; // Input type ID
336
+ schema: Record<string, InputFieldDefinition>; // Binary layout schema
337
+ new (): IInputInstance;
338
+ }
339
+
340
+ interface IInputInstance {
341
+ tick: number;
342
+ playerId: number;
343
+ [key: string]: any; // Input-specific fields
344
+ }
345
+
346
+ // Player Resource (per-player state, e.g., score)
347
+ interface IPlayerResourceConstructor {
348
+ name: string;
349
+ schema: Record<string, TypedArrayConstructor>;
350
+
351
+ calculateSize(maxPlayers: number, memTracker: MemoryTracker): void;
352
+ new (maxPlayers: number, buffer: ArrayBuffer, memTracker: MemoryTracker): IPlayerResourceInstance;
353
+ }
354
+
355
+ interface IPlayerResourceInstance {
356
+ unsafe: Record<string, TypedArray>; // Field name -> TypedArray[playerId]
357
+ }
358
+ ```
359
+
360
+ ### Managers (exported from Mem)
361
+
362
+ ```typescript
363
+ class PRNGManager {
364
+ readonly prng: PRNG; // Deterministic random number generator
365
+ }
366
+
367
+ class PRNG {
368
+ next(): number; // [0, 1) uniform random
369
+ nextInt(max: number): number; // [0, max) integer
370
+ nextIntInRange(min: number, max: number): number; // [min, max) integer
371
+ }
372
+
373
+ class EntitiesManager {
374
+ createEntity(prefab?: Prefab): number; // Create entity, returns entityId
375
+ destroyEntity(entityId: number): void; // Destroy entity (deferred until end of tick)
376
+ hasComponent(entityId: number, componentId: number): boolean;
377
+ }
378
+
379
+ class PlayerResourcesManager {
380
+ readonly PlayerResources: IPlayerResourceInstance; // Access per-player resources
381
+ }
382
+ ```
383
+
384
+ ## 4. Preconditions
385
+
386
+ - **`await MathOps.init()` must be called before starting ECSRunner** — MathOps uses WASM and needs async initialization
387
+ - **Systems must be registered before calling `simulation.start()`** — Throws error if no systems registered
388
+ - **ECSRunner constructor requires valid `ECSDeps` schema** — Components/singletons/filters/inputs must be generated by codegen tool
389
+ - **Component IDs must be powers of 2** — Required for bitmask filtering (1, 2, 4, 8, 16, ...)
390
+ - **System execution order matters** — Systems run in the order passed to ECSRunner constructor
391
+
392
+ ## 5. Postconditions
393
+
394
+ - After `simulation.start()`, `simulation.update(dt)` runs the tick loop until stopped
395
+ - After `mem.applySnapshot(snapshot)`, all game state reverts to the snapshot's tick
396
+ - After `simulation.update()`, `interpolationFactor` is in [0, 1] for smooth rendering
397
+ - Systems registered via `ECSRunner` are injected with all dependencies via `DIContainer`
398
+
399
+ ## 6. Invariants & Constraints
400
+
401
+ - **Single ArrayBuffer constraint:** All game state MUST fit in the allocated ArrayBuffer. Exceeding this size is undefined behavior.
402
+ - **Determinism guarantee:** Given identical inputs and seed, simulation produces identical results across all platforms.
403
+ - **System execution order:** Systems MUST run in the same order every tick. Changing order breaks determinism.
404
+ - **Component SoA layout:** Components are stored as Struct of Arrays. Access via `component.unsafe.fieldName[entityId]`, NOT `component[entityId].fieldName`.
405
+ - **Entity destruction is deferred:** `destroyEntity()` marks entity for deletion, but actual cleanup happens at end of tick.
406
+ - **Signal Predicted→Verified/Cancelled flow:** Signals emitted at tick T are verified/cancelled at tick T + maxInputDelayTick.
407
+ - **Snapshot rate:** Snapshots are saved every `snapshotRate` ticks. Rollback finds nearest past snapshot.
408
+
409
+ ## 7. Safety Notes (AI Agent)
410
+
411
+ ### DO NOT
412
+
413
+ - **DO NOT use `Math.random()`, `Date.now()`, or async I/O inside systems** — This breaks determinism. Use `PRNG` for randomness.
414
+ - **DO NOT allocate JS objects inside systems** — Components are SoA arrays in ArrayBuffer. Allocating objects breaks snapshot/rollback.
415
+ - **DO NOT reorder systems** — Execution order is critical for determinism. Changing order causes desyncs.
416
+ - **DO NOT mutate component data outside of systems** — Systems are the only place game logic should run.
417
+ - **DO NOT access `component[entityId]`** — Components use SoA layout. Use `component.unsafe.fieldName[entityId]` instead.
418
+ - **DO NOT call `destroyEntity()` and then access the entity in the same tick** — Destruction is deferred until end of tick.
419
+ - **DO NOT modify `ECSConfig` after ECSRunner constructor** — Config is readonly and baked into managers during initialization.
420
+ - **DO NOT forget to register systems** — Simulation throws error if `start()` is called without systems.
421
+
422
+ ### Common Mistakes
423
+
424
+ **Using non-deterministic APIs:**
425
+ ```typescript
426
+ // ❌ WRONG
427
+ class MovementSystem {
428
+ run() {
429
+ const randomSpeed = Math.random() * 10; // ← NON-DETERMINISTIC
430
+ entity.velocity = randomSpeed;
431
+ }
432
+ }
433
+
434
+ // ✅ CORRECT
435
+ class MovementSystem {
436
+ constructor(private prng: PRNG) {}
437
+ run() {
438
+ const randomSpeed = this.prng.next() * 10; // ← Deterministic PRNG
439
+ entity.velocity = randomSpeed;
440
+ }
441
+ }
442
+ ```
443
+
444
+ **Allocating objects in systems:**
445
+ ```typescript
446
+ // ❌ WRONG
447
+ class CollisionSystem {
448
+ run() {
449
+ const collisions = []; // ← JS object allocation breaks rollback
450
+ for (const entity of filter) {
451
+ collisions.push({ a: entity, b: other });
452
+ }
453
+ }
454
+ }
455
+
456
+ // ✅ CORRECT
457
+ class CollisionSystem {
458
+ run() {
459
+ // Store collision data in components (SoA arrays)
460
+ for (const entity of filter) {
461
+ component.unsafe.collidingWith[entity] = other;
462
+ }
463
+ }
464
+ }
465
+ ```
466
+
467
+ **Accessing components incorrectly:**
468
+ ```typescript
469
+ // ❌ WRONG
470
+ const position = transform2d[entityId]; // ← Components are NOT indexed by entity
471
+
472
+ // ✅ CORRECT
473
+ const positionX = transform2d.unsafe.positionX[entityId];
474
+ const positionY = transform2d.unsafe.positionY[entityId];
475
+ ```
476
+
477
+ **Forgetting system order matters:**
478
+ ```typescript
479
+ // ❌ WRONG - Order changed between sessions
480
+ const runner1 = new Runner(config, provider, [PhysicsSystem, MovementSystem], ...);
481
+ const runner2 = new Runner(config, provider, [MovementSystem, PhysicsSystem], ...); // ← DESYNC
482
+
483
+ // ✅ CORRECT - Same order always
484
+ const systemOrder = [MovementSystem, PhysicsSystem];
485
+ const runner = new Runner(config, provider, systemOrder, ...);
486
+ ```
487
+
488
+ ## 8. Usage Examples
489
+
490
+ ### Creating an ECS Runner
491
+
492
+ ```typescript
493
+ import { ECSRunner, ECSConfig, LocalInputProvider, MathOps } from '@lagless/core';
494
+ import { MyGameSchema } from './generated/schema'; // From codegen
495
+ import * as Systems from './systems';
496
+ import * as Signals from './signals';
497
+
498
+ class MyGameRunner extends ECSRunner {
499
+ constructor() {
500
+ const config = new ECSConfig({
501
+ fps: 60,
502
+ maxEntities: 500,
503
+ maxPlayers: 4,
504
+ });
505
+
506
+ const inputProvider = new LocalInputProvider();
507
+
508
+ const systems = [
509
+ Systems.InputSystem,
510
+ Systems.MovementSystem,
511
+ Systems.PhysicsSystem,
512
+ Systems.CollisionSystem,
513
+ Systems.RenderSystem,
514
+ ];
515
+
516
+ const signals = [
517
+ Signals.GameOverSignal,
518
+ Signals.ScoreChangedSignal,
519
+ ];
520
+
521
+ super(config, inputProvider, systems, signals, MyGameSchema);
522
+ }
523
+ }
524
+
525
+ // Usage
526
+ await MathOps.init(); // MUST call before ECS
527
+ const runner = new MyGameRunner();
528
+ runner.start();
529
+
530
+ // Game loop
531
+ requestAnimationFrame(function loop() {
532
+ const dt = getDeltaTime();
533
+ runner.update(dt);
534
+ requestAnimationFrame(loop);
535
+ });
536
+ ```
537
+
538
+ ### Writing a System
539
+
540
+ ```typescript
541
+ import { ECSSystem } from '@lagless/core';
542
+
543
+ @ECSSystem() // Decorator enables DI
544
+ export class MovementSystem {
545
+ constructor(
546
+ private transform2d: Transform2d, // Component
547
+ private velocity2d: Velocity2d, // Component
548
+ private filter: MovingEntitiesFilter // Filter
549
+ ) {}
550
+
551
+ run(dt: number): void {
552
+ for (const entityId of this.filter) {
553
+ const vx = this.velocity2d.unsafe.velocityX[entityId];
554
+ const vy = this.velocity2d.unsafe.velocityY[entityId];
555
+
556
+ this.transform2d.unsafe.positionX[entityId] += vx * dt;
557
+ this.transform2d.unsafe.positionY[entityId] += vy * dt;
558
+ }
559
+ }
560
+ }
561
+ ```
562
+
563
+ ### Using Signals
564
+
565
+ ```typescript
566
+ import { Signal, ECSSignal } from '@lagless/core';
567
+
568
+ interface GameOverData {
569
+ winnerId: number;
570
+ }
571
+
572
+ @ECSSignal()
573
+ export class GameOverSignal extends Signal<GameOverData> {}
574
+
575
+ // In a system: emit signal
576
+ class GameLogicSystem {
577
+ constructor(
578
+ private gameOver: GameOverSignal,
579
+ private gameState: GameState
580
+ ) {}
581
+
582
+ run(): void {
583
+ if (this.gameState.unsafe.playersLeft[0] === 1) {
584
+ const tick = this.gameState.unsafe.currentTick[0];
585
+ const winnerId = this.findLastPlayer();
586
+ this.gameOver.emit(tick, { winnerId });
587
+ }
588
+ }
589
+ }
590
+
591
+ // In UI: subscribe to signal
592
+ gameOver.Predicted.on((event) => {
593
+ console.log(`Game over! Winner: ${event.data.winnerId} (predicted)`);
594
+ showGameOverScreen(event.data.winnerId);
595
+ });
596
+
597
+ gameOver.Verified.on((event) => {
598
+ console.log(`Game over! Winner: ${event.data.winnerId} (verified)`);
599
+ });
600
+
601
+ gameOver.Cancelled.on((event) => {
602
+ console.log(`Game over was mispredicted, hiding screen`);
603
+ hideGameOverScreen();
604
+ });
605
+ ```
606
+
607
+ ### Creating Entities
608
+
609
+ ```typescript
610
+ import { Prefab, EntitiesManager } from '@lagless/core';
611
+
612
+ class SpawnSystem {
613
+ constructor(
614
+ private entities: EntitiesManager,
615
+ private transform2d: Transform2d,
616
+ private circleBody: CircleBody
617
+ ) {}
618
+
619
+ spawnPlayer(x: number, y: number): number {
620
+ const prefab = Prefab.create()
621
+ .with(Transform2d, { positionX: x, positionY: y, rotation: 0 })
622
+ .with(CircleBody, { radius: 10 });
623
+
624
+ return this.entities.createEntity(prefab);
625
+ }
626
+ }
627
+ ```
628
+
629
+ ### Snapshot and Rollback
630
+
631
+ ```typescript
632
+ // ECSSimulation handles this automatically, but you can trigger manually:
633
+
634
+ // Save snapshot
635
+ const snapshot = simulation.mem.exportSnapshot();
636
+ snapshotHistory.set(currentTick, snapshot);
637
+
638
+ // Rollback to tick
639
+ const rollbackTick = 100;
640
+ const snapshot = snapshotHistory.getNearest(rollbackTick);
641
+ simulation.mem.applySnapshot(snapshot);
642
+ snapshotHistory.rollback(rollbackTick); // Clear snapshots >= tick
643
+
644
+ // Replay from rollbackTick to currentTick
645
+ while (sim.tick < currentTick) {
646
+ sim.simulateTick();
647
+ }
648
+ ```
649
+
650
+ ## 9. Testing Guidance
651
+
652
+ **Framework:** Vitest (see `libs/core/src/lib/di/di.test.ts` for existing tests)
653
+
654
+ **Running tests:**
655
+ ```bash
656
+ # From monorepo root
657
+ nx test core
658
+
659
+ # Or with direct runner
660
+ npm test -- libs/core
661
+ ```
662
+
663
+ **Existing test patterns:**
664
+ - `di.test.ts` — DI Container dependency resolution, decorator tests
665
+
666
+ **When adding tests:**
667
+ - **Use deterministic seeds:** Pass explicit seed to `ECSConfig` for reproducible tests
668
+ - **Test system execution order:** Verify output doesn't change if systems run in wrong order (should catch non-determinism)
669
+ - **Test rollback:** Save snapshot, mutate state, restore snapshot, verify state is identical
670
+ - **Test signals:** Emit Predicted, rollback, verify Cancelled fires
671
+ - **Use `mem.getHash()` for desync detection:** Compare hashes between two simulations with same inputs
672
+
673
+ **Example test pattern:**
674
+ ```typescript
675
+ import { describe, it, expect } from 'vitest';
676
+ import { ECSSimulation, ECSConfig, Mem } from '@lagless/core';
677
+
678
+ describe('ECSSimulation rollback', () => {
679
+ it('should restore state after rollback', () => {
680
+ const config = new ECSConfig({ seed: [1, 2, 3, ...] });
681
+ const sim = new ECSSimulation(config, deps, inputProvider);
682
+
683
+ // Save snapshot at tick 10
684
+ sim.update(16.666 * 10);
685
+ const snapshot = sim.mem.exportSnapshot();
686
+ const hash1 = sim.mem.getHash();
687
+
688
+ // Mutate state
689
+ sim.update(16.666 * 5);
690
+ expect(sim.mem.getHash()).not.toBe(hash1);
691
+
692
+ // Rollback
693
+ sim.mem.applySnapshot(snapshot);
694
+ expect(sim.mem.getHash()).toBe(hash1);
695
+ });
696
+ });
697
+ ```
698
+
699
+ ## 10. Change Checklist
700
+
701
+ When modifying this module:
702
+
703
+ 1. **Verify determinism:** Test on multiple platforms (Windows/Mac/Linux, different browsers)
704
+ 2. **Maintain system order:** Document any system ordering requirements
705
+ 3. **Update schema generation:** If changing component/singleton/filter types, update codegen templates
706
+ 4. **Test rollback:** Add tests for snapshot/rollback if changing Mem layout
707
+ 5. **Check allocation:** Profile to ensure systems don't allocate JS objects
708
+ 6. **Update this README:** Document new APIs in Public API section
709
+ 7. **Preserve SoA layout:** Components MUST remain Struct of Arrays for snapshot/rollback to work
710
+ 8. **DO NOT break DI:** Decorator changes must preserve backward compatibility with existing systems
711
+
712
+ ## 11. Integration Notes
713
+
714
+ ### Used By
715
+
716
+ - **`circle-sumo-simulation`:**
717
+ - Extends `ECSRunner` to create `CircleSumoRunner`
718
+ - Uses codegen to generate components (Transform2d, Velocity2d, CircleBody, etc.)
719
+ - Systems implement game logic (movement, collision, scoring)
720
+ - Signals for game events (GameOver, HighImpact, PlayerFinishedGame)
721
+
722
+ ### Common Integration Patterns
723
+
724
+ **ECS Runner Setup:**
725
+ ```typescript
726
+ import { ECSRunner, ECSConfig } from '@lagless/core';
727
+
728
+ export class MyGameRunner extends ECSRunner {
729
+ constructor() {
730
+ // 1. Configure simulation
731
+ const config = new ECSConfig({ fps: 60, maxEntities: 1000 });
732
+
733
+ // 2. Choose input provider
734
+ const inputProvider = isReplay ? new ReplayInputProvider(replayData) : new LocalInputProvider();
735
+
736
+ // 3. Define system execution order (CRITICAL for determinism)
737
+ const systems = [
738
+ InputProcessingSystem, // Read player inputs
739
+ MovementSystem, // Update positions
740
+ PhysicsSystem, // Apply physics
741
+ CollisionSystem, // Detect collisions
742
+ GameLogicSystem, // Handle game rules
743
+ DestructionSystem, // Clean up destroyed entities
744
+ ];
745
+
746
+ // 4. Define signals
747
+ const signals = [GameOverSignal, ScoreChangedSignal];
748
+
749
+ // 5. Pass generated schema from codegen
750
+ super(config, inputProvider, systems, signals, GeneratedSchema);
751
+ }
752
+ }
753
+ ```
754
+
755
+ **Rendering with Interpolation:**
756
+ ```typescript
757
+ // In your render loop (runs at display refresh rate, e.g., 144 Hz)
758
+ function render() {
759
+ const factor = runner.Simulation.interpolationFactor; // [0, 1]
760
+
761
+ for (const entityId of visibleEntities) {
762
+ const transform = getTransform2dComponent(entityId);
763
+
764
+ // Interpolate between prev and current transform
765
+ const result = interpolateTransform2dCursor(transform, factor);
766
+
767
+ sprite.x = result.x;
768
+ sprite.y = result.y;
769
+ sprite.rotation = result.rotation;
770
+ }
771
+
772
+ requestAnimationFrame(render);
773
+ }
774
+ ```
775
+
776
+ **Network Integration (with `@lagless/net-wire`):**
777
+ ```typescript
778
+ import { ClockSync, InputDelayController } from '@lagless/net-wire';
779
+
780
+ // Setup
781
+ const clockSync = new ClockSync(...);
782
+ const runner = new MyGameRunner();
783
+
784
+ // When clock sync is ready
785
+ clockSync.on('ready', () => {
786
+ runner.Simulation.clock.phaseNudger.activate();
787
+ });
788
+
789
+ // On tick input from server
790
+ connection.on('tickInput', (msg) => {
791
+ const serverTick = msg.tick;
792
+ const localTick = runner.Simulation.tick;
793
+
794
+ // Nudge local clock to sync with server
795
+ runner.Simulation.clock.phaseNudger.onServerTickHint(serverTick, localTick);
796
+
797
+ // Add server input to input provider
798
+ runner.InputProviderInstance.addServerInput(msg);
799
+ });
800
+ ```
801
+
802
+ ## 12. Appendix
803
+
804
+ ### Memory Layout (Single ArrayBuffer)
805
+
806
+ ```
807
+ ┌─────────────────────────────────────────────────────────────────────┐
808
+ │ Single ArrayBuffer │
809
+ ├─────────────────────────────────────────────────────────────────────┤
810
+ │ TickManager │ 8 bytes: Uint32Array[1] for current tick │
811
+ ├─────────────────────────────────────────────────────────────────────┤
812
+ │ PRNGManager │ 64 bytes: PRNG state (xoshiro256++) │
813
+ ├─────────────────────────────────────────────────────────────────────┤
814
+ │ ComponentsManager │ N components × maxEntities × field sizes │
815
+ │ - Component 1 │ - Field 1: TypedArray[maxEntities] │
816
+ │ - Component 2 │ - Field 2: TypedArray[maxEntities] │
817
+ │ - ... │ - ... │
818
+ ├─────────────────────────────────────────────────────────────────────┤
819
+ │ SingletonsManager │ M singletons × field sizes (1 instance) │
820
+ │ - Singleton 1 │ - Field 1: TypedArray[1] │
821
+ │ - ... │ - ... │
822
+ ├─────────────────────────────────────────────────────────────────────┤
823
+ │ FiltersManager │ F filters × bitmask arrays │
824
+ │ - Filter 1 │ - Bitmask: Uint32Array[ceil(maxEntities/32)] │
825
+ │ - ... │ - ... │
826
+ ├─────────────────────────────────────────────────────────────────────┤
827
+ │ EntitiesManager │ Entity lifecycle tracking │
828
+ │ - Free list │ - Uint32Array for free entity IDs │
829
+ │ - Component masks │ - Bitmask per entity │
830
+ ├─────────────────────────────────────────────────────────────────────┤
831
+ │ PlayerResourcesManager│ Player resources × maxPlayers │
832
+ │ - Resource Field 1 │ - TypedArray[maxPlayers] │
833
+ │ - ... │ - ... │
834
+ └─────────────────────────────────────────────────────────────────────┘
835
+ ```
836
+
837
+ **Key points:**
838
+ - All managers write to the same ArrayBuffer sequentially
839
+ - Each manager calculates its size first, then writes at the correct offset
840
+ - `MemoryTracker` tracks current write position (ptr)
841
+ - Snapshot = `arrayBuffer.slice(0)` (clones entire buffer)
842
+ - Rollback = `new Uint8Array(dest).set(new Uint8Array(src))` (overwrites bytes)
843
+
844
+ ### Component SoA Layout Example
845
+
846
+ **Component definition (from codegen):**
847
+ ```typescript
848
+ class Transform2d {
849
+ static ID = 1; // Power of 2
850
+ static schema = {
851
+ positionX: Float32Array,
852
+ positionY: Float32Array,
853
+ rotation: Float32Array,
854
+ prevPositionX: Float32Array,
855
+ prevPositionY: Float32Array,
856
+ prevRotation: Float32Array,
857
+ };
858
+
859
+ constructor(maxEntities: number, buffer: ArrayBuffer, tracker: MemoryTracker) {
860
+ const byteOffset = tracker.ptr;
861
+ this.unsafe = {
862
+ positionX: new Float32Array(buffer, byteOffset, maxEntities),
863
+ positionY: new Float32Array(buffer, byteOffset + maxEntities * 4, maxEntities),
864
+ rotation: new Float32Array(buffer, byteOffset + maxEntities * 8, maxEntities),
865
+ // ... other fields
866
+ };
867
+ tracker.advance(maxEntities * 6 * 4); // 6 fields × 4 bytes
868
+ }
869
+ }
870
+ ```
871
+
872
+ **Memory layout (maxEntities = 1000):**
873
+ ```
874
+ positionX: [float, float, float, ...] (1000 floats = 4000 bytes)
875
+ positionY: [float, float, float, ...] (1000 floats = 4000 bytes)
876
+ rotation: [float, float, float, ...] (1000 floats = 4000 bytes)
877
+ prevPositionX: [float, float, float, ...] (1000 floats = 4000 bytes)
878
+ prevPositionY: [float, float, float, ...] (1000 floats = 4000 bytes)
879
+ prevRotation: [float, float, float, ...] (1000 floats = 4000 bytes)
880
+ ──────────────────────────
881
+ Total: 24000 bytes
882
+ ```
883
+
884
+ **Access pattern:**
885
+ ```typescript
886
+ // Get entity 42's position
887
+ const x = transform2d.unsafe.positionX[42];
888
+ const y = transform2d.unsafe.positionY[42];
889
+
890
+ // Set entity 42's rotation
891
+ transform2d.unsafe.rotation[42] = MathOps.PI_HALF;
892
+ ```
893
+
894
+ ### Filter Bitmask System
895
+
896
+ Filters use bitmasks to efficiently iterate entities with specific components.
897
+
898
+ **Example:**
899
+ ```typescript
900
+ // Filter definition (from codegen)
901
+ class MovingEntitiesFilter extends AbstractFilter {
902
+ static requiredComponents = [Transform2d.ID, Velocity2d.ID];
903
+ // requiredComponents = [1, 2] (powers of 2)
904
+ // Bitmask = 1 | 2 = 3 (binary: 11)
905
+ }
906
+
907
+ // Entity component masks
908
+ entity[0] mask: 0000 (no components)
909
+ entity[1] mask: 0001 (Transform2d only)
910
+ entity[2] mask: 0011 (Transform2d + Velocity2d) ← matches filter
911
+ entity[3] mask: 0111 (Transform2d + Velocity2d + CircleBody) ← matches filter
912
+
913
+ // Iteration
914
+ for (const entityId of movingEntitiesFilter) {
915
+ // Only entities 2 and 3 are visited
916
+ }
917
+ ```
918
+
919
+ **Why powers of 2?**
920
+ - Component IDs are powers of 2: 1, 2, 4, 8, 16, 32, ...
921
+ - Bitmask operations: `mask & requiredMask === requiredMask` checks if entity has all required components
922
+ - Efficient: Single bitwise AND operation, no array lookups
923
+
924
+ ### Signal Lifecycle Example
925
+
926
+ ```
927
+ Tick 10: System emits GameOver signal
928
+ → Predicted fires → UI shows "Game Over" screen
929
+ → Signal added to _awaitingVerification[10]
930
+
931
+ Tick 11-17: Simulation continues...
932
+
933
+ Tick 18 (= 10 + maxInputDelayTick=8):
934
+ → Check _pending[10] (signals still present after all inputs confirmed)
935
+ → Signal exists in both pending and awaiting
936
+ → Verified fires → "Game Over" is confirmed
937
+
938
+ Alternative scenario (rollback):
939
+ Tick 15: Late input arrives for tick 9 → Rollback to tick 9
940
+ Tick 9-17: Replay simulation with new inputs
941
+ Tick 10: GameOver signal NOT emitted this time (different outcome)
942
+ Tick 18: Check _pending[10]
943
+ → Signal NOT in pending (was cancelled by rollback)
944
+ → Cancelled fires → UI hides "Game Over" screen
945
+ ```
946
+
947
+ ### System Execution Order Example
948
+
949
+ **Correct order (deterministic):**
950
+ ```
951
+ 1. InputProcessingSystem — Reads RPC inputs, updates player commands
952
+ 2. MovementSystem — Applies velocity to position
953
+ 3. PhysicsSystem — Applies gravity, friction
954
+ 4. CollisionSystem — Detects collisions, applies impulses
955
+ 5. GameLogicSystem — Checks win conditions, emits signals
956
+ 6. DestructionSystem — Destroys marked entities
957
+ ```
958
+
959
+ **Why this order matters:**
960
+ - Movement must happen before collision detection (or collisions use stale positions)
961
+ - Physics must happen before collision (or impulses are applied twice)
962
+ - Destruction must be last (so systems don't access destroyed entities)
963
+
964
+ **Wrong order causes desyncs:**
965
+ ```
966
+ Client A: [Movement, Collision, Physics] → Entity 5 at (10.5, 20.3)
967
+ Client B: [Movement, Physics, Collision] → Entity 5 at (10.7, 20.1) ← DESYNC
968
+ ```
969
+
970
+ Even tiny differences (0.2 pixels) accumulate over time and cause divergence.