@ecsia/rollback 0.25.0
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 +21 -0
- package/README.md +107 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/rollback.d.ts +66 -0
- package/dist/rollback.d.ts.map +1 -0
- package/dist/rollback.js +205 -0
- package/dist/rollback.js.map +1 -0
- package/dist/session.d.ts +88 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +265 -0
- package/dist/session.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andy Aragon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @ecsia/rollback
|
|
2
|
+
|
|
3
|
+
Checkpointing for [**ecsia**](https://github.com/andymai/ecsia), an entity component
|
|
4
|
+
system (ECS) for TypeScript — entities are ids, components are typed data attached to
|
|
5
|
+
them, and systems are functions that run over entities with matching components.
|
|
6
|
+
|
|
7
|
+
`@ecsia/rollback` captures a whole world into a reusable image and restores it **in
|
|
8
|
+
place**, so a rollback-netcode or prediction loop can rewind to a checkpoint and
|
|
9
|
+
re-simulate. A restore is **handle-stable**: every entity keeps the exact handle it had
|
|
10
|
+
at capture, every entity reference stored in a component still resolves, and queries
|
|
11
|
+
stay valid — no remap table, unlike a network snapshot from
|
|
12
|
+
[`@ecsia/serialization`](https://www.npmjs.com/package/@ecsia/serialization), which
|
|
13
|
+
re-mints entities on the receiver.
|
|
14
|
+
|
|
15
|
+
On top of that it ships the loop itself: `createRollbackSession` predicts the inputs
|
|
16
|
+
that have not arrived, checkpoints every frame into a bounded ring, and — when a
|
|
17
|
+
confirmed input contradicts a prediction — rewinds and re-simulates automatically.
|
|
18
|
+
|
|
19
|
+
Images are reusable: after the live set stops growing, capturing into an existing image
|
|
20
|
+
allocates nothing, so a ring of images costs no per-frame garbage.
|
|
21
|
+
|
|
22
|
+
> **Status:** 0.x. v1 refuses (loudly) to capture a world that uses relations, has
|
|
23
|
+
> entities in cold archetypes, or registers a rich (`'string'` / `object<T>`) field —
|
|
24
|
+
> that state is not in the image, and failing fast beats restoring a partial world.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pnpm add @ecsia/rollback @ecsia/core
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { createWorld } from '@ecsia/core'
|
|
34
|
+
import { createRollbackSurface } from '@ecsia/rollback'
|
|
35
|
+
|
|
36
|
+
const world = createWorld({ components: [] })
|
|
37
|
+
const rollback = createRollbackSurface(world)
|
|
38
|
+
const checkpoint = rollback.newImage()
|
|
39
|
+
|
|
40
|
+
rollback.captureImage(checkpoint) // between frames, world.phase === 'serial'
|
|
41
|
+
// ...simulate, mispredict...
|
|
42
|
+
rollback.restoreImage(checkpoint) // every handle is back, in place
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The rollback loop
|
|
46
|
+
|
|
47
|
+
`createRollbackSession` owns the predict → confirm → rollback → re-simulate cycle. It
|
|
48
|
+
takes no dependency on a scheduler (you hand it one fixed step) and knows nothing about
|
|
49
|
+
your input format (opaque bytes it buffers and byte-compares); `applyInputs` is where you
|
|
50
|
+
write them into whatever components your systems read.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { createWorld, defineComponent } from '@ecsia/core'
|
|
54
|
+
import { createRollbackSession } from '@ecsia/rollback'
|
|
55
|
+
|
|
56
|
+
const Intent = defineComponent({ move: 'i32', fire: 'i32' }, { name: 'intent' })
|
|
57
|
+
const world = createWorld({ components: [Intent] })
|
|
58
|
+
const avatars = new Map<number, ReturnType<typeof world.spawnWith>>([
|
|
59
|
+
[0, world.spawnWith(Intent)],
|
|
60
|
+
[1, world.spawnWith(Intent)],
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
const session = createRollbackSession(world, {
|
|
64
|
+
maxRollbackFrames: 8, // ≈133 ms at 60Hz — the deepest correction the ring can absorb
|
|
65
|
+
players: [0, 1],
|
|
66
|
+
step: () => runOneFixedStep(), // e.g. () => scheduler.update(1 / 60)
|
|
67
|
+
applyInputs: (_frame, inputs) => {
|
|
68
|
+
for (const player of inputs.players) {
|
|
69
|
+
const bytes = inputs.get(player)
|
|
70
|
+
const intent = world.entity(avatars.get(player as number)!).write(Intent)
|
|
71
|
+
intent.move = bytes[0] ?? 0
|
|
72
|
+
intent.fire = bytes[1] ?? 0
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Per frame: feed the local input, advance. Remote inputs are predicted until they land.
|
|
78
|
+
for (let i = 0; i < 2; i++) {
|
|
79
|
+
session.recordInput(0, session.currentFrame + 1, new Uint8Array([1, 0]))
|
|
80
|
+
session.advance()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A remote input for a PAST frame that contradicts its prediction rewinds and re-simulates
|
|
84
|
+
// inside this call; one that matches costs nothing but an advancing confirmedFrame. Only a frame
|
|
85
|
+
// this session actually simulated is accepted — the first is the tick it was created at, plus one.
|
|
86
|
+
session.recordInput(1, session.currentFrame - 1, new Uint8Array([0, 1]))
|
|
87
|
+
|
|
88
|
+
declare function runOneFixedStep(): void
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`step()` must advance `world.tick` exactly once and must end at a frame boundary (its
|
|
92
|
+
observer drain done) — the session checkpoints immediately after it returns, and an image
|
|
93
|
+
rewinds state, not the event stream. Both are asserted, not assumed.
|
|
94
|
+
|
|
95
|
+
A correction for a frame further back than `maxRollbackFrames` is **unrecoverable**: the
|
|
96
|
+
session leaves the world untouched (never a partial rewind) and reports it through
|
|
97
|
+
`onUnrecoverable`, or throws when you did not supply one. Resync from a fresh
|
|
98
|
+
authoritative state at that point.
|
|
99
|
+
|
|
100
|
+
## Links
|
|
101
|
+
|
|
102
|
+
- Repository & full docs: https://github.com/andymai/ecsia
|
|
103
|
+
- Umbrella package: [`@ecsia/kit`](https://github.com/andymai/ecsia)
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
[MIT](./LICENSE) © Andy Aragon
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createRollbackSurface } from './rollback.js';
|
|
2
|
+
export type { RollbackSurface, RollbackImage } from './rollback.js';
|
|
3
|
+
export { createRollbackSession } from './session.js';
|
|
4
|
+
export type { Frame, FrameInputs, InputImage, PlayerId, PredictionPolicy, RollbackOptions, RollbackSession, UnrecoverableRollback, } from './session.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AACrD,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACpD,YAAY,EACV,KAAK,EACL,WAAW,EACX,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,GACtB,MAAM,cAAc,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ecsia/rollback — handle-stable whole-world capture/restore: the checkpoint substrate a rollback
|
|
2
|
+
// netcode / prediction loop re-simulates from. An OPT-IN package that attaches to @ecsia/core
|
|
3
|
+
// through the `__installRollback` seam; core never imports it, so a world that never rolls back
|
|
4
|
+
// bundles none of this.
|
|
5
|
+
//
|
|
6
|
+
// The opposite of @ecsia/serialization: a snapshot re-mints entities on the receiver and hands back
|
|
7
|
+
// a remap table, while a restore rewrites the live world IN PLACE — every entity keeps its ORIGINAL
|
|
8
|
+
// handle, every stored `eid` still resolves, and query iteration stays valid without a remap.
|
|
9
|
+
export { createRollbackSurface } from './rollback.js';
|
|
10
|
+
export { createRollbackSession } from './session.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,8FAA8F;AAC9F,gGAAgG;AAChG,wBAAwB;AACxB,EAAE;AACF,oGAAoG;AACpG,oGAAoG;AACpG,8FAA8F;AAE9F,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAErD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { World } from '@ecsia/core';
|
|
2
|
+
/**
|
|
3
|
+
* Opaque, reusable capture buffer. Allocate one per rollback-ring slot and reuse it: a capture only
|
|
4
|
+
* allocates when the live set outgrows the image's buffers (doubling), so a steady-state ring
|
|
5
|
+
* allocates nothing per frame. An image holds the world's Column references and is NOT portable to
|
|
6
|
+
* another world.
|
|
7
|
+
*/
|
|
8
|
+
declare const rollbackImageBrand: unique symbol;
|
|
9
|
+
export interface RollbackImage {
|
|
10
|
+
readonly [rollbackImageBrand]: true;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The capture/restore handle for one world.
|
|
14
|
+
*
|
|
15
|
+
* WHAT AN IMAGE COVERS
|
|
16
|
+
* • per-archetype SoA columns + the dense row→handle list over `[0, count + held)`, plus both
|
|
17
|
+
* occupancy words. The HELD range (deferred-dead rows an onRemove observer has yet to read) is
|
|
18
|
+
* part of the image: `world.phase === 'serial'` is equally true before the observer drain, so a
|
|
19
|
+
* capture can legitimately see `held > 0` — restoring the occupancy without those rows' bytes
|
|
20
|
+
* would fire removal observers with whatever the live world wrote there since.
|
|
21
|
+
* • the five entity identity/record regions + the four allocator cursors (handle stability)
|
|
22
|
+
* • the component bitmask: the u32 words AND the out-of-stride sparse overflow
|
|
23
|
+
* • the changeVersion stamps, when stamping is enabled
|
|
24
|
+
* • world.tick
|
|
25
|
+
*
|
|
26
|
+
* WHAT IT DELIBERATELY OMITS: the frame-transient reactivity structures — the write/shape log rings,
|
|
27
|
+
* the Changed lists, the per-frame added/removed query flavors and the topic rings. A rollback ring
|
|
28
|
+
* checkpoints between frames, where those are empty; not capturing them is what keeps the image small.
|
|
29
|
+
*
|
|
30
|
+
* CONSEQUENCE — the EVENT STREAM is not rewound, only the STATE. Capturing mid-frame (serial phase is
|
|
31
|
+
* equally true before the observer drain) leaves those rings outside the image, so a despawn journaled
|
|
32
|
+
* after the capture still drains after a restore: the observer fires for an entity the restore brought
|
|
33
|
+
* back. Held rows ARE captured, so such an event reports its at-capture values rather than a live
|
|
34
|
+
* entity's — the values are sound, the event count/identity is not. Checkpoint at a frame boundary
|
|
35
|
+
* (after the drain) if a client must not observe revoked events.
|
|
36
|
+
*/
|
|
37
|
+
export interface RollbackSurface {
|
|
38
|
+
/** A fresh, empty image (its buffers grow on first capture). */
|
|
39
|
+
newImage(): RollbackImage;
|
|
40
|
+
/**
|
|
41
|
+
* Capture the ENTIRE live world into `img`, overwriting whatever it held. Serial-phase only.
|
|
42
|
+
*
|
|
43
|
+
* THROWS on the v1 limitations, deliberately fail-fast rather than silently restoring a partial
|
|
44
|
+
* world: a world with a relation defined (relation state lives in JS maps behind a MONOTONIC
|
|
45
|
+
* synthetic pair-id counter, so re-simulating mints different pair ids and different archetype
|
|
46
|
+
* signatures), entities resident in the cold-archetype overflow store (per-TYPE blocks, outside
|
|
47
|
+
* the per-archetype hot walk), or a registered rich (`'string'` / `object<T>`) field (sidecar
|
|
48
|
+
* values the image cannot reach).
|
|
49
|
+
*/
|
|
50
|
+
captureImage(img: RollbackImage): void;
|
|
51
|
+
/**
|
|
52
|
+
* Restore `img` over the live world IN PLACE and HANDLE-STABLY. Every entity alive at capture is
|
|
53
|
+
* alive again at its ORIGINAL handle; entities spawned after the capture are gone; archetypes
|
|
54
|
+
* created after the capture are emptied. Serial-phase only; same guards as `captureImage`.
|
|
55
|
+
*/
|
|
56
|
+
restoreImage(img: RollbackImage): void;
|
|
57
|
+
/** RESTORE-ONLY tick assignment (`world.tick` is otherwise increment-only). */
|
|
58
|
+
setTick(tick: number): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Attach the rollback surface to `world`. Call once per world and keep the returned handle; each
|
|
62
|
+
* call re-installs the seam and mints a fresh image-ownership identity.
|
|
63
|
+
*/
|
|
64
|
+
export declare function createRollbackSurface(world: World): RollbackSurface;
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=rollback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollback.d.ts","sourceRoot":"","sources":["../src/rollback.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAoE,KAAK,EAAE,MAAM,aAAa,CAAA;AAE1G;;;;;GAKG;AACH,OAAO,CAAC,MAAM,kBAAkB,EAAE,OAAO,MAAM,CAAA;AAC/C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,CAAC,kBAAkB,CAAC,EAAE,IAAI,CAAA;CACpC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,QAAQ,IAAI,aAAa,CAAA;IACzB;;;;;;;;;OASG;IACH,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAAA;IACtC;;;;OAIG;IACH,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAAA;IACtC,+EAA+E;IAC/E,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AA2ED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAuKnE"}
|
package/dist/rollback.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// The capture/restore mechanism: read the whole live world into a reusable image and write it back
|
|
2
|
+
// IN PLACE, handle-stably. Drives `world.__installRollback()`; @ecsia/core never imports this.
|
|
3
|
+
//
|
|
4
|
+
// Growth discipline mirrors core's buffer layer: image buffers double, are reused across captures,
|
|
5
|
+
// and NEVER alias a live column view (a fallback grow re-creates those).
|
|
6
|
+
import { snapshotInto } from '@ecsia/core';
|
|
7
|
+
class Image {
|
|
8
|
+
/** The surface that minted this image — an image is not portable between worlds. */
|
|
9
|
+
owner = null;
|
|
10
|
+
seq = 0;
|
|
11
|
+
tick = 0;
|
|
12
|
+
identity = {
|
|
13
|
+
sparse: new Uint32Array(0),
|
|
14
|
+
dense: new Uint32Array(0),
|
|
15
|
+
generation: new Uint32Array(0),
|
|
16
|
+
recordArchetypeId: new Uint32Array(0),
|
|
17
|
+
recordArchetypeRow: new Uint32Array(0),
|
|
18
|
+
aliveCount: 0,
|
|
19
|
+
denseLen: 0,
|
|
20
|
+
spawned: 0,
|
|
21
|
+
despawned: 0,
|
|
22
|
+
};
|
|
23
|
+
bitmaskWords = new Uint32Array(0);
|
|
24
|
+
bitmaskWordCount = 0;
|
|
25
|
+
bitmaskIndexCount = 0;
|
|
26
|
+
bitmaskSparse = new Map();
|
|
27
|
+
changeVersion = new Uint32Array(0);
|
|
28
|
+
changeVersionCount = 0;
|
|
29
|
+
archetypes = new Map();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `arr` when it already holds `length` elements, else a fresh doubled one. Contents are NOT
|
|
33
|
+
* preserved — a capture rewrites the whole prefix it later reads back.
|
|
34
|
+
*/
|
|
35
|
+
function ensureU32(arr, length) {
|
|
36
|
+
if (arr.length >= length)
|
|
37
|
+
return arr;
|
|
38
|
+
let cap = arr.length > 0 ? arr.length : 1;
|
|
39
|
+
while (cap < length)
|
|
40
|
+
cap *= 2;
|
|
41
|
+
return new Uint32Array(cap);
|
|
42
|
+
}
|
|
43
|
+
function scratchFor(col, elements) {
|
|
44
|
+
const Ctor = col.view.constructor;
|
|
45
|
+
let cap = 1;
|
|
46
|
+
while (cap < elements)
|
|
47
|
+
cap *= 2;
|
|
48
|
+
return new Ctor(cap);
|
|
49
|
+
}
|
|
50
|
+
function writeBack(col, data, length) {
|
|
51
|
+
const src = data.subarray(0, length);
|
|
52
|
+
col.view.set(src, 0);
|
|
53
|
+
}
|
|
54
|
+
function sizeIdentity(img, n) {
|
|
55
|
+
img.sparse = ensureU32(img.sparse, n);
|
|
56
|
+
img.dense = ensureU32(img.dense, n);
|
|
57
|
+
img.generation = ensureU32(img.generation, n);
|
|
58
|
+
img.recordArchetypeId = ensureU32(img.recordArchetypeId, n);
|
|
59
|
+
img.recordArchetypeRow = ensureU32(img.recordArchetypeRow, n);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Attach the rollback surface to `world`. Call once per world and keep the returned handle; each
|
|
63
|
+
* call re-installs the seam and mints a fresh image-ownership identity.
|
|
64
|
+
*/
|
|
65
|
+
export function createRollbackSurface(world) {
|
|
66
|
+
const host = world.__installRollback();
|
|
67
|
+
const owner = {};
|
|
68
|
+
const assertSerial = (verb) => {
|
|
69
|
+
if (world.phase !== 'serial') {
|
|
70
|
+
throw new Error(`rollback ${verb}() must run while the world is in its serial phase (between frames, outside scheduler.update / worker waves)`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const assertOwned = (verb, img) => {
|
|
74
|
+
if (img.owner !== owner) {
|
|
75
|
+
throw new Error(`rollback ${verb}(): this image belongs to a different rollback surface — an image holds that world's column references, so it is not portable`);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const unsupported = (verb, what, remedy) => new Error(`rollback ${verb}(): ${what} — rollback would silently restore a partial world. ${remedy}`);
|
|
79
|
+
const coldResidents = () => {
|
|
80
|
+
let n = 0;
|
|
81
|
+
for (const arch of host.archetypes.byId)
|
|
82
|
+
if (arch.cold)
|
|
83
|
+
n += arch.count;
|
|
84
|
+
return n;
|
|
85
|
+
};
|
|
86
|
+
const assertCoverable = (verb) => {
|
|
87
|
+
const relations = world.__serialize.relations();
|
|
88
|
+
if (relations !== undefined && relations.relations().length > 0) {
|
|
89
|
+
throw unsupported(verb, 'relation state (JS maps behind a MONOTONIC synthetic pair-id counter) is not in the image, so re-simulating mints different pair ids and different archetype signatures', 'Relation-aware rollback is a follow-up; checkpoint relation-free worlds until then.');
|
|
90
|
+
}
|
|
91
|
+
const cold = coldResidents();
|
|
92
|
+
if (cold > 0) {
|
|
93
|
+
throw unsupported(verb, `${cold} entities live in the cold-archetype overflow store, outside the per-archetype hot walk the image captures`, 'Raise maxHotArchetypes so every archetype stays hot, or world.warm() the cold signatures before checkpointing.');
|
|
94
|
+
}
|
|
95
|
+
if (world.__serialize.richFields().length > 0) {
|
|
96
|
+
throw unsupported(verb, "rich ('string' / object<T>) field values live in the main-thread sidecar, which the image cannot reach", 'Rich-field rollback is a follow-up; checkpoint worlds with numeric-only components until then.');
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const cellFor = (img, arch) => {
|
|
100
|
+
let cell = img.archetypes.get(arch.id);
|
|
101
|
+
if (cell === undefined) {
|
|
102
|
+
cell = { seq: 0, count: 0, held: 0, rows: new Uint32Array(0), cells: [] };
|
|
103
|
+
img.archetypes.set(arch.id, cell);
|
|
104
|
+
}
|
|
105
|
+
return cell;
|
|
106
|
+
};
|
|
107
|
+
const captureArchetype = (img, arch) => {
|
|
108
|
+
const entry = cellFor(img, arch);
|
|
109
|
+
entry.seq = img.seq;
|
|
110
|
+
entry.count = arch.count;
|
|
111
|
+
entry.held = arch.held;
|
|
112
|
+
// The held (deferred-dead) rows sit directly above the live region and are part of the image:
|
|
113
|
+
// their column bytes are what onRemove handlers read when the drain finally releases them.
|
|
114
|
+
const rowCount = arch.count + arch.held;
|
|
115
|
+
entry.rows = ensureU32(entry.rows, rowCount);
|
|
116
|
+
entry.rows.set(arch.rows.subarray(0, rowCount), 0);
|
|
117
|
+
let i = 0;
|
|
118
|
+
for (const set of arch.columnSets.values()) {
|
|
119
|
+
for (const col of set.columns) {
|
|
120
|
+
const elements = rowCount * col.layout.stride;
|
|
121
|
+
let cell = entry.cells[i];
|
|
122
|
+
// Re-resolve per capture: a cold→hot warm promotion attaches columns to an archetype that
|
|
123
|
+
// had none, so the cell list is not fixed for an archetype id.
|
|
124
|
+
if (cell === undefined || cell.col !== col) {
|
|
125
|
+
cell = { col, data: scratchFor(col, elements), length: 0 };
|
|
126
|
+
entry.cells[i] = cell;
|
|
127
|
+
}
|
|
128
|
+
else if (cell.data.length < elements) {
|
|
129
|
+
cell.data = scratchFor(col, elements);
|
|
130
|
+
}
|
|
131
|
+
cell.length = snapshotInto(col, rowCount, cell.data, 0);
|
|
132
|
+
i += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
entry.cells.length = i;
|
|
136
|
+
};
|
|
137
|
+
const restoreArchetype = (img, arch) => {
|
|
138
|
+
const entry = img.archetypes.get(arch.id);
|
|
139
|
+
if (entry === undefined || entry.seq !== img.seq) {
|
|
140
|
+
// Not in the image: an archetype the re-sim created (or filled) after the checkpoint. Leaving
|
|
141
|
+
// it populated is silent cross-frame corruption — its rows reference entities that, post
|
|
142
|
+
// restore, live elsewhere or are dead.
|
|
143
|
+
host.archetypes.rollbackSetOccupancy(arch, 0, 0);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
arch.rows.set(entry.rows.subarray(0, entry.count + entry.held), 0);
|
|
147
|
+
for (const cell of entry.cells)
|
|
148
|
+
writeBack(cell.col, cell.data, cell.length);
|
|
149
|
+
host.archetypes.rollbackSetOccupancy(arch, entry.count, entry.held);
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
newImage() {
|
|
153
|
+
const img = new Image();
|
|
154
|
+
img.owner = owner;
|
|
155
|
+
return img;
|
|
156
|
+
},
|
|
157
|
+
captureImage(image) {
|
|
158
|
+
assertSerial('captureImage');
|
|
159
|
+
assertCoverable('captureImage');
|
|
160
|
+
const img = image;
|
|
161
|
+
assertOwned('captureImage', img);
|
|
162
|
+
img.seq += 1;
|
|
163
|
+
img.tick = world.tick;
|
|
164
|
+
const indexCount = host.entities.index.denseLen;
|
|
165
|
+
sizeIdentity(img.identity, indexCount);
|
|
166
|
+
host.entities.captureIdentity(img.identity);
|
|
167
|
+
for (const arch of host.archetypes.byId) {
|
|
168
|
+
if (arch.cold)
|
|
169
|
+
continue;
|
|
170
|
+
captureArchetype(img, arch);
|
|
171
|
+
}
|
|
172
|
+
img.bitmaskWords = ensureU32(img.bitmaskWords, indexCount * host.bitmask.stride);
|
|
173
|
+
img.bitmaskIndexCount = indexCount;
|
|
174
|
+
img.bitmaskWordCount = host.bitmask.captureInto(img.bitmaskWords, indexCount, img.bitmaskSparse);
|
|
175
|
+
img.changeVersion = ensureU32(img.changeVersion, indexCount);
|
|
176
|
+
img.changeVersionCount = host.changeVersion().captureInto(img.changeVersion, indexCount);
|
|
177
|
+
},
|
|
178
|
+
restoreImage(image) {
|
|
179
|
+
assertSerial('restoreImage');
|
|
180
|
+
assertCoverable('restoreImage');
|
|
181
|
+
const img = image;
|
|
182
|
+
assertOwned('restoreImage', img);
|
|
183
|
+
if (img.seq === 0)
|
|
184
|
+
throw new Error('rollback restoreImage(): the image has never been captured into');
|
|
185
|
+
// Widest index the bitmask must be coherent over: the checkpoint's, or the current denseLen
|
|
186
|
+
// when the re-sim minted past it (those slots must lose their bits).
|
|
187
|
+
const clearThrough = Math.max(img.bitmaskIndexCount, host.entities.index.denseLen);
|
|
188
|
+
for (const arch of host.archetypes.byId) {
|
|
189
|
+
if (arch.cold)
|
|
190
|
+
continue;
|
|
191
|
+
restoreArchetype(img, arch);
|
|
192
|
+
}
|
|
193
|
+
host.entities.restoreIdentity(img.identity);
|
|
194
|
+
host.bitmask.restoreFrom(img.bitmaskWords, img.bitmaskIndexCount, clearThrough, img.bitmaskSparse);
|
|
195
|
+
host.changeVersion().restoreFrom(img.changeVersion, img.changeVersionCount);
|
|
196
|
+
host.setTick(img.tick);
|
|
197
|
+
host.resyncQueries();
|
|
198
|
+
},
|
|
199
|
+
setTick(tick) {
|
|
200
|
+
assertSerial('setTick');
|
|
201
|
+
host.setTick(tick >>> 0);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=rollback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollback.js","sourceRoot":"","sources":["../src/rollback.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { World } from '@ecsia/core';
|
|
2
|
+
/** A fixed-step simulation frame. Aligned 1:1 with `world.tick`: one `step()` == one tick. */
|
|
3
|
+
export type Frame = number;
|
|
4
|
+
export type PlayerId = number | string;
|
|
5
|
+
/** App-defined opaque input bytes. The engine never interprets them — it buffers and compares. */
|
|
6
|
+
export type InputImage = Uint8Array;
|
|
7
|
+
/**
|
|
8
|
+
* How to synthesize a remote player's input for a frame that has no confirmed one yet.
|
|
9
|
+
* `lastConfirmed` is that player's most recently confirmed input, or null before their first.
|
|
10
|
+
* Default: repeat it (the GGPO default — correct for held-button controls).
|
|
11
|
+
*/
|
|
12
|
+
export type PredictionPolicy = (player: PlayerId, frame: Frame, lastConfirmed: InputImage | null) => InputImage;
|
|
13
|
+
/**
|
|
14
|
+
* One frame's inputs, as handed to `applyInputs`. A REUSED view valid only for the duration of that
|
|
15
|
+
* call — read it, don't retain it. The returned bytes alias the session's input buffer.
|
|
16
|
+
*/
|
|
17
|
+
export interface FrameInputs {
|
|
18
|
+
readonly frame: Frame;
|
|
19
|
+
/** The session's players, in the order given to {@link RollbackOptions.players}. */
|
|
20
|
+
readonly players: readonly PlayerId[];
|
|
21
|
+
/** This frame's input for `player` — real or predicted. */
|
|
22
|
+
get(player: PlayerId): InputImage;
|
|
23
|
+
/** True when this frame's input for `player` is a prediction rather than a confirmed input. */
|
|
24
|
+
isPredicted(player: PlayerId): boolean;
|
|
25
|
+
}
|
|
26
|
+
/** A rollback the session could not perform — the app must resync from a fresh authoritative state. */
|
|
27
|
+
export interface UnrecoverableRollback {
|
|
28
|
+
/** The frame the correction was for. */
|
|
29
|
+
readonly frame: Frame;
|
|
30
|
+
readonly currentFrame: Frame;
|
|
31
|
+
readonly maxRollbackFrames: number;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
}
|
|
34
|
+
export interface RollbackOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Maximum rollback DEPTH in frames: a correction for a frame more than this far behind
|
|
37
|
+
* `currentFrame` is unrecoverable. Must exceed worst-case input latency. (The ring itself holds
|
|
38
|
+
* one more image than this — a rollback to the deepest allowed frame restores the checkpoint
|
|
39
|
+
* taken BEFORE it.)
|
|
40
|
+
*/
|
|
41
|
+
readonly maxRollbackFrames: number;
|
|
42
|
+
readonly players: readonly PlayerId[];
|
|
43
|
+
/**
|
|
44
|
+
* ONE fixed-step advance of the app's frame loop — typically `() => scheduler.update(dt)`. It MUST
|
|
45
|
+
* advance `world.tick` exactly once and MUST end at a frame boundary (its observer drain done), or
|
|
46
|
+
* the session throws: the checkpoint taken right after it would otherwise be mid-frame.
|
|
47
|
+
*/
|
|
48
|
+
readonly step: () => void;
|
|
49
|
+
/** Write `inputs` into the world so `step()` reads them. Called before every step, re-sims included. */
|
|
50
|
+
readonly applyInputs: (frame: Frame, inputs: FrameInputs) => void;
|
|
51
|
+
/** Default: repeat the player's last confirmed input (empty bytes before their first). */
|
|
52
|
+
readonly predict?: PredictionPolicy;
|
|
53
|
+
/** Misprediction test. Default: byte equality. */
|
|
54
|
+
readonly equalsInput?: (a: InputImage, b: InputImage) => boolean;
|
|
55
|
+
/** Called instead of throwing when a rollback is unrecoverable (the world is left untouched). */
|
|
56
|
+
readonly onUnrecoverable?: (info: UnrecoverableRollback) => void;
|
|
57
|
+
}
|
|
58
|
+
export interface RollbackSession {
|
|
59
|
+
/**
|
|
60
|
+
* Advance one frame: predict any missing input, `applyInputs`, `step()`, checkpoint. Returns the
|
|
61
|
+
* frame just simulated.
|
|
62
|
+
*/
|
|
63
|
+
advance(): Frame;
|
|
64
|
+
/**
|
|
65
|
+
* Record a REAL input. A frame at or behind `currentFrame` whose prediction it contradicts
|
|
66
|
+
* triggers the rollback + re-simulation synchronously, inside this call; one that matches the
|
|
67
|
+
* prediction costs nothing but an advancing `confirmedFrame`. Future frames are buffered (up to
|
|
68
|
+
* `maxRollbackFrames` ahead). Transport is the caller's job.
|
|
69
|
+
*/
|
|
70
|
+
recordInput(player: PlayerId, frame: Frame, input: InputImage): void;
|
|
71
|
+
readonly currentFrame: Frame;
|
|
72
|
+
/** The latest simulated frame no player's input was predicted at. */
|
|
73
|
+
readonly confirmedFrame: Frame;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Drive a world through a rollback-netcode loop. Creates its own {@link RollbackSurface} and
|
|
77
|
+
* checkpoints the world as it stands NOW as frame `world.tick` — construct it at a frame boundary,
|
|
78
|
+
* with the world in the state every peer agrees on.
|
|
79
|
+
*
|
|
80
|
+
* FRAME↔TICK: a session frame IS `world.tick`. `currentFrame` starts at the world's tick and every
|
|
81
|
+
* `step()` must advance both by exactly one; the session asserts it rather than assuming it.
|
|
82
|
+
*
|
|
83
|
+
* BOUND: `world.tick` is u32 while `currentFrame` is an unbounded JS number, so a single session
|
|
84
|
+
* running past 2^32 frames (~2.3 years at 60 Hz) trips that assert rather than silently desyncing.
|
|
85
|
+
* Unhandled by design — no real session reaches it, and failing loudly beats a wrapped comparison.
|
|
86
|
+
*/
|
|
87
|
+
export declare function createRollbackSession(world: World, opts: RollbackOptions): RollbackSession;
|
|
88
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAExC,8FAA8F;AAC9F,MAAM,MAAM,KAAK,GAAG,MAAM,CAAA;AAE1B,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAA;AAEtC,kGAAkG;AAClG,MAAM,MAAM,UAAU,GAAG,UAAU,CAAA;AAEnC;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,GAAG,IAAI,KAAK,UAAU,CAAA;AAE/G;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAA;IACrC,2DAA2D;IAC3D,GAAG,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAA;IACjC,+FAA+F;IAC/F,WAAW,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAA;CACvC;AAED,uGAAuG;AACvG,MAAM,WAAW,qBAAqB;IACpC,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAA;IAC5B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAA;IACrC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAA;IACzB,wGAAwG;IACxG,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;IACjE,0FAA0F;IAC1F,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAA;IACnC,kDAAkD;IAClD,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,KAAK,OAAO,CAAA;IAChE,iGAAiG;IACjG,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,CAAA;CACjE;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,IAAI,KAAK,CAAA;IAChB;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IACpE,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAA;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,cAAc,EAAE,KAAK,CAAA;CAC/B;AAoDD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,eAAe,GAAG,eAAe,CAkP1F"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// The GGPO-style predict → confirm → rollback → re-simulate loop over the capture/restore seam
|
|
2
|
+
// (rollback.ts): a bounded checkpoint ring, a per-player frame-indexed input buffer, and a
|
|
3
|
+
// re-simulation from the last known-good frame when a prediction turns out wrong.
|
|
4
|
+
//
|
|
5
|
+
// DELIBERATELY DECOUPLED, twice over. There is no @ecsia/scheduler dependency — the caller hands in
|
|
6
|
+
// `step`, ONE fixed-step advance of its own frame loop. And the engine is schema-agnostic — an input
|
|
7
|
+
// is opaque bytes it buffers, predicts and byte-compares; `applyInputs` is what writes those bytes
|
|
8
|
+
// into whatever components the systems read.
|
|
9
|
+
import { createRollbackSurface } from './rollback.js';
|
|
10
|
+
const EMPTY_INPUT = new Uint8Array(0);
|
|
11
|
+
function bytesEqual(a, b) {
|
|
12
|
+
if (a.length !== b.length)
|
|
13
|
+
return false;
|
|
14
|
+
for (let i = 0; i < a.length; i++)
|
|
15
|
+
if (a[i] !== b[i])
|
|
16
|
+
return false;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
function capacityFor(length) {
|
|
20
|
+
let cap = 1;
|
|
21
|
+
while (cap < length)
|
|
22
|
+
cap *= 2;
|
|
23
|
+
return cap;
|
|
24
|
+
}
|
|
25
|
+
function writeSlot(slot, frame, input, confirmed) {
|
|
26
|
+
if (slot.bytes.length < input.length) {
|
|
27
|
+
slot.bytes = new Uint8Array(capacityFor(input.length));
|
|
28
|
+
slot.view = null;
|
|
29
|
+
}
|
|
30
|
+
if (slot.length !== input.length)
|
|
31
|
+
slot.view = null;
|
|
32
|
+
slot.bytes.set(input, 0);
|
|
33
|
+
slot.length = input.length;
|
|
34
|
+
slot.frame = frame;
|
|
35
|
+
slot.confirmed = confirmed;
|
|
36
|
+
}
|
|
37
|
+
function slotView(slot) {
|
|
38
|
+
slot.view ??= slot.bytes.subarray(0, slot.length);
|
|
39
|
+
return slot.view;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Drive a world through a rollback-netcode loop. Creates its own {@link RollbackSurface} and
|
|
43
|
+
* checkpoints the world as it stands NOW as frame `world.tick` — construct it at a frame boundary,
|
|
44
|
+
* with the world in the state every peer agrees on.
|
|
45
|
+
*
|
|
46
|
+
* FRAME↔TICK: a session frame IS `world.tick`. `currentFrame` starts at the world's tick and every
|
|
47
|
+
* `step()` must advance both by exactly one; the session asserts it rather than assuming it.
|
|
48
|
+
*
|
|
49
|
+
* BOUND: `world.tick` is u32 while `currentFrame` is an unbounded JS number, so a single session
|
|
50
|
+
* running past 2^32 frames (~2.3 years at 60 Hz) trips that assert rather than silently desyncing.
|
|
51
|
+
* Unhandled by design — no real session reaches it, and failing loudly beats a wrapped comparison.
|
|
52
|
+
*/
|
|
53
|
+
export function createRollbackSession(world, opts) {
|
|
54
|
+
const maxRollbackFrames = opts.maxRollbackFrames;
|
|
55
|
+
if (!Number.isInteger(maxRollbackFrames) || maxRollbackFrames < 1) {
|
|
56
|
+
throw new Error(`rollback session: maxRollbackFrames must be a positive integer (got ${String(maxRollbackFrames)})`);
|
|
57
|
+
}
|
|
58
|
+
if (opts.players.length === 0)
|
|
59
|
+
throw new Error('rollback session: players must not be empty');
|
|
60
|
+
const players = [...opts.players];
|
|
61
|
+
const playerIndex = new Map();
|
|
62
|
+
for (const id of players) {
|
|
63
|
+
if (playerIndex.has(id))
|
|
64
|
+
throw new Error(`rollback session: duplicate player id ${String(id)}`);
|
|
65
|
+
playerIndex.set(id, playerIndex.size);
|
|
66
|
+
}
|
|
67
|
+
const predict = opts.predict ?? ((_player, _frame, lastConfirmed) => lastConfirmed ?? EMPTY_INPUT);
|
|
68
|
+
const equals = opts.equalsInput ?? bytesEqual;
|
|
69
|
+
const surface = createRollbackSurface(world);
|
|
70
|
+
// Held-row census only: the frame-boundary proof below. No state is installed by reading it.
|
|
71
|
+
const host = world.__installRollback();
|
|
72
|
+
// depth + 1: rolling back to depth `maxRollbackFrames` restores the checkpoint of the frame BEFORE
|
|
73
|
+
// it, so the ring must still hold one image older than the deepest re-simulated frame.
|
|
74
|
+
const ringSize = maxRollbackFrames + 1;
|
|
75
|
+
const images = [];
|
|
76
|
+
const imageFrames = [];
|
|
77
|
+
for (let i = 0; i < ringSize; i++) {
|
|
78
|
+
images.push(surface.newImage());
|
|
79
|
+
imageFrames.push(-1);
|
|
80
|
+
}
|
|
81
|
+
// The input window must cover the deepest re-simulated frame (currentFrame - maxRollbackFrames + 1)
|
|
82
|
+
// through the furthest buffered future one (currentFrame + maxRollbackFrames) at once.
|
|
83
|
+
const inputCapacity = maxRollbackFrames * 2;
|
|
84
|
+
const states = players.map((id) => ({
|
|
85
|
+
id,
|
|
86
|
+
slots: Array.from({ length: inputCapacity }, () => ({
|
|
87
|
+
frame: -1,
|
|
88
|
+
bytes: new Uint8Array(0),
|
|
89
|
+
length: 0,
|
|
90
|
+
confirmed: false,
|
|
91
|
+
view: null,
|
|
92
|
+
})),
|
|
93
|
+
lastConfirmedFrame: -1,
|
|
94
|
+
lastConfirmed: new Uint8Array(0),
|
|
95
|
+
lastConfirmedLength: 0,
|
|
96
|
+
lastConfirmedView: null,
|
|
97
|
+
}));
|
|
98
|
+
const stateOf = (player) => {
|
|
99
|
+
const i = playerIndex.get(player);
|
|
100
|
+
if (i === undefined) {
|
|
101
|
+
throw new Error(`rollback session: unknown player ${String(player)} — players are fixed at createRollbackSession`);
|
|
102
|
+
}
|
|
103
|
+
return states[i];
|
|
104
|
+
};
|
|
105
|
+
const startFrame = world.tick;
|
|
106
|
+
let currentFrame = startFrame;
|
|
107
|
+
let confirmedFrame = startFrame;
|
|
108
|
+
let viewFrame = startFrame;
|
|
109
|
+
const slotAt = (state, frame) => state.slots[frame % inputCapacity];
|
|
110
|
+
const inputs = {
|
|
111
|
+
get frame() {
|
|
112
|
+
return viewFrame;
|
|
113
|
+
},
|
|
114
|
+
players,
|
|
115
|
+
get(player) {
|
|
116
|
+
const slot = slotAt(stateOf(player), viewFrame);
|
|
117
|
+
if (slot.frame !== viewFrame) {
|
|
118
|
+
throw new Error(`rollback session: no buffered input for player ${String(player)} at frame ${viewFrame}`);
|
|
119
|
+
}
|
|
120
|
+
return slotView(slot);
|
|
121
|
+
},
|
|
122
|
+
isPredicted(player) {
|
|
123
|
+
const slot = slotAt(stateOf(player), viewFrame);
|
|
124
|
+
return slot.frame !== viewFrame || !slot.confirmed;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const lastConfirmedOf = (state) => {
|
|
128
|
+
if (state.lastConfirmedFrame < 0)
|
|
129
|
+
return null;
|
|
130
|
+
state.lastConfirmedView ??= state.lastConfirmed.subarray(0, state.lastConfirmedLength);
|
|
131
|
+
return state.lastConfirmedView;
|
|
132
|
+
};
|
|
133
|
+
const noteConfirmed = (state, frame, input) => {
|
|
134
|
+
if (frame < state.lastConfirmedFrame)
|
|
135
|
+
return;
|
|
136
|
+
if (state.lastConfirmed.length < input.length) {
|
|
137
|
+
state.lastConfirmed = new Uint8Array(capacityFor(input.length));
|
|
138
|
+
state.lastConfirmedView = null;
|
|
139
|
+
}
|
|
140
|
+
if (state.lastConfirmedLength !== input.length)
|
|
141
|
+
state.lastConfirmedView = null;
|
|
142
|
+
state.lastConfirmed.set(input, 0);
|
|
143
|
+
state.lastConfirmedLength = input.length;
|
|
144
|
+
state.lastConfirmedFrame = frame;
|
|
145
|
+
};
|
|
146
|
+
const applyFrame = (frame) => {
|
|
147
|
+
for (const state of states) {
|
|
148
|
+
const slot = slotAt(state, frame);
|
|
149
|
+
// A re-simulated frame that is STILL unconfirmed re-predicts: a correction that landed since
|
|
150
|
+
// moved `lastConfirmed`, and the better guess is the whole point of replaying it.
|
|
151
|
+
if (slot.frame === frame && slot.confirmed)
|
|
152
|
+
continue;
|
|
153
|
+
writeSlot(slot, frame, predict(state.id, frame, lastConfirmedOf(state)), false);
|
|
154
|
+
}
|
|
155
|
+
viewFrame = frame;
|
|
156
|
+
opts.applyInputs(frame, inputs);
|
|
157
|
+
};
|
|
158
|
+
const runStep = (frame) => {
|
|
159
|
+
opts.step();
|
|
160
|
+
if (world.tick !== frame) {
|
|
161
|
+
throw new Error(`rollback session: step() left world.tick at ${world.tick}, expected ${frame} — one session frame is exactly ONE fixed step (one world.tick increment). ` +
|
|
162
|
+
'Pass a step that runs a single frame (e.g. scheduler.update(dt)) and never advance the tick outside the session.');
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const captureInto = (frame) => {
|
|
166
|
+
// An image rewinds STATE, not the EVENT STREAM (see RollbackSurface): the reactivity log rings
|
|
167
|
+
// and the pending observer window sit OUTSIDE it, so a checkpoint taken mid-frame lets an event
|
|
168
|
+
// journaled after it still drain once the restore has revoked what it describes. A checkpoint is
|
|
169
|
+
// therefore only ever taken here — immediately after `step()` returns, which is a post-drain
|
|
170
|
+
// frame boundary — and the observable symptom of a step that ended early is asserted, not
|
|
171
|
+
// assumed: held rows are what the drain releases, so any left mean the drain has not run.
|
|
172
|
+
let held = 0;
|
|
173
|
+
for (const arch of host.archetypes.byId)
|
|
174
|
+
held += arch.held;
|
|
175
|
+
if (held > 0) {
|
|
176
|
+
throw new Error(`rollback session: frame ${frame} cannot be checkpointed — ${held} deferred-dead rows are still held for an observer drain that step() has not run. ` +
|
|
177
|
+
'A checkpoint must be taken at a frame boundary: an image rewinds state, not the event stream, so those events would drain after a restore that revoked them.');
|
|
178
|
+
}
|
|
179
|
+
const slot = frame % ringSize;
|
|
180
|
+
surface.captureImage(images[slot]);
|
|
181
|
+
imageFrames[slot] = frame;
|
|
182
|
+
};
|
|
183
|
+
const unrecoverable = (frame, message) => {
|
|
184
|
+
if (opts.onUnrecoverable === undefined)
|
|
185
|
+
throw new Error(`rollback session: ${message}`);
|
|
186
|
+
opts.onUnrecoverable({ frame, currentFrame, maxRollbackFrames, message });
|
|
187
|
+
};
|
|
188
|
+
const rollbackTo = (frame) => {
|
|
189
|
+
const depth = currentFrame - frame;
|
|
190
|
+
if (depth >= maxRollbackFrames) {
|
|
191
|
+
unrecoverable(frame, `frame ${frame} is ${depth} frames behind frame ${currentFrame}, past the ${maxRollbackFrames}-frame rollback window — the world is untouched (no partial rewind); resync from a fresh authoritative state`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const baseline = frame - 1;
|
|
195
|
+
const slot = baseline % ringSize;
|
|
196
|
+
if (imageFrames[slot] !== baseline) {
|
|
197
|
+
unrecoverable(frame, `the checkpoint for frame ${baseline} is no longer in the ring (slot holds frame ${String(imageFrames[slot])}) — the world is untouched (no partial rewind); resync from a fresh authoritative state`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
surface.restoreImage(images[slot]);
|
|
201
|
+
// The image carries the tick, so this is a check of frame↔tick alignment, not a repair: a
|
|
202
|
+
// disagreement means a checkpoint was keyed to a frame it was not taken at.
|
|
203
|
+
if (world.tick !== baseline) {
|
|
204
|
+
throw new Error(`rollback session: the checkpoint keyed to frame ${baseline} restored world.tick ${world.tick} — frame/tick alignment is broken`);
|
|
205
|
+
}
|
|
206
|
+
for (let f = frame; f <= currentFrame; f++) {
|
|
207
|
+
applyFrame(f);
|
|
208
|
+
runStep(f);
|
|
209
|
+
captureInto(f);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const allConfirmedAt = (frame) => {
|
|
213
|
+
for (const state of states) {
|
|
214
|
+
const slot = slotAt(state, frame);
|
|
215
|
+
if (slot.frame !== frame || !slot.confirmed)
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
};
|
|
220
|
+
const advanceConfirmed = () => {
|
|
221
|
+
while (confirmedFrame < currentFrame && allConfirmedAt(confirmedFrame + 1))
|
|
222
|
+
confirmedFrame += 1;
|
|
223
|
+
};
|
|
224
|
+
// The frame-`startFrame` checkpoint: what a rollback to `startFrame + 1` rewinds to. Nothing has
|
|
225
|
+
// been stepped yet, so this is a frame boundary by construction.
|
|
226
|
+
captureInto(startFrame);
|
|
227
|
+
return {
|
|
228
|
+
advance() {
|
|
229
|
+
const frame = currentFrame + 1;
|
|
230
|
+
applyFrame(frame);
|
|
231
|
+
runStep(frame);
|
|
232
|
+
captureInto(frame);
|
|
233
|
+
currentFrame = frame;
|
|
234
|
+
advanceConfirmed();
|
|
235
|
+
return frame;
|
|
236
|
+
},
|
|
237
|
+
recordInput(player, frame, input) {
|
|
238
|
+
const state = stateOf(player);
|
|
239
|
+
if (!Number.isInteger(frame) || frame <= startFrame) {
|
|
240
|
+
throw new Error(`rollback session: frame ${String(frame)} is not a frame this session simulates — its first frame is ${startFrame + 1} (frames are world ticks)`);
|
|
241
|
+
}
|
|
242
|
+
if (frame > currentFrame + maxRollbackFrames) {
|
|
243
|
+
throw new Error(`rollback session: input for frame ${frame} is more than ${maxRollbackFrames} frames ahead of frame ${currentFrame} — the input buffer cannot hold it without evicting a frame a rollback still needs`);
|
|
244
|
+
}
|
|
245
|
+
const slot = slotAt(state, frame);
|
|
246
|
+
if (frame <= currentFrame && slot.frame !== frame) {
|
|
247
|
+
unrecoverable(frame, `frame ${frame} has already left the ${inputCapacity}-frame input buffer, so its prediction cannot be checked — the world is untouched (no partial rewind); resync from a fresh authoritative state`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const mispredicted = slot.frame === frame && !slot.confirmed && !equals(input, slotView(slot));
|
|
251
|
+
writeSlot(slot, frame, input, true);
|
|
252
|
+
noteConfirmed(state, frame, input);
|
|
253
|
+
if (frame <= currentFrame && mispredicted)
|
|
254
|
+
rollbackTo(frame);
|
|
255
|
+
advanceConfirmed();
|
|
256
|
+
},
|
|
257
|
+
get currentFrame() {
|
|
258
|
+
return currentFrame;
|
|
259
|
+
},
|
|
260
|
+
get confirmedFrame() {
|
|
261
|
+
return confirmedFrame;
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,2FAA2F;AAC3F,kFAAkF;AAClF,EAAE;AACF,oGAAoG;AACpG,qGAAqG;AACrG,mGAAmG;AACnG,6CAA6C;AAE7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAqFrD,MAAM,WAAW,GAAe,IAAI,UAAU,CAAC,CAAC,CAAC,CAAA;AAEjD,SAAS,UAAU,CAAC,CAAa,EAAE,CAAa;IAC9C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAA;IAClE,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,WAAW,CAAC,MAAc;IACjC,IAAI,GAAG,GAAG,CAAC,CAAA;IACX,OAAO,GAAG,GAAG,MAAM;QAAE,GAAG,IAAI,CAAC,CAAA;IAC7B,OAAO,GAAG,CAAA;AACZ,CAAC;AAqBD,SAAS,SAAS,CAAC,IAAe,EAAE,KAAY,EAAE,KAAiB,EAAE,SAAkB;IACrF,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACrC,IAAI,CAAC,KAAK,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;QACtD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IACxB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAA;IAC1B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IAClB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;AAC5B,CAAC;AAED,SAAS,QAAQ,CAAC,IAAe;IAC/B,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IACjD,OAAO,IAAI,CAAC,IAAI,CAAA;AAClB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAY,EAAE,IAAqB;IACvE,MAAM,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,CAAA;IAChD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,iBAAiB,CAAC,IAAI,iBAAiB,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,uEAAuE,MAAM,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAA;IACtH,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;IAE7F,MAAM,OAAO,GAAwB,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAA;IACtD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC/C,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QACzB,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;QAC/F,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;IACvC,CAAC;IAED,MAAM,OAAO,GAAqB,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,CAAC,aAAa,IAAI,WAAW,CAAC,CAAA;IACpH,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,IAAI,UAAU,CAAA;IAE7C,MAAM,OAAO,GAAoB,qBAAqB,CAAC,KAAK,CAAC,CAAA;IAC7D,6FAA6F;IAC7F,MAAM,IAAI,GAAG,KAAK,CAAC,iBAAiB,EAAE,CAAA;IAEtC,mGAAmG;IACnG,uFAAuF;IACvF,MAAM,QAAQ,GAAG,iBAAiB,GAAG,CAAC,CAAA;IACtC,MAAM,MAAM,GAAoB,EAAE,CAAA;IAClC,MAAM,WAAW,GAAa,EAAE,CAAA;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAA;QAC/B,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IACtB,CAAC;IAED,oGAAoG;IACpG,uFAAuF;IACvF,MAAM,aAAa,GAAG,iBAAiB,GAAG,CAAC,CAAA;IAC3C,MAAM,MAAM,GAAkB,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACjD,EAAE;QACF,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;YAClD,KAAK,EAAE,CAAC,CAAC;YACT,KAAK,EAAE,IAAI,UAAU,CAAC,CAAC,CAAC;YACxB,MAAM,EAAE,CAAC;YACT,SAAS,EAAE,KAAK;YAChB,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;QACH,kBAAkB,EAAE,CAAC,CAAC;QACtB,aAAa,EAAE,IAAI,UAAU,CAAC,CAAC,CAAC;QAChC,mBAAmB,EAAE,CAAC;QACtB,iBAAiB,EAAE,IAAI;KACxB,CAAC,CAAC,CAAA;IAEH,MAAM,OAAO,GAAG,CAAC,MAAgB,EAAe,EAAE;QAChD,MAAM,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACjC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,CAAC,MAAM,CAAC,+CAA+C,CAAC,CAAA;QACpH,CAAC;QACD,OAAO,MAAM,CAAC,CAAC,CAAgB,CAAA;IACjC,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAA;IAC7B,IAAI,YAAY,GAAG,UAAU,CAAA;IAC7B,IAAI,cAAc,GAAG,UAAU,CAAA;IAC/B,IAAI,SAAS,GAAG,UAAU,CAAA;IAE1B,MAAM,MAAM,GAAG,CAAC,KAAkB,EAAE,KAAY,EAAa,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,aAAa,CAAc,CAAA;IAE/G,MAAM,MAAM,GAAgB;QAC1B,IAAI,KAAK;YACP,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,OAAO;QACP,GAAG,CAAC,MAAgB;YAClB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAA;YAC/C,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CAAC,kDAAkD,MAAM,CAAC,MAAM,CAAC,aAAa,SAAS,EAAE,CAAC,CAAA;YAC3G,CAAC;YACD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAA;QACvB,CAAC;QACD,WAAW,CAAC,MAAgB;YAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAA;YAC/C,OAAO,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,SAAS,CAAA;QACpD,CAAC;KACF,CAAA;IAED,MAAM,eAAe,GAAG,CAAC,KAAkB,EAAqB,EAAE;QAChE,IAAI,KAAK,CAAC,kBAAkB,GAAG,CAAC;YAAE,OAAO,IAAI,CAAA;QAC7C,KAAK,CAAC,iBAAiB,KAAK,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,mBAAmB,CAAC,CAAA;QACtF,OAAO,KAAK,CAAC,iBAAiB,CAAA;IAChC,CAAC,CAAA;IAED,MAAM,aAAa,GAAG,CAAC,KAAkB,EAAE,KAAY,EAAE,KAAiB,EAAQ,EAAE;QAClF,IAAI,KAAK,GAAG,KAAK,CAAC,kBAAkB;YAAE,OAAM;QAC5C,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAC9C,KAAK,CAAC,aAAa,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;YAC/D,KAAK,CAAC,iBAAiB,GAAG,IAAI,CAAA;QAChC,CAAC;QACD,IAAI,KAAK,CAAC,mBAAmB,KAAK,KAAK,CAAC,MAAM;YAAE,KAAK,CAAC,iBAAiB,GAAG,IAAI,CAAA;QAC9E,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;QACjC,KAAK,CAAC,mBAAmB,GAAG,KAAK,CAAC,MAAM,CAAA;QACxC,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAA;IAClC,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,CAAC,KAAY,EAAQ,EAAE;QACxC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YACjC,6FAA6F;YAC7F,kFAAkF;YAClF,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,IAAI,IAAI,CAAC,SAAS;gBAAE,SAAQ;YACpD,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;QACjF,CAAC;QACD,SAAS,GAAG,KAAK,CAAA;QACjB,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACjC,CAAC,CAAA;IAED,MAAM,OAAO,GAAG,CAAC,KAAY,EAAQ,EAAE;QACrC,IAAI,CAAC,IAAI,EAAE,CAAA;QACX,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CACb,+CAA+C,KAAK,CAAC,IAAI,cAAc,KAAK,6EAA6E;gBACvJ,kHAAkH,CACrH,CAAA;QACH,CAAC;IACH,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,CAAC,KAAY,EAAQ,EAAE;QACzC,+FAA+F;QAC/F,gGAAgG;QAChG,iGAAiG;QACjG,6FAA6F;QAC7F,0FAA0F;QAC1F,0FAA0F;QAC1F,IAAI,IAAI,GAAG,CAAC,CAAA;QACZ,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI;YAAE,IAAI,IAAI,IAAI,CAAC,IAAI,CAAA;QAC1D,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CACb,2BAA2B,KAAK,6BAA6B,IAAI,oFAAoF;gBACnJ,8JAA8J,CACjK,CAAA;QACH,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,GAAG,QAAQ,CAAA;QAC7B,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAkB,CAAC,CAAA;QACnD,WAAW,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;IAC3B,CAAC,CAAA;IAED,MAAM,aAAa,GAAG,CAAC,KAAY,EAAE,OAAe,EAAQ,EAAE;QAC5D,IAAI,IAAI,CAAC,eAAe,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,OAAO,EAAE,CAAC,CAAA;QACvF,IAAI,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAA;IAC3E,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,CAAC,KAAY,EAAQ,EAAE;QACxC,MAAM,KAAK,GAAG,YAAY,GAAG,KAAK,CAAA;QAClC,IAAI,KAAK,IAAI,iBAAiB,EAAE,CAAC;YAC/B,aAAa,CACX,KAAK,EACL,SAAS,KAAK,OAAO,KAAK,wBAAwB,YAAY,cAAc,iBAAiB,8GAA8G,CAC5M,CAAA;YACD,OAAM;QACR,CAAC;QACD,MAAM,QAAQ,GAAG,KAAK,GAAG,CAAC,CAAA;QAC1B,MAAM,IAAI,GAAG,QAAQ,GAAG,QAAQ,CAAA;QAChC,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;YACnC,aAAa,CACX,KAAK,EACL,4BAA4B,QAAQ,+CAA+C,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,yFAAyF,CACtM,CAAA;YACD,OAAM;QACR,CAAC;QACD,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAkB,CAAC,CAAA;QACnD,0FAA0F;QAC1F,4EAA4E;QAC5E,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CACb,mDAAmD,QAAQ,wBAAwB,KAAK,CAAC,IAAI,mCAAmC,CACjI,CAAA;QACH,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3C,UAAU,CAAC,CAAC,CAAC,CAAA;YACb,OAAO,CAAC,CAAC,CAAC,CAAA;YACV,WAAW,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC;IACH,CAAC,CAAA;IAED,MAAM,cAAc,GAAG,CAAC,KAAY,EAAW,EAAE;QAC/C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YACjC,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS;gBAAE,OAAO,KAAK,CAAA;QAC3D,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC,CAAA;IAED,MAAM,gBAAgB,GAAG,GAAS,EAAE;QAClC,OAAO,cAAc,GAAG,YAAY,IAAI,cAAc,CAAC,cAAc,GAAG,CAAC,CAAC;YAAE,cAAc,IAAI,CAAC,CAAA;IACjG,CAAC,CAAA;IAED,iGAAiG;IACjG,iEAAiE;IACjE,WAAW,CAAC,UAAU,CAAC,CAAA;IAEvB,OAAO;QACL,OAAO;YACL,MAAM,KAAK,GAAG,YAAY,GAAG,CAAC,CAAA;YAC9B,UAAU,CAAC,KAAK,CAAC,CAAA;YACjB,OAAO,CAAC,KAAK,CAAC,CAAA;YACd,WAAW,CAAC,KAAK,CAAC,CAAA;YAClB,YAAY,GAAG,KAAK,CAAA;YACpB,gBAAgB,EAAE,CAAA;YAClB,OAAO,KAAK,CAAA;QACd,CAAC;QAED,WAAW,CAAC,MAAgB,EAAE,KAAY,EAAE,KAAiB;YAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;YAC7B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,UAAU,EAAE,CAAC;gBACpD,MAAM,IAAI,KAAK,CACb,2BAA2B,MAAM,CAAC,KAAK,CAAC,+DAA+D,UAAU,GAAG,CAAC,2BAA2B,CACjJ,CAAA;YACH,CAAC;YACD,IAAI,KAAK,GAAG,YAAY,GAAG,iBAAiB,EAAE,CAAC;gBAC7C,MAAM,IAAI,KAAK,CACb,qCAAqC,KAAK,iBAAiB,iBAAiB,0BAA0B,YAAY,oFAAoF,CACvM,CAAA;YACH,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YACjC,IAAI,KAAK,IAAI,YAAY,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;gBAClD,aAAa,CACX,KAAK,EACL,SAAS,KAAK,yBAAyB,aAAa,gJAAgJ,CACrM,CAAA;gBACD,OAAM;YACR,CAAC;YACD,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;YAC9F,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;YACnC,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;YAClC,IAAI,KAAK,IAAI,YAAY,IAAI,YAAY;gBAAE,UAAU,CAAC,KAAK,CAAC,CAAA;YAC5D,gBAAgB,EAAE,CAAA;QACpB,CAAC;QAED,IAAI,YAAY;YACd,OAAO,YAAY,CAAA;QACrB,CAAC;QAED,IAAI,cAAc;YAChB,OAAO,cAAc,CAAA;QACvB,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ecsia/rollback",
|
|
3
|
+
"version": "0.25.0",
|
|
4
|
+
"description": "Handle-stable whole-world capture and restore for ecsia: the checkpoint substrate for rollback netcode and prediction.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ecs",
|
|
7
|
+
"entity-component-system",
|
|
8
|
+
"ecsia",
|
|
9
|
+
"rollback",
|
|
10
|
+
"netcode",
|
|
11
|
+
"prediction",
|
|
12
|
+
"snapshot",
|
|
13
|
+
"data-oriented",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Andy Aragon",
|
|
18
|
+
"homepage": "https://github.com/andymai/ecsia",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/andymai/ecsia.git",
|
|
22
|
+
"directory": "packages/rollback"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/andymai/ecsia/issues"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.13"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"main": "./dist/index.js",
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -b"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@ecsia/core": "workspace:*",
|
|
51
|
+
"@ecsia/schema": "workspace:*"
|
|
52
|
+
}
|
|
53
|
+
}
|