@series-inc/rundot-syncplay 5.23.0-beta.7
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 +610 -0
- package/dist/animation.d.ts +119 -0
- package/dist/animation.js +488 -0
- package/dist/asset-cooking.d.ts +82 -0
- package/dist/asset-cooking.js +364 -0
- package/dist/authority-room.d.ts +217 -0
- package/dist/authority-room.js +701 -0
- package/dist/bot-documents.d.ts +115 -0
- package/dist/bot-documents.js +549 -0
- package/dist/bot-primitives.d.ts +152 -0
- package/dist/bot-primitives.js +472 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +53 -0
- package/dist/canonical.d.ts +12 -0
- package/dist/canonical.js +288 -0
- package/dist/certification.d.ts +455 -0
- package/dist/certification.js +1855 -0
- package/dist/chaos-certification.d.ts +56 -0
- package/dist/chaos-certification.js +1397 -0
- package/dist/cjs/animation.js +495 -0
- package/dist/cjs/asset-cooking.js +372 -0
- package/dist/cjs/authority-room.js +704 -0
- package/dist/cjs/bot-documents.js +556 -0
- package/dist/cjs/bot-primitives.js +482 -0
- package/dist/cjs/browser.js +96 -0
- package/dist/cjs/canonical.js +299 -0
- package/dist/cjs/certification.js +1860 -0
- package/dist/cjs/chaos-certification.js +1402 -0
- package/dist/cjs/collections.js +36 -0
- package/dist/cjs/collision.js +96 -0
- package/dist/cjs/commands.js +987 -0
- package/dist/cjs/config-bundle.js +491 -0
- package/dist/cjs/creator.js +30 -0
- package/dist/cjs/deterministic-primitives.js +134 -0
- package/dist/cjs/deterministic-session.js +1410 -0
- package/dist/cjs/ecs-lite.js +155 -0
- package/dist/cjs/effects-adapter.js +97 -0
- package/dist/cjs/engine-complete.js +2297 -0
- package/dist/cjs/engine-core-proof.js +70 -0
- package/dist/cjs/engine-parity-matrix.js +874 -0
- package/dist/cjs/engine-parity.js +3183 -0
- package/dist/cjs/engine-runtime.js +3141 -0
- package/dist/cjs/errors.js +22 -0
- package/dist/cjs/events.js +247 -0
- package/dist/cjs/examples/counter-duel.js +157 -0
- package/dist/cjs/examples/fixed-arena.js +368 -0
- package/dist/cjs/index.js +229 -0
- package/dist/cjs/input-authority.js +150 -0
- package/dist/cjs/input-button.js +256 -0
- package/dist/cjs/input-timeline.js +418 -0
- package/dist/cjs/instant-replay.js +66 -0
- package/dist/cjs/lag-compensation3d.js +837 -0
- package/dist/cjs/local-authority-server-runner.js +1121 -0
- package/dist/cjs/local-authority-wire.js +1497 -0
- package/dist/cjs/local-authority.js +817 -0
- package/dist/cjs/local-ws-adapter.js +150 -0
- package/dist/cjs/match-log.js +61 -0
- package/dist/cjs/math.js +157 -0
- package/dist/cjs/mock-network-adapter.js +26 -0
- package/dist/cjs/movement.js +703 -0
- package/dist/cjs/movement3d.js +1246 -0
- package/dist/cjs/multiclient-authority.js +308 -0
- package/dist/cjs/multiplayer-service.js +434 -0
- package/dist/cjs/navigation.js +347 -0
- package/dist/cjs/navmesh.js +1437 -0
- package/dist/cjs/networked-client.js +691 -0
- package/dist/cjs/networked-input-authority.js +443 -0
- package/dist/cjs/node.js +47 -0
- package/dist/cjs/number-lab.js +78 -0
- package/dist/cjs/offline-session.js +370 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pause-resume.js +43 -0
- package/dist/cjs/physics-cert.js +426 -0
- package/dist/cjs/physics2d.js +751 -0
- package/dist/cjs/physics3d.js +1996 -0
- package/dist/cjs/prediction-culling.js +203 -0
- package/dist/cjs/protocol-lab.js +1299 -0
- package/dist/cjs/r3f-render-adapter.js +209 -0
- package/dist/cjs/random.js +59 -0
- package/dist/cjs/replay-bundle.js +1598 -0
- package/dist/cjs/replay-inspect.js +236 -0
- package/dist/cjs/replay-scoring.js +68 -0
- package/dist/cjs/replay.js +356 -0
- package/dist/cjs/reports.js +181 -0
- package/dist/cjs/rollback-history.js +251 -0
- package/dist/cjs/runtime-guards.js +111 -0
- package/dist/cjs/sample-lab.js +145 -0
- package/dist/cjs/sample-scenes.js +752 -0
- package/dist/cjs/schema-artifacts.js +359 -0
- package/dist/cjs/schema-codegen.js +544 -0
- package/dist/cjs/schema-dsl-binary.js +655 -0
- package/dist/cjs/schema.js +319 -0
- package/dist/cjs/sdk-offline.js +99 -0
- package/dist/cjs/session-promotion.js +74 -0
- package/dist/cjs/session-snapshot.js +265 -0
- package/dist/cjs/session-wire.js +216 -0
- package/dist/cjs/signals.js +103 -0
- package/dist/cjs/sparse-set.js +169 -0
- package/dist/cjs/synctest.js +136 -0
- package/dist/cjs/system-lifecycle.js +286 -0
- package/dist/cjs/timers.js +61 -0
- package/dist/cjs/tools/static-checker.js +1468 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/venus-mp-budget.js +125 -0
- package/dist/cjs/ws-frame.js +255 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11379 -0
- package/dist/collections.d.ts +9 -0
- package/dist/collections.js +30 -0
- package/dist/collision.d.ts +25 -0
- package/dist/collision.js +85 -0
- package/dist/commands.d.ts +137 -0
- package/dist/commands.js +977 -0
- package/dist/config-bundle.d.ts +89 -0
- package/dist/config-bundle.js +483 -0
- package/dist/creator.d.ts +20 -0
- package/dist/creator.js +11 -0
- package/dist/deterministic-primitives.d.ts +59 -0
- package/dist/deterministic-primitives.js +126 -0
- package/dist/deterministic-session.d.ts +197 -0
- package/dist/deterministic-session.js +1401 -0
- package/dist/ecs-lite.d.ts +26 -0
- package/dist/ecs-lite.js +152 -0
- package/dist/effects-adapter.d.ts +21 -0
- package/dist/effects-adapter.js +94 -0
- package/dist/engine-complete.d.ts +45 -0
- package/dist/engine-complete.js +2292 -0
- package/dist/engine-core-proof.d.ts +26 -0
- package/dist/engine-core-proof.js +66 -0
- package/dist/engine-parity-matrix.d.ts +300 -0
- package/dist/engine-parity-matrix.js +869 -0
- package/dist/engine-parity.d.ts +74 -0
- package/dist/engine-parity.js +3178 -0
- package/dist/engine-runtime.d.ts +351 -0
- package/dist/engine-runtime.js +3123 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +45 -0
- package/dist/events.js +242 -0
- package/dist/examples/counter-duel.d.ts +43 -0
- package/dist/examples/counter-duel.js +150 -0
- package/dist/examples/fixed-arena.d.ts +110 -0
- package/dist/examples/fixed-arena.js +356 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +66 -0
- package/dist/input-authority.d.ts +82 -0
- package/dist/input-authority.js +147 -0
- package/dist/input-button.d.ts +36 -0
- package/dist/input-button.js +249 -0
- package/dist/input-timeline.d.ts +65 -0
- package/dist/input-timeline.js +412 -0
- package/dist/instant-replay.d.ts +59 -0
- package/dist/instant-replay.js +62 -0
- package/dist/lag-compensation3d.d.ts +146 -0
- package/dist/lag-compensation3d.js +827 -0
- package/dist/local-authority-server-runner.d.ts +79 -0
- package/dist/local-authority-server-runner.js +1118 -0
- package/dist/local-authority-wire.d.ts +142 -0
- package/dist/local-authority-wire.js +1477 -0
- package/dist/local-authority.d.ts +160 -0
- package/dist/local-authority.js +810 -0
- package/dist/local-ws-adapter.d.ts +14 -0
- package/dist/local-ws-adapter.js +146 -0
- package/dist/match-log.d.ts +74 -0
- package/dist/match-log.js +57 -0
- package/dist/math.d.ts +41 -0
- package/dist/math.js +154 -0
- package/dist/mock-network-adapter.d.ts +13 -0
- package/dist/mock-network-adapter.js +23 -0
- package/dist/movement.d.ts +155 -0
- package/dist/movement.js +694 -0
- package/dist/movement3d.d.ts +195 -0
- package/dist/movement3d.js +1238 -0
- package/dist/multiclient-authority.d.ts +30 -0
- package/dist/multiclient-authority.js +305 -0
- package/dist/multiplayer-service.d.ts +153 -0
- package/dist/multiplayer-service.js +429 -0
- package/dist/navigation.d.ts +91 -0
- package/dist/navigation.js +338 -0
- package/dist/navmesh.d.ts +236 -0
- package/dist/navmesh.js +1423 -0
- package/dist/networked-client.d.ts +124 -0
- package/dist/networked-client.js +688 -0
- package/dist/networked-input-authority.d.ts +51 -0
- package/dist/networked-input-authority.js +439 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +19 -0
- package/dist/number-lab.d.ts +15 -0
- package/dist/number-lab.js +75 -0
- package/dist/offline-session.d.ts +2 -0
- package/dist/offline-session.js +367 -0
- package/dist/pause-resume.d.ts +48 -0
- package/dist/pause-resume.js +40 -0
- package/dist/physics-cert.d.ts +40 -0
- package/dist/physics-cert.js +389 -0
- package/dist/physics2d.d.ts +142 -0
- package/dist/physics2d.js +741 -0
- package/dist/physics3d.d.ts +285 -0
- package/dist/physics3d.js +1979 -0
- package/dist/prediction-culling.d.ts +34 -0
- package/dist/prediction-culling.js +200 -0
- package/dist/protocol-lab.d.ts +115 -0
- package/dist/protocol-lab.js +1295 -0
- package/dist/r3f-render-adapter.d.ts +61 -0
- package/dist/r3f-render-adapter.js +203 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +55 -0
- package/dist/replay-bundle.d.ts +150 -0
- package/dist/replay-bundle.js +1586 -0
- package/dist/replay-inspect.d.ts +60 -0
- package/dist/replay-inspect.js +228 -0
- package/dist/replay-scoring.d.ts +28 -0
- package/dist/replay-scoring.js +65 -0
- package/dist/replay.d.ts +17 -0
- package/dist/replay.js +351 -0
- package/dist/reports.d.ts +176 -0
- package/dist/reports.js +174 -0
- package/dist/rollback-history.d.ts +46 -0
- package/dist/rollback-history.js +247 -0
- package/dist/runtime-guards.d.ts +1 -0
- package/dist/runtime-guards.js +108 -0
- package/dist/sample-lab.d.ts +32 -0
- package/dist/sample-lab.js +134 -0
- package/dist/sample-scenes.d.ts +86 -0
- package/dist/sample-scenes.js +745 -0
- package/dist/schema-artifacts.d.ts +49 -0
- package/dist/schema-artifacts.js +356 -0
- package/dist/schema-codegen.d.ts +21 -0
- package/dist/schema-codegen.js +538 -0
- package/dist/schema-dsl-binary.d.ts +75 -0
- package/dist/schema-dsl-binary.js +648 -0
- package/dist/schema.d.ts +87 -0
- package/dist/schema.js +315 -0
- package/dist/sdk-offline.d.ts +34 -0
- package/dist/sdk-offline.js +96 -0
- package/dist/session-promotion.d.ts +26 -0
- package/dist/session-promotion.js +70 -0
- package/dist/session-snapshot.d.ts +95 -0
- package/dist/session-snapshot.js +255 -0
- package/dist/session-wire.d.ts +89 -0
- package/dist/session-wire.js +211 -0
- package/dist/signals.d.ts +31 -0
- package/dist/signals.js +99 -0
- package/dist/sparse-set.d.ts +34 -0
- package/dist/sparse-set.js +165 -0
- package/dist/synctest.d.ts +31 -0
- package/dist/synctest.js +133 -0
- package/dist/system-lifecycle.d.ts +72 -0
- package/dist/system-lifecycle.js +281 -0
- package/dist/timers.d.ts +17 -0
- package/dist/timers.js +53 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/static-checker.d.ts +31 -0
- package/dist/tools/static-checker.js +1462 -0
- package/dist/tools/vite-plugin.d.ts +69 -0
- package/dist/tools/vite-plugin.js +203 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.js +1 -0
- package/dist/venus-mp-budget.d.ts +29 -0
- package/dist/venus-mp-budget.js +122 -0
- package/dist/ws-frame.d.ts +49 -0
- package/dist/ws-frame.js +247 -0
- package/package.json +169 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SyncplayError = void 0;
|
|
4
|
+
exports.invariant = invariant;
|
|
5
|
+
class SyncplayError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
frame;
|
|
8
|
+
slot;
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'SyncplayError';
|
|
12
|
+
this.code = options.code;
|
|
13
|
+
this.frame = options.frame;
|
|
14
|
+
this.slot = options.slot;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.SyncplayError = SyncplayError;
|
|
18
|
+
function invariant(condition, message, code) {
|
|
19
|
+
if (!condition) {
|
|
20
|
+
throw new SyncplayError(message, { code });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDeterministicEventTimeline = createDeterministicEventTimeline;
|
|
4
|
+
exports.runDeterministicEventKeywordFixture = runDeterministicEventKeywordFixture;
|
|
5
|
+
exports.filterDeterministicEventsForPlayer = filterDeterministicEventsForPlayer;
|
|
6
|
+
const canonical_js_1 = require("./canonical.js");
|
|
7
|
+
const errors_js_1 = require("./errors.js");
|
|
8
|
+
function createDeterministicEventTimeline() {
|
|
9
|
+
const records = [];
|
|
10
|
+
const emittedVerifiedOnly = new Set();
|
|
11
|
+
let nextSequence = 0;
|
|
12
|
+
return {
|
|
13
|
+
emitPredicted(eventId, frame, payload, options) {
|
|
14
|
+
validateEvent(eventId, frame);
|
|
15
|
+
const record = makeRecord(eventId, frame, nextSequence, payload, 'predicted', normalizeEmitOptions(options));
|
|
16
|
+
nextSequence += 1;
|
|
17
|
+
records.push(record);
|
|
18
|
+
return cloneEventRecord(record);
|
|
19
|
+
},
|
|
20
|
+
emitSynced(eventId, frame, payload, options) {
|
|
21
|
+
validateEvent(eventId, frame);
|
|
22
|
+
const record = makeRecord(eventId, frame, nextSequence, payload, 'synced', normalizeEmitOptions(options));
|
|
23
|
+
nextSequence += 1;
|
|
24
|
+
records.push(record);
|
|
25
|
+
return cloneEventRecord(record);
|
|
26
|
+
},
|
|
27
|
+
emitVerifiedOnly(eventId, frame, payload, options) {
|
|
28
|
+
validateEvent(eventId, frame);
|
|
29
|
+
const emitOptions = normalizeEmitOptions(options);
|
|
30
|
+
const key = emitOptions.dedupeKey ?? makeDedupeKey(eventId, frame, payload, emitOptions.notHashedKeys);
|
|
31
|
+
if (emittedVerifiedOnly.has(key)) {
|
|
32
|
+
throw new errors_js_1.SyncplayError(`Verified-only event ${eventId} already emitted for ${key}`, {
|
|
33
|
+
code: 'events.duplicate-verified-only',
|
|
34
|
+
frame,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
emittedVerifiedOnly.add(key);
|
|
38
|
+
const record = makeRecord(eventId, frame, nextSequence, payload, 'verifiedOnly', { ...emitOptions, dedupeKey: key });
|
|
39
|
+
nextSequence += 1;
|
|
40
|
+
records.push(record);
|
|
41
|
+
return cloneEventRecord(record);
|
|
42
|
+
},
|
|
43
|
+
confirmThroughFrame(frame) {
|
|
44
|
+
validateFrame(frame);
|
|
45
|
+
const confirmed = [];
|
|
46
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
47
|
+
const record = records[index];
|
|
48
|
+
if (record.predictionState === 'predicted' && record.frame <= frame) {
|
|
49
|
+
const next = { ...record, predictionState: 'confirmed' };
|
|
50
|
+
records[index] = next;
|
|
51
|
+
confirmed.push(next);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return confirmed.map((record) => cloneEventRecord(record));
|
|
55
|
+
},
|
|
56
|
+
cancelFromFrame(frame) {
|
|
57
|
+
validateFrame(frame);
|
|
58
|
+
const canceled = [];
|
|
59
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
60
|
+
const record = records[index];
|
|
61
|
+
if (record.predictionState === 'predicted' && record.frame >= frame) {
|
|
62
|
+
const next = { ...record, predictionState: 'canceled' };
|
|
63
|
+
records[index] = next;
|
|
64
|
+
canceled.push(next);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return canceled.map((record) => cloneEventRecord(record));
|
|
68
|
+
},
|
|
69
|
+
records() {
|
|
70
|
+
return records.map((record) => cloneEventRecord(record));
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function runDeterministicEventKeywordFixture() {
|
|
75
|
+
const first = runEventKeywordScenario();
|
|
76
|
+
const restored = runEventKeywordScenario();
|
|
77
|
+
const checksum = (0, canonical_js_1.defaultChecksum)(first);
|
|
78
|
+
const restoredChecksum = (0, canonical_js_1.defaultChecksum)(restored);
|
|
79
|
+
return {
|
|
80
|
+
checksum,
|
|
81
|
+
restoredChecksum,
|
|
82
|
+
rollbackEquivalent: checksum === restoredChecksum,
|
|
83
|
+
syncedEvents: first.records.filter((record) => record.predictionState === 'synced').length,
|
|
84
|
+
confirmedEvents: first.records.filter((record) => record.predictionState === 'confirmed').length,
|
|
85
|
+
canceledEvents: first.records.filter((record) => record.predictionState === 'canceled').length,
|
|
86
|
+
verifiedOnlyEvents: first.records.filter((record) => record.predictionState === 'verifiedOnly').length,
|
|
87
|
+
notHashedDuplicateRejected: first.notHashedDuplicateRejected,
|
|
88
|
+
notHashedDedupeStable: first.notHashedDedupeStable,
|
|
89
|
+
syncedSurvivedConfirmCancel: first.syncedSurvivedConfirmCancel,
|
|
90
|
+
localEvents: first.localPlayerRecords.length,
|
|
91
|
+
remoteEvents: first.remotePlayerRecords.length,
|
|
92
|
+
localRemoteDispatchStable: (0, canonical_js_1.defaultChecksum)(first.localPlayerRecords) === (0, canonical_js_1.defaultChecksum)(restored.localPlayerRecords)
|
|
93
|
+
&& (0, canonical_js_1.defaultChecksum)(first.remotePlayerRecords) === (0, canonical_js_1.defaultChecksum)(restored.remotePlayerRecords),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function filterDeterministicEventsForPlayer(records, localSlot) {
|
|
97
|
+
if (!Number.isInteger(localSlot) || localSlot < 0) {
|
|
98
|
+
throw new errors_js_1.SyncplayError('Local event player slot must be a non-negative integer', {
|
|
99
|
+
code: 'events.local-slot',
|
|
100
|
+
slot: localSlot,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return records.filter((record) => shouldDispatchRecordToPlayer(record, localSlot)).map((record) => cloneEventRecord(record));
|
|
104
|
+
}
|
|
105
|
+
function runEventKeywordScenario() {
|
|
106
|
+
const timeline = createDeterministicEventTimeline();
|
|
107
|
+
timeline.emitPredicted('dash', 2, { slot: 0 }, { deliveryScope: 'local', playerSlot: 0 });
|
|
108
|
+
timeline.emitPredicted('remote.dash', 2, { slot: 0 }, { deliveryScope: 'remote', playerSlot: 0 });
|
|
109
|
+
const synced = timeline.emitSynced('spawn.synced', 2, { slot: 1, cosmetic: 'spark' }, { notHashedKeys: ['cosmetic'] });
|
|
110
|
+
const verified = timeline.emitVerifiedOnly('score', 3, { slot: 1, cosmetic: 'spark' }, { notHashedKeys: ['cosmetic'] });
|
|
111
|
+
let duplicateRejected = false;
|
|
112
|
+
try {
|
|
113
|
+
timeline.emitVerifiedOnly('score', 3, { slot: 1, cosmetic: 'smoke' }, { notHashedKeys: ['cosmetic'] });
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
duplicateRejected = error instanceof errors_js_1.SyncplayError;
|
|
117
|
+
}
|
|
118
|
+
timeline.confirmThroughFrame(2);
|
|
119
|
+
timeline.emitPredicted('late.predicted', 5, { slot: 2 });
|
|
120
|
+
timeline.cancelFromFrame(4);
|
|
121
|
+
const records = timeline.records();
|
|
122
|
+
return {
|
|
123
|
+
records,
|
|
124
|
+
localPlayerRecords: filterDeterministicEventsForPlayer(records, 0),
|
|
125
|
+
remotePlayerRecords: filterDeterministicEventsForPlayer(records, 1),
|
|
126
|
+
notHashedDuplicateRejected: duplicateRejected,
|
|
127
|
+
notHashedDedupeStable: verified.dedupeKey === `score:3:${(0, canonical_js_1.defaultChecksum)({ slot: 1 })}`,
|
|
128
|
+
syncedSurvivedConfirmCancel: records.some((record) => record.sequence === synced.sequence && record.predictionState === 'synced'),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function makeRecord(eventId, frame, sequence, payload, predictionState, options) {
|
|
132
|
+
return {
|
|
133
|
+
eventId,
|
|
134
|
+
frame,
|
|
135
|
+
sequence,
|
|
136
|
+
payload: (0, canonical_js_1.cloneCanonical)(payload),
|
|
137
|
+
predictionState,
|
|
138
|
+
dedupeKey: options.dedupeKey ?? makeDedupeKey(eventId, frame, payload, options.notHashedKeys),
|
|
139
|
+
notHashedKeys: [...(options.notHashedKeys ?? [])].sort(compareStrings),
|
|
140
|
+
deliveryScope: options.deliveryScope ?? 'all',
|
|
141
|
+
playerSlot: options.playerSlot,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function cloneEventRecord(record) {
|
|
145
|
+
return {
|
|
146
|
+
eventId: record.eventId,
|
|
147
|
+
frame: record.frame,
|
|
148
|
+
sequence: record.sequence,
|
|
149
|
+
payload: (0, canonical_js_1.cloneCanonical)(record.payload),
|
|
150
|
+
predictionState: record.predictionState,
|
|
151
|
+
dedupeKey: record.dedupeKey,
|
|
152
|
+
notHashedKeys: [...record.notHashedKeys],
|
|
153
|
+
deliveryScope: record.deliveryScope,
|
|
154
|
+
playerSlot: record.playerSlot,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function normalizeEmitOptions(options) {
|
|
158
|
+
if (options === undefined) {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
if (typeof options === 'string') {
|
|
162
|
+
return { dedupeKey: options };
|
|
163
|
+
}
|
|
164
|
+
if (options.deliveryScope !== undefined && options.deliveryScope !== 'all' && options.deliveryScope !== 'local' && options.deliveryScope !== 'remote') {
|
|
165
|
+
throw new errors_js_1.SyncplayError(`Unsupported event delivery scope ${options.deliveryScope}`, {
|
|
166
|
+
code: 'events.delivery-scope',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (options.playerSlot !== undefined &&
|
|
170
|
+
(!Number.isInteger(options.playerSlot) || options.playerSlot < 0)) {
|
|
171
|
+
throw new errors_js_1.SyncplayError('Event playerSlot must be a non-negative integer', {
|
|
172
|
+
code: 'events.player-slot',
|
|
173
|
+
slot: options.playerSlot,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
dedupeKey: options.dedupeKey,
|
|
178
|
+
notHashedKeys: options.notHashedKeys === undefined ? undefined : [...options.notHashedKeys].sort(compareStrings),
|
|
179
|
+
deliveryScope: options.deliveryScope ?? 'all',
|
|
180
|
+
playerSlot: options.playerSlot,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function shouldDispatchRecordToPlayer(record, localSlot) {
|
|
184
|
+
const scope = record.deliveryScope ?? 'all';
|
|
185
|
+
if (scope === 'all') {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (scope !== 'local' && scope !== 'remote') {
|
|
189
|
+
throw new errors_js_1.SyncplayError(`Unsupported event delivery scope ${scope}`, {
|
|
190
|
+
code: 'events.delivery-scope',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (record.playerSlot === undefined) {
|
|
194
|
+
throw new errors_js_1.SyncplayError(`Event ${record.eventId} is missing playerSlot for ${scope} delivery`, {
|
|
195
|
+
code: 'events.player-slot',
|
|
196
|
+
frame: record.frame,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (!Number.isInteger(record.playerSlot) || record.playerSlot < 0) {
|
|
200
|
+
throw new errors_js_1.SyncplayError('Event playerSlot must be a non-negative integer', {
|
|
201
|
+
code: 'events.player-slot',
|
|
202
|
+
frame: record.frame,
|
|
203
|
+
slot: record.playerSlot,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return scope === 'local' ? record.playerSlot === localSlot : record.playerSlot !== localSlot;
|
|
207
|
+
}
|
|
208
|
+
function makeDedupeKey(eventId, frame, payload, notHashedKeys = []) {
|
|
209
|
+
return `${eventId}:${frame}:${(0, canonical_js_1.defaultChecksum)(stripNotHashedPayloadKeys(payload, notHashedKeys))}`;
|
|
210
|
+
}
|
|
211
|
+
function stripNotHashedPayloadKeys(payload, notHashedKeys) {
|
|
212
|
+
if (notHashedKeys.length === 0 || payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
213
|
+
return payload;
|
|
214
|
+
}
|
|
215
|
+
const ignored = new Set(notHashedKeys);
|
|
216
|
+
const source = payload;
|
|
217
|
+
const stripped = {};
|
|
218
|
+
for (const key of stableRecordKeys(source)) {
|
|
219
|
+
if (!ignored.has(key)) {
|
|
220
|
+
stripped[key] = source[key];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return stripped;
|
|
224
|
+
}
|
|
225
|
+
function validateEvent(eventId, frame) {
|
|
226
|
+
if (eventId.length === 0) {
|
|
227
|
+
throw new errors_js_1.SyncplayError('eventId is required', { code: 'events.event-id' });
|
|
228
|
+
}
|
|
229
|
+
(0, canonical_js_1.canonicalStringify)(eventId);
|
|
230
|
+
validateFrame(frame);
|
|
231
|
+
}
|
|
232
|
+
function validateFrame(frame) {
|
|
233
|
+
if (!Number.isInteger(frame) || frame < 0) {
|
|
234
|
+
throw new errors_js_1.SyncplayError('Event frame must be a non-negative integer', {
|
|
235
|
+
code: 'events.frame',
|
|
236
|
+
frame,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function compareStrings(left, right) {
|
|
241
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
242
|
+
}
|
|
243
|
+
function stableRecordKeys(record) {
|
|
244
|
+
return Reflect.ownKeys(record)
|
|
245
|
+
.filter((key) => typeof key === 'string')
|
|
246
|
+
.sort(compareStrings);
|
|
247
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.counterDuelDefaultInput = void 0;
|
|
4
|
+
exports.createCounterDuelSession = createCounterDuelSession;
|
|
5
|
+
exports.runCounterDuelExample = runCounterDuelExample;
|
|
6
|
+
exports.verifyCounterDuelReplay = verifyCounterDuelReplay;
|
|
7
|
+
exports.makeCounterDuelInput = makeCounterDuelInput;
|
|
8
|
+
const canonical_js_1 = require("../canonical.js");
|
|
9
|
+
const events_js_1 = require("../events.js");
|
|
10
|
+
const offline_session_js_1 = require("../offline-session.js");
|
|
11
|
+
const random_js_1 = require("../random.js");
|
|
12
|
+
const replay_js_1 = require("../replay.js");
|
|
13
|
+
exports.counterDuelDefaultInput = {
|
|
14
|
+
attack: false,
|
|
15
|
+
targetSlot: 0,
|
|
16
|
+
};
|
|
17
|
+
function createCounterDuelSession(players, seed, options = {}) {
|
|
18
|
+
return (0, offline_session_js_1.createOfflineSession)({
|
|
19
|
+
tickRate: 30,
|
|
20
|
+
playerCount: players,
|
|
21
|
+
initialState: createInitialCounterDuelState(players),
|
|
22
|
+
step: counterDuelStep,
|
|
23
|
+
defaultInput: exports.counterDuelDefaultInput,
|
|
24
|
+
inputSchemaId: 'counter-duel-input/v1',
|
|
25
|
+
stateSchemaId: 'counter-duel-state/v1',
|
|
26
|
+
deterministicVersion: 'counter-duel@1',
|
|
27
|
+
randomSeed: seed,
|
|
28
|
+
snapshotBufferSize: options.snapshotBufferSize,
|
|
29
|
+
replayRecordingMode: options.replayRecordingMode,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function runCounterDuelExample(options) {
|
|
33
|
+
validateCounterDuelOptions(options);
|
|
34
|
+
const session = createCounterDuelSession(options.players, options.seed);
|
|
35
|
+
const inputRandom = new random_js_1.DeterministicRandom(options.seed);
|
|
36
|
+
const eventTimeline = (0, events_js_1.createDeterministicEventTimeline)();
|
|
37
|
+
for (let frame = 0; frame < options.frames; frame += 1) {
|
|
38
|
+
const previousState = session.getState();
|
|
39
|
+
const frameInputs = Array.from({ length: options.players }, (_, slot) => makeCounterDuelInput(inputRandom, options.players, slot));
|
|
40
|
+
for (let slot = 0; slot < options.players; slot += 1) {
|
|
41
|
+
session.setInput(slot, frameInputs[slot]);
|
|
42
|
+
}
|
|
43
|
+
session.stepFrames(1);
|
|
44
|
+
const nextState = session.getState();
|
|
45
|
+
for (let slot = 0; slot < options.players; slot += 1) {
|
|
46
|
+
if (frameInputs[slot].attack && previousState.cooldown[slot] === 0) {
|
|
47
|
+
const target = normalizeTarget(frameInputs[slot].targetSlot, options.players, slot);
|
|
48
|
+
eventTimeline.emitVerifiedOnly('counter-duel.hit', nextState.frame, {
|
|
49
|
+
attacker: slot,
|
|
50
|
+
target,
|
|
51
|
+
score: nextState.score[slot],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const replay = session.exportReplay();
|
|
57
|
+
if (options.verify === true) {
|
|
58
|
+
verifyCounterDuelReplay(replay);
|
|
59
|
+
}
|
|
60
|
+
const finalChecksum = replay.checksums.at(-1)?.checksum ?? (0, canonical_js_1.defaultChecksum)(session.getState());
|
|
61
|
+
return {
|
|
62
|
+
frames: options.frames,
|
|
63
|
+
players: options.players,
|
|
64
|
+
events: eventTimeline.records().length,
|
|
65
|
+
replayChecksums: replay.checksums.length,
|
|
66
|
+
finalChecksum,
|
|
67
|
+
verified: options.verify === true,
|
|
68
|
+
result: 'pass',
|
|
69
|
+
replay,
|
|
70
|
+
eventTimeline: eventTimeline.records(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function verifyCounterDuelReplay(replay) {
|
|
74
|
+
const result = (0, replay_js_1.verifyReplay)({
|
|
75
|
+
replay,
|
|
76
|
+
step: counterDuelStep,
|
|
77
|
+
defaultInput: exports.counterDuelDefaultInput,
|
|
78
|
+
inputSchemaId: 'counter-duel-input/v1',
|
|
79
|
+
stateSchemaId: 'counter-duel-state/v1',
|
|
80
|
+
deterministicVersion: 'counter-duel@1',
|
|
81
|
+
hydrateInput: hydrateCounterDuelInput,
|
|
82
|
+
});
|
|
83
|
+
if (result.mismatches !== 0) {
|
|
84
|
+
throw new Error(`Counter Duel replay mismatch: ${result.mismatches}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function createInitialCounterDuelState(players) {
|
|
88
|
+
return {
|
|
89
|
+
frame: 0,
|
|
90
|
+
health: Array.from({ length: players }, () => 3),
|
|
91
|
+
score: Array.from({ length: players }, () => 0),
|
|
92
|
+
cooldown: Array.from({ length: players }, () => 0),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function counterDuelStep(state, inputs) {
|
|
96
|
+
const health = [...state.health];
|
|
97
|
+
const score = [...state.score];
|
|
98
|
+
const cooldown = state.cooldown.map((value) => Math.max(0, value - 1));
|
|
99
|
+
for (let slot = 0; slot < inputs.length; slot += 1) {
|
|
100
|
+
const input = inputs[slot];
|
|
101
|
+
if (!input.attack || cooldown[slot] > 0) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const target = normalizeTarget(input.targetSlot, inputs.length, slot);
|
|
105
|
+
health[target] -= 1;
|
|
106
|
+
cooldown[slot] = 12;
|
|
107
|
+
if (health[target] <= 0) {
|
|
108
|
+
score[slot] += 1;
|
|
109
|
+
health[target] = 3;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
frame: state.frame + 1,
|
|
114
|
+
health,
|
|
115
|
+
score,
|
|
116
|
+
cooldown,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function makeCounterDuelInput(random, players, slot) {
|
|
120
|
+
const attackRoll = random.nextInt(0, 99);
|
|
121
|
+
return {
|
|
122
|
+
attack: attackRoll < 15,
|
|
123
|
+
targetSlot: findLastTarget(slot, players),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function findLastTarget(slot, players) {
|
|
127
|
+
return (slot + 1) % players;
|
|
128
|
+
}
|
|
129
|
+
function normalizeTarget(targetSlot, players, selfSlot) {
|
|
130
|
+
if (!Number.isInteger(targetSlot) || targetSlot < 0 || targetSlot >= players || targetSlot === selfSlot) {
|
|
131
|
+
return (selfSlot + 1) % players;
|
|
132
|
+
}
|
|
133
|
+
return targetSlot;
|
|
134
|
+
}
|
|
135
|
+
function hydrateCounterDuelInput(recordedInput) {
|
|
136
|
+
if (recordedInput === null ||
|
|
137
|
+
typeof recordedInput !== 'object' ||
|
|
138
|
+
typeof recordedInput.attack !== 'boolean' ||
|
|
139
|
+
typeof recordedInput.targetSlot !== 'number') {
|
|
140
|
+
throw new Error('Invalid Counter Duel replay input');
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
attack: recordedInput.attack,
|
|
144
|
+
targetSlot: recordedInput.targetSlot,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function validateCounterDuelOptions(options) {
|
|
148
|
+
if (!Number.isInteger(options.players) || options.players < 2 || options.players > 8) {
|
|
149
|
+
throw new Error('Counter Duel requires 2 to 8 players');
|
|
150
|
+
}
|
|
151
|
+
if (!Number.isInteger(options.frames) || options.frames < 1) {
|
|
152
|
+
throw new Error('Counter Duel requires at least one frame');
|
|
153
|
+
}
|
|
154
|
+
if (!Number.isInteger(options.seed)) {
|
|
155
|
+
throw new Error('Counter Duel seed must be an integer');
|
|
156
|
+
}
|
|
157
|
+
}
|