@driftengine/network 4.7.4 → 4.8.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/dist/lockstep.d.ts +15 -0
- package/dist/lockstep.js +27 -0
- package/dist/transport.d.ts +7 -0
- package/dist/transport.js +11 -0
- package/dist/webrtc.d.ts +2 -1
- package/dist/webrtc.js +2 -2
- package/dist/websocket.d.ts +6 -1
- package/dist/websocket.js +2 -2
- package/package.json +3 -3
- package/src/lockstep.ts +26 -0
- package/src/transport.ts +12 -0
- package/src/webrtc.ts +4 -2
- package/src/websocket.ts +8 -2
package/dist/lockstep.d.ts
CHANGED
|
@@ -106,6 +106,8 @@ export declare class LockstepSession<S> {
|
|
|
106
106
|
/** Digests we published, by tick, so a peer's claim has something to be compared against. */
|
|
107
107
|
private readonly ourDigests;
|
|
108
108
|
private statusValue;
|
|
109
|
+
/** The tick of this session's first `submit`, or -1 before it. See `open`. */
|
|
110
|
+
private openedAt;
|
|
109
111
|
private desyncValue;
|
|
110
112
|
private haltReason;
|
|
111
113
|
private confirmedValue;
|
|
@@ -136,6 +138,19 @@ export declare class LockstepSession<S> {
|
|
|
136
138
|
* prediction is supposed to prevent.
|
|
137
139
|
*/
|
|
138
140
|
submit(tick: number, payload: Uint8Array): void;
|
|
141
|
+
/**
|
|
142
|
+
* The ticks before anybody's first input, settled as neutral for every participant.
|
|
143
|
+
*
|
|
144
|
+
* **Nobody can publish an input for them**: the earliest a peer publishes is its first tick plus
|
|
145
|
+
* the input delay. So nothing will ever arrive to confirm or correct them, and whatever a peer
|
|
146
|
+
* guesses for them is final. A guess repeats the newest input the log holds, and for this peer's
|
|
147
|
+
* own participant that was the input just submitted for a *later* tick, which no other peer can
|
|
148
|
+
* know: a player holding a control on the first tick split the worlds there, and the first
|
|
149
|
+
* fingerprint halted the session. Neutral is the one answer every peer reaches alone, because
|
|
150
|
+
* nobody had pressed anything before the game began. Lockstep peers start on the same tick, so
|
|
151
|
+
* they all settle the same ticks.
|
|
152
|
+
*/
|
|
153
|
+
private open;
|
|
139
154
|
/**
|
|
140
155
|
* Keep this input, and the few before it, for the next packet to carry.
|
|
141
156
|
*
|
package/dist/lockstep.js
CHANGED
|
@@ -58,6 +58,8 @@ export class LockstepSession {
|
|
|
58
58
|
/** Digests we published, by tick, so a peer's claim has something to be compared against. */
|
|
59
59
|
ourDigests = new Map();
|
|
60
60
|
statusValue = 'running';
|
|
61
|
+
/** The tick of this session's first `submit`, or -1 before it. See `open`. */
|
|
62
|
+
openedAt = -1;
|
|
61
63
|
desyncValue = null;
|
|
62
64
|
haltReason = '';
|
|
63
65
|
confirmedValue = -1;
|
|
@@ -111,6 +113,8 @@ export class LockstepSession {
|
|
|
111
113
|
if (this.statusValue !== 'running')
|
|
112
114
|
return;
|
|
113
115
|
const at = tick + this.inputDelay;
|
|
116
|
+
if (this.openedAt < 0)
|
|
117
|
+
this.open(tick);
|
|
114
118
|
if (!this.inputs.holds(at)) {
|
|
115
119
|
this.halt(`an input for tick ${at} is outside the log's window`);
|
|
116
120
|
return;
|
|
@@ -124,6 +128,29 @@ export class LockstepSession {
|
|
|
124
128
|
}
|
|
125
129
|
this.transport.send(BROADCAST, this.outgoing.subarray(0, length));
|
|
126
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* The ticks before anybody's first input, settled as neutral for every participant.
|
|
133
|
+
*
|
|
134
|
+
* **Nobody can publish an input for them**: the earliest a peer publishes is its first tick plus
|
|
135
|
+
* the input delay. So nothing will ever arrive to confirm or correct them, and whatever a peer
|
|
136
|
+
* guesses for them is final. A guess repeats the newest input the log holds, and for this peer's
|
|
137
|
+
* own participant that was the input just submitted for a *later* tick, which no other peer can
|
|
138
|
+
* know: a player holding a control on the first tick split the worlds there, and the first
|
|
139
|
+
* fingerprint halted the session. Neutral is the one answer every peer reaches alone, because
|
|
140
|
+
* nobody had pressed anything before the game began. Lockstep peers start on the same tick, so
|
|
141
|
+
* they all settle the same ticks.
|
|
142
|
+
*/
|
|
143
|
+
open(tick) {
|
|
144
|
+
this.openedAt = tick;
|
|
145
|
+
const neutral = new Uint8Array(this.inputs.inputBytes);
|
|
146
|
+
for (let at = tick; at < tick + this.inputDelay; at++) {
|
|
147
|
+
if (!this.inputs.holds(at))
|
|
148
|
+
continue;
|
|
149
|
+
for (let participant = 0; participant < this.participants; participant++) {
|
|
150
|
+
this.inputs.set(participant, at, neutral);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
127
154
|
/**
|
|
128
155
|
* Keep this input, and the few before it, for the next packet to carry.
|
|
129
156
|
*
|
package/dist/transport.d.ts
CHANGED
|
@@ -61,3 +61,10 @@ export interface Transport {
|
|
|
61
61
|
drain(into: MessageSink): void;
|
|
62
62
|
close(): void;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The message as a view of an `ArrayBuffer`, which is what a socket or a channel will send.
|
|
66
|
+
*
|
|
67
|
+
* Everything this package sends is already one, so this is a check and no copy. A message on
|
|
68
|
+
* shared memory, which a browser refuses to send, is copied off it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function sendable(message: Uint8Array): Uint8Array<ArrayBuffer>;
|
package/dist/transport.js
CHANGED
|
@@ -38,3 +38,14 @@
|
|
|
38
38
|
*/
|
|
39
39
|
/** Send to everyone the transport can reach. */
|
|
40
40
|
export const BROADCAST = -1;
|
|
41
|
+
/**
|
|
42
|
+
* The message as a view of an `ArrayBuffer`, which is what a socket or a channel will send.
|
|
43
|
+
*
|
|
44
|
+
* Everything this package sends is already one, so this is a check and no copy. A message on
|
|
45
|
+
* shared memory, which a browser refuses to send, is copied off it.
|
|
46
|
+
*/
|
|
47
|
+
export function sendable(message) {
|
|
48
|
+
return message.buffer instanceof ArrayBuffer
|
|
49
|
+
? message
|
|
50
|
+
: message.slice();
|
|
51
|
+
}
|
package/dist/webrtc.d.ts
CHANGED
|
@@ -35,7 +35,8 @@ import { type MessageSink, type PeerId, type Transport, type TransportState } fr
|
|
|
35
35
|
export interface DataChannelLike {
|
|
36
36
|
readonly readyState: string;
|
|
37
37
|
binaryType?: string;
|
|
38
|
-
|
|
38
|
+
/** An `ArrayBuffer`'s view, as for a socket: see `SocketLike.send`. */
|
|
39
|
+
send(data: ArrayBufferView<ArrayBuffer> | ArrayBuffer): void;
|
|
39
40
|
close(): void;
|
|
40
41
|
addEventListener(type: string, listener: (event: unknown) => void): void;
|
|
41
42
|
}
|
package/dist/webrtc.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* That is a weaker guarantee than the loopback transport has, and it is why WebSocket is the
|
|
31
31
|
* default rather than this.
|
|
32
32
|
*/
|
|
33
|
-
import { BROADCAST, } from './transport.js';
|
|
33
|
+
import { BROADCAST, sendable, } from './transport.js';
|
|
34
34
|
/**
|
|
35
35
|
* A channel configured the way a simulation wants one.
|
|
36
36
|
*
|
|
@@ -83,7 +83,7 @@ export class WebRtcTransport {
|
|
|
83
83
|
send(_to, message) {
|
|
84
84
|
if (this.state !== 'open')
|
|
85
85
|
return;
|
|
86
|
-
this.channel.send(message);
|
|
86
|
+
this.channel.send(sendable(message));
|
|
87
87
|
}
|
|
88
88
|
drain(into) {
|
|
89
89
|
for (const message of this.arrived)
|
package/dist/websocket.d.ts
CHANGED
|
@@ -28,7 +28,12 @@ import { type MessageSink, type PeerId, type Transport, type TransportState } fr
|
|
|
28
28
|
export interface SocketLike {
|
|
29
29
|
readonly readyState: number;
|
|
30
30
|
binaryType?: string;
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* A view of an `ArrayBuffer`, never of shared memory: that is what a browser's `WebSocket.send`
|
|
33
|
+
* takes, and declaring any buffer here made a browser socket not fit its own transport, so
|
|
34
|
+
* `new WebSocketTransport({ socket: new WebSocket(url) })` did not typecheck.
|
|
35
|
+
*/
|
|
36
|
+
send(data: ArrayBufferView<ArrayBuffer> | ArrayBuffer): void;
|
|
32
37
|
close(): void;
|
|
33
38
|
addEventListener(type: string, listener: (event: unknown) => void): void;
|
|
34
39
|
}
|
package/dist/websocket.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* the credentials and the reconnection policy. A transport that opened its own connection would be
|
|
24
24
|
* making four decisions that belong to whoever is paying for the server.
|
|
25
25
|
*/
|
|
26
|
-
import { BROADCAST, } from './transport.js';
|
|
26
|
+
import { BROADCAST, sendable, } from './transport.js';
|
|
27
27
|
/** The `readyState` values, which are the same three numbers everywhere. */
|
|
28
28
|
const OPEN = 1;
|
|
29
29
|
const CLOSED = 3;
|
|
@@ -73,7 +73,7 @@ export class WebSocketTransport {
|
|
|
73
73
|
send(_to, message) {
|
|
74
74
|
if (this.state !== 'open')
|
|
75
75
|
return;
|
|
76
|
-
this.socket.send(message);
|
|
76
|
+
this.socket.send(sendable(message));
|
|
77
77
|
}
|
|
78
78
|
drain(into) {
|
|
79
79
|
for (const message of this.arrived)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/network",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"description": "A transport seam, rewind and replay, lockstep and an authoritative host, on the fixed-step loop",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@driftengine/entities": "4.
|
|
30
|
+
"@driftengine/entities": "4.8.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@driftengine/core": "4.
|
|
33
|
+
"@driftengine/core": "4.8.0"
|
|
34
34
|
},
|
|
35
35
|
"author": "Drift Technologies",
|
|
36
36
|
"repository": {
|
package/src/lockstep.ts
CHANGED
|
@@ -122,6 +122,8 @@ export class LockstepSession<S> {
|
|
|
122
122
|
private readonly ourDigests = new Map<number, string>();
|
|
123
123
|
|
|
124
124
|
private statusValue: LockstepStatus = 'running';
|
|
125
|
+
/** The tick of this session's first `submit`, or -1 before it. See `open`. */
|
|
126
|
+
private openedAt = -1;
|
|
125
127
|
private desyncValue: Desync | null = null;
|
|
126
128
|
private haltReason = '';
|
|
127
129
|
private confirmedValue = -1;
|
|
@@ -180,6 +182,7 @@ export class LockstepSession<S> {
|
|
|
180
182
|
submit(tick: number, payload: Uint8Array): void {
|
|
181
183
|
if (this.statusValue !== 'running') return;
|
|
182
184
|
const at = tick + this.inputDelay;
|
|
185
|
+
if (this.openedAt < 0) this.open(tick);
|
|
183
186
|
|
|
184
187
|
if (!this.inputs.holds(at)) {
|
|
185
188
|
this.halt(`an input for tick ${at} is outside the log's window`);
|
|
@@ -203,6 +206,29 @@ export class LockstepSession<S> {
|
|
|
203
206
|
this.transport.send(BROADCAST, this.outgoing.subarray(0, length));
|
|
204
207
|
}
|
|
205
208
|
|
|
209
|
+
/**
|
|
210
|
+
* The ticks before anybody's first input, settled as neutral for every participant.
|
|
211
|
+
*
|
|
212
|
+
* **Nobody can publish an input for them**: the earliest a peer publishes is its first tick plus
|
|
213
|
+
* the input delay. So nothing will ever arrive to confirm or correct them, and whatever a peer
|
|
214
|
+
* guesses for them is final. A guess repeats the newest input the log holds, and for this peer's
|
|
215
|
+
* own participant that was the input just submitted for a *later* tick, which no other peer can
|
|
216
|
+
* know: a player holding a control on the first tick split the worlds there, and the first
|
|
217
|
+
* fingerprint halted the session. Neutral is the one answer every peer reaches alone, because
|
|
218
|
+
* nobody had pressed anything before the game began. Lockstep peers start on the same tick, so
|
|
219
|
+
* they all settle the same ticks.
|
|
220
|
+
*/
|
|
221
|
+
private open(tick: number): void {
|
|
222
|
+
this.openedAt = tick;
|
|
223
|
+
const neutral = new Uint8Array(this.inputs.inputBytes);
|
|
224
|
+
for (let at = tick; at < tick + this.inputDelay; at++) {
|
|
225
|
+
if (!this.inputs.holds(at)) continue;
|
|
226
|
+
for (let participant = 0; participant < this.participants; participant++) {
|
|
227
|
+
this.inputs.set(participant, at, neutral);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
206
232
|
/**
|
|
207
233
|
* Keep this input, and the few before it, for the next packet to carry.
|
|
208
234
|
*
|
package/src/transport.ts
CHANGED
|
@@ -67,3 +67,15 @@ export interface Transport {
|
|
|
67
67
|
drain(into: MessageSink): void;
|
|
68
68
|
close(): void;
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The message as a view of an `ArrayBuffer`, which is what a socket or a channel will send.
|
|
73
|
+
*
|
|
74
|
+
* Everything this package sends is already one, so this is a check and no copy. A message on
|
|
75
|
+
* shared memory, which a browser refuses to send, is copied off it.
|
|
76
|
+
*/
|
|
77
|
+
export function sendable(message: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
78
|
+
return message.buffer instanceof ArrayBuffer
|
|
79
|
+
? (message as Uint8Array<ArrayBuffer>)
|
|
80
|
+
: message.slice();
|
|
81
|
+
}
|
package/src/webrtc.ts
CHANGED
|
@@ -36,13 +36,15 @@ import {
|
|
|
36
36
|
type PeerId,
|
|
37
37
|
type Transport,
|
|
38
38
|
type TransportState,
|
|
39
|
+
sendable,
|
|
39
40
|
} from './transport.ts';
|
|
40
41
|
|
|
41
42
|
/** What an `RTCDataChannel` satisfies. Declared rather than imported; see `websocket.ts`. */
|
|
42
43
|
export interface DataChannelLike {
|
|
43
44
|
readonly readyState: string;
|
|
44
45
|
binaryType?: string;
|
|
45
|
-
|
|
46
|
+
/** An `ArrayBuffer`'s view, as for a socket: see `SocketLike.send`. */
|
|
47
|
+
send(data: ArrayBufferView<ArrayBuffer> | ArrayBuffer): void;
|
|
46
48
|
close(): void;
|
|
47
49
|
addEventListener(type: string, listener: (event: unknown) => void): void;
|
|
48
50
|
}
|
|
@@ -116,7 +118,7 @@ export class WebRtcTransport implements Transport {
|
|
|
116
118
|
|
|
117
119
|
send(_to: PeerId, message: Uint8Array): void {
|
|
118
120
|
if (this.state !== 'open') return;
|
|
119
|
-
this.channel.send(message);
|
|
121
|
+
this.channel.send(sendable(message));
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
drain(into: MessageSink): void {
|
package/src/websocket.ts
CHANGED
|
@@ -29,13 +29,19 @@ import {
|
|
|
29
29
|
type PeerId,
|
|
30
30
|
type Transport,
|
|
31
31
|
type TransportState,
|
|
32
|
+
sendable,
|
|
32
33
|
} from './transport.ts';
|
|
33
34
|
|
|
34
35
|
/** What a browser `WebSocket` and a Node `ws` both satisfy. Named here so neither is imported. */
|
|
35
36
|
export interface SocketLike {
|
|
36
37
|
readonly readyState: number;
|
|
37
38
|
binaryType?: string;
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* A view of an `ArrayBuffer`, never of shared memory: that is what a browser's `WebSocket.send`
|
|
41
|
+
* takes, and declaring any buffer here made a browser socket not fit its own transport, so
|
|
42
|
+
* `new WebSocketTransport({ socket: new WebSocket(url) })` did not typecheck.
|
|
43
|
+
*/
|
|
44
|
+
send(data: ArrayBufferView<ArrayBuffer> | ArrayBuffer): void;
|
|
39
45
|
close(): void;
|
|
40
46
|
addEventListener(type: string, listener: (event: unknown) => void): void;
|
|
41
47
|
}
|
|
@@ -108,7 +114,7 @@ export class WebSocketTransport implements Transport {
|
|
|
108
114
|
*/
|
|
109
115
|
send(_to: PeerId, message: Uint8Array): void {
|
|
110
116
|
if (this.state !== 'open') return;
|
|
111
|
-
this.socket.send(message);
|
|
117
|
+
this.socket.send(sendable(message));
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
drain(into: MessageSink): void {
|