@hopple/netcode 0.0.0-stage → 0.1.1-ci.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Hopple authors
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 CHANGED
@@ -1,3 +1,198 @@
1
- # Temporary Holding Version
1
+ # @hopple/netcode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ What a game bundles to play a match over the network, on the Hopple SDK's
4
+ match: its messages as bytes, the lobby between games, and a lockstep helper,
5
+ with rollback.
6
+
7
+ ```bash
8
+ npm install @hopple/sdk-preview-network @hopple/netcode
9
+ ```
10
+
11
+ A game **bundles** it, as it bundles
12
+ [`@hopple/deterministic`](https://www.npmjs.com/package/@hopple/deterministic):
13
+ whatever decides the game is frozen into the game when it is built, so every
14
+ device in a match runs the same bytes. It needs nothing but the game's own
15
+ `@hopple/sdk` and
16
+ [`@hopple/sdk-preview-network`](https://www.npmjs.com/package/@hopple/sdk-preview-network), peer
17
+ dependencies, runs the same in browsers and in Node, and a game takes only the
18
+ parts it imports. Install `@hopple/sdk-preview-network` beside it, as above, even where a
19
+ package manager would install it for you: depending on it is what tells
20
+ `hopple` the game plays over the network, and at which revision.
21
+
22
+ The only contract is the network preview's match: one log, every message in one order on
23
+ every device, the sender's own included, and what each player kept there for
24
+ whoever comes later. A game is welcome to write any part of this its own way.
25
+
26
+ ## Bytes
27
+
28
+ A match's messages are bytes. `ByteWriter` and `ByteReader`, from
29
+ [`@hopple/bytes`](https://www.npmjs.com/package/@hopple/bytes) and exported
30
+ here as well, write numbers, strings and packed bits, and read them back
31
+ exactly: integers as varints (a byte under 128), floats as their bits,
32
+ strings as UTF-8.
33
+
34
+ ```ts
35
+ import { ByteReader, ByteWriter } from '@hopple/netcode';
36
+
37
+ match.send(new ByteWriter().uint(PICK).uint(fighter).bool(ready).finish());
38
+
39
+ const read = new ByteReader(data); // throws a RangeError on a malformed message
40
+ ```
41
+
42
+ ## The lobby
43
+
44
+ `@hopple/netcode/session` is the lobby every networked game has between its
45
+ games: each player's pick and readiness, the host's settings, and the start of
46
+ each game.
47
+
48
+ ```ts
49
+ import { ByteWriter } from '@hopple/netcode';
50
+ import { session } from '@hopple/netcode/session';
51
+
52
+ const lobby = session({
53
+ match: sdk.match,
54
+ // What a player picks, and how it travels: written after the session's own bytes.
55
+ pick: { initial: 0, write: (out, fighter) => out.uint(fighter), read: (input) => input.uint() },
56
+ // The host's settings.
57
+ settings: { initial: { rounds: 3 }, write: (out, s) => out.uint(s.rounds), read: (input) => ({ rounds: input.uint() }) },
58
+ // What the host's start carries of the game's own.
59
+ config: { write: (out, c) => out.uint(c.rounds), read: (input) => ({ rounds: input.uint() }) },
60
+ // Every device in the match, at the same place in the log: build the game from `start` alone.
61
+ onStart: (start) => play(start),
62
+ });
63
+
64
+ await sdk.match.play({ maxPlayers: 4 }); // the party's match, or a new one
65
+ lobby.pick(fighter);
66
+ lobby.ready(true);
67
+ for (const one of lobby.players) drawPlayer(one.person.name, one.pick, one.ready, one.host);
68
+
69
+ if (lobby.hosting) {
70
+ lobby.configure({ rounds: 5 });
71
+ if (lobby.waiting.length === 0) lobby.start({ rounds: lobby.settings.rounds });
72
+ }
73
+ ```
74
+
75
+ - **Nothing is said twice.** A pick and readiness are one message kept under
76
+ one key, the host's settings one kept under another: whoever joins later, or
77
+ whose game loads again, finds them in `match.kept`. A game loaded again picks
78
+ up its own pick, readiness and, as host, settings.
79
+ - **The host starts each game with one message** carrying a run number, a
80
+ seed, who sits where (every player in the order they joined, or the seats
81
+ `start` is given), what each picked as the host saw it, and the game's
82
+ config. Every device hears it at the same `seq` and builds the same game; a
83
+ device whose person has no seat watches. A start from anybody else starts
84
+ nothing. Nobody is ready for the next game until they say so again.
85
+ - **The host is the match's**: the player who made it, then whoever it was
86
+ handed to, then, once the host has been disconnected or away for 10 s, the
87
+ one still there who has played it longest. Read `lobby.hosting` every frame, not once. A new
88
+ host keeps the settings the last one left. `lobby.waiting` leaves out
89
+ anybody whose connection is down: seated anyway, a lockstep run hands their
90
+ seat to the game until they are back.
91
+ - **Being in the match is the game's.** The session follows
92
+ `sdk.match.current` and never joins or makes a match itself: the game calls
93
+ `sdk.match.play()` or `create()`, or the host puts the player in one from
94
+ an invite or a link, and the session is the lobby of whichever match that
95
+ is. A player whose connection comes back says their pick, and as host the
96
+ settings, again, as what was said as it went may have gone with it.
97
+ - Its messages start with `SESSION_TAG`, and `isSessionMessage` tells them
98
+ apart; it keeps under keys 0 and 1 unless `keys` says otherwise.
99
+
100
+ ## Lockstep, if it suits
101
+
102
+ `@hopple/netcode/lockstep` is one way to keep a match's devices in step: every
103
+ device runs the same game, step by step, on every player's input for that
104
+ step. Its game is one that computes the same everywhere: `@hopple/deterministic`.
105
+
106
+ ```ts
107
+ import { hash } from '@hopple/deterministic';
108
+ import { lockstep } from '@hopple/netcode/lockstep';
109
+
110
+ // The session's start has what a run needs: the match, its `seq`, the run, the seats and `me`.
111
+ function play(start) {
112
+ game = newGame(start.seed, start.picks, start.config);
113
+ run = lockstep({
114
+ ...start,
115
+ sample: () => Uint8Array.of(buttons()), // this device's input, a few bytes
116
+ step: (inputs) => game.step(inputs), // every seat's, or null for one the game plays
117
+ hash: () => hash(game.summary()),
118
+ onDesync: (tick) => report(tick),
119
+ onLost: () => leave(),
120
+ });
121
+ }
122
+
123
+ sdk.loop.tick((dt) => {
124
+ run?.advance(dt * 1000);
125
+ draw();
126
+ });
127
+ ```
128
+
129
+ - **Several players on one device.** The session's start says how many play
130
+ on each seat's device, as the match counts them (`start.locals`), and every
131
+ player of the game seat by seat (`start.players`: `seat`, `person`, `local`,
132
+ `me`). Given `locals`, which `...start` hands it, `sample` returns one input
133
+ for each player on this device and `step` is given one for each player of the
134
+ match; a device's players still travel in one message, and a device handed to
135
+ the game hands over each of its players.
136
+
137
+ ```ts
138
+ const mine = start.players.filter((one) => one.me).length;
139
+ sample: () => sdk.input.players.slice(0, mine).map((player) => bits(player.read())),
140
+ step: (inputs) => game.step(inputs), // one for each of start.players
141
+ ```
142
+
143
+ Without the session, a run is made in the listener that hears the message
144
+ starting it, with that message's `seq`, a `run` number that differs from the
145
+ match's other runs, and the same `seats` on every device.
146
+
147
+ - **Input delay, as the network needs.** Each device's input is read some
148
+ steps ahead of the step it is for, so it can cross the relay in time. Every
149
+ device times its own round trips and says how many steps it needs, and all of
150
+ them read with the most any player needs: the same delay for everybody, so
151
+ nobody's controls answer later than another's. It grows as soon as somebody
152
+ needs more and shrinks after a few seconds of needing less. A game sets its
153
+ bounds, the same on every device: `delay: { min, max, start }` (2, 20 and 6
154
+ steps by default; a step is 16.7 ms). Round trips longer than `max` slow the
155
+ match down; `min` equal to `max` holds the delay fixed. `run.delay` is the
156
+ delay now, and `run.rtt` the last round trip.
157
+ - **Sampling.** `sample` is asked for each step in order, but not always once a
158
+ frame: several times while the delay grows, and now and then not at all while
159
+ it shrinks. Give each press to the one call after it.
160
+ - **Waiting and catching up.** A step runs once every seat's input for it is
161
+ in, the device's own included. Until then the game waits; after a hitch, or
162
+ time in the background, it catches up, up to `catchUp` steps a frame.
163
+ - **Seats handed to the game.** A seat whose person left the match, or kept
164
+ everybody waiting for `timeoutMs`, reaches `step` as `null` from a step every
165
+ device agrees on: the game plays it (its AI, or nobody). Its device, once
166
+ caught up, asks for it back.
167
+ - **Rollback, optionally.** With `rollback: { steps, save, restore, predict? }`
168
+ a device does not wait for the others' inputs: it runs on, up to `steps`
169
+ ahead of the log, guessing that each player still presses what they last
170
+ pressed (`predict` says otherwise, for inputs that say what went down this
171
+ step). When an input differs from the guess, it `restore`s the game to before
172
+ that step and runs it and the steps after it again, with `again` true: what
173
+ those played and showed has been. The delay then covers only the round trip
174
+ beyond what guessing covers, so a player's own controls answer after a step
175
+ or two. `run.confirmed` is how far the log has settled; hold back anything
176
+ that cannot be taken back (achievements, scores, the match's end) until it
177
+ passes the step. Somebody who only watches runs confirmed steps alone.
178
+
179
+ Lockstep suits games whose state is large or hard to copy, that are slow
180
+ enough not to mind the delay (turns, strategy, puzzles), or whose steps are
181
+ expensive. Rollback suits fast games with a small state, where a press must
182
+ answer at once: a bat, a fighter, a racer. It costs a `save` a step and up to
183
+ `steps` steps run again in a frame, and others' moves can jump when a guess
184
+ was wrong.
185
+ - **Desyncs.** Every `hashEvery` steps the devices compare fingerprints, and
186
+ `onDesync` says from which step they differ.
187
+ - **A device whose connection dropped** hears what it missed once it is back,
188
+ and carries on (its seat may have been handed to the game meanwhile, and is
189
+ asked back). One that missed more than the relay still held, a gap in its
190
+ numbering, cannot follow any more: `onLost`.
191
+ - **Thirty messages a second** at most from each device, a few bytes each.
192
+ - Messages of the run start with `LOCKSTEP_TAG`; a game's own messages in the
193
+ same match start with any other byte, and `isLockstepMessage` tells them
194
+ apart.
195
+
196
+ ## License
197
+
198
+ MIT; see `LICENSE`.
package/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { ByteReader, ByteWriter } from "@hopple/bytes";
2
+ export { ByteReader, ByteWriter };
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { ByteReader, ByteWriter } from "@hopple/bytes";
2
+ export { ByteReader, ByteWriter };
package/lockstep.d.ts ADDED
@@ -0,0 +1,252 @@
1
+ //#region src/lockstep.d.ts
2
+ /**
3
+ * One way to run a match in lockstep: every device runs the same game, step by
4
+ * step, on every player's input for that step. Optional: a game is welcome to
5
+ * keep its devices in step its own way.
6
+ *
7
+ * **The match is a log.** Every device in a match — the sender included —
8
+ * hears every message in the relay's one order, and every decision below is
9
+ * made from that log alone. Two devices
10
+ * that heard the same log run the same steps with the same inputs, whenever
11
+ * the messages happened to reach each of them.
12
+ *
13
+ * **Input delay, as long as the slowest needs.** Each device's input for step t
14
+ * is read some steps earlier, the delay, and sent, so it has time to cross the
15
+ * relay before anybody needs it. A step runs once every seat's input for it has
16
+ * come back through the log, this device's own included; until then the game
17
+ * waits, and catches up after. Every device times its own messages' round trip
18
+ * and says how many steps it needs, and every device reads with the most any
19
+ * playing seat needs, within the game's bounds: the same delay for everybody,
20
+ * so nobody's controls answer later than another's. It grows as soon as a seat
21
+ * needs more, and shrinks once they have all needed less for a few seconds.
22
+ * Where each device reads from changes nothing in the log's order, so the
23
+ * delay can change without the devices agreeing on when.
24
+ *
25
+ * **Seats are handed to the game, and back.** A seat whose person left the
26
+ * match, or went quiet for `timeoutMs` while the others waited, is dropped:
27
+ * from a step every device works out from the log, its input arrives as `null`,
28
+ * and the game plays it (its AI, or nobody). Its device, once caught up, asks
29
+ * for it back, and gets it from a step that, again, the log decides. A drop
30
+ * takes effect from the first step the seat's input is missing for in the log
31
+ * before the drop, and a return after the furthest step any device could have
32
+ * run, so no device has ever run a step the others will run differently.
33
+ *
34
+ * **Desyncs and lost devices are reported.** Every `hashEvery` steps each device
35
+ * that plays sends a fingerprint of its state, and a difference is reported with
36
+ * the step it was found at. A device whose connection dropped for a while
37
+ * hears what it missed once it is back, and carries on: what it sent as the
38
+ * connection went, and never heard come back before a later message of its
39
+ * own, it sends again. One that missed more than the relay still held — a gap
40
+ * in the relay's numbering — cannot follow the log any longer, and says so.
41
+ *
42
+ * **Rollback, optionally.** With `rollback`, a device does not wait for the
43
+ * others' inputs: it runs on, up to `rollback.steps` steps ahead of the log,
44
+ * guessing what they press from what they pressed last, and shows that. When an
45
+ * input comes in that differs from the guess, it puts the game back where it was
46
+ * before that step (`restore`) and runs the steps again, so the picture catches
47
+ * up with what was pressed. The delay then has only to cover the round trip
48
+ * beyond what guessing covers, and a player's own controls answer after a step
49
+ * or two whatever the connection. Only the log confirms a step, this device's
50
+ * own input included, so what every device agrees on, fingerprints, drops and
51
+ * returns, is decided as it is without rollback; a guess past a drop or a return
52
+ * is simply run again. Somebody who only watches runs confirmed steps alone.
53
+ *
54
+ * Messages are small: varints, inputs as the game's own bytes, `sendEvery`
55
+ * steps a message. A match's other messages (a game's lobby, say) share the
56
+ * log: tell them apart with `isLockstepMessage`.
57
+ *
58
+ * @module lockstep
59
+ */
60
+ /**
61
+ * The first byte of every message this helper sends. A game sending messages
62
+ * of its own in the same match starts them with any other byte.
63
+ *
64
+ * @group Lockstep
65
+ */
66
+ declare const LOCKSTEP_TAG = 245;
67
+ /**
68
+ * What the helper needs of a match: `@hopple/sdk-preview-network`'s `Match` has it all.
69
+ *
70
+ * @group Lockstep
71
+ */
72
+ type LockstepMatch = {
73
+ /** Who plays now, for noticing a seat's person has left. */
74
+ readonly players: readonly {
75
+ readonly id: string;
76
+ }[];
77
+ send(data: Uint8Array): void;
78
+ onMessage(listener: (data: Uint8Array, from: {
79
+ readonly id: string;
80
+ }, seq: number) => void): () => void;
81
+ on(listener: () => void): () => void;
82
+ };
83
+ /**
84
+ * What a game gives to run ahead of the log: a way to go back, and how far.
85
+ *
86
+ * @group Lockstep
87
+ */
88
+ type Rollback<S> = {
89
+ /**
90
+ * The most steps the game may run ahead of what the log has confirmed, on
91
+ * guesses: the same on every device. Round trips beyond it, less a couple of
92
+ * steps kept for jitter, are covered by the delay. Every step ahead may be run
93
+ * again in a frame: it is a budget of the game's time as much as of the
94
+ * network's.
95
+ */
96
+ steps: number;
97
+ /** The game's whole state as it is now, to come back to: anything `restore` takes back. */
98
+ save(): S;
99
+ /** Put the game back as `save` found it. */
100
+ restore(saved: S): void;
101
+ /**
102
+ * What a seat presses at a step its input has not come in for, from the last
103
+ * input it is known to have pressed. That input by default: a game whose
104
+ * inputs say what went down this step, rather than what is held, takes that
105
+ * out.
106
+ */
107
+ predict?(last: Uint8Array, seat: number): Uint8Array;
108
+ };
109
+ /**
110
+ * @group Lockstep
111
+ */
112
+ type LockstepOptions<S = unknown> = {
113
+ match: LockstepMatch;
114
+ /**
115
+ * The relay's `seq` of the message that started this run, as every device
116
+ * heard it. Make the run in the listener that heard it: it hears only what
117
+ * follows.
118
+ */
119
+ seq: number;
120
+ /**
121
+ * A number for this run, the same on every device and different from the
122
+ * match's other runs (a rematch is another run), so their messages are told
123
+ * apart.
124
+ */
125
+ run: number;
126
+ /** Who plays, by person id, in seat order: the same list on every device. */
127
+ seats: readonly string[];
128
+ /** This device's person id. A device whose person has no seat only watches. */
129
+ me: string;
130
+ /**
131
+ * Steps between reading an input and running it, as the round trips through
132
+ * the relay need, less what `rollback` guesses: bounds for it, the same on
133
+ * every device. `min` equal to `max` holds it fixed.
134
+ */
135
+ delay?: {
136
+ /** The least. 2 by default. */
137
+ min?: number;
138
+ /** The most. 20 by default: 333 ms at 60 steps a second. Round trips longer than it, and what guessing covers, slow the run down. */
139
+ max?: number;
140
+ /** The delay the first steps need, before any round trip is timed (guessing takes its share of it). 6 by default. */
141
+ start?: number;
142
+ };
143
+ /**
144
+ * How many players play on each seat's device, in seat order: `start.locals`
145
+ * from the session, the same on every device. With it, `sample` gives one
146
+ * input for each player on this device, `step` is given one for each player
147
+ * of the match (seat by seat, each seat's players in order) and `predict`'s
148
+ * `seat` is that player's place among them; a device's players still travel
149
+ * in one message. Left out, each seat is one input.
150
+ */
151
+ locals?: readonly number[];
152
+ /**
153
+ * This device's input for step `tick`: a few bytes, the game's own encoding,
154
+ * or with `locals` one for each of its players.
155
+ * Asked for each step in order: usually once a step, but several times in a
156
+ * frame while the delay grows, and not for a step now and then while it
157
+ * shrinks, so a press goes to the one call after it, whenever that comes.
158
+ * An empty array is what every seat presses before its first input arrives.
159
+ */
160
+ sample(tick: number): Uint8Array | readonly Uint8Array[];
161
+ /**
162
+ * Run step `tick`: each seat's input, in seat order, or `null` for a seat
163
+ * handed to the game; with `locals`, each player's. The same on every
164
+ * device once confirmed.
165
+ *
166
+ * With `rollback`, a step may be run on guesses and then `again`, after the
167
+ * game was put back to before it: what it played or showed the first time was
168
+ * played and shown, and anything that cannot be taken back (an achievement, a
169
+ * score, the end of the match) waits until `confirmed` has passed the step.
170
+ */
171
+ step(inputs: readonly (Uint8Array | null)[], tick: number, again: boolean): void;
172
+ /** A fingerprint of the game's state after a step, to compare between devices: `hash` from this package. */
173
+ hash?(): string;
174
+ /** Steps between fingerprints. 60 by default. */
175
+ hashEvery?: number;
176
+ /** Two devices' states differ after step `tick`. Said once. */
177
+ onDesync?(tick: number): void;
178
+ /** This device missed some of the match's messages and cannot follow it any longer. */
179
+ onLost?(): void;
180
+ /** How long a step is. A sixtieth of a second by default. */
181
+ tickMs?: number;
182
+ /** Steps in a message, at most. 2 by default: thirty messages a second. */
183
+ sendEvery?: number;
184
+ /** How long a seat may say nothing, while the others wait for it, before it is handed to the game. 3 s by default; `Infinity` waits for ever. */
185
+ timeoutMs?: number;
186
+ /** The most steps one `advance` runs to catch up. 30 by default. */
187
+ catchUp?: number;
188
+ /** The wall clock, in milliseconds: for timeouts and pacing, never for the game. */
189
+ now?: () => number;
190
+ /** Run ahead of the log on guesses, and back when they were wrong. Without it, every step waits for every input. */
191
+ rollback?: Rollback<S>;
192
+ };
193
+ /**
194
+ * A lockstep run in progress.
195
+ *
196
+ * @group Lockstep
197
+ */
198
+ type Lockstep = {
199
+ /**
200
+ * Let `ms` of real time pass: run every step that is due and ready, and
201
+ * catch up when behind. Call it once a frame. Returns how many steps ran.
202
+ */
203
+ advance(ms: number): number;
204
+ /** Steps the game has run: the next step to run. Ahead of `confirmed` by the steps it guessed. */
205
+ readonly tick: number;
206
+ /** Steps the log has every input for: the same on every device, and never run again. `tick` without rollback. */
207
+ readonly confirmed: number;
208
+ /** The people the next step waits for, by id: empty when it is not waiting. */
209
+ readonly waiting: readonly string[];
210
+ /** `running`, `lost` once this device missed messages, or `stopped`. */
211
+ readonly status: 'running' | 'lost' | 'stopped';
212
+ /** Whether a seat is played by its person at the next step, rather than handed to the game. */
213
+ playing(seat: number): boolean;
214
+ /** The last round trip of this device's messages through the relay, in ms, or `null` before one. */
215
+ readonly rtt: number | null;
216
+ /** The delay inputs are read with now, in steps: at `max`, the connection is slower than the game allows. */
217
+ readonly delay: number;
218
+ /** Stop listening and running, sending what input it has read first. */
219
+ stop(): void;
220
+ };
221
+ /**
222
+ * Whether a match message is one of a lockstep run's, for a game's own
223
+ * listener to skip.
224
+ *
225
+ * @group Lockstep
226
+ */
227
+ declare function isLockstepMessage(data: Uint8Array): boolean;
228
+ /**
229
+ * Run a match in lockstep. See the module for how.
230
+ *
231
+ * ```ts
232
+ * const run = lockstep({
233
+ * match, seq, run: 1, seats, me: match.me!.id,
234
+ * sample: () => Uint8Array.of(pad.bits()),
235
+ * step: (inputs) => world.step(inputs),
236
+ * hash: () => hash(world.summary()),
237
+ * });
238
+ * sdk.loop.tick((dt) => { run.advance(dt * 1000); draw(); });
239
+ * ```
240
+ *
241
+ * With rollback, the game also says how to go back:
242
+ *
243
+ * ```ts
244
+ * rollback: { steps: 8, save: () => world.save(), restore: (saved) => world.restore(saved) },
245
+ * step: (inputs, tick, again) => world.step(inputs, { quiet: again }),
246
+ * ```
247
+ *
248
+ * @group Lockstep
249
+ */
250
+ declare function lockstep<S>(options: LockstepOptions<S>): Lockstep;
251
+ //#endregion
252
+ export { LOCKSTEP_TAG, Lockstep, LockstepMatch, LockstepOptions, Rollback, isLockstepMessage, lockstep };