@pylonsync/realtime 0.22.12 → 0.22.14
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/connection.d.ts +6 -1
- package/dist/game.d.ts +5 -2
- package/dist/interpolation.d.ts +8 -0
- package/dist/wire.d.ts +1 -1
- package/package.json +1 -1
- package/src/connection.ts +8 -3
- package/src/game.test.ts +12 -1
- package/src/game.ts +7 -3
- package/src/interpolation.test.ts +18 -0
- package/src/interpolation.ts +15 -0
- package/src/rewind.fixtures.json +3571 -0
- package/src/rewind.test.ts +31 -0
- package/src/wire.ts +7 -1
package/dist/connection.d.ts
CHANGED
|
@@ -124,7 +124,12 @@ export interface ShardClient<TSnapshot = unknown, TInput = unknown> {
|
|
|
124
124
|
* acknowledge, or 0 when the connection is not open (the input is not
|
|
125
125
|
* sent, and `onError` hears why).
|
|
126
126
|
*/
|
|
127
|
-
|
|
127
|
+
/**
|
|
128
|
+
* `viewTick`: the tick (fractional) the client was drawing when the
|
|
129
|
+
* player acted, for a shard with lag compensation. `connectShardGame`
|
|
130
|
+
* sets it from the last `frame`.
|
|
131
|
+
*/
|
|
132
|
+
send: (input: TInput, viewTick?: number) => number;
|
|
128
133
|
close: () => void;
|
|
129
134
|
readonly connected: boolean;
|
|
130
135
|
/** The transport of the open (or last) connection, or null before one opened. */
|
package/dist/game.d.ts
CHANGED
|
@@ -65,8 +65,11 @@ export interface ShardGame<TInput = unknown> {
|
|
|
65
65
|
* frame.
|
|
66
66
|
*/
|
|
67
67
|
frame(now?: number): number;
|
|
68
|
-
/**
|
|
69
|
-
*
|
|
68
|
+
/**
|
|
69
|
+
* Send an input; every predictor made by `predict` records it. The input
|
|
70
|
+
* carries the tick the last `frame` drew, for a shard with lag
|
|
71
|
+
* compensation. Returns its sequence number, or 0 when it was not sent.
|
|
72
|
+
*/
|
|
70
73
|
send(input: TInput): number;
|
|
71
74
|
/**
|
|
72
75
|
* A predictor that records every input `send` sends, forgets inputs the
|
package/dist/interpolation.d.ts
CHANGED
|
@@ -72,12 +72,20 @@ export declare class EntityInterpolator {
|
|
|
72
72
|
/** The newest tick the last `update` drew; a render tick never goes
|
|
73
73
|
* below it. */
|
|
74
74
|
private floor;
|
|
75
|
+
private drawn;
|
|
75
76
|
/** Lives that ended, oldest first, so `record` can drop them when
|
|
76
77
|
* `update` does not run (a hidden tab keeps receiving frames). */
|
|
77
78
|
private ended;
|
|
78
79
|
constructor(options?: InterpolationOptions);
|
|
79
80
|
/** The newest tick recorded, or -1. */
|
|
80
81
|
get latestTick(): number;
|
|
82
|
+
/**
|
|
83
|
+
* The tick the last `update` drew: its render tick, raised to what an
|
|
84
|
+
* earlier update drew and capped at the newest tick received, or -1 before
|
|
85
|
+
* the first update. This is the tick the player saw: the view tick for lag
|
|
86
|
+
* compensation.
|
|
87
|
+
*/
|
|
88
|
+
get drawnTick(): number;
|
|
81
89
|
/** Forget everything. The next `update` removes every entity. */
|
|
82
90
|
clear(): void;
|
|
83
91
|
/**
|
package/dist/wire.d.ts
CHANGED
|
@@ -87,7 +87,7 @@ export declare function decodeShardRejection(codec: number, payload: Uint8Array,
|
|
|
87
87
|
* other codec (and a connection that has not seen a frame yet) gets JSON
|
|
88
88
|
* text, which the server always accepts.
|
|
89
89
|
*/
|
|
90
|
-
export declare function encodeShardInput(codec: number | null, input: unknown, clientSeq: number): string | Uint8Array;
|
|
90
|
+
export declare function encodeShardInput(codec: number | null, input: unknown, clientSeq: number, viewTick?: number): string | Uint8Array;
|
|
91
91
|
/** Acks one message may carry (the server refuses more). */
|
|
92
92
|
export declare const MAX_ACKS_PER_MESSAGE = 512;
|
|
93
93
|
/**
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.22.
|
|
6
|
+
"version": "0.22.14",
|
|
7
7
|
"description": "Pylon realtime shard client pieces with no framework: the wire protocol, codecs, and the entity replication decoder and table.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./src/index.ts",
|
package/src/connection.ts
CHANGED
|
@@ -147,7 +147,12 @@ export interface ShardClient<TSnapshot = unknown, TInput = unknown> {
|
|
|
147
147
|
* acknowledge, or 0 when the connection is not open (the input is not
|
|
148
148
|
* sent, and `onError` hears why).
|
|
149
149
|
*/
|
|
150
|
-
|
|
150
|
+
/**
|
|
151
|
+
* `viewTick`: the tick (fractional) the client was drawing when the
|
|
152
|
+
* player acted, for a shard with lag compensation. `connectShardGame`
|
|
153
|
+
* sets it from the last `frame`.
|
|
154
|
+
*/
|
|
155
|
+
send: (input: TInput, viewTick?: number) => number;
|
|
151
156
|
close: () => void;
|
|
152
157
|
readonly connected: boolean;
|
|
153
158
|
/** The transport of the open (or last) connection, or null before one opened. */
|
|
@@ -1041,13 +1046,13 @@ export function connectShard<TSnapshot = unknown, TInput = unknown>(
|
|
|
1041
1046
|
onClose(fn) {
|
|
1042
1047
|
closeHandlers.push(fn);
|
|
1043
1048
|
},
|
|
1044
|
-
send(input: TInput): number {
|
|
1049
|
+
send(input: TInput, viewTick?: number): number {
|
|
1045
1050
|
if (!link || !connected || !link.open) {
|
|
1046
1051
|
dispatchError(new Error("Cannot send: shard connection is not open"));
|
|
1047
1052
|
return 0;
|
|
1048
1053
|
}
|
|
1049
1054
|
clientSeq += 1;
|
|
1050
|
-
link.send(encodeShardInput(codec, input, clientSeq));
|
|
1055
|
+
link.send(encodeShardInput(codec, input, clientSeq, viewTick));
|
|
1051
1056
|
sentAt.set(clientSeq, now());
|
|
1052
1057
|
if (sentAt.size > MAX_TIMED) sentAt.delete(sentAt.keys().next().value as number);
|
|
1053
1058
|
return clientSeq;
|
package/src/game.test.ts
CHANGED
|
@@ -100,7 +100,8 @@ test("a render loop draws entities between frames, and prediction follows acks",
|
|
|
100
100
|
const s2 = game.send({ dx: 1 });
|
|
101
101
|
const s3 = game.send({ dx: 1 });
|
|
102
102
|
expect([s1, s2, s3]).toEqual([1, 2, 3]);
|
|
103
|
-
|
|
103
|
+
// Each input carries the tick the last frame drew, for lag compensation.
|
|
104
|
+
expect(JSON.parse(ws.sent[0])).toEqual({ input: { dx: 1 }, client_seq: 1, view_tick: renderTick });
|
|
104
105
|
deliver(11, 2, { update: [{ id: 1, from: [0, 0, 0], pos: [1.5, 0, 0] }] });
|
|
105
106
|
expect(predicted).toBeCloseTo(2.5);
|
|
106
107
|
expect(game.ack).toBe(2);
|
|
@@ -135,3 +136,13 @@ test("a render loop draws entities between frames, and prediction follows acks",
|
|
|
135
136
|
expect(game.left).toEqual([2]);
|
|
136
137
|
game.close();
|
|
137
138
|
});
|
|
139
|
+
|
|
140
|
+
test("an input sent before the first frame carries no view tick", () => {
|
|
141
|
+
const game = connectShardGame<{ dx: number }>("zone-nv", { subscriberId: "p1", baseUrl: "h", now: () => 0 });
|
|
142
|
+
const ws = sockets[sockets.length - 1];
|
|
143
|
+
ws.open();
|
|
144
|
+
ws.deliver(replicationFrame(1, 0, { full: true, spawn: [{ id: 1, pos: [0, 0, 0] }] }));
|
|
145
|
+
expect(game.send({ dx: 1 })).toBe(1);
|
|
146
|
+
expect(JSON.parse(ws.sent[0])).toEqual({ input: { dx: 1 }, client_seq: 1 });
|
|
147
|
+
game.close();
|
|
148
|
+
});
|
package/src/game.ts
CHANGED
|
@@ -77,8 +77,11 @@ export interface ShardGame<TInput = unknown> {
|
|
|
77
77
|
* frame.
|
|
78
78
|
*/
|
|
79
79
|
frame(now?: number): number;
|
|
80
|
-
/**
|
|
81
|
-
*
|
|
80
|
+
/**
|
|
81
|
+
* Send an input; every predictor made by `predict` records it. The input
|
|
82
|
+
* carries the tick the last `frame` drew, for a shard with lag
|
|
83
|
+
* compensation. Returns its sequence number, or 0 when it was not sent.
|
|
84
|
+
*/
|
|
82
85
|
send(input: TInput): number;
|
|
83
86
|
/**
|
|
84
87
|
* A predictor that records every input `send` sends, forgets inputs the
|
|
@@ -160,7 +163,8 @@ export function connectShardGame<TInput = unknown>(
|
|
|
160
163
|
return renderTick;
|
|
161
164
|
},
|
|
162
165
|
send(input) {
|
|
163
|
-
const
|
|
166
|
+
const drawn = interpolator.drawnTick;
|
|
167
|
+
const seq = connection.send(input, drawn >= 0 ? drawn : undefined);
|
|
164
168
|
for (const p of predictors) p.push(seq, input);
|
|
165
169
|
return seq;
|
|
166
170
|
},
|
|
@@ -21,6 +21,24 @@ function harness(options?: InterpolationOptions) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
describe("EntityInterpolator", () => {
|
|
24
|
+
test("drawnTick is the tick an update placed entities at, never going back", () => {
|
|
25
|
+
const h = harness();
|
|
26
|
+
expect(h.interp.drawnTick).toBe(-1);
|
|
27
|
+
h.frame(1, { full: true, spawn: [{ id: 1, pos: [0, 0, 0] }] });
|
|
28
|
+
h.frame(9, { update: [{ id: 1, from: [0, 0, 0], pos: [8, 0, 0] }] });
|
|
29
|
+
h.at(5.5);
|
|
30
|
+
expect(h.interp.drawnTick).toBe(5.5);
|
|
31
|
+
// An earlier render tick is raised to what was drawn.
|
|
32
|
+
h.at(3);
|
|
33
|
+
expect(h.interp.drawnTick).toBe(5.5);
|
|
34
|
+
// Frames stopped: past the newest tick received (9), the picture is
|
|
35
|
+
// tick 9's.
|
|
36
|
+
h.at(14);
|
|
37
|
+
expect(h.interp.drawnTick).toBe(9);
|
|
38
|
+
h.interp.clear();
|
|
39
|
+
expect(h.interp.drawnTick).toBe(-1);
|
|
40
|
+
});
|
|
41
|
+
|
|
24
42
|
test("draws an entity between the samples around the render tick", () => {
|
|
25
43
|
const h = harness();
|
|
26
44
|
h.frame(1, { full: true, spawn: [{ id: 1, pos: [0, 0, 0] }] });
|
package/src/interpolation.ts
CHANGED
|
@@ -90,6 +90,7 @@ export class EntityInterpolator {
|
|
|
90
90
|
/** The newest tick the last `update` drew; a render tick never goes
|
|
91
91
|
* below it. */
|
|
92
92
|
private floor = -Infinity;
|
|
93
|
+
private drawn = -1;
|
|
93
94
|
/** Lives that ended, oldest first, so `record` can drop them when
|
|
94
95
|
* `update` does not run (a hidden tab keeps receiving frames). */
|
|
95
96
|
private ended: Array<{ id: number; life: Life }> = [];
|
|
@@ -105,12 +106,23 @@ export class EntityInterpolator {
|
|
|
105
106
|
return this.lastTick;
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The tick the last `update` drew: its render tick, raised to what an
|
|
111
|
+
* earlier update drew and capped at the newest tick received, or -1 before
|
|
112
|
+
* the first update. This is the tick the player saw: the view tick for lag
|
|
113
|
+
* compensation.
|
|
114
|
+
*/
|
|
115
|
+
get drawnTick(): number {
|
|
116
|
+
return this.drawn;
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
/** Forget everything. The next `update` removes every entity. */
|
|
109
120
|
clear(): void {
|
|
110
121
|
this.lives.clear();
|
|
111
122
|
this.ended = [];
|
|
112
123
|
this.lastTick = -1;
|
|
113
124
|
this.floor = -Infinity;
|
|
125
|
+
this.drawn = -1;
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
/**
|
|
@@ -167,6 +179,9 @@ export class EntityInterpolator {
|
|
|
167
179
|
renderTick = Math.max(renderTick, this.floor);
|
|
168
180
|
// What this update draws is at most the newest tick recorded.
|
|
169
181
|
this.floor = Math.min(renderTick, this.lastTick);
|
|
182
|
+
// Past the newest tick received, entities hold at their last samples:
|
|
183
|
+
// the picture is that tick's.
|
|
184
|
+
this.drawn = this.floor;
|
|
170
185
|
for (const [id, lives] of this.lives) {
|
|
171
186
|
// Lives that ended by the render tick are over.
|
|
172
187
|
while (lives.length > 0 && lives[0].despawnTick !== null && lives[0].despawnTick <= renderTick) {
|