@lagless/misc 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.
package/README.md ADDED
@@ -0,0 +1,596 @@
1
+ # @lagless/misc
2
+
3
+ ## 1. Responsibility & Context
4
+
5
+ Provides utility classes and functions used across the Lagless ECS framework: time management (`SimulationClock`, `PhaseNudger`), snapshot storage for rollback (`SnapshotHistory`), circular buffers (`RingBuffer`), UUID generation with bot detection (`UUID`), Transform2d interpolation helpers, and visual rollback smoothing (`VisualSmoother2d`). This library sits between low-level primitives and the core ECS engine, providing common abstractions needed by networking, rendering, and simulation layers.
6
+
7
+ ## 2. Architecture Role
8
+
9
+ **Utility layer** — sits above `@lagless/math` (peer dependency) and provides utilities used by `@lagless/core` and `@lagless/net-wire`.
10
+
11
+ **Downstream consumers:**
12
+ - `@lagless/core` — Uses `SimulationClock` for tick loop timing, `SnapshotHistory` for rollback storage, `RingBuffer` for input buffering
13
+ - `@lagless/net-wire` — Uses `RingBuffer` for network packet buffering
14
+ - `circle-sumo-simulation` — Uses `UUID` for player identification, `interpolateTransform2d` for smooth rendering between ticks
15
+
16
+ **Upstream dependencies:**
17
+ - `@lagless/math` (peer dependency) — Used by `transform2d-utils` for angle interpolation
18
+
19
+ ## 3. Public API
20
+
21
+ ### now()
22
+
23
+ ```typescript
24
+ export const now: () => number
25
+ ```
26
+
27
+ Cross-platform `performance.now()` wrapper. Returns high-resolution timestamp in milliseconds. Works in browsers (via `globalThis.performance.now`) and Node.js (via `node:perf_hooks`).
28
+
29
+ ### UUID
30
+
31
+ UUID v4 generation and validation with "masked UUID" support for bot detection.
32
+
33
+ **Standard UUID generation:**
34
+ - `UUID.generate(): UUID` — Generate standard RFC 4122 v4 UUID (122 bits entropy)
35
+
36
+ **Masked UUID (bot detection):**
37
+ - `UUID.generateMasked(): UUID` — Generate UUID where last 4 bytes are FNV-1a hash of first 12 bytes (90 bits entropy). Used to mark bot players.
38
+ - `UUID.isMaskedUint8(bytes: Uint8Array): boolean` — Check if 16-byte array is a masked UUID (validates hash)
39
+ - `UUID.isMaskedString(uuidStr: string): boolean` — Check if UUID string is masked. Returns false for invalid strings.
40
+
41
+ **Conversion:**
42
+ - `UUID.fromString(uuidStr: string): UUID` — Parse canonical UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000")
43
+ - `UUID.fromUint8(uuidUint8: Uint8Array): UUID` — Create UUID from 16-byte array
44
+ - `uuid.asString(): string` — Convert to canonical string format (cached after first call)
45
+ - `uuid.asUint8(): Uint8Array` — Convert to 16-byte array (creates new copy)
46
+
47
+ ### RingBuffer<T>
48
+
49
+ Fixed-size circular buffer with FIFO semantics. Overwrites oldest elements when full.
50
+
51
+ ```typescript
52
+ class RingBuffer<T> {
53
+ constructor(size: number);
54
+ add(item: T): number; // Add item, returns index, wraps around when full
55
+ get(atIdx: number): T | undefined; // Get item at index
56
+ clear(): void; // Reset buffer
57
+ [Symbol.iterator](): Iterator<T>; // Iterate over all added items
58
+ }
59
+ ```
60
+
61
+ ### SnapshotHistory<T>
62
+
63
+ Stores snapshots indexed by tick for rollback netcode. Maintains snapshots in tick order with efficient binary search for nearest-past-snapshot retrieval.
64
+
65
+ ```typescript
66
+ class SnapshotHistory<T> {
67
+ constructor(maxSize: number);
68
+ set(tick: number, snapshot: T): void; // Store snapshot at tick (must be non-decreasing)
69
+ getNearest(tick: number): T; // Get snapshot with greatest tick < requested tick (binary search)
70
+ rollback(tick: number): void; // Remove all snapshots with tick >= given tick
71
+ }
72
+ ```
73
+
74
+ **Key behavior:**
75
+ - Ticks must be non-decreasing (monotonic). Call `rollback()` before writing older ticks.
76
+ - Overwrites snapshot if same tick is set twice (useful for repeated rollback-replay)
77
+ - `getNearest()` throws if no snapshot exists with tick < requested tick
78
+ - Uses ring buffer internally for O(1) insertion when full
79
+
80
+ ### SimulationClock
81
+
82
+ Manages game time accumulation with `PhaseNudger` integration for server-side clock sync.
83
+
84
+ ```typescript
85
+ class SimulationClock {
86
+ constructor(frameLength: number, maxNudgePerFrame: number);
87
+
88
+ readonly phaseNudger: PhaseNudger; // Time debt manager for server sync
89
+
90
+ get startedTime(): number; // Timestamp when start() was called (from now())
91
+ get accumulatedTime(): number; // Total accumulated time in milliseconds
92
+
93
+ start(): void; // Start the clock (must be called before getElapsedTime())
94
+ getElapsedTime(): number; // Real time since start() in milliseconds
95
+ update(dt: number): void; // Accumulate time delta + phase nudge adjustments
96
+ }
97
+ ```
98
+
99
+ **Typical usage:**
100
+ ```typescript
101
+ const clock = new SimulationClock(16.666, 2); // 60 FPS, max 2ms nudge per frame
102
+ clock.start();
103
+
104
+ // In game loop:
105
+ const dt = getDeltaTime();
106
+ clock.update(dt); // Adds dt + phaseNudger.drain()
107
+ ```
108
+
109
+ ### PhaseNudger
110
+
111
+ Gradually adjusts simulation time to synchronize with server tick hints. Prevents abrupt jumps by draining "time debt" incrementally.
112
+
113
+ ```typescript
114
+ class PhaseNudger {
115
+ constructor(frameLength: number, maxNudgePerFrame: number);
116
+
117
+ get isActive(): boolean; // True after activate() is called
118
+ get currentDebtMs(): number; // Current phase debt in milliseconds
119
+
120
+ activate(): void; // Enable phase nudging (call when ClockSync is ready)
121
+ onServerTickHint(serverTick: number, localTick: number): void; // Accumulate time debt based on tick difference
122
+ drain(): number; // Drain small portion of debt per frame, returns ms to add to time
123
+ reset(): void; // Reset debt to zero (use for hard sync)
124
+ }
125
+ ```
126
+
127
+ **How it works:**
128
+ - Server sends tick hints → `onServerTickHint()` calculates tick difference → accumulates debt with 0.3 weight
129
+ - Every frame, `drain()` returns small correction (limited by `maxNudgePerFrame`)
130
+ - Large debt (≥50ms) drains faster (50% per frame, capped at `frameLength`)
131
+ - Small debt drains gradually for smoothness
132
+
133
+ ### Transform2d Interpolation Helpers
134
+
135
+ Functions for interpolating Transform2d component values between ticks for smooth rendering.
136
+
137
+ ```typescript
138
+ interface Transform2dCursorLike {
139
+ positionX: number; positionY: number; rotation: number;
140
+ prevPositionX: number; prevPositionY: number; prevRotation: number;
141
+ }
142
+
143
+ // Interpolate transform between prev and current state
144
+ export function interpolateTransform2d(
145
+ prevPositionX: number, prevPositionY: number,
146
+ positionX: number, positionY: number,
147
+ prevRotation: number, rotation: number,
148
+ interpolationFactor: number,
149
+ ): { readonly x: number; readonly y: number; readonly rotation: number };
150
+
151
+ // Zero-allocation variant (writes to ref)
152
+ export function interpolateTransform2dToRef(
153
+ prevPositionX: number, prevPositionY: number,
154
+ positionX: number, positionY: number,
155
+ prevRotation: number, rotation: number,
156
+ interpolationFactor: number,
157
+ ref: { x: number; y: number; rotation: number },
158
+ teleportThresholdSquared?: number, // Default: 300
159
+ ): void;
160
+
161
+ // Cursor-based convenience wrappers
162
+ export function interpolateTransform2dCursor(
163
+ cursor: Transform2dCursorLike,
164
+ interpolationFactor: number,
165
+ ): { readonly x: number; readonly y: number; readonly rotation: number };
166
+
167
+ export function interpolateTransform2dCursorToRef(
168
+ cursor: Transform2dCursorLike,
169
+ interpolationFactor: number,
170
+ ref: { x: number; y: number; rotation: number },
171
+ ): void;
172
+ ```
173
+
174
+ **Key behavior:**
175
+ - Uses `MathOps.lerpAngle()` for rotation (shortest path)
176
+ - Detects teleportation: if distance² ≥ threshold², skip interpolation (snap to target)
177
+ - Y coordinate is negated (game coordinate system convention)
178
+ - Non-`ToRef` variants return a shared buffer (not thread-safe, reused on next call)
179
+
180
+ ### VisualSmoother2d
181
+
182
+ Handles both sim↔render interpolation and rollback lag smoothing. Takes raw ECS prev/current values + interpolationFactor, outputs smoothed render position. One instance per rendered entity.
183
+
184
+ ```typescript
185
+ class VisualSmoother2d {
186
+ x: number; // Smoothed X (read after update)
187
+ y: number; // Smoothed Y (read after update)
188
+ rotation: number; // Smoothed rotation (read after update)
189
+
190
+ readonly isSmoothing: boolean; // True while offset is non-zero
191
+
192
+ constructor(options?: VisualSmoother2dOptions);
193
+
194
+ update(
195
+ prevPositionX: number, prevPositionY: number,
196
+ positionX: number, positionY: number,
197
+ prevRotation: number, rotation: number,
198
+ interpolationFactor: number,
199
+ ): void;
200
+
201
+ reset(): void;
202
+ }
203
+
204
+ interface VisualSmoother2dOptions {
205
+ positionJumpThreshold?: number; // px, detects rollback jumps (default: 10)
206
+ rotationJumpThreshold?: number; // radians (default: PI/4)
207
+ smoothingHalfLifeMs?: number; // offset decay half-life (default: 200)
208
+ teleportThreshold?: number; // px, jumps above this snap instantly (default: Infinity)
209
+ }
210
+ ```
211
+
212
+ **How it works:**
213
+ 1. Computes sim-interpolated position from prev/current + factor
214
+ 2. Compares with previous frame's sim position — if jump > threshold, it's a rollback
215
+ 3. Absorbs the jump into an offset (entity stays visually in place)
216
+ 4. Decays offset exponentially each frame (`pow(0.5, dt / halfLifeMs)`)
217
+ 5. Output = sim position + decaying offset
218
+
219
+ **Normal frames:** offset = 0, output = pure interpolation, zero added latency.
220
+ **After rollback:** entity smoothly slides to correct position over ~200-400ms instead of teleporting.
221
+
222
+ **Key detail:** `_lastSimX/Y` stores the raw sim position (not smoothed) to avoid a feedback loop where the offset causes repeated false jump detections.
223
+
224
+ ## 4. Preconditions
225
+
226
+ - **`SimulationClock.start()` must be called before `getElapsedTime()`** — Throws error if called before start
227
+ - **`SnapshotHistory.set()` requires non-decreasing ticks** — Call `rollback()` before writing older ticks
228
+ - **`PhaseNudger.activate()` should be called when ClockSync is ready** — Nudging is disabled until activated
229
+ - **`UUID.fromString()` requires valid canonical UUID format** — Throws `TypeError` for invalid strings
230
+
231
+ ## 5. Postconditions
232
+
233
+ - `SimulationClock.update(dt)` advances `accumulatedTime` by `dt + phaseNudger.drain()`
234
+ - `SnapshotHistory.getNearest(tick)` returns the snapshot with the greatest tick < requested tick
235
+ - `UUID.generateMasked()` produces UUIDs that pass `isMaskedUint8()` validation
236
+ - `interpolateTransform2d*()` functions produce smooth interpolation for rendering between ticks
237
+
238
+ ## 6. Invariants & Constraints
239
+
240
+ - **SnapshotHistory monotonicity:** Ticks must be non-decreasing. Violating this throws an error.
241
+ - **RingBuffer wraps around:** When full, oldest elements are overwritten (FIFO)
242
+ - **PhaseNudger debt accumulation:** Uses weighted average (0.3) to prevent oscillation from noisy server hints
243
+ - **UUID masked format:** Last 4 bytes = FNV-1a hash of first 12 bytes (after RFC 4122 version/variant bits set)
244
+ - **Transform2d interpolation coordinate system:** Y is negated, rotation is negated (matches Pixi.js convention)
245
+
246
+ ## 7. Safety Notes (AI Agent)
247
+
248
+ ### DO NOT
249
+
250
+ - **DO NOT call `SimulationClock.getElapsedTime()` before `start()`** — This throws an error
251
+ - **DO NOT write older ticks to `SnapshotHistory` without calling `rollback()` first** — This throws an error
252
+ - **DO NOT rely on `interpolateTransform2d()` return value persistence** — It returns a shared buffer that is reused on the next call. Use `...ToRef()` variant for persistent results.
253
+ - **DO NOT use `PhaseNudger` directly inside ECS systems** — It's managed by `SimulationClock`, which calls `drain()` automatically
254
+ - **DO NOT assume masked UUIDs are cryptographically secure** — They have 90 bits entropy (vs 122 in standard v4) and are detectable by hash validation
255
+ - **DO NOT mutate `RingBuffer` during iteration** — Iterator behavior is undefined if buffer is modified during iteration
256
+
257
+ ### Common Mistakes
258
+
259
+ - Forgetting to call `clock.start()` before using `getElapsedTime()` → throws error
260
+ - Writing ticks out of order to `SnapshotHistory` without rollback → throws error
261
+ - Storing `interpolateTransform2d()` result → next call overwrites it (use `...ToRef()` instead)
262
+ - Using `UUID.isMaskedString()` to validate UUID format → it returns false for invalid UUIDs (not an error)
263
+
264
+ ## 8. Usage Examples
265
+
266
+ ### SimulationClock with PhaseNudger
267
+
268
+ ```typescript
269
+ import { SimulationClock } from '@lagless/misc';
270
+
271
+ const FRAME_LENGTH = 16.666; // 60 FPS
272
+ const MAX_NUDGE_PER_FRAME = 2; // Max 2ms correction per frame
273
+
274
+ const clock = new SimulationClock(FRAME_LENGTH, MAX_NUDGE_PER_FRAME);
275
+ clock.start();
276
+
277
+ // When ClockSync is ready
278
+ clock.phaseNudger.activate();
279
+
280
+ // On server tick hint (from network)
281
+ clock.phaseNudger.onServerTickHint(serverTick, localTick);
282
+
283
+ // In game loop
284
+ function gameLoop(dt: number) {
285
+ clock.update(dt); // Advances accumulatedTime with phase correction
286
+
287
+ const ticks = Math.floor(clock.accumulatedTime / FRAME_LENGTH);
288
+ for (let i = 0; i < ticks; i++) {
289
+ runSimulationTick();
290
+ }
291
+ }
292
+ ```
293
+
294
+ ### SnapshotHistory for Rollback
295
+
296
+ ```typescript
297
+ import { SnapshotHistory } from '@lagless/misc';
298
+
299
+ const history = new SnapshotHistory<ArrayBuffer>(100); // Store up to 100 snapshots
300
+
301
+ // Store snapshots after each tick
302
+ function saveTick(tick: number, worldState: ArrayBuffer) {
303
+ history.set(tick, worldState.slice(0)); // Clone ArrayBuffer
304
+ }
305
+
306
+ // Rollback to tick
307
+ function rollbackTo(tick: number) {
308
+ const snapshot = history.getNearest(tick); // Get snapshot with tick < target
309
+ restoreWorldState(snapshot);
310
+ history.rollback(tick); // Remove snapshots >= tick
311
+ }
312
+
313
+ // Now you can write new snapshots starting from tick
314
+ saveTick(tick, newWorldState);
315
+ ```
316
+
317
+ ### UUID with Masked Bot Detection
318
+
319
+ ```typescript
320
+ import { UUID } from '@lagless/misc';
321
+
322
+ // Human player
323
+ const playerUuid = UUID.generate();
324
+ console.log(playerUuid.asString()); // "550e8400-e29b-41d4-a716-446655440000"
325
+ console.log(UUID.isMaskedString(playerUuid.asString())); // false
326
+
327
+ // Bot player
328
+ const botUuid = UUID.generateMasked();
329
+ console.log(botUuid.asString()); // "7c9e6679-7425-40de-944b-e07fc1f90ae7"
330
+ console.log(UUID.isMaskedString(botUuid.asString())); // true
331
+
332
+ // Check at runtime
333
+ function handlePlayer(uuid: UUID) {
334
+ if (UUID.isMaskedUint8(uuid.asUint8())) {
335
+ console.log('Bot detected');
336
+ }
337
+ }
338
+ ```
339
+
340
+ ### RingBuffer
341
+
342
+ ```typescript
343
+ import { RingBuffer } from '@lagless/misc';
344
+
345
+ const buffer = new RingBuffer<number>(3);
346
+
347
+ buffer.add(1); // idx 0
348
+ buffer.add(2); // idx 1
349
+ buffer.add(3); // idx 2
350
+ buffer.add(4); // idx 0 (overwrites 1)
351
+
352
+ console.log(buffer.get(0)); // 4
353
+ console.log(buffer.get(1)); // 2
354
+ console.log(buffer.get(2)); // 3
355
+
356
+ // Iterate (visits all added items, including overwritten slots)
357
+ for (const item of buffer) {
358
+ console.log(item); // 4, 2, 3
359
+ }
360
+ ```
361
+
362
+ ### Transform2d Interpolation (Rendering)
363
+
364
+ ```typescript
365
+ import { interpolateTransform2dCursorToRef } from '@lagless/misc';
366
+
367
+ // In rendering loop (between simulation ticks)
368
+ function render(interpolationFactor: number) {
369
+ const result = { x: 0, y: 0, rotation: 0 };
370
+
371
+ for (const entityId of entities) {
372
+ const transform = getTransform2dComponent(entityId);
373
+
374
+ // Interpolate between prev and current transform
375
+ interpolateTransform2dCursorToRef(transform, interpolationFactor, result);
376
+
377
+ // Use result for rendering
378
+ sprite.x = result.x;
379
+ sprite.y = result.y;
380
+ sprite.rotation = result.rotation;
381
+ }
382
+ }
383
+ ```
384
+
385
+ ### VisualSmoother2d (Multiplayer Rendering)
386
+
387
+ ```typescript
388
+ import { VisualSmoother2d } from '@lagless/misc';
389
+
390
+ // One smoother per rendered entity (e.g. in React ref)
391
+ const smoother = new VisualSmoother2d();
392
+
393
+ // Each render frame — feed raw ECS data, read smoothed output
394
+ function onUpdate(entity: number) {
395
+ smoother.update(
396
+ transform2d.unsafe.prevPositionX[entity],
397
+ transform2d.unsafe.prevPositionY[entity],
398
+ transform2d.unsafe.positionX[entity],
399
+ transform2d.unsafe.positionY[entity],
400
+ transform2d.unsafe.prevRotation[entity],
401
+ transform2d.unsafe.rotation[entity],
402
+ simulation.interpolationFactor,
403
+ );
404
+ container.x = smoother.x;
405
+ container.y = smoother.y;
406
+ container.rotation = smoother.rotation;
407
+ }
408
+ ```
409
+
410
+ ## 9. Testing Guidance
411
+
412
+ No tests currently exist for this library. When adding tests, consider:
413
+
414
+ **Framework suggestion:** Vitest (used by other Lagless libraries)
415
+
416
+ **Test coverage priorities:**
417
+ 1. `SnapshotHistory` — Binary search correctness, rollback behavior, edge cases (empty, single element)
418
+ 2. `PhaseNudger` — Debt accumulation, drain rate, large correction rejection
419
+ 3. `UUID` — Masked UUID validation (hash correctness), string parsing edge cases
420
+ 4. `RingBuffer` — Wrap-around behavior, iteration during overwrites
421
+ 5. `SimulationClock` — Time accumulation with phase nudge integration
422
+
423
+ **Example test pattern:**
424
+ ```typescript
425
+ import { describe, it, expect } from 'vitest';
426
+ import { SnapshotHistory } from '@lagless/misc';
427
+
428
+ describe('SnapshotHistory', () => {
429
+ it('should retrieve nearest snapshot before target tick', () => {
430
+ const history = new SnapshotHistory<string>(10);
431
+ history.set(10, 'tick10');
432
+ history.set(20, 'tick20');
433
+ history.set(30, 'tick30');
434
+
435
+ expect(history.getNearest(25)).toBe('tick20');
436
+ expect(history.getNearest(31)).toBe('tick30');
437
+ });
438
+
439
+ it('should throw if no snapshot exists before target tick', () => {
440
+ const history = new SnapshotHistory<string>(10);
441
+ history.set(10, 'tick10');
442
+
443
+ expect(() => history.getNearest(5)).toThrow();
444
+ });
445
+ });
446
+ ```
447
+
448
+ ## 10. Change Checklist
449
+
450
+ When modifying this module:
451
+
452
+ 1. **Verify rollback correctness:** Changes to `SnapshotHistory` must preserve binary search invariants
453
+ 2. **Profile allocation:** `interpolateTransform2dToRef()` must remain zero-allocation
454
+ 3. **Test PhaseNudger stability:** Ensure debt accumulation doesn't oscillate with noisy server hints
455
+ 4. **Maintain UUID masked format:** FNV-1a hash of first 12 bytes must be embedded in last 4 bytes
456
+ 5. **Update this README:** Document new APIs in Public API section
457
+ 6. **Add tests:** Cover new functionality with unit tests
458
+ 7. **Check cross-platform behavior:** `now()` must work in Node.js and all browsers
459
+
460
+ ## 11. Integration Notes
461
+
462
+ ### Used By
463
+
464
+ - **`@lagless/core`:**
465
+ - `SimulationClock` — Drives tick loop timing in `ECSSimulation`
466
+ - `SnapshotHistory` — Stores world state snapshots for rollback
467
+ - `RingBuffer` — Buffers incoming RPC inputs
468
+
469
+ - **`@lagless/net-wire`:**
470
+ - `RingBuffer` — Buffers network packets
471
+
472
+ - **`circle-sumo-simulation`:**
473
+ - `UUID` — Identifies players, detects bots with `isMaskedUint8()`
474
+ - `interpolateTransform2d*` — Smooths rendering between ticks in game view
475
+
476
+ ### Common Integration Patterns
477
+
478
+ **Rollback Netcode Pattern:**
479
+ ```typescript
480
+ import { SnapshotHistory, SimulationClock } from '@lagless/misc';
481
+
482
+ class ECSSimulation {
483
+ private clock = new SimulationClock(16.666, 2);
484
+ private snapshots = new SnapshotHistory<ArrayBuffer>(100);
485
+
486
+ start() {
487
+ this.clock.start();
488
+ }
489
+
490
+ update(dt: number) {
491
+ this.clock.update(dt);
492
+
493
+ const targetTick = Math.floor(this.clock.accumulatedTime / 16.666);
494
+ while (this.currentTick < targetTick) {
495
+ this.runTick();
496
+ this.snapshots.set(this.currentTick, this.saveSnapshot());
497
+ this.currentTick++;
498
+ }
499
+ }
500
+
501
+ rollbackTo(tick: number) {
502
+ const snapshot = this.snapshots.getNearest(tick);
503
+ this.restoreSnapshot(snapshot);
504
+ this.snapshots.rollback(tick);
505
+ }
506
+ }
507
+ ```
508
+
509
+ **Server Clock Sync:**
510
+ ```typescript
511
+ import { SimulationClock } from '@lagless/misc';
512
+ import { ClockSync } from '@lagless/net-wire';
513
+
514
+ const clock = new SimulationClock(16.666, 2);
515
+ const clockSync = new ClockSync(...);
516
+
517
+ // When clock sync is ready
518
+ clockSync.on('ready', () => {
519
+ clock.phaseNudger.activate();
520
+ });
521
+
522
+ // On every tick input from server
523
+ connection.on('tickInput', (msg) => {
524
+ const serverTick = msg.tick;
525
+ const localTick = Math.floor(clock.accumulatedTime / 16.666);
526
+ clock.phaseNudger.onServerTickHint(serverTick, localTick);
527
+ });
528
+ ```
529
+
530
+ ## 12. Appendix
531
+
532
+ ### UUID Masked Format Details
533
+
534
+ A **masked UUID** embeds a checksum in the last 4 bytes, allowing bot detection without a database lookup.
535
+
536
+ **Structure:**
537
+ ```
538
+ Byte 0-5: Random data
539
+ Byte 6-7: Version/variant bits (RFC 4122 v4)
540
+ Byte 8-11: Random data
541
+ Byte 12-15: FNV-1a hash of bytes 0-11
542
+ ```
543
+
544
+ **FNV-1a Hash (32-bit):**
545
+ ```
546
+ hash = 0x811c9dc5 (offset basis)
547
+ for each byte:
548
+ hash ^= byte
549
+ hash *= 0x01000193 (FNV prime)
550
+ ```
551
+
552
+ **Entropy:** 90 bits (vs 122 in standard v4 UUID)
553
+
554
+ **False positive rate:** 1 in ~4.3 billion (2^32)
555
+
556
+ **Why this works:**
557
+ - Standard UUIDs are random → hash of first 12 bytes doesn't match last 4 bytes
558
+ - Masked UUIDs are generated with embedded hash → validation passes
559
+ - No server roundtrip needed to identify bots
560
+
561
+ ### SnapshotHistory Ring Buffer Implementation
562
+
563
+ Internally uses two arrays (`_ticks` and `_snapshots`) as a ring buffer:
564
+ - `_head` — Physical index of oldest element
565
+ - `_count` — Number of stored elements
566
+ - `indexAt(logicalIndex)` — Maps logical index [0, count) to physical index [0, maxSize)
567
+
568
+ **Binary search** for `getNearest()`:
569
+ - Searches logical indices [0, count)
570
+ - Finds greatest tick < target using standard binary search
571
+ - O(log n) complexity
572
+
573
+ **Rollback** uses binary search to find first tick ≥ target, then truncates count.
574
+
575
+ ### PhaseNudger Tuning Parameters
576
+
577
+ **`LARGE_DEBT_THRESHOLD_MS = 50`** — Debt above this drains faster (50% per frame vs gradual)
578
+
579
+ **`MAX_SINGLE_CORRECTION_MS = 5000`** — Reject server hints with corrections > 5s (likely bad data)
580
+
581
+ **`weight = 0.3`** — Weighted accumulation: `debt = debt * 0.7 + correction * 0.3`. Prevents oscillation from noisy hints.
582
+
583
+ **Recommended `maxNudgePerFrame`:**
584
+ - 1-2ms for 60 FPS (imperceptible to players)
585
+ - Higher values = faster convergence but more visible speed changes
586
+
587
+ ### Transform2d Interpolation Coordinate System
588
+
589
+ **Pixi.js convention (Circle Sumo uses this):**
590
+ - Y-axis points down → Y is negated during interpolation
591
+ - Rotation is counter-clockwise → Rotation is negated
592
+
593
+ **Teleport detection:**
594
+ - Calculates distance² between prev and current position
595
+ - If distance² ≥ threshold² (default 300² = 90,000), skip interpolation (snap to target)
596
+ - Prevents interpolation artifacts when player teleports or respawns
@@ -0,0 +1,9 @@
1
+ export * from './lib/now.js';
2
+ export * from './lib/uuid.js';
3
+ export * from './lib/ring-buffer.js';
4
+ export * from './lib/snapshot-history.js';
5
+ export * from './lib/simulation-clock.js';
6
+ export * from './lib/transform2d-utils.js';
7
+ export * from './lib/visual-smoother-2d.js';
8
+ export * from './lib/logger.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './lib/now.js';
2
+ export * from './lib/uuid.js';
3
+ export * from './lib/ring-buffer.js';
4
+ export * from './lib/snapshot-history.js';
5
+ export * from './lib/simulation-clock.js';
6
+ export * from './lib/transform2d-utils.js';
7
+ export * from './lib/visual-smoother-2d.js';
8
+ export * from './lib/logger.js';
@@ -0,0 +1,24 @@
1
+ export declare enum LogLevel {
2
+ Debug = 0,
3
+ Info = 1,
4
+ Warn = 2,
5
+ Error = 3,
6
+ Silent = 4
7
+ }
8
+ export interface LogSink {
9
+ debug(tag: string, message: string, ...args: unknown[]): void;
10
+ info(tag: string, message: string, ...args: unknown[]): void;
11
+ warn(tag: string, message: string, ...args: unknown[]): void;
12
+ error(tag: string, message: string, ...args: unknown[]): void;
13
+ }
14
+ export declare function setLogLevel(level: LogLevel): void;
15
+ export declare function getLogLevel(): LogLevel;
16
+ export declare function setLogSink(sink: LogSink): void;
17
+ export type Logger = ReturnType<typeof createLogger>;
18
+ export declare function createLogger(tag: string): {
19
+ debug(message: string, ...args: unknown[]): void;
20
+ info(message: string, ...args: unknown[]): void;
21
+ warn(message: string, ...args: unknown[]): void;
22
+ error(message: string, ...args: unknown[]): void;
23
+ };
24
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA,oBAAY,QAAQ;IAClB,KAAK,IAAI;IACT,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;IACT,MAAM,IAAI;CACX;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC9D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC7D,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC/D;AAYD,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAEjD;AAED,wBAAgB,WAAW,IAAI,QAAQ,CAEtC;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAE9C;AAED,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AAErD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM;mBAErB,MAAM,WAAW,OAAO,EAAE,GAAG,IAAI;kBAGlC,MAAM,WAAW,OAAO,EAAE,GAAG,IAAI;kBAGjC,MAAM,WAAW,OAAO,EAAE,GAAG,IAAI;mBAGhC,MAAM,WAAW,OAAO,EAAE,GAAG,IAAI;EAInD"}
@@ -0,0 +1,45 @@
1
+ export var LogLevel;
2
+ (function (LogLevel) {
3
+ LogLevel[LogLevel["Debug"] = 0] = "Debug";
4
+ LogLevel[LogLevel["Info"] = 1] = "Info";
5
+ LogLevel[LogLevel["Warn"] = 2] = "Warn";
6
+ LogLevel[LogLevel["Error"] = 3] = "Error";
7
+ LogLevel[LogLevel["Silent"] = 4] = "Silent";
8
+ })(LogLevel || (LogLevel = {}));
9
+ const consoleSink = {
10
+ debug: (tag, msg, ...args) => console.debug(`[${tag}]`, msg, ...args),
11
+ info: (tag, msg, ...args) => console.log(`[${tag}]`, msg, ...args),
12
+ warn: (tag, msg, ...args) => console.warn(`[${tag}]`, msg, ...args),
13
+ error: (tag, msg, ...args) => console.error(`[${tag}]`, msg, ...args),
14
+ };
15
+ let globalLevel = LogLevel.Debug;
16
+ let globalSink = consoleSink;
17
+ export function setLogLevel(level) {
18
+ globalLevel = level;
19
+ }
20
+ export function getLogLevel() {
21
+ return globalLevel;
22
+ }
23
+ export function setLogSink(sink) {
24
+ globalSink = sink;
25
+ }
26
+ export function createLogger(tag) {
27
+ return {
28
+ debug(message, ...args) {
29
+ if (globalLevel <= LogLevel.Debug)
30
+ globalSink.debug(tag, message, ...args);
31
+ },
32
+ info(message, ...args) {
33
+ if (globalLevel <= LogLevel.Info)
34
+ globalSink.info(tag, message, ...args);
35
+ },
36
+ warn(message, ...args) {
37
+ if (globalLevel <= LogLevel.Warn)
38
+ globalSink.warn(tag, message, ...args);
39
+ },
40
+ error(message, ...args) {
41
+ if (globalLevel <= LogLevel.Error)
42
+ globalSink.error(tag, message, ...args);
43
+ },
44
+ };
45
+ }