@colyseus/core 0.17.44 → 0.18.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/build/MatchMaker.cjs +22 -13
- package/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.d.ts +10 -0
- package/build/MatchMaker.mjs +22 -14
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Protocol.cjs +102 -37
- package/build/Protocol.cjs.map +2 -2
- package/build/Protocol.d.ts +33 -2
- package/build/Protocol.mjs +102 -37
- package/build/Protocol.mjs.map +2 -2
- package/build/Rewind.cjs +385 -0
- package/build/Rewind.cjs.map +7 -0
- package/build/Rewind.d.ts +291 -0
- package/build/Rewind.mjs +359 -0
- package/build/Rewind.mjs.map +7 -0
- package/build/Room.cjs +425 -149
- package/build/Room.cjs.map +3 -3
- package/build/Room.d.ts +314 -27
- package/build/Room.mjs +434 -152
- package/build/Room.mjs.map +3 -3
- package/build/RoomMessages.cjs +265 -0
- package/build/RoomMessages.cjs.map +7 -0
- package/build/RoomMessages.d.ts +49 -0
- package/build/RoomMessages.mjs +240 -0
- package/build/RoomMessages.mjs.map +7 -0
- package/build/RoomPlugin.cjs +252 -0
- package/build/RoomPlugin.cjs.map +7 -0
- package/build/RoomPlugin.d.ts +271 -0
- package/build/RoomPlugin.mjs +220 -0
- package/build/RoomPlugin.mjs.map +7 -0
- package/build/Server.cjs +49 -11
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +26 -0
- package/build/Server.mjs +50 -12
- package/build/Server.mjs.map +2 -2
- package/build/Transport.cjs +64 -2
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +108 -5
- package/build/Transport.mjs +63 -2
- package/build/Transport.mjs.map +2 -2
- package/build/errors/RoomExceptions.cjs +7 -4
- package/build/errors/RoomExceptions.cjs.map +2 -2
- package/build/errors/RoomExceptions.d.ts +15 -3
- package/build/errors/RoomExceptions.mjs +5 -3
- package/build/errors/RoomExceptions.mjs.map +2 -2
- package/build/index.cjs +30 -4
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +9 -5
- package/build/index.mjs +29 -6
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +536 -0
- package/build/input/InputBuffer.cjs.map +7 -0
- package/build/input/InputBuffer.d.ts +226 -0
- package/build/input/InputBuffer.mjs +507 -0
- package/build/input/InputBuffer.mjs.map +7 -0
- package/build/input/RoomInput.cjs +306 -0
- package/build/input/RoomInput.cjs.map +7 -0
- package/build/input/RoomInput.d.ts +91 -0
- package/build/input/RoomInput.mjs +287 -0
- package/build/input/RoomInput.mjs.map +7 -0
- package/build/input/types.cjs +18 -0
- package/build/input/types.cjs.map +7 -0
- package/build/input/types.d.ts +476 -0
- package/build/input/types.mjs +0 -0
- package/build/input/types.mjs.map +7 -0
- package/build/internal.cjs +61 -0
- package/build/internal.cjs.map +7 -0
- package/build/internal.d.ts +9 -0
- package/build/internal.mjs +29 -0
- package/build/internal.mjs.map +7 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
- package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
- package/build/matchmaker/driver.cjs.map +1 -1
- package/build/matchmaker/driver.d.ts +12 -0
- package/build/matchmaker/driver.mjs.map +1 -1
- package/build/presence/LocalPresence.d.ts +1 -1
- package/build/rooms/LobbyRoom.cjs +8 -10
- package/build/rooms/LobbyRoom.cjs.map +2 -2
- package/build/rooms/LobbyRoom.d.ts +4 -3
- package/build/rooms/LobbyRoom.mjs +8 -10
- package/build/rooms/LobbyRoom.mjs.map +2 -2
- package/build/rooms/QueueRoom.cjs +1 -1
- package/build/rooms/QueueRoom.cjs.map +2 -2
- package/build/rooms/QueueRoom.mjs +1 -1
- package/build/rooms/QueueRoom.mjs.map +2 -2
- package/build/rooms/RelayRoom.cjs +12 -16
- package/build/rooms/RelayRoom.cjs.map +2 -2
- package/build/rooms/RelayRoom.d.ts +32 -11
- package/build/rooms/RelayRoom.mjs +10 -16
- package/build/rooms/RelayRoom.mjs.map +2 -2
- package/build/router/default_routes.cjs +4 -1
- package/build/router/default_routes.cjs.map +2 -2
- package/build/router/default_routes.mjs +4 -1
- package/build/router/default_routes.mjs.map +2 -2
- package/build/router/index.cjs +71 -5
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.d.ts +30 -6
- package/build/router/index.mjs +72 -7
- package/build/router/index.mjs.map +3 -3
- package/build/serializer/NoneSerializer.cjs +2 -2
- package/build/serializer/NoneSerializer.cjs.map +2 -2
- package/build/serializer/NoneSerializer.d.ts +3 -3
- package/build/serializer/NoneSerializer.mjs +2 -2
- package/build/serializer/NoneSerializer.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs +44 -14
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +3 -3
- package/build/serializer/SchemaSerializer.mjs +46 -16
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/build/serializer/Serializer.cjs.map +1 -1
- package/build/serializer/Serializer.d.ts +12 -2
- package/build/utils/DevMode.cjs +14 -14
- package/build/utils/DevMode.cjs.map +2 -2
- package/build/utils/DevMode.d.ts +0 -2
- package/build/utils/DevMode.mjs +14 -12
- package/build/utils/DevMode.mjs.map +2 -2
- package/build/utils/UserSessionIndex.cjs +162 -0
- package/build/utils/UserSessionIndex.cjs.map +7 -0
- package/build/utils/UserSessionIndex.d.ts +166 -0
- package/build/utils/UserSessionIndex.mjs +130 -0
- package/build/utils/UserSessionIndex.mjs.map +7 -0
- package/build/utils/Utils.cjs +3 -0
- package/build/utils/Utils.cjs.map +2 -2
- package/build/utils/Utils.d.ts +8 -0
- package/build/utils/Utils.mjs +2 -0
- package/build/utils/Utils.mjs.map +2 -2
- package/package.json +19 -14
- package/src/MatchMaker.ts +47 -15
- package/src/Protocol.ts +130 -59
- package/src/Rewind.ts +572 -0
- package/src/Room.ts +649 -235
- package/src/RoomMessages.ts +342 -0
- package/src/RoomPlugin.ts +563 -0
- package/src/Server.ts +82 -15
- package/src/Transport.ts +172 -9
- package/src/errors/RoomExceptions.ts +18 -4
- package/src/index.ts +23 -3
- package/src/input/InputBuffer.ts +549 -0
- package/src/input/RoomInput.ts +337 -0
- package/src/input/types.ts +503 -0
- package/src/internal.ts +46 -0
- package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
- package/src/matchmaker/driver.ts +13 -0
- package/src/rooms/LobbyRoom.ts +12 -8
- package/src/rooms/QueueRoom.ts +1 -1
- package/src/rooms/RelayRoom.ts +9 -15
- package/src/router/default_routes.ts +6 -1
- package/src/router/index.ts +117 -11
- package/src/serializer/NoneSerializer.ts +3 -3
- package/src/serializer/SchemaSerializer.ts +110 -19
- package/src/serializer/Serializer.ts +13 -2
- package/src/utils/DevMode.ts +18 -13
- package/src/utils/UserSessionIndex.ts +311 -0
- package/src/utils/Utils.ts +9 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { decode, encode, Encoder, Reflection, type Iterator } from '@colyseus/schema';
|
|
2
|
+
import { InputDecoder } from '@colyseus/schema/input';
|
|
3
|
+
import { HandshakeSection, InputFlags, ProtocolModifier } from '@colyseus/shared-types';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
InputAccessorImpl, InputBufferImpl, NO_OP_INPUT_ACCESSOR,
|
|
7
|
+
compileSanitizer, validateSubSteps,
|
|
8
|
+
} from './InputBuffer.ts';
|
|
9
|
+
import type {
|
|
10
|
+
InputAccessor, InputAPI, NormalizedInputOptions,
|
|
11
|
+
DefineInputOptions, IdleDeclared,
|
|
12
|
+
} from './types.ts';
|
|
13
|
+
import type { Client, ClientPrivate } from '../Transport.ts';
|
|
14
|
+
import type { Room } from '../Room.ts';
|
|
15
|
+
import { debugAndPrintError } from '../Debug.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Module-level cache of `Reflection.encode` output keyed by input
|
|
19
|
+
* constructor — pays the encoding cost once per Room class regardless of
|
|
20
|
+
* room instance count. WeakMap so unused classes can be GC'd.
|
|
21
|
+
*/
|
|
22
|
+
const _inputReflectionCache = new WeakMap<Function, Uint8Array>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Runtime behind {@link InputAPI}. A class, not a per-`define()` object literal:
|
|
26
|
+
* literal (and `defineProperty`) accessors carry their closure identity in the
|
|
27
|
+
* hidden class, so every room instance would get a UNIQUE map — sending shared
|
|
28
|
+
* `this.inputs.*` call sites megamorphic. Prototype getters are created once,
|
|
29
|
+
* every instance shares one hidden class (reads stay monomorphic), and
|
|
30
|
+
* `define()` allocates a single 1-field object instead of 7 closures + a map
|
|
31
|
+
* lineage per room.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
class InputAPIImpl {
|
|
36
|
+
private input: RoomInput;
|
|
37
|
+
constructor(input: RoomInput) { this.input = input; }
|
|
38
|
+
/** Registry, not room.clients: an accessor outlives its client through the
|
|
39
|
+
* reconnection window, so a dropped seat still synthesizes the idle policy. */
|
|
40
|
+
get(sessionId: string): InputAccessor<any> {
|
|
41
|
+
return this.input.accessors.get(sessionId) ?? NO_OP_INPUT_ACCESSOR;
|
|
42
|
+
}
|
|
43
|
+
// Live reads off `options` (mutated in place by a later setTimestep/
|
|
44
|
+
// setFixedTimestep back-fill, so a derived tickRate is reflected); 1/hz is
|
|
45
|
+
// correctly-rounded IEEE-754 → bit-identical to the client's stepSeconds.
|
|
46
|
+
get tickRate(): number | undefined { return this.input.options.tickRate; }
|
|
47
|
+
get stepSeconds(): number | undefined { const hz = this.input.options.tickRate; return hz ? 1 / hz : undefined; }
|
|
48
|
+
get stepMs(): number | undefined { const hz = this.input.options.tickRate; return hz ? 1000 / hz : undefined; }
|
|
49
|
+
// Sub-step trio: same derivation as the client handle ((1/hz)/n) — bit-identical dt.
|
|
50
|
+
get subSteps(): number { return this.input.options.subSteps ?? 1; }
|
|
51
|
+
get subStepSeconds(): number | undefined { const hz = this.input.options.tickRate; return hz ? (1 / hz) / (this.input.options.subSteps ?? 1) : undefined; }
|
|
52
|
+
get subStepMs(): number | undefined { const hz = this.input.options.tickRate; return hz ? (1000 / hz) / (this.input.options.subSteps ?? 1) : undefined; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Per-room input subsystem, owned by {@link Room} and created lazily on the
|
|
57
|
+
* first {@link Room.defineInput} call — rooms without inputs allocate none of
|
|
58
|
+
* it. Owns the input options, the per-session accessor registry (decoupled
|
|
59
|
+
* from `this.clients` so an entry outlives a dropped client across its
|
|
60
|
+
* reconnection window), the wire stamp mode, and the encode/decode/handshake
|
|
61
|
+
* machinery. Reads back into its Room for context it doesn't own (the patch
|
|
62
|
+
* rate and the rewind timeline mode).
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export class RoomInput {
|
|
67
|
+
/** Owning room — for `patchRate` and the rewind timeline mode. */
|
|
68
|
+
private room: Room<any>;
|
|
69
|
+
|
|
70
|
+
/** Input configuration (ctor, seqField, bufferMaxSize, idle, sanitize,
|
|
71
|
+
* tickRate, subSteps). `tickRate`/`subSteps` may be back-filled by
|
|
72
|
+
* `setTimestep`/`setFixedTimestep`. Set in {@link define}. */
|
|
73
|
+
options!: NormalizedInputOptions;
|
|
74
|
+
|
|
75
|
+
/** The `InputAPI` returned to userland from `defineInput`, also the
|
|
76
|
+
* framework-owned handle the rewind binding resolves accessors through. */
|
|
77
|
+
api!: InputAPI<any>;
|
|
78
|
+
|
|
79
|
+
/** sessionId → accessor. Decoupled from `this.clients`: an entry outlives its
|
|
80
|
+
* client across the reconnection window, so {@link InputAPI.get} keeps
|
|
81
|
+
* synthesizing the idle policy for an absent seat (its buffer was cleared on
|
|
82
|
+
* leave → idle). Set on join, deleted on full leave. Read by
|
|
83
|
+
* {@link InputAPIImpl.get} (hence not `private`). */
|
|
84
|
+
readonly accessors: Map<string, InputAccessor<any>> = new Map();
|
|
85
|
+
|
|
86
|
+
// Wire stamp mode derived from the rewind timeline, resolved once on first
|
|
87
|
+
// handshake/decode then frozen so advertise + decode never disagree mid-session:
|
|
88
|
+
// the renderTime/reckonTime prefix per input.
|
|
89
|
+
#stampRender = false;
|
|
90
|
+
#stampReckon = false;
|
|
91
|
+
#stampResolved = false;
|
|
92
|
+
|
|
93
|
+
/** One shared now-resolver handed to every accessor (see {@link InputAccessorImpl}). */
|
|
94
|
+
private nowOf: () => number;
|
|
95
|
+
|
|
96
|
+
constructor(room: Room<any>) {
|
|
97
|
+
this.room = room;
|
|
98
|
+
this.nowOf = () => this.room.clock.elapsedTime;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the input configuration + `InputAPI` (the body of
|
|
103
|
+
* {@link Room.defineInput}). Returns the api; the Room hands it back to
|
|
104
|
+
* userland and keeps the framework-owned reference.
|
|
105
|
+
*/
|
|
106
|
+
define<
|
|
107
|
+
C extends new () => any,
|
|
108
|
+
O extends DefineInputOptions<InstanceType<C>> = DefineInputOptions<InstanceType<C>>,
|
|
109
|
+
>(
|
|
110
|
+
type: C,
|
|
111
|
+
opts?: O,
|
|
112
|
+
): InputAPI<InstanceType<C>, IdleDeclared<O, InstanceType<C>>> {
|
|
113
|
+
// Normalize the step declaration to the canonical wire form (Hz). stepSeconds
|
|
114
|
+
// / stepMs are the unit-safe spellings; both sides derive dt = 1/tickRate, so
|
|
115
|
+
// one declared number drives the server's physics step AND the client's.
|
|
116
|
+
const tickRate =
|
|
117
|
+
opts?.stepSeconds !== undefined ? 1 / opts.stepSeconds :
|
|
118
|
+
opts?.stepMs !== undefined ? 1000 / opts.stepMs :
|
|
119
|
+
opts?.tickRate;
|
|
120
|
+
if (
|
|
121
|
+
opts?.tickRate !== undefined && opts.stepMs === undefined && opts.stepSeconds === undefined &&
|
|
122
|
+
(!Number.isInteger(opts.tickRate) || opts.tickRate > 240)
|
|
123
|
+
) {
|
|
124
|
+
console.warn(
|
|
125
|
+
`[defineInput] tickRate is a rate in Hz (got ${opts.tickRate}); a value ` +
|
|
126
|
+
`like 1000/rate is a step interval in ms — pass { stepMs } instead.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
this.options = {
|
|
130
|
+
ctor: type,
|
|
131
|
+
// Opt-in: a default would silently drop frames for any app with a numeric `seq` field.
|
|
132
|
+
seqField: opts?.seqField,
|
|
133
|
+
bufferMaxSize: opts?.bufferMaxSize ?? 32,
|
|
134
|
+
idle: opts?.idle,
|
|
135
|
+
// Compiled once (map → dense min/max walk); applied per decoded frame.
|
|
136
|
+
sanitize: opts?.sanitize !== undefined ? compileSanitizer(opts.sanitize) : undefined,
|
|
137
|
+
tickRate,
|
|
138
|
+
subSteps: validateSubSteps(opts?.subSteps, 'defineInput'),
|
|
139
|
+
};
|
|
140
|
+
if (!_inputReflectionCache.has(type)) {
|
|
141
|
+
// SDK-deserializable ctor bytes (Reflection.decode rebuilds the ctor client-side).
|
|
142
|
+
_inputReflectionCache.set(type, Reflection.encode(new Encoder(new type())));
|
|
143
|
+
}
|
|
144
|
+
const api = new InputAPIImpl(this) as InputAPI<InstanceType<C>, IdleDeclared<O, InstanceType<C>>>;
|
|
145
|
+
this.api = api;
|
|
146
|
+
return api;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- per-client lifecycle (called from Room._onJoin/_onLeave/etc.) ---
|
|
150
|
+
|
|
151
|
+
/** Allocate the per-client input instance + decoder, the ring buffer (opt-in
|
|
152
|
+
* via `bufferMaxSize > 0`, for rollback/lockstep), and the accessor. */
|
|
153
|
+
allocate(client: Client & ClientPrivate): void {
|
|
154
|
+
client._input = new this.options.ctor();
|
|
155
|
+
client._inputDecoder = new InputDecoder(client._input);
|
|
156
|
+
client._reckonBaseline = 0; // mirrors the SDK's delta-coded stamp baseline (reset together on (re)connect)
|
|
157
|
+
const maxSize = this.options.bufferMaxSize;
|
|
158
|
+
if (maxSize > 0) {
|
|
159
|
+
// ctor → idle synthesis; client ref → idle ctx; idle policy → total drain()/next().
|
|
160
|
+
client._inputBuffer = new InputBufferImpl(maxSize, this.options.seqField, this.options.ctor, client, this.options.idle);
|
|
161
|
+
}
|
|
162
|
+
client._inputAccessor = new InputAccessorImpl(client, this.nowOf);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @internal Raw reckon stamp for {@link Room.allowRewindState}'s
|
|
166
|
+
* `bindReckonTime` — bypasses the accessor's resolved getter so the rewind
|
|
167
|
+
* midpoint fallback still engages for unstamped clients. instanceof-narrowed:
|
|
168
|
+
* the registry only ever holds `InputAccessorImpl` (see {@link allocate}),
|
|
169
|
+
* so the 0 fallback covers only a missing session. */
|
|
170
|
+
rawReckonTime(sessionId: string): number {
|
|
171
|
+
const acc = this.accessors.get(sessionId);
|
|
172
|
+
return acc instanceof InputAccessorImpl ? acc.rawReckonTime : 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Register a (re)joined client's accessor. On reconnect this overwrites the
|
|
176
|
+
* dropped session's stale entry. Called after the client is in `this.clients`
|
|
177
|
+
* (so a pre-push failure can't leak it) and before onJoin (so `get()` resolves
|
|
178
|
+
* there). */
|
|
179
|
+
register(sessionId: string, client: ClientPrivate): void {
|
|
180
|
+
this.accessors.set(sessionId, client._inputAccessor!);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Freeze a leaving seat: drop pending inputs so a held (reconnecting) session
|
|
184
|
+
* idles from the first tick rather than replaying last-known moves. `_input`
|
|
185
|
+
* (the idle ctx's `latest`) is left intact. */
|
|
186
|
+
freeze(client: ClientPrivate): void {
|
|
187
|
+
client._inputBuffer?.clear();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Drop a fully-gone session's accessor (delete of a missing key is a no-op). */
|
|
191
|
+
release(sessionId: string): void {
|
|
192
|
+
this.accessors.delete(sessionId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Release every held accessor (room dispose). */
|
|
196
|
+
dispose(): void {
|
|
197
|
+
this.accessors.clear();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- encode / decode (called from Room._onMessage) ---
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Sanitize the freshly-decoded `client._input`, then (when buffering is on)
|
|
204
|
+
* push a clone into the per-client buffer. Honors the framework seq
|
|
205
|
+
* (unreliable) / a user `inputOptions.seqField` (reliable) to dedupe the
|
|
206
|
+
* redundancy ring.
|
|
207
|
+
*/
|
|
208
|
+
capture(client: ClientPrivate, renderTime: number = 0, reckonTime: number = 0, seq?: number): void {
|
|
209
|
+
// Sanitize before anything reads it (latest, the clone below, the idle ctx).
|
|
210
|
+
this.options.sanitize?.(client._input);
|
|
211
|
+
const buf = client._inputBuffer;
|
|
212
|
+
if (!buf) { return; } // no consumer registered — skip the clone allocation
|
|
213
|
+
const inst = client._input!;
|
|
214
|
+
if (seq !== undefined) {
|
|
215
|
+
// Unreliable: dedup the ring by the framework wire seq (user seqField is for .at() only).
|
|
216
|
+
if (!buf.accept(seq)) { return; }
|
|
217
|
+
} else {
|
|
218
|
+
// Reliable: no framework seq (implicit count). Honor a user `seqField` if set.
|
|
219
|
+
const seqField = this.options.seqField;
|
|
220
|
+
if (seqField !== undefined) {
|
|
221
|
+
const value = (inst as any)[seqField] as number;
|
|
222
|
+
if (typeof value === 'number' && !buf.accept(value)) { return; }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
buf.push(inst.clone() as any, renderTime, reckonTime, seq);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Decode a `ROOM_INPUT_RELIABLE` frame: an optional TIMED stamp prefix (shape
|
|
229
|
+
* set by this room's derived stamp mode) then the input body. `it` starts at
|
|
230
|
+
* offset 1, so the body begins at `it.offset`. */
|
|
231
|
+
decodeReliable(client: ClientPrivate, buffer: Buffer, it: Iterator, modifiers: number): void {
|
|
232
|
+
if (!client._inputDecoder) { return; }
|
|
233
|
+
// Optional stamp prefix (TIMED bit), DELTA-CODED, length set by this room's
|
|
234
|
+
// stamp mode (the timeline value is reconstructed against the per-client
|
|
235
|
+
// baseline; the wire carries only the signed change from the previous frame):
|
|
236
|
+
// both: [varint Δreckon][uint16 renderDelta] → renderTime = reckon − renderDelta
|
|
237
|
+
// reckon-only: [varint Δreckon]
|
|
238
|
+
// render-only: [varint Δrender]
|
|
239
|
+
let renderTime = 0;
|
|
240
|
+
let reckonTime = 0;
|
|
241
|
+
if (modifiers & ProtocolModifier.TIMED) {
|
|
242
|
+
this.#resolveWireModes();
|
|
243
|
+
// Reconstruct the absolute timeline value: baseline += signed delta. Both
|
|
244
|
+
// sides start at 0 (so the first delta is absolute) and re-zero together on
|
|
245
|
+
// (re)connect; reliable+in-order keeps the mirror locked.
|
|
246
|
+
const stamp = client._reckonBaseline! += decode.number(buffer, it);
|
|
247
|
+
if (this.#stampReckon && this.#stampRender) {
|
|
248
|
+
reckonTime = stamp;
|
|
249
|
+
const renderDelta = decode.uint16(buffer, it);
|
|
250
|
+
renderTime = stamp > renderDelta ? stamp - renderDelta : 0;
|
|
251
|
+
} else if (this.#stampReckon) {
|
|
252
|
+
reckonTime = stamp;
|
|
253
|
+
} else {
|
|
254
|
+
renderTime = stamp;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
client._inputDecoder.decode(buffer.subarray(it.offset, buffer.byteLength));
|
|
259
|
+
} catch (e: any) {
|
|
260
|
+
debugAndPrintError(e);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
client._lastInputReceivedAt = performance.now();
|
|
264
|
+
// Mirrors the SDK's sent count; echoed back as lastInputSeq for RTT.
|
|
265
|
+
client._receivedInputCount = (client._receivedInputCount ?? 0) + 1;
|
|
266
|
+
this.capture(client, renderTime, reckonTime);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Decode a `ROOM_INPUT_UNRELIABLE` redundancy ring — each slot carries its
|
|
270
|
+
* framework seq (base seq + position) for ring dedupe, no user seqField. */
|
|
271
|
+
decodeUnreliable(client: ClientPrivate, buffer: Buffer, _modifiers: number): void {
|
|
272
|
+
if (!client._inputDecoder) { return; }
|
|
273
|
+
try {
|
|
274
|
+
client._inputDecoder.decodeAll(buffer.subarray(1), (_inst, seq) => this.capture(client, 0, 0, seq));
|
|
275
|
+
} catch (e: any) {
|
|
276
|
+
debugAndPrintError(e);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
client._lastInputReceivedAt = performance.now();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- handshake + stamp mode ---
|
|
283
|
+
|
|
284
|
+
/** Resolve the wire stamp mode from the rewind timeline (once, then frozen). */
|
|
285
|
+
#resolveWireModes(): void {
|
|
286
|
+
if (this.#stampResolved) return;
|
|
287
|
+
this.#stampResolved = true;
|
|
288
|
+
const tl = this.room._timelineMode();
|
|
289
|
+
this.#stampRender = tl?.snapshot ?? false;
|
|
290
|
+
this.#stampReckon = tl?.reckon ?? false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The join-handshake input sections: INPUT_REFLECTION (the SDK-deserializable
|
|
295
|
+
* input ctor bytes) and, when any runtime config differs from defaults,
|
|
296
|
+
* INPUT_OPTIONS (`[flags uint8][tickRate varint?][patchRate varint?][subSteps
|
|
297
|
+
* varint?]`). The client mirrors RENDER_TIME/RECKON_TIME (auto-stamp timeline),
|
|
298
|
+
* tickRate (predict at dt=1/tickRate), patchRate (reconcile cadence), and
|
|
299
|
+
* subSteps (physics sub-steps per input, omitted when 1). Returns `undefined`
|
|
300
|
+
* when there's nothing to add.
|
|
301
|
+
*/
|
|
302
|
+
handshakeSections(): Array<{ tag: number; bytes: Uint8Array }> | undefined {
|
|
303
|
+
let sections: Array<{ tag: number; bytes: Uint8Array }> | undefined;
|
|
304
|
+
const inputBytes = _inputReflectionCache.get(this.options.ctor);
|
|
305
|
+
if (inputBytes !== undefined) {
|
|
306
|
+
sections = [{ tag: HandshakeSection.INPUT_REFLECTION, bytes: inputBytes }];
|
|
307
|
+
}
|
|
308
|
+
this.#resolveWireModes();
|
|
309
|
+
const stampRender = this.#stampRender;
|
|
310
|
+
const stampReckon = this.#stampReckon;
|
|
311
|
+
const tickRate = this.options.tickRate;
|
|
312
|
+
const patchRate = (typeof this.room.patchRate === "number" && this.room.patchRate > 0)
|
|
313
|
+
? Math.round(this.room.patchRate) : undefined;
|
|
314
|
+
const subSteps = (this.options.subSteps !== undefined && this.options.subSteps > 1)
|
|
315
|
+
? this.options.subSteps : undefined;
|
|
316
|
+
if (stampRender || stampReckon || tickRate || patchRate || subSteps) {
|
|
317
|
+
let flags = 0;
|
|
318
|
+
if (stampRender) { flags |= InputFlags.RENDER_TIME; }
|
|
319
|
+
if (stampReckon) { flags |= InputFlags.RECKON_TIME; }
|
|
320
|
+
if (tickRate) { flags |= InputFlags.FIXED_TIMESTEP; }
|
|
321
|
+
if (patchRate) { flags |= InputFlags.PATCH_RATE; }
|
|
322
|
+
if (subSteps) { flags |= InputFlags.SUB_STEPS; }
|
|
323
|
+
// 24B: worst case = flags + float64 tickRate (9) + uint32 patchRate (5) + subSteps (2).
|
|
324
|
+
const buf = new Uint8Array(24);
|
|
325
|
+
const sit = { offset: 0 };
|
|
326
|
+
buf[sit.offset++] = flags;
|
|
327
|
+
if (tickRate) { encode.number(buf, tickRate, sit); }
|
|
328
|
+
if (patchRate) { encode.number(buf, patchRate, sit); }
|
|
329
|
+
if (subSteps) { encode.number(buf, subSteps, sit); }
|
|
330
|
+
(sections ??= []).push({
|
|
331
|
+
tag: HandshakeSection.INPUT_OPTIONS,
|
|
332
|
+
bytes: buf.subarray(0, sit.offset),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return sections;
|
|
336
|
+
}
|
|
337
|
+
}
|