@lagless/desync-diagnostics 0.0.48
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/LICENSE +26 -0
- package/out-tsc/index.d.ts +13 -0
- package/out-tsc/index.d.ts.map +1 -0
- package/out-tsc/lib/attach.d.ts +5 -0
- package/out-tsc/lib/attach.d.ts.map +1 -0
- package/out-tsc/lib/diagnostics-collector.d.ts +41 -0
- package/out-tsc/lib/diagnostics-collector.d.ts.map +1 -0
- package/out-tsc/lib/diagnostics-protocol.d.ts +52 -0
- package/out-tsc/lib/diagnostics-protocol.d.ts.map +1 -0
- package/out-tsc/lib/divergence-analysis.d.ts +21 -0
- package/out-tsc/lib/divergence-analysis.d.ts.map +1 -0
- package/out-tsc/lib/hash-bytes.d.ts +6 -0
- package/out-tsc/lib/hash-bytes.d.ts.map +1 -0
- package/out-tsc/lib/performance-profiler.d.ts +43 -0
- package/out-tsc/lib/performance-profiler.d.ts.map +1 -0
- package/out-tsc/lib/report-generator.d.ts +55 -0
- package/out-tsc/lib/report-generator.d.ts.map +1 -0
- package/out-tsc/lib/types.d.ts +39 -0
- package/out-tsc/lib/types.d.ts.map +1 -0
- package/out-tsc/lib/use-desync-diagnostics.d.ts +9 -0
- package/out-tsc/lib/use-desync-diagnostics.d.ts.map +1 -0
- package/package.json +38 -0
- package/src/index.ts +26 -0
- package/src/lib/attach.ts +10 -0
- package/src/lib/diagnostics-collector.spec.ts +354 -0
- package/src/lib/diagnostics-collector.ts +210 -0
- package/src/lib/diagnostics-protocol.ts +44 -0
- package/src/lib/divergence-analysis.spec.ts +178 -0
- package/src/lib/divergence-analysis.ts +179 -0
- package/src/lib/hash-bytes.spec.ts +29 -0
- package/src/lib/hash-bytes.ts +11 -0
- package/src/lib/performance-profiler.spec.ts +412 -0
- package/src/lib/performance-profiler.ts +245 -0
- package/src/lib/report-generator.spec.ts +147 -0
- package/src/lib/report-generator.ts +117 -0
- package/src/lib/types.ts +41 -0
- package/src/lib/use-desync-diagnostics.ts +101 -0
- package/tsconfig.json +21 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { analyzeDivergence } from './divergence-analysis.js';
|
|
2
|
+
import type { DiagnosticsReport } from './report-generator.js';
|
|
3
|
+
|
|
4
|
+
function makeReport(
|
|
5
|
+
playerSlot: number,
|
|
6
|
+
timeline: Array<{ tick: number; hash: number; physicsHash: number; wasRollback?: boolean }>,
|
|
7
|
+
rollbacks?: Array<{ atSimTick: number; rollbackToTick: number; timestamp: number }>,
|
|
8
|
+
): DiagnosticsReport {
|
|
9
|
+
return {
|
|
10
|
+
version: 2,
|
|
11
|
+
generatedAt: new Date().toISOString(),
|
|
12
|
+
playerSlot,
|
|
13
|
+
config: { fps: 60, maxPlayers: 4, frameLength: 16.67, snapshotRate: 1, maxEntities: 1000 },
|
|
14
|
+
summary: {
|
|
15
|
+
totalTicks: timeline.length,
|
|
16
|
+
totalRollbacks: rollbacks?.length ?? 0,
|
|
17
|
+
firstDivergenceTick: null,
|
|
18
|
+
verifiedTickGapCount: 0,
|
|
19
|
+
latestPhysicsHash: timeline.length > 0 ? timeline[timeline.length - 1].physicsHash : 0,
|
|
20
|
+
oldestTick: timeline.length > 0 ? timeline[0].tick : 0,
|
|
21
|
+
newestTick: timeline.length > 0 ? timeline[timeline.length - 1].tick : 0,
|
|
22
|
+
},
|
|
23
|
+
timeline: timeline.map((t) => ({
|
|
24
|
+
tick: t.tick,
|
|
25
|
+
hash: t.hash,
|
|
26
|
+
physicsHash: t.physicsHash,
|
|
27
|
+
velocityHash: 0,
|
|
28
|
+
verifiedTick: t.tick - 1,
|
|
29
|
+
wasRollback: t.wasRollback ?? false,
|
|
30
|
+
inputCountBySlot: [0, 0, 0, 0],
|
|
31
|
+
})),
|
|
32
|
+
rollbacks: rollbacks ?? [],
|
|
33
|
+
inputHistory: {},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('analyzeDivergence', () => {
|
|
38
|
+
it('should return no divergence when all clients match', () => {
|
|
39
|
+
const timeline = [
|
|
40
|
+
{ tick: 1, hash: 100, physicsHash: 1000 },
|
|
41
|
+
{ tick: 2, hash: 200, physicsHash: 2000 },
|
|
42
|
+
{ tick: 3, hash: 300, physicsHash: 3000 },
|
|
43
|
+
];
|
|
44
|
+
const result = analyzeDivergence([
|
|
45
|
+
makeReport(0, timeline),
|
|
46
|
+
makeReport(1, timeline),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
expect(result.firstEcsDivergenceTick).toBeNull();
|
|
50
|
+
expect(result.firstPhysicsDivergenceTick).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should find first ECS divergence tick', () => {
|
|
54
|
+
const result = analyzeDivergence([
|
|
55
|
+
makeReport(0, [
|
|
56
|
+
{ tick: 1, hash: 100, physicsHash: 1000 },
|
|
57
|
+
{ tick: 2, hash: 200, physicsHash: 2000 },
|
|
58
|
+
{ tick: 3, hash: 999, physicsHash: 3000 },
|
|
59
|
+
]),
|
|
60
|
+
makeReport(1, [
|
|
61
|
+
{ tick: 1, hash: 100, physicsHash: 1000 },
|
|
62
|
+
{ tick: 2, hash: 200, physicsHash: 2000 },
|
|
63
|
+
{ tick: 3, hash: 300, physicsHash: 3000 },
|
|
64
|
+
]),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
expect(result.firstEcsDivergenceTick).toBe(3);
|
|
68
|
+
expect(result.firstPhysicsDivergenceTick).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should find first physics divergence tick', () => {
|
|
72
|
+
const result = analyzeDivergence([
|
|
73
|
+
makeReport(0, [
|
|
74
|
+
{ tick: 1, hash: 100, physicsHash: 1000 },
|
|
75
|
+
{ tick: 2, hash: 200, physicsHash: 9999 },
|
|
76
|
+
]),
|
|
77
|
+
makeReport(1, [
|
|
78
|
+
{ tick: 1, hash: 100, physicsHash: 1000 },
|
|
79
|
+
{ tick: 2, hash: 200, physicsHash: 2000 },
|
|
80
|
+
]),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
expect(result.firstEcsDivergenceTick).toBeNull();
|
|
84
|
+
expect(result.firstPhysicsDivergenceTick).toBe(2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should use LAST occurrence of a tick in timeline (rollback resimulation)', () => {
|
|
88
|
+
// Client 0 has tick 2 twice — original (hash=200) and resimulated (hash=250)
|
|
89
|
+
// The final hash is 250, which matches client 1's hash of 250
|
|
90
|
+
const result = analyzeDivergence([
|
|
91
|
+
makeReport(0, [
|
|
92
|
+
{ tick: 1, hash: 100, physicsHash: 1000 },
|
|
93
|
+
{ tick: 2, hash: 200, physicsHash: 2000 }, // original — stale
|
|
94
|
+
{ tick: 3, hash: 300, physicsHash: 3000 },
|
|
95
|
+
{ tick: 2, hash: 250, physicsHash: 2500, wasRollback: true }, // resimulated — final
|
|
96
|
+
{ tick: 3, hash: 350, physicsHash: 3500, wasRollback: true },
|
|
97
|
+
]),
|
|
98
|
+
makeReport(1, [
|
|
99
|
+
{ tick: 1, hash: 100, physicsHash: 1000 },
|
|
100
|
+
{ tick: 2, hash: 250, physicsHash: 2500 },
|
|
101
|
+
{ tick: 3, hash: 350, physicsHash: 3500 },
|
|
102
|
+
]),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
expect(result.firstEcsDivergenceTick).toBeNull();
|
|
106
|
+
expect(result.firstPhysicsDivergenceTick).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should build checkpoint comparison at specified interval', () => {
|
|
110
|
+
const timeline = Array.from({ length: 120 }, (_, i) => ({
|
|
111
|
+
tick: i + 1,
|
|
112
|
+
hash: (i + 1) * 10,
|
|
113
|
+
physicsHash: (i + 1) * 100,
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
const result = analyzeDivergence([
|
|
117
|
+
makeReport(0, timeline),
|
|
118
|
+
makeReport(1, timeline),
|
|
119
|
+
], 60);
|
|
120
|
+
|
|
121
|
+
// Checkpoint at tick 60 and 120
|
|
122
|
+
expect(result.checkpointComparison.length).toBe(2);
|
|
123
|
+
expect(result.checkpointComparison[0].tick).toBe(60);
|
|
124
|
+
expect(result.checkpointComparison[0].ecsMatch).toBe(true);
|
|
125
|
+
expect(result.checkpointComparison[0].physicsMatch).toBe(true);
|
|
126
|
+
expect(result.checkpointComparison[1].tick).toBe(120);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should detect rollback overlap windows', () => {
|
|
130
|
+
const timeline = Array.from({ length: 20 }, (_, i) => ({
|
|
131
|
+
tick: i + 1,
|
|
132
|
+
hash: (i + 1) * 10,
|
|
133
|
+
physicsHash: (i + 1) * 100,
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
const result = analyzeDivergence([
|
|
137
|
+
makeReport(0, timeline, [
|
|
138
|
+
{ atSimTick: 15, rollbackToTick: 10, timestamp: 1000 },
|
|
139
|
+
]),
|
|
140
|
+
makeReport(1, timeline, [
|
|
141
|
+
{ atSimTick: 16, rollbackToTick: 12, timestamp: 1001 },
|
|
142
|
+
]),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
// Overlap: client 0 resims 10-15, client 1 resims 12-16 → overlap is 12-15
|
|
146
|
+
expect(result.rollbackOverlapWindows.length).toBeGreaterThan(0);
|
|
147
|
+
const window = result.rollbackOverlapWindows[0];
|
|
148
|
+
expect(window.startTick).toBe(12);
|
|
149
|
+
expect(window.endTick).toBe(15);
|
|
150
|
+
expect(window.affectedSlots).toContain(0);
|
|
151
|
+
expect(window.affectedSlots).toContain(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should return no divergence for a single client', () => {
|
|
155
|
+
const result = analyzeDivergence([
|
|
156
|
+
makeReport(0, [
|
|
157
|
+
{ tick: 1, hash: 100, physicsHash: 1000 },
|
|
158
|
+
]),
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
expect(result.firstEcsDivergenceTick).toBeNull();
|
|
162
|
+
expect(result.firstPhysicsDivergenceTick).toBeNull();
|
|
163
|
+
expect(result.checkpointComparison).toHaveLength(0);
|
|
164
|
+
expect(result.rollbackOverlapWindows).toHaveLength(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle clients with empty timelines', () => {
|
|
168
|
+
const result = analyzeDivergence([
|
|
169
|
+
makeReport(0, []),
|
|
170
|
+
makeReport(1, []),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
expect(result.firstEcsDivergenceTick).toBeNull();
|
|
174
|
+
expect(result.firstPhysicsDivergenceTick).toBeNull();
|
|
175
|
+
expect(result.checkpointComparison).toHaveLength(0);
|
|
176
|
+
expect(result.rollbackOverlapWindows).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { DiagnosticsReport } from './report-generator.js';
|
|
2
|
+
|
|
3
|
+
export interface CheckpointComparison {
|
|
4
|
+
tick: number;
|
|
5
|
+
ecsHashes: Record<number, number>;
|
|
6
|
+
physicsHashes: Record<number, number>;
|
|
7
|
+
ecsMatch: boolean;
|
|
8
|
+
physicsMatch: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RollbackOverlapWindow {
|
|
12
|
+
startTick: number;
|
|
13
|
+
endTick: number;
|
|
14
|
+
affectedSlots: number[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DivergenceAnalysis {
|
|
18
|
+
firstEcsDivergenceTick: number | null;
|
|
19
|
+
firstPhysicsDivergenceTick: number | null;
|
|
20
|
+
checkpointComparison: CheckpointComparison[];
|
|
21
|
+
rollbackOverlapWindows: RollbackOverlapWindow[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FinalHashEntry {
|
|
25
|
+
hash: number;
|
|
26
|
+
physicsHash: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a map of tick → final hash for a client's timeline.
|
|
31
|
+
* For ticks that appear multiple times (rollback resimulation),
|
|
32
|
+
* the LAST occurrence is the correct "final" state.
|
|
33
|
+
*/
|
|
34
|
+
function buildFinalHashMap(report: DiagnosticsReport): Map<number, FinalHashEntry> {
|
|
35
|
+
const map = new Map<number, FinalHashEntry>();
|
|
36
|
+
for (const record of report.timeline) {
|
|
37
|
+
map.set(record.tick, { hash: record.hash, physicsHash: record.physicsHash });
|
|
38
|
+
}
|
|
39
|
+
return map;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_CHECKPOINT_INTERVAL = 60;
|
|
43
|
+
|
|
44
|
+
export function analyzeDivergence(
|
|
45
|
+
clients: DiagnosticsReport[],
|
|
46
|
+
checkpointInterval = DEFAULT_CHECKPOINT_INTERVAL,
|
|
47
|
+
): DivergenceAnalysis {
|
|
48
|
+
if (clients.length < 2) {
|
|
49
|
+
return {
|
|
50
|
+
firstEcsDivergenceTick: null,
|
|
51
|
+
firstPhysicsDivergenceTick: null,
|
|
52
|
+
checkpointComparison: [],
|
|
53
|
+
rollbackOverlapWindows: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const finalHashMaps = clients.map((c) => buildFinalHashMap(c));
|
|
58
|
+
|
|
59
|
+
// Collect all ticks present in ALL clients
|
|
60
|
+
const allTicks = new Set<number>();
|
|
61
|
+
for (const [tick] of finalHashMaps[0]) {
|
|
62
|
+
if (finalHashMaps.every((m) => m.has(tick))) {
|
|
63
|
+
allTicks.add(tick);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sortedTicks = [...allTicks].sort((a, b) => a - b);
|
|
68
|
+
|
|
69
|
+
// Find first divergence ticks
|
|
70
|
+
let firstEcsDivergenceTick: number | null = null;
|
|
71
|
+
let firstPhysicsDivergenceTick: number | null = null;
|
|
72
|
+
|
|
73
|
+
for (const tick of sortedTicks) {
|
|
74
|
+
if (firstEcsDivergenceTick === null) {
|
|
75
|
+
const ecsHashes = finalHashMaps.map((m) => m.get(tick)!.hash);
|
|
76
|
+
if (!ecsHashes.every((h) => h === ecsHashes[0])) {
|
|
77
|
+
firstEcsDivergenceTick = tick;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (firstPhysicsDivergenceTick === null) {
|
|
81
|
+
const physicsHashes = finalHashMaps.map((m) => m.get(tick)!.physicsHash);
|
|
82
|
+
if (!physicsHashes.every((h) => h === physicsHashes[0])) {
|
|
83
|
+
firstPhysicsDivergenceTick = tick;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (firstEcsDivergenceTick !== null && firstPhysicsDivergenceTick !== null) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Build checkpoint comparison at interval
|
|
92
|
+
const checkpointComparison: CheckpointComparison[] = [];
|
|
93
|
+
for (const tick of sortedTicks) {
|
|
94
|
+
if (tick % checkpointInterval !== 0) continue;
|
|
95
|
+
|
|
96
|
+
const ecsHashes: Record<number, number> = {};
|
|
97
|
+
const physicsHashes: Record<number, number> = {};
|
|
98
|
+
for (let i = 0; i < clients.length; i++) {
|
|
99
|
+
const entry = finalHashMaps[i].get(tick)!;
|
|
100
|
+
ecsHashes[clients[i].playerSlot] = entry.hash;
|
|
101
|
+
physicsHashes[clients[i].playerSlot] = entry.physicsHash;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ecsValues = Object.values(ecsHashes);
|
|
105
|
+
const physicsValues = Object.values(physicsHashes);
|
|
106
|
+
|
|
107
|
+
checkpointComparison.push({
|
|
108
|
+
tick,
|
|
109
|
+
ecsHashes,
|
|
110
|
+
physicsHashes,
|
|
111
|
+
ecsMatch: ecsValues.every((h) => h === ecsValues[0]),
|
|
112
|
+
physicsMatch: physicsValues.every((h) => h === physicsValues[0]),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Find rollback overlap windows
|
|
117
|
+
const rollbackOverlapWindows = findRollbackOverlaps(clients);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
firstEcsDivergenceTick,
|
|
121
|
+
firstPhysicsDivergenceTick,
|
|
122
|
+
checkpointComparison,
|
|
123
|
+
rollbackOverlapWindows,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface RollbackRange {
|
|
128
|
+
slot: number;
|
|
129
|
+
startTick: number;
|
|
130
|
+
endTick: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findRollbackOverlaps(clients: DiagnosticsReport[]): RollbackOverlapWindow[] {
|
|
134
|
+
// Collect all rollback ranges from all clients
|
|
135
|
+
const ranges: RollbackRange[] = [];
|
|
136
|
+
for (const client of clients) {
|
|
137
|
+
for (const rb of client.rollbacks) {
|
|
138
|
+
ranges.push({
|
|
139
|
+
slot: client.playerSlot,
|
|
140
|
+
startTick: rb.rollbackToTick,
|
|
141
|
+
endTick: rb.atSimTick,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Find overlapping ranges between different slots
|
|
147
|
+
const windows: RollbackOverlapWindow[] = [];
|
|
148
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
149
|
+
for (let j = i + 1; j < ranges.length; j++) {
|
|
150
|
+
if (ranges[i].slot === ranges[j].slot) continue;
|
|
151
|
+
|
|
152
|
+
const overlapStart = Math.max(ranges[i].startTick, ranges[j].startTick);
|
|
153
|
+
const overlapEnd = Math.min(ranges[i].endTick, ranges[j].endTick);
|
|
154
|
+
|
|
155
|
+
if (overlapStart < overlapEnd) {
|
|
156
|
+
// Check if this window is already covered
|
|
157
|
+
const existing = windows.find(
|
|
158
|
+
(w) => w.startTick === overlapStart && w.endTick === overlapEnd,
|
|
159
|
+
);
|
|
160
|
+
if (existing) {
|
|
161
|
+
if (!existing.affectedSlots.includes(ranges[i].slot)) {
|
|
162
|
+
existing.affectedSlots.push(ranges[i].slot);
|
|
163
|
+
}
|
|
164
|
+
if (!existing.affectedSlots.includes(ranges[j].slot)) {
|
|
165
|
+
existing.affectedSlots.push(ranges[j].slot);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
windows.push({
|
|
169
|
+
startTick: overlapStart,
|
|
170
|
+
endTick: overlapEnd,
|
|
171
|
+
affectedSlots: [ranges[i].slot, ranges[j].slot],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return windows.sort((a, b) => a.startTick - b.startTick);
|
|
179
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { hashBytes } from './hash-bytes.js';
|
|
2
|
+
|
|
3
|
+
describe('hashBytes', () => {
|
|
4
|
+
it('should return 0 for empty input', () => {
|
|
5
|
+
expect(hashBytes(new Uint8Array(0))).toBe(0);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('should produce consistent hash for same input', () => {
|
|
9
|
+
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
10
|
+
const hash1 = hashBytes(data);
|
|
11
|
+
const hash2 = hashBytes(data);
|
|
12
|
+
expect(hash1).toBe(hash2);
|
|
13
|
+
expect(hash1).not.toBe(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should produce different hashes for different input', () => {
|
|
17
|
+
const a = new Uint8Array([1, 2, 3]);
|
|
18
|
+
const b = new Uint8Array([3, 2, 1]);
|
|
19
|
+
expect(hashBytes(a)).not.toBe(hashBytes(b));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return a 32-bit unsigned integer', () => {
|
|
23
|
+
const data = new Uint8Array(1000);
|
|
24
|
+
for (let i = 0; i < data.length; i++) data[i] = i & 0xFF;
|
|
25
|
+
const hash = hashBytes(data);
|
|
26
|
+
expect(hash).toBeGreaterThanOrEqual(0);
|
|
27
|
+
expect(hash).toBeLessThanOrEqual(0xFFFFFFFF);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polynomial hash (same algorithm as Mem.getHash) but using direct Uint8Array indexing
|
|
3
|
+
* instead of DataView for better performance on large buffers (e.g. Rapier snapshots).
|
|
4
|
+
*/
|
|
5
|
+
export function hashBytes(data: Uint8Array): number {
|
|
6
|
+
let hash = 0;
|
|
7
|
+
for (let i = 0; i < data.length; i++) {
|
|
8
|
+
hash = (hash * 31 + data[i]) >>> 0;
|
|
9
|
+
}
|
|
10
|
+
return hash;
|
|
11
|
+
}
|