@colyseus/core 0.18.0 → 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 +3 -7
- package/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.mjs +4 -8
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Protocol.cjs +3 -3
- package/build/Protocol.cjs.map +1 -1
- package/build/Protocol.d.ts +3 -3
- package/build/Protocol.mjs +3 -3
- package/build/Protocol.mjs.map +1 -1
- 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 +280 -281
- package/build/Room.cjs.map +3 -3
- package/build/Room.d.ts +185 -81
- package/build/Room.mjs +286 -286
- package/build/Room.mjs.map +2 -2
- 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/Server.cjs +88 -31
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +36 -0
- package/build/Server.mjs +88 -31
- package/build/Server.mjs.map +2 -2
- package/build/Transport.cjs +26 -0
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +69 -2
- package/build/Transport.mjs +25 -0
- 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 +19 -2
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +7 -4
- package/build/index.mjs +17 -4
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +439 -16
- package/build/input/InputBuffer.cjs.map +2 -2
- package/build/input/InputBuffer.d.ts +186 -96
- package/build/input/InputBuffer.mjs +436 -15
- package/build/input/InputBuffer.mjs.map +2 -2
- 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/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/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 +6 -1
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.mjs +6 -1
- package/build/router/index.mjs.map +2 -2
- package/build/router/node.cjs +37 -2
- package/build/router/node.cjs.map +2 -2
- package/build/router/node.d.ts +10 -0
- package/build/router/node.mjs +35 -1
- package/build/router/node.mjs.map +2 -2
- 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/Env.cjs +8 -4
- package/build/utils/Env.cjs.map +3 -3
- package/build/utils/Env.mjs +8 -4
- package/build/utils/Env.mjs.map +2 -2
- 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 +11 -11
- package/src/MatchMaker.ts +7 -9
- package/src/Protocol.ts +3 -3
- package/src/Rewind.ts +572 -0
- package/src/Room.ts +409 -448
- package/src/RoomMessages.ts +342 -0
- package/src/Server.ts +112 -38
- package/src/Transport.ts +97 -2
- package/src/errors/RoomExceptions.ts +18 -4
- package/src/index.ts +13 -2
- package/src/input/InputBuffer.ts +464 -107
- package/src/input/RoomInput.ts +337 -0
- package/src/input/types.ts +503 -0
- package/src/rooms/QueueRoom.ts +1 -1
- package/src/router/default_routes.ts +6 -1
- package/src/router/index.ts +6 -1
- package/src/router/node.ts +44 -0
- 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/Env.ts +12 -4
- package/src/utils/Utils.ts +9 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// packages/core/src/input/RoomInput.ts
|
|
2
|
+
import { decode, encode, Encoder, Reflection } from "@colyseus/schema";
|
|
3
|
+
import { InputDecoder } from "@colyseus/schema/input";
|
|
4
|
+
import { HandshakeSection, InputFlags, ProtocolModifier } from "@colyseus/shared-types";
|
|
5
|
+
import {
|
|
6
|
+
InputAccessorImpl,
|
|
7
|
+
InputBufferImpl,
|
|
8
|
+
NO_OP_INPUT_ACCESSOR,
|
|
9
|
+
compileSanitizer,
|
|
10
|
+
validateSubSteps
|
|
11
|
+
} from "./InputBuffer.mjs";
|
|
12
|
+
import { debugAndPrintError } from "../Debug.mjs";
|
|
13
|
+
var _inputReflectionCache = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
var InputAPIImpl = class {
|
|
15
|
+
constructor(input) {
|
|
16
|
+
this.input = input;
|
|
17
|
+
}
|
|
18
|
+
/** Registry, not room.clients: an accessor outlives its client through the
|
|
19
|
+
* reconnection window, so a dropped seat still synthesizes the idle policy. */
|
|
20
|
+
get(sessionId) {
|
|
21
|
+
return this.input.accessors.get(sessionId) ?? NO_OP_INPUT_ACCESSOR;
|
|
22
|
+
}
|
|
23
|
+
// Live reads off `options` (mutated in place by a later setTimestep/
|
|
24
|
+
// setFixedTimestep back-fill, so a derived tickRate is reflected); 1/hz is
|
|
25
|
+
// correctly-rounded IEEE-754 → bit-identical to the client's stepSeconds.
|
|
26
|
+
get tickRate() {
|
|
27
|
+
return this.input.options.tickRate;
|
|
28
|
+
}
|
|
29
|
+
get stepSeconds() {
|
|
30
|
+
const hz = this.input.options.tickRate;
|
|
31
|
+
return hz ? 1 / hz : void 0;
|
|
32
|
+
}
|
|
33
|
+
get stepMs() {
|
|
34
|
+
const hz = this.input.options.tickRate;
|
|
35
|
+
return hz ? 1e3 / hz : void 0;
|
|
36
|
+
}
|
|
37
|
+
// Sub-step trio: same derivation as the client handle ((1/hz)/n) — bit-identical dt.
|
|
38
|
+
get subSteps() {
|
|
39
|
+
return this.input.options.subSteps ?? 1;
|
|
40
|
+
}
|
|
41
|
+
get subStepSeconds() {
|
|
42
|
+
const hz = this.input.options.tickRate;
|
|
43
|
+
return hz ? 1 / hz / (this.input.options.subSteps ?? 1) : void 0;
|
|
44
|
+
}
|
|
45
|
+
get subStepMs() {
|
|
46
|
+
const hz = this.input.options.tickRate;
|
|
47
|
+
return hz ? 1e3 / hz / (this.input.options.subSteps ?? 1) : void 0;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var RoomInput = class {
|
|
51
|
+
constructor(room) {
|
|
52
|
+
/** sessionId → accessor. Decoupled from `this.clients`: an entry outlives its
|
|
53
|
+
* client across the reconnection window, so {@link InputAPI.get} keeps
|
|
54
|
+
* synthesizing the idle policy for an absent seat (its buffer was cleared on
|
|
55
|
+
* leave → idle). Set on join, deleted on full leave. Read by
|
|
56
|
+
* {@link InputAPIImpl.get} (hence not `private`). */
|
|
57
|
+
this.accessors = /* @__PURE__ */ new Map();
|
|
58
|
+
// Wire stamp mode derived from the rewind timeline, resolved once on first
|
|
59
|
+
// handshake/decode then frozen so advertise + decode never disagree mid-session:
|
|
60
|
+
// the renderTime/reckonTime prefix per input.
|
|
61
|
+
this.#stampRender = false;
|
|
62
|
+
this.#stampReckon = false;
|
|
63
|
+
this.#stampResolved = false;
|
|
64
|
+
this.room = room;
|
|
65
|
+
this.nowOf = () => this.room.clock.elapsedTime;
|
|
66
|
+
}
|
|
67
|
+
#stampRender;
|
|
68
|
+
#stampReckon;
|
|
69
|
+
#stampResolved;
|
|
70
|
+
/**
|
|
71
|
+
* Build the input configuration + `InputAPI` (the body of
|
|
72
|
+
* {@link Room.defineInput}). Returns the api; the Room hands it back to
|
|
73
|
+
* userland and keeps the framework-owned reference.
|
|
74
|
+
*/
|
|
75
|
+
define(type, opts) {
|
|
76
|
+
const tickRate = opts?.stepSeconds !== void 0 ? 1 / opts.stepSeconds : opts?.stepMs !== void 0 ? 1e3 / opts.stepMs : opts?.tickRate;
|
|
77
|
+
if (opts?.tickRate !== void 0 && opts.stepMs === void 0 && opts.stepSeconds === void 0 && (!Number.isInteger(opts.tickRate) || opts.tickRate > 240)) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`[defineInput] tickRate is a rate in Hz (got ${opts.tickRate}); a value like 1000/rate is a step interval in ms \u2014 pass { stepMs } instead.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
this.options = {
|
|
83
|
+
ctor: type,
|
|
84
|
+
// Opt-in: a default would silently drop frames for any app with a numeric `seq` field.
|
|
85
|
+
seqField: opts?.seqField,
|
|
86
|
+
bufferMaxSize: opts?.bufferMaxSize ?? 32,
|
|
87
|
+
idle: opts?.idle,
|
|
88
|
+
// Compiled once (map → dense min/max walk); applied per decoded frame.
|
|
89
|
+
sanitize: opts?.sanitize !== void 0 ? compileSanitizer(opts.sanitize) : void 0,
|
|
90
|
+
tickRate,
|
|
91
|
+
subSteps: validateSubSteps(opts?.subSteps, "defineInput")
|
|
92
|
+
};
|
|
93
|
+
if (!_inputReflectionCache.has(type)) {
|
|
94
|
+
_inputReflectionCache.set(type, Reflection.encode(new Encoder(new type())));
|
|
95
|
+
}
|
|
96
|
+
const api = new InputAPIImpl(this);
|
|
97
|
+
this.api = api;
|
|
98
|
+
return api;
|
|
99
|
+
}
|
|
100
|
+
// --- per-client lifecycle (called from Room._onJoin/_onLeave/etc.) ---
|
|
101
|
+
/** Allocate the per-client input instance + decoder, the ring buffer (opt-in
|
|
102
|
+
* via `bufferMaxSize > 0`, for rollback/lockstep), and the accessor. */
|
|
103
|
+
allocate(client) {
|
|
104
|
+
client._input = new this.options.ctor();
|
|
105
|
+
client._inputDecoder = new InputDecoder(client._input);
|
|
106
|
+
client._reckonBaseline = 0;
|
|
107
|
+
const maxSize = this.options.bufferMaxSize;
|
|
108
|
+
if (maxSize > 0) {
|
|
109
|
+
client._inputBuffer = new InputBufferImpl(maxSize, this.options.seqField, this.options.ctor, client, this.options.idle);
|
|
110
|
+
}
|
|
111
|
+
client._inputAccessor = new InputAccessorImpl(client, this.nowOf);
|
|
112
|
+
}
|
|
113
|
+
/** @internal Raw reckon stamp for {@link Room.allowRewindState}'s
|
|
114
|
+
* `bindReckonTime` — bypasses the accessor's resolved getter so the rewind
|
|
115
|
+
* midpoint fallback still engages for unstamped clients. instanceof-narrowed:
|
|
116
|
+
* the registry only ever holds `InputAccessorImpl` (see {@link allocate}),
|
|
117
|
+
* so the 0 fallback covers only a missing session. */
|
|
118
|
+
rawReckonTime(sessionId) {
|
|
119
|
+
const acc = this.accessors.get(sessionId);
|
|
120
|
+
return acc instanceof InputAccessorImpl ? acc.rawReckonTime : 0;
|
|
121
|
+
}
|
|
122
|
+
/** Register a (re)joined client's accessor. On reconnect this overwrites the
|
|
123
|
+
* dropped session's stale entry. Called after the client is in `this.clients`
|
|
124
|
+
* (so a pre-push failure can't leak it) and before onJoin (so `get()` resolves
|
|
125
|
+
* there). */
|
|
126
|
+
register(sessionId, client) {
|
|
127
|
+
this.accessors.set(sessionId, client._inputAccessor);
|
|
128
|
+
}
|
|
129
|
+
/** Freeze a leaving seat: drop pending inputs so a held (reconnecting) session
|
|
130
|
+
* idles from the first tick rather than replaying last-known moves. `_input`
|
|
131
|
+
* (the idle ctx's `latest`) is left intact. */
|
|
132
|
+
freeze(client) {
|
|
133
|
+
client._inputBuffer?.clear();
|
|
134
|
+
}
|
|
135
|
+
/** Drop a fully-gone session's accessor (delete of a missing key is a no-op). */
|
|
136
|
+
release(sessionId) {
|
|
137
|
+
this.accessors.delete(sessionId);
|
|
138
|
+
}
|
|
139
|
+
/** Release every held accessor (room dispose). */
|
|
140
|
+
dispose() {
|
|
141
|
+
this.accessors.clear();
|
|
142
|
+
}
|
|
143
|
+
// --- encode / decode (called from Room._onMessage) ---
|
|
144
|
+
/**
|
|
145
|
+
* Sanitize the freshly-decoded `client._input`, then (when buffering is on)
|
|
146
|
+
* push a clone into the per-client buffer. Honors the framework seq
|
|
147
|
+
* (unreliable) / a user `inputOptions.seqField` (reliable) to dedupe the
|
|
148
|
+
* redundancy ring.
|
|
149
|
+
*/
|
|
150
|
+
capture(client, renderTime = 0, reckonTime = 0, seq) {
|
|
151
|
+
this.options.sanitize?.(client._input);
|
|
152
|
+
const buf = client._inputBuffer;
|
|
153
|
+
if (!buf) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const inst = client._input;
|
|
157
|
+
if (seq !== void 0) {
|
|
158
|
+
if (!buf.accept(seq)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
const seqField = this.options.seqField;
|
|
163
|
+
if (seqField !== void 0) {
|
|
164
|
+
const value = inst[seqField];
|
|
165
|
+
if (typeof value === "number" && !buf.accept(value)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
buf.push(inst.clone(), renderTime, reckonTime, seq);
|
|
171
|
+
}
|
|
172
|
+
/** Decode a `ROOM_INPUT_RELIABLE` frame: an optional TIMED stamp prefix (shape
|
|
173
|
+
* set by this room's derived stamp mode) then the input body. `it` starts at
|
|
174
|
+
* offset 1, so the body begins at `it.offset`. */
|
|
175
|
+
decodeReliable(client, buffer, it, modifiers) {
|
|
176
|
+
if (!client._inputDecoder) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let renderTime = 0;
|
|
180
|
+
let reckonTime = 0;
|
|
181
|
+
if (modifiers & ProtocolModifier.TIMED) {
|
|
182
|
+
this.#resolveWireModes();
|
|
183
|
+
const stamp = client._reckonBaseline += decode.number(buffer, it);
|
|
184
|
+
if (this.#stampReckon && this.#stampRender) {
|
|
185
|
+
reckonTime = stamp;
|
|
186
|
+
const renderDelta = decode.uint16(buffer, it);
|
|
187
|
+
renderTime = stamp > renderDelta ? stamp - renderDelta : 0;
|
|
188
|
+
} else if (this.#stampReckon) {
|
|
189
|
+
reckonTime = stamp;
|
|
190
|
+
} else {
|
|
191
|
+
renderTime = stamp;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
client._inputDecoder.decode(buffer.subarray(it.offset, buffer.byteLength));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
debugAndPrintError(e);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
client._lastInputReceivedAt = performance.now();
|
|
201
|
+
client._receivedInputCount = (client._receivedInputCount ?? 0) + 1;
|
|
202
|
+
this.capture(client, renderTime, reckonTime);
|
|
203
|
+
}
|
|
204
|
+
/** Decode a `ROOM_INPUT_UNRELIABLE` redundancy ring — each slot carries its
|
|
205
|
+
* framework seq (base seq + position) for ring dedupe, no user seqField. */
|
|
206
|
+
decodeUnreliable(client, buffer, _modifiers) {
|
|
207
|
+
if (!client._inputDecoder) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
client._inputDecoder.decodeAll(buffer.subarray(1), (_inst, seq) => this.capture(client, 0, 0, seq));
|
|
212
|
+
} catch (e) {
|
|
213
|
+
debugAndPrintError(e);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
client._lastInputReceivedAt = performance.now();
|
|
217
|
+
}
|
|
218
|
+
// --- handshake + stamp mode ---
|
|
219
|
+
/** Resolve the wire stamp mode from the rewind timeline (once, then frozen). */
|
|
220
|
+
#resolveWireModes() {
|
|
221
|
+
if (this.#stampResolved) return;
|
|
222
|
+
this.#stampResolved = true;
|
|
223
|
+
const tl = this.room._timelineMode();
|
|
224
|
+
this.#stampRender = tl?.snapshot ?? false;
|
|
225
|
+
this.#stampReckon = tl?.reckon ?? false;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The join-handshake input sections: INPUT_REFLECTION (the SDK-deserializable
|
|
229
|
+
* input ctor bytes) and, when any runtime config differs from defaults,
|
|
230
|
+
* INPUT_OPTIONS (`[flags uint8][tickRate varint?][patchRate varint?][subSteps
|
|
231
|
+
* varint?]`). The client mirrors RENDER_TIME/RECKON_TIME (auto-stamp timeline),
|
|
232
|
+
* tickRate (predict at dt=1/tickRate), patchRate (reconcile cadence), and
|
|
233
|
+
* subSteps (physics sub-steps per input, omitted when 1). Returns `undefined`
|
|
234
|
+
* when there's nothing to add.
|
|
235
|
+
*/
|
|
236
|
+
handshakeSections() {
|
|
237
|
+
let sections;
|
|
238
|
+
const inputBytes = _inputReflectionCache.get(this.options.ctor);
|
|
239
|
+
if (inputBytes !== void 0) {
|
|
240
|
+
sections = [{ tag: HandshakeSection.INPUT_REFLECTION, bytes: inputBytes }];
|
|
241
|
+
}
|
|
242
|
+
this.#resolveWireModes();
|
|
243
|
+
const stampRender = this.#stampRender;
|
|
244
|
+
const stampReckon = this.#stampReckon;
|
|
245
|
+
const tickRate = this.options.tickRate;
|
|
246
|
+
const patchRate = typeof this.room.patchRate === "number" && this.room.patchRate > 0 ? Math.round(this.room.patchRate) : void 0;
|
|
247
|
+
const subSteps = this.options.subSteps !== void 0 && this.options.subSteps > 1 ? this.options.subSteps : void 0;
|
|
248
|
+
if (stampRender || stampReckon || tickRate || patchRate || subSteps) {
|
|
249
|
+
let flags = 0;
|
|
250
|
+
if (stampRender) {
|
|
251
|
+
flags |= InputFlags.RENDER_TIME;
|
|
252
|
+
}
|
|
253
|
+
if (stampReckon) {
|
|
254
|
+
flags |= InputFlags.RECKON_TIME;
|
|
255
|
+
}
|
|
256
|
+
if (tickRate) {
|
|
257
|
+
flags |= InputFlags.FIXED_TIMESTEP;
|
|
258
|
+
}
|
|
259
|
+
if (patchRate) {
|
|
260
|
+
flags |= InputFlags.PATCH_RATE;
|
|
261
|
+
}
|
|
262
|
+
if (subSteps) {
|
|
263
|
+
flags |= InputFlags.SUB_STEPS;
|
|
264
|
+
}
|
|
265
|
+
const buf = new Uint8Array(24);
|
|
266
|
+
const sit = { offset: 0 };
|
|
267
|
+
buf[sit.offset++] = flags;
|
|
268
|
+
if (tickRate) {
|
|
269
|
+
encode.number(buf, tickRate, sit);
|
|
270
|
+
}
|
|
271
|
+
if (patchRate) {
|
|
272
|
+
encode.number(buf, patchRate, sit);
|
|
273
|
+
}
|
|
274
|
+
if (subSteps) {
|
|
275
|
+
encode.number(buf, subSteps, sit);
|
|
276
|
+
}
|
|
277
|
+
(sections ??= []).push({
|
|
278
|
+
tag: HandshakeSection.INPUT_OPTIONS,
|
|
279
|
+
bytes: buf.subarray(0, sit.offset)
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return sections;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
export {
|
|
286
|
+
RoomInput
|
|
287
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/input/RoomInput.ts"],
|
|
4
|
+
"sourcesContent": ["import { decode, encode, Encoder, Reflection, type Iterator } from '@colyseus/schema';\nimport { InputDecoder } from '@colyseus/schema/input';\nimport { HandshakeSection, InputFlags, ProtocolModifier } from '@colyseus/shared-types';\n\nimport {\n InputAccessorImpl, InputBufferImpl, NO_OP_INPUT_ACCESSOR,\n compileSanitizer, validateSubSteps,\n} from './InputBuffer.ts';\nimport type {\n InputAccessor, InputAPI, NormalizedInputOptions,\n DefineInputOptions, IdleDeclared,\n} from './types.ts';\nimport type { Client, ClientPrivate } from '../Transport.ts';\nimport type { Room } from '../Room.ts';\nimport { debugAndPrintError } from '../Debug.ts';\n\n/**\n * Module-level cache of `Reflection.encode` output keyed by input\n * constructor \u2014 pays the encoding cost once per Room class regardless of\n * room instance count. WeakMap so unused classes can be GC'd.\n */\nconst _inputReflectionCache = new WeakMap<Function, Uint8Array>();\n\n/**\n * Runtime behind {@link InputAPI}. A class, not a per-`define()` object literal:\n * literal (and `defineProperty`) accessors carry their closure identity in the\n * hidden class, so every room instance would get a UNIQUE map \u2014 sending shared\n * `this.inputs.*` call sites megamorphic. Prototype getters are created once,\n * every instance shares one hidden class (reads stay monomorphic), and\n * `define()` allocates a single 1-field object instead of 7 closures + a map\n * lineage per room.\n *\n * @internal\n */\nclass InputAPIImpl {\n private input: RoomInput;\n constructor(input: RoomInput) { this.input = input; }\n /** Registry, not room.clients: an accessor outlives its client through the\n * reconnection window, so a dropped seat still synthesizes the idle policy. */\n get(sessionId: string): InputAccessor<any> {\n return this.input.accessors.get(sessionId) ?? NO_OP_INPUT_ACCESSOR;\n }\n // Live reads off `options` (mutated in place by a later setTimestep/\n // setFixedTimestep back-fill, so a derived tickRate is reflected); 1/hz is\n // correctly-rounded IEEE-754 \u2192 bit-identical to the client's stepSeconds.\n get tickRate(): number | undefined { return this.input.options.tickRate; }\n get stepSeconds(): number | undefined { const hz = this.input.options.tickRate; return hz ? 1 / hz : undefined; }\n get stepMs(): number | undefined { const hz = this.input.options.tickRate; return hz ? 1000 / hz : undefined; }\n // Sub-step trio: same derivation as the client handle ((1/hz)/n) \u2014 bit-identical dt.\n get subSteps(): number { return this.input.options.subSteps ?? 1; }\n get subStepSeconds(): number | undefined { const hz = this.input.options.tickRate; return hz ? (1 / hz) / (this.input.options.subSteps ?? 1) : undefined; }\n get subStepMs(): number | undefined { const hz = this.input.options.tickRate; return hz ? (1000 / hz) / (this.input.options.subSteps ?? 1) : undefined; }\n}\n\n/**\n * Per-room input subsystem, owned by {@link Room} and created lazily on the\n * first {@link Room.defineInput} call \u2014 rooms without inputs allocate none of\n * it. Owns the input options, the per-session accessor registry (decoupled\n * from `this.clients` so an entry outlives a dropped client across its\n * reconnection window), the wire stamp mode, and the encode/decode/handshake\n * machinery. Reads back into its Room for context it doesn't own (the patch\n * rate and the rewind timeline mode).\n *\n * @internal\n */\nexport class RoomInput {\n /** Owning room \u2014 for `patchRate` and the rewind timeline mode. */\n private room: Room<any>;\n\n /** Input configuration (ctor, seqField, bufferMaxSize, idle, sanitize,\n * tickRate, subSteps). `tickRate`/`subSteps` may be back-filled by\n * `setTimestep`/`setFixedTimestep`. Set in {@link define}. */\n options!: NormalizedInputOptions;\n\n /** The `InputAPI` returned to userland from `defineInput`, also the\n * framework-owned handle the rewind binding resolves accessors through. */\n api!: InputAPI<any>;\n\n /** sessionId \u2192 accessor. Decoupled from `this.clients`: an entry outlives its\n * client across the reconnection window, so {@link InputAPI.get} keeps\n * synthesizing the idle policy for an absent seat (its buffer was cleared on\n * leave \u2192 idle). Set on join, deleted on full leave. Read by\n * {@link InputAPIImpl.get} (hence not `private`). */\n readonly accessors: Map<string, InputAccessor<any>> = new Map();\n\n // Wire stamp mode derived from the rewind timeline, resolved once on first\n // handshake/decode then frozen so advertise + decode never disagree mid-session:\n // the renderTime/reckonTime prefix per input.\n #stampRender = false;\n #stampReckon = false;\n #stampResolved = false;\n\n /** One shared now-resolver handed to every accessor (see {@link InputAccessorImpl}). */\n private nowOf: () => number;\n\n constructor(room: Room<any>) {\n this.room = room;\n this.nowOf = () => this.room.clock.elapsedTime;\n }\n\n /**\n * Build the input configuration + `InputAPI` (the body of\n * {@link Room.defineInput}). Returns the api; the Room hands it back to\n * userland and keeps the framework-owned reference.\n */\n define<\n C extends new () => any,\n O extends DefineInputOptions<InstanceType<C>> = DefineInputOptions<InstanceType<C>>,\n >(\n type: C,\n opts?: O,\n ): InputAPI<InstanceType<C>, IdleDeclared<O, InstanceType<C>>> {\n // Normalize the step declaration to the canonical wire form (Hz). stepSeconds\n // / stepMs are the unit-safe spellings; both sides derive dt = 1/tickRate, so\n // one declared number drives the server's physics step AND the client's.\n const tickRate =\n opts?.stepSeconds !== undefined ? 1 / opts.stepSeconds :\n opts?.stepMs !== undefined ? 1000 / opts.stepMs :\n opts?.tickRate;\n if (\n opts?.tickRate !== undefined && opts.stepMs === undefined && opts.stepSeconds === undefined &&\n (!Number.isInteger(opts.tickRate) || opts.tickRate > 240)\n ) {\n console.warn(\n `[defineInput] tickRate is a rate in Hz (got ${opts.tickRate}); a value ` +\n `like 1000/rate is a step interval in ms \u2014 pass { stepMs } instead.`,\n );\n }\n this.options = {\n ctor: type,\n // Opt-in: a default would silently drop frames for any app with a numeric `seq` field.\n seqField: opts?.seqField,\n bufferMaxSize: opts?.bufferMaxSize ?? 32,\n idle: opts?.idle,\n // Compiled once (map \u2192 dense min/max walk); applied per decoded frame.\n sanitize: opts?.sanitize !== undefined ? compileSanitizer(opts.sanitize) : undefined,\n tickRate,\n subSteps: validateSubSteps(opts?.subSteps, 'defineInput'),\n };\n if (!_inputReflectionCache.has(type)) {\n // SDK-deserializable ctor bytes (Reflection.decode rebuilds the ctor client-side).\n _inputReflectionCache.set(type, Reflection.encode(new Encoder(new type())));\n }\n const api = new InputAPIImpl(this) as InputAPI<InstanceType<C>, IdleDeclared<O, InstanceType<C>>>;\n this.api = api;\n return api;\n }\n\n // --- per-client lifecycle (called from Room._onJoin/_onLeave/etc.) ---\n\n /** Allocate the per-client input instance + decoder, the ring buffer (opt-in\n * via `bufferMaxSize > 0`, for rollback/lockstep), and the accessor. */\n allocate(client: Client & ClientPrivate): void {\n client._input = new this.options.ctor();\n client._inputDecoder = new InputDecoder(client._input);\n client._reckonBaseline = 0; // mirrors the SDK's delta-coded stamp baseline (reset together on (re)connect)\n const maxSize = this.options.bufferMaxSize;\n if (maxSize > 0) {\n // ctor \u2192 idle synthesis; client ref \u2192 idle ctx; idle policy \u2192 total drain()/next().\n client._inputBuffer = new InputBufferImpl(maxSize, this.options.seqField, this.options.ctor, client, this.options.idle);\n }\n client._inputAccessor = new InputAccessorImpl(client, this.nowOf);\n }\n\n /** @internal Raw reckon stamp for {@link Room.allowRewindState}'s\n * `bindReckonTime` \u2014 bypasses the accessor's resolved getter so the rewind\n * midpoint fallback still engages for unstamped clients. instanceof-narrowed:\n * the registry only ever holds `InputAccessorImpl` (see {@link allocate}),\n * so the 0 fallback covers only a missing session. */\n rawReckonTime(sessionId: string): number {\n const acc = this.accessors.get(sessionId);\n return acc instanceof InputAccessorImpl ? acc.rawReckonTime : 0;\n }\n\n /** Register a (re)joined client's accessor. On reconnect this overwrites the\n * dropped session's stale entry. Called after the client is in `this.clients`\n * (so a pre-push failure can't leak it) and before onJoin (so `get()` resolves\n * there). */\n register(sessionId: string, client: ClientPrivate): void {\n this.accessors.set(sessionId, client._inputAccessor!);\n }\n\n /** Freeze a leaving seat: drop pending inputs so a held (reconnecting) session\n * idles from the first tick rather than replaying last-known moves. `_input`\n * (the idle ctx's `latest`) is left intact. */\n freeze(client: ClientPrivate): void {\n client._inputBuffer?.clear();\n }\n\n /** Drop a fully-gone session's accessor (delete of a missing key is a no-op). */\n release(sessionId: string): void {\n this.accessors.delete(sessionId);\n }\n\n /** Release every held accessor (room dispose). */\n dispose(): void {\n this.accessors.clear();\n }\n\n // --- encode / decode (called from Room._onMessage) ---\n\n /**\n * Sanitize the freshly-decoded `client._input`, then (when buffering is on)\n * push a clone into the per-client buffer. Honors the framework seq\n * (unreliable) / a user `inputOptions.seqField` (reliable) to dedupe the\n * redundancy ring.\n */\n capture(client: ClientPrivate, renderTime: number = 0, reckonTime: number = 0, seq?: number): void {\n // Sanitize before anything reads it (latest, the clone below, the idle ctx).\n this.options.sanitize?.(client._input);\n const buf = client._inputBuffer;\n if (!buf) { return; } // no consumer registered \u2014 skip the clone allocation\n const inst = client._input!;\n if (seq !== undefined) {\n // Unreliable: dedup the ring by the framework wire seq (user seqField is for .at() only).\n if (!buf.accept(seq)) { return; }\n } else {\n // Reliable: no framework seq (implicit count). Honor a user `seqField` if set.\n const seqField = this.options.seqField;\n if (seqField !== undefined) {\n const value = (inst as any)[seqField] as number;\n if (typeof value === 'number' && !buf.accept(value)) { return; }\n }\n }\n buf.push(inst.clone() as any, renderTime, reckonTime, seq);\n }\n\n /** Decode a `ROOM_INPUT_RELIABLE` frame: an optional TIMED stamp prefix (shape\n * set by this room's derived stamp mode) then the input body. `it` starts at\n * offset 1, so the body begins at `it.offset`. */\n decodeReliable(client: ClientPrivate, buffer: Buffer, it: Iterator, modifiers: number): void {\n if (!client._inputDecoder) { return; }\n // Optional stamp prefix (TIMED bit), DELTA-CODED, length set by this room's\n // stamp mode (the timeline value is reconstructed against the per-client\n // baseline; the wire carries only the signed change from the previous frame):\n // both: [varint \u0394reckon][uint16 renderDelta] \u2192 renderTime = reckon \u2212 renderDelta\n // reckon-only: [varint \u0394reckon]\n // render-only: [varint \u0394render]\n let renderTime = 0;\n let reckonTime = 0;\n if (modifiers & ProtocolModifier.TIMED) {\n this.#resolveWireModes();\n // Reconstruct the absolute timeline value: baseline += signed delta. Both\n // sides start at 0 (so the first delta is absolute) and re-zero together on\n // (re)connect; reliable+in-order keeps the mirror locked.\n const stamp = client._reckonBaseline! += decode.number(buffer, it);\n if (this.#stampReckon && this.#stampRender) {\n reckonTime = stamp;\n const renderDelta = decode.uint16(buffer, it);\n renderTime = stamp > renderDelta ? stamp - renderDelta : 0;\n } else if (this.#stampReckon) {\n reckonTime = stamp;\n } else {\n renderTime = stamp;\n }\n }\n try {\n client._inputDecoder.decode(buffer.subarray(it.offset, buffer.byteLength));\n } catch (e: any) {\n debugAndPrintError(e);\n return;\n }\n client._lastInputReceivedAt = performance.now();\n // Mirrors the SDK's sent count; echoed back as lastInputSeq for RTT.\n client._receivedInputCount = (client._receivedInputCount ?? 0) + 1;\n this.capture(client, renderTime, reckonTime);\n }\n\n /** Decode a `ROOM_INPUT_UNRELIABLE` redundancy ring \u2014 each slot carries its\n * framework seq (base seq + position) for ring dedupe, no user seqField. */\n decodeUnreliable(client: ClientPrivate, buffer: Buffer, _modifiers: number): void {\n if (!client._inputDecoder) { return; }\n try {\n client._inputDecoder.decodeAll(buffer.subarray(1), (_inst, seq) => this.capture(client, 0, 0, seq));\n } catch (e: any) {\n debugAndPrintError(e);\n return;\n }\n client._lastInputReceivedAt = performance.now();\n }\n\n // --- handshake + stamp mode ---\n\n /** Resolve the wire stamp mode from the rewind timeline (once, then frozen). */\n #resolveWireModes(): void {\n if (this.#stampResolved) return;\n this.#stampResolved = true;\n const tl = this.room._timelineMode();\n this.#stampRender = tl?.snapshot ?? false;\n this.#stampReckon = tl?.reckon ?? false;\n }\n\n /**\n * The join-handshake input sections: INPUT_REFLECTION (the SDK-deserializable\n * input ctor bytes) and, when any runtime config differs from defaults,\n * INPUT_OPTIONS (`[flags uint8][tickRate varint?][patchRate varint?][subSteps\n * varint?]`). The client mirrors RENDER_TIME/RECKON_TIME (auto-stamp timeline),\n * tickRate (predict at dt=1/tickRate), patchRate (reconcile cadence), and\n * subSteps (physics sub-steps per input, omitted when 1). Returns `undefined`\n * when there's nothing to add.\n */\n handshakeSections(): Array<{ tag: number; bytes: Uint8Array }> | undefined {\n let sections: Array<{ tag: number; bytes: Uint8Array }> | undefined;\n const inputBytes = _inputReflectionCache.get(this.options.ctor);\n if (inputBytes !== undefined) {\n sections = [{ tag: HandshakeSection.INPUT_REFLECTION, bytes: inputBytes }];\n }\n this.#resolveWireModes();\n const stampRender = this.#stampRender;\n const stampReckon = this.#stampReckon;\n const tickRate = this.options.tickRate;\n const patchRate = (typeof this.room.patchRate === \"number\" && this.room.patchRate > 0)\n ? Math.round(this.room.patchRate) : undefined;\n const subSteps = (this.options.subSteps !== undefined && this.options.subSteps > 1)\n ? this.options.subSteps : undefined;\n if (stampRender || stampReckon || tickRate || patchRate || subSteps) {\n let flags = 0;\n if (stampRender) { flags |= InputFlags.RENDER_TIME; }\n if (stampReckon) { flags |= InputFlags.RECKON_TIME; }\n if (tickRate) { flags |= InputFlags.FIXED_TIMESTEP; }\n if (patchRate) { flags |= InputFlags.PATCH_RATE; }\n if (subSteps) { flags |= InputFlags.SUB_STEPS; }\n // 24B: worst case = flags + float64 tickRate (9) + uint32 patchRate (5) + subSteps (2).\n const buf = new Uint8Array(24);\n const sit = { offset: 0 };\n buf[sit.offset++] = flags;\n if (tickRate) { encode.number(buf, tickRate, sit); }\n if (patchRate) { encode.number(buf, patchRate, sit); }\n if (subSteps) { encode.number(buf, subSteps, sit); }\n (sections ??= []).push({\n tag: HandshakeSection.INPUT_OPTIONS,\n bytes: buf.subarray(0, sit.offset),\n });\n }\n return sections;\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAAA,SAAS,QAAQ,QAAQ,SAAS,kBAAiC;AACnE,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB,YAAY,wBAAwB;AAE/D;AAAA,EACE;AAAA,EAAmB;AAAA,EAAiB;AAAA,EACpC;AAAA,EAAkB;AAAA,OACb;AAOP,SAAS,0BAA0B;AAOnC,IAAM,wBAAwB,oBAAI,QAA8B;AAahE,IAAM,eAAN,MAAmB;AAAA,EAEjB,YAAY,OAAkB;AAAE,SAAK,QAAQ;AAAA,EAAO;AAAA;AAAA;AAAA,EAGpD,IAAI,WAAuC;AACzC,WAAO,KAAK,MAAM,UAAU,IAAI,SAAS,KAAK;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAIA,IAAI,WAA+B;AAAE,WAAO,KAAK,MAAM,QAAQ;AAAA,EAAU;AAAA,EACzE,IAAI,cAAkC;AAAE,UAAM,KAAK,KAAK,MAAM,QAAQ;AAAU,WAAO,KAAK,IAAI,KAAK;AAAA,EAAW;AAAA,EAChH,IAAI,SAA6B;AAAE,UAAM,KAAK,KAAK,MAAM,QAAQ;AAAU,WAAO,KAAK,MAAO,KAAK;AAAA,EAAW;AAAA;AAAA,EAE9G,IAAI,WAAmB;AAAE,WAAO,KAAK,MAAM,QAAQ,YAAY;AAAA,EAAG;AAAA,EAClE,IAAI,iBAAqC;AAAE,UAAM,KAAK,KAAK,MAAM,QAAQ;AAAU,WAAO,KAAM,IAAI,MAAO,KAAK,MAAM,QAAQ,YAAY,KAAK;AAAA,EAAW;AAAA,EAC1J,IAAI,YAAgC;AAAE,UAAM,KAAK,KAAK,MAAM,QAAQ;AAAU,WAAO,KAAM,MAAO,MAAO,KAAK,MAAM,QAAQ,YAAY,KAAK;AAAA,EAAW;AAC1J;AAaO,IAAM,YAAN,MAAgB;AAAA,EA8BrB,YAAY,MAAiB;AAZ7B;AAAA;AAAA;AAAA;AAAA;AAAA,SAAS,YAA6C,oBAAI,IAAI;AAK9D;AAAA;AAAA;AAAA,wBAAe;AACf,wBAAe;AACf,0BAAiB;AAMf,SAAK,OAAO;AACZ,SAAK,QAAQ,MAAM,KAAK,KAAK,MAAM;AAAA,EACrC;AAAA,EAVA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,OAIE,MACA,MAC6D;AAI7D,UAAM,WACJ,MAAM,gBAAgB,SAAY,IAAI,KAAK,cAC3C,MAAM,WAAW,SAAY,MAAO,KAAK,SACzC,MAAM;AACR,QACE,MAAM,aAAa,UAAa,KAAK,WAAW,UAAa,KAAK,gBAAgB,WACjF,CAAC,OAAO,UAAU,KAAK,QAAQ,KAAK,KAAK,WAAW,MACrD;AACA,cAAQ;AAAA,QACN,+CAA+C,KAAK,QAAQ;AAAA,MAE9D;AAAA,IACF;AACA,SAAK,UAAU;AAAA,MACb,MAAM;AAAA;AAAA,MAEN,UAAU,MAAM;AAAA,MAChB,eAAe,MAAM,iBAAiB;AAAA,MACtC,MAAM,MAAM;AAAA;AAAA,MAEZ,UAAU,MAAM,aAAa,SAAY,iBAAiB,KAAK,QAAQ,IAAI;AAAA,MAC3E;AAAA,MACA,UAAU,iBAAiB,MAAM,UAAU,aAAa;AAAA,IAC1D;AACA,QAAI,CAAC,sBAAsB,IAAI,IAAI,GAAG;AAEpC,4BAAsB,IAAI,MAAM,WAAW,OAAO,IAAI,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC;AAAA,IAC5E;AACA,UAAM,MAAM,IAAI,aAAa,IAAI;AACjC,SAAK,MAAM;AACX,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,QAAsC;AAC7C,WAAO,SAAS,IAAI,KAAK,QAAQ,KAAK;AACtC,WAAO,gBAAgB,IAAI,aAAa,OAAO,MAAM;AACrD,WAAO,kBAAkB;AACzB,UAAM,UAAU,KAAK,QAAQ;AAC7B,QAAI,UAAU,GAAG;AAEf,aAAO,eAAe,IAAI,gBAAgB,SAAS,KAAK,QAAQ,UAAU,KAAK,QAAQ,MAAM,QAAQ,KAAK,QAAQ,IAAI;AAAA,IACxH;AACA,WAAO,iBAAiB,IAAI,kBAAkB,QAAQ,KAAK,KAAK;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,WAA2B;AACvC,UAAM,MAAM,KAAK,UAAU,IAAI,SAAS;AACxC,WAAO,eAAe,oBAAoB,IAAI,gBAAgB;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,WAAmB,QAA6B;AACvD,SAAK,UAAU,IAAI,WAAW,OAAO,cAAe;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,QAA6B;AAClC,WAAO,cAAc,MAAM;AAAA,EAC7B;AAAA;AAAA,EAGA,QAAQ,WAAyB;AAC/B,SAAK,UAAU,OAAO,SAAS;AAAA,EACjC;AAAA;AAAA,EAGA,UAAgB;AACd,SAAK,UAAU,MAAM;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,QAAQ,QAAuB,aAAqB,GAAG,aAAqB,GAAG,KAAoB;AAEjG,SAAK,QAAQ,WAAW,OAAO,MAAM;AACrC,UAAM,MAAM,OAAO;AACnB,QAAI,CAAC,KAAK;AAAE;AAAA,IAAQ;AACpB,UAAM,OAAO,OAAO;AACpB,QAAI,QAAQ,QAAW;AAErB,UAAI,CAAC,IAAI,OAAO,GAAG,GAAG;AAAE;AAAA,MAAQ;AAAA,IAClC,OAAO;AAEL,YAAM,WAAW,KAAK,QAAQ;AAC9B,UAAI,aAAa,QAAW;AAC1B,cAAM,QAAS,KAAa,QAAQ;AACpC,YAAI,OAAO,UAAU,YAAY,CAAC,IAAI,OAAO,KAAK,GAAG;AAAE;AAAA,QAAQ;AAAA,MACjE;AAAA,IACF;AACA,QAAI,KAAK,KAAK,MAAM,GAAU,YAAY,YAAY,GAAG;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,QAAuB,QAAgB,IAAc,WAAyB;AAC3F,QAAI,CAAC,OAAO,eAAe;AAAE;AAAA,IAAQ;AAOrC,QAAI,aAAa;AACjB,QAAI,aAAa;AACjB,QAAI,YAAY,iBAAiB,OAAO;AACtC,WAAK,kBAAkB;AAIvB,YAAM,QAAQ,OAAO,mBAAoB,OAAO,OAAO,QAAQ,EAAE;AACjE,UAAI,KAAK,gBAAgB,KAAK,cAAc;AAC1C,qBAAa;AACb,cAAM,cAAc,OAAO,OAAO,QAAQ,EAAE;AAC5C,qBAAa,QAAQ,cAAc,QAAQ,cAAc;AAAA,MAC3D,WAAW,KAAK,cAAc;AAC5B,qBAAa;AAAA,MACf,OAAO;AACL,qBAAa;AAAA,MACf;AAAA,IACF;AACA,QAAI;AACF,aAAO,cAAc,OAAO,OAAO,SAAS,GAAG,QAAQ,OAAO,UAAU,CAAC;AAAA,IAC3E,SAAS,GAAQ;AACf,yBAAmB,CAAC;AACpB;AAAA,IACF;AACA,WAAO,uBAAuB,YAAY,IAAI;AAE9C,WAAO,uBAAuB,OAAO,uBAAuB,KAAK;AACjE,SAAK,QAAQ,QAAQ,YAAY,UAAU;AAAA,EAC7C;AAAA;AAAA;AAAA,EAIA,iBAAiB,QAAuB,QAAgB,YAA0B;AAChF,QAAI,CAAC,OAAO,eAAe;AAAE;AAAA,IAAQ;AACrC,QAAI;AACF,aAAO,cAAc,UAAU,OAAO,SAAS,CAAC,GAAG,CAAC,OAAO,QAAQ,KAAK,QAAQ,QAAQ,GAAG,GAAG,GAAG,CAAC;AAAA,IACpG,SAAS,GAAQ;AACf,yBAAmB,CAAC;AACpB;AAAA,IACF;AACA,WAAO,uBAAuB,YAAY,IAAI;AAAA,EAChD;AAAA;AAAA;AAAA,EAKA,oBAA0B;AACxB,QAAI,KAAK,eAAgB;AACzB,SAAK,iBAAiB;AACtB,UAAM,KAAK,KAAK,KAAK,cAAc;AACnC,SAAK,eAAe,IAAI,YAAY;AACpC,SAAK,eAAe,IAAI,UAAU;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,oBAA2E;AACzE,QAAI;AACJ,UAAM,aAAa,sBAAsB,IAAI,KAAK,QAAQ,IAAI;AAC9D,QAAI,eAAe,QAAW;AAC5B,iBAAW,CAAC,EAAE,KAAK,iBAAiB,kBAAkB,OAAO,WAAW,CAAC;AAAA,IAC3E;AACA,SAAK,kBAAkB;AACvB,UAAM,cAAc,KAAK;AACzB,UAAM,cAAc,KAAK;AACzB,UAAM,WAAW,KAAK,QAAQ;AAC9B,UAAM,YAAa,OAAO,KAAK,KAAK,cAAc,YAAY,KAAK,KAAK,YAAY,IAChF,KAAK,MAAM,KAAK,KAAK,SAAS,IAAI;AACtC,UAAM,WAAY,KAAK,QAAQ,aAAa,UAAa,KAAK,QAAQ,WAAW,IAC7E,KAAK,QAAQ,WAAW;AAC5B,QAAI,eAAe,eAAe,YAAY,aAAa,UAAU;AACnE,UAAI,QAAQ;AACZ,UAAI,aAAa;AAAE,iBAAS,WAAW;AAAA,MAAa;AACpD,UAAI,aAAa;AAAE,iBAAS,WAAW;AAAA,MAAa;AACpD,UAAI,UAAU;AAAE,iBAAS,WAAW;AAAA,MAAgB;AACpD,UAAI,WAAW;AAAE,iBAAS,WAAW;AAAA,MAAY;AACjD,UAAI,UAAU;AAAE,iBAAS,WAAW;AAAA,MAAW;AAE/C,YAAM,MAAM,IAAI,WAAW,EAAE;AAC7B,YAAM,MAAM,EAAE,QAAQ,EAAE;AACxB,UAAI,IAAI,QAAQ,IAAI;AACpB,UAAI,UAAU;AAAE,eAAO,OAAO,KAAK,UAAU,GAAG;AAAA,MAAG;AACnD,UAAI,WAAW;AAAE,eAAO,OAAO,KAAK,WAAW,GAAG;AAAA,MAAG;AACrD,UAAI,UAAU;AAAE,eAAO,OAAO,KAAK,UAAU,GAAG;AAAA,MAAG;AACnD,OAAC,aAAa,CAAC,GAAG,KAAK;AAAA,QACrB,KAAK,iBAAiB;AAAA,QACtB,OAAO,IAAI,SAAS,GAAG,IAAI,MAAM;AAAA,MACnC,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
|
|
16
|
+
// packages/core/src/input/types.ts
|
|
17
|
+
var types_exports = {};
|
|
18
|
+
module.exports = __toCommonJS(types_exports);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/input/types.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Types for the room input subsystem: the option shapes accepted by\n * `Room.defineInput()`, the per-client {@link InputAccessor} / room-wide\n * {@link InputAPI} surfaces, and the internal normalized options. Runtime\n * lives in `InputBuffer.ts`.\n */\n\n/**\n * Names of fields on `I` whose values are `number` \u2014 used by\n * `Room.defineInput()` to constrain `seqField` to actually-numeric fields\n * on the input schema. Filters out booleans, strings, methods, etc.\n */\nexport type NumericFieldsOf<I> = {\n [K in keyof I]: I[K] extends number ? (K extends string ? K : never) : never;\n}[keyof I];\n\n/**\n * Context handed to the `idle` callback form \u2014 see {@link IdleInput}. A reused\n * per-client scratch: read it inside the callback, don't store it. It carries\n * MECHANISM only \u2014 derived judgments (e.g. liveness) stay in userland; look the\n * client up via `this.clients.get(ctx.sessionId)` when your policy needs it.\n */\nexport interface IdleContext<I> {\n /** Last decoded input (`undefined` before the client's first). */\n latest: I | undefined;\n sessionId: string;\n}\n\n/**\n * What to synthesize when the buffer is empty (declared room-wide at\n * `defineInput({ idle })`, or per-call via {@link ConsumeOptions.idle}):\n * - `true` \u2014 pure schema defaults.\n * - `Partial<I>` \u2014 defaults overlaid with these fields. A full schema instance\n * (e.g. {@link InputAccessor.latest}) also works \u2014 fields are copied BY NAME,\n * so its prototype accessors are read correctly.\n * - callback \u2014 invoked LAZILY (only on an actually-empty tick) with an\n * {@link IdleContext}, returning either of the above. The right form when the\n * overrides take work to compute (entity lookups, held-button carry-over).\n *\n * \u26A0 Do NOT build overrides by spreading a schema instance (`{ ...latest, x }`):\n * schema fields live on the prototype, so the spread copies NOTHING of them.\n * Return `latest` itself, or name the fields (`{ x, plant: !!latest?.plant }`).\n */\nexport type IdleInput<I> = true | Partial<I> | ((ctx: IdleContext<I>) => true | Partial<I>);\n\n/**\n * Options for {@link InputAccessor.drain} / {@link InputAccessor.next}.\n * `idle` overrides the room-level `defineInput({ idle })` policy for this call\n * (see {@link IdleInput}); pass `false` to suppress it (force skip behavior).\n */\nexport interface ConsumeOptions<I> {\n idle?: IdleInput<I> | false;\n}\n\n/**\n * Input sanitization, declared at `defineInput({ sanitize })` \u2014 never trust the\n * wire. Applied to each decoded frame IN PLACE, before anything reads it\n * (`latest`, the buffer, the `idle` callback's ctx):\n * - **Map form** \u2014 per-field `[min, max]` range clamps with NaN-safe semantics:\n * `NaN` (both comparisons false) lands on `min`, closing the classic\n * `Math.min(NaN, \u2026)` poisoning hole.\n * - **Callback form** \u2014 arbitrary in-place fix-up (wrap an angle, enforce a\n * cross-field rule) for anything beyond ranges.\n *\n * Sanitizers MODIFY, they never reject \u2014 a malformed value becomes a legal one\n * instead of dropping the frame. Must not touch the `seqField` (it runs before\n * dedupe). Semantic validation (\"slot must name an owned weapon\") stays in\n * your sim \u2014 this handles value domains, not game rules.\n */\nexport type SanitizeInput<I> =\n | Partial<Record<NumericFieldsOf<I>, readonly [number, number]>>\n | ((input: I) => void);\n\n/** Options for `Room.defineInput()`. The user-facing twin of the internal\n * normalized {@link NormalizedInputOptions}. */\nexport interface DefineInputOptions<I = any> {\n seqField?: NumericFieldsOf<I>;\n bufferMaxSize?: number;\n /**\n * Input sanitization \u2014 never trust the wire. Applied IN PLACE to every\n * decoded frame before anything reads it (`latest`, the buffer, the `idle`\n * ctx). Map form = per-field `[min, max]` clamps with NaN-safe semantics\n * (NaN \u2192 min \u2014 closes the `Math.min(NaN, \u2026)` poisoning hole); callback form\n * = arbitrary in-place fix-up. Sanitizers MODIFY, never reject. See\n * {@link SanitizeInput}.\n *\n * ```ts\n * sanitize: { moveF: [-1, 1], pitch: [-PITCH_LIMIT, PITCH_LIMIT], dt: [0, MAX_DT] },\n * // or: sanitize: (f) => { f.angle = wrapAngle(f.angle); },\n * ```\n */\n sanitize?: SanitizeInput<I>;\n /**\n * Room-level absence policy: when a tick has no buffered input, bare\n * `drain()` / `next()` synthesize ONE \"idle\" frame from it \u2014 the schema's\n * defaults overlaid with the policy's overrides \u2014 so the sim loop needs no\n * empty-branch. Prefer the callback form, invoked lazily (only on\n * actually-empty ticks) with an {@link IdleContext} (`latest` + `sessionId`):\n *\n * ```ts\n * idle: ({ latest, sessionId }) => {\n * const p = this.state.players.get(sessionId); // closes over the room\n * return p ? { yaw: p.yaw, plant: !!latest?.plant } : true;\n * }\n * ```\n *\n * Not declaring it keeps the skip behavior (`drain()` \u2192 `[]`). Per-call\n * `{ idle }` overrides this default; `{ idle: false }` suppresses it.\n * Synthesized frames never advance the reconcile ack or `renderTime`.\n */\n idle?: IdleInput<I>;\n /**\n * Fixed step rate in **Hz** cascaded to the client via the join handshake;\n * it predicts at dt = 1/tickRate for deterministic rollback. Defaults to\n * the `setTimestep` rate \u2014 pass this only when the prediction\n * step differs from it. NOTE: a *rate*, not an interval \u2014 `1000/30` is the\n * step in ms, a classic mistake; use {@link stepMs} for that. Superseded by\n * {@link stepMs} / {@link stepSeconds} when those are given.\n */\n tickRate?: number;\n /**\n * Fixed step as a duration in **milliseconds** \u2014 the unit-safe alternative\n * to {@link tickRate} (`stepMs: 1000/30` is unambiguous where\n * `tickRate: 1000/30` is a bug). Normalized to the canonical Hz value\n * (`1000/stepMs`). Takes precedence over `tickRate`.\n */\n stepMs?: number;\n /**\n * Fixed step as a duration in **seconds** (e.g. `1/30`). Normalized to the\n * canonical Hz value (`1/stepSeconds`). Highest precedence.\n */\n stepSeconds?: number;\n /**\n * Physics sub-steps per input tick (integer \u2265 1, default 1) \u2014 decouples the\n * PHYSICS rate from the input/network rate. One input still drives exactly\n * one fixed step (the replay invariant), but the simulation integrates\n * `subSteps` engine steps of `stepSeconds/subSteps` inside it, identically\n * on client and server \u2014 physics at `tickRate * subSteps` Hz while sending\n * `tickRate` inputs/sec. Cascaded to clients via the join handshake;\n * both sides read the derived numbers off their step context\n * (`ctx.subSteps` / `ctx.subDt`) so N and dt can't drift apart.\n * Usually declared via {@link Room.setFixedTimestep}'s `subSteps` option\n * instead \u2014 pass it here only when the room runs its own loop.\n */\n subSteps?: number;\n}\n\n/** `true` when the defineInput opts declared an `idle` policy \u2014 narrows the\n * returned {@link InputAPI} so bare `next()` types non-optional `I`.\n * @internal \u2014 exported for {@link RoomInput.define}. */\nexport type IdleDeclared<O, I> = O extends { idle: IdleInput<I> } ? true : false;\n\n/**\n * Internal: input configuration captured by `Room.defineInput()` \u2014 the\n * normalized form of {@link DefineInputOptions} (step declarations resolved to\n * Hz, sanitizer compiled). The schema constructor is stored here so the\n * runtime doesn't need to know it through the public `room.inputs` (the\n * `.get(sessionId)` accessor).\n *\n * @internal\n */\nexport interface NormalizedInputOptions {\n /**\n * Schema constructor used to allocate per-client input instances on join.\n * Captured by `defineInput()` from its `type` argument.\n *\n * Typed loosely (`new () => any`) to sidestep type-identity issues across\n * duplicate `@colyseus/schema` installs; the runtime calls\n * `instance.clone()` and friends, which match by shape.\n */\n ctor: new () => any;\n\n /**\n * Name of a monotonically-increasing numeric field on the input schema used\n * to order and dedupe incoming frames. Unset by default (opt-in) \u2014 dedupe and\n * `.at()` lookup are off unless you name a field here. When set, the framework:\n * - Drops redundant frames (`input[seqField]` \u2264 the last-seen value are\n * discarded before they enter the buffer) \u2014 the unreliable-mode\n * ring-redundancy pattern.\n * - Powers `room.inputs.get(sessionId).at(value)` lookups.\n *\n * Despite the name, \"seq\" here is broader than an integer counter \u2014 any\n * monotonic numeric field works:\n * - **Sequence counter** (`\"seq\"`, `\"tick\"`, `\"frame\"`) \u2014 typical for\n * lockstep / rollback netcode.\n * - **Timestamp** (`\"timestamp\"`, milliseconds or seconds) \u2014 useful for\n * variable-rate clients, lag compensation, hit registration (a float-seconds\n * timestamp stamped on each saved move is the common shape).\n *\n * Whichever you use, the field must increase monotonically across frames\n * for dedupe to work.\n */\n seqField?: string;\n\n /**\n * > 0 enables per-client buffering of cloned snapshots \u2014 required for\n * `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated\n * data. Oldest drops on overflow. Set to `0` to disable (`.latest` still\n * works).\n */\n bufferMaxSize: number;\n\n /**\n * Fixed step rate (Hz) advertised to clients via the join handshake; they\n * predict at dt = 1/tickRate. Set explicitly via `defineInput`, or derived\n * from `setTimestep`. Unset = not advertised.\n */\n tickRate?: number;\n\n /**\n * Physics sub-steps per input tick (integer \u2265 1) advertised to clients via\n * the join handshake \u2014 the simulation integrates `subSteps` engine steps of\n * `(1/tickRate)/subSteps` per input on BOTH sides, so physics runs at\n * `tickRate * subSteps` Hz on a `tickRate` input/network rate. Set via\n * `setFixedTimestep(..., { subSteps })` (or `defineInput`). Unset/1 = not\n * advertised (input rate == physics rate).\n */\n subSteps?: number;\n\n /**\n * Room-level absence policy: bare `drain()` / `next()` synthesize one idle\n * frame from it when a tick has no input. See {@link IdleInput}.\n */\n idle?: IdleInput<any>;\n\n /**\n * COMPILED sanitizer (see {@link SanitizeInput} / `compileSanitizer`) \u2014\n * applied in place to each decoded frame before it becomes visible to\n * `latest` / the buffer / the idle ctx.\n */\n sanitize?: (instance: any) => void;\n}\n\n/**\n * Per-client input accessor returned by `room.inputs.get(sessionId)`. Combines the\n * latest decoded instance with the (optional) snapshot ring buffer.\n *\n * - {@link latest} \u2014 the bound Schema instance, mutated in place by the\n * decoder. Cheapest read; use when only the most recent state matters.\n * - {@link consume} / `for (const inp of accessor)` / {@link drain} /\n * {@link next} / {@link take} / {@link peek} / {@link at} \u2014 populated when\n * `defineInput()` was called with `bufferMaxSize > 0` (default 32). Use for\n * rollback netcode / lockstep where every frame matters.\n *\n * **Per-entity vs shared world** \u2014 pick the consume primitive by who integrates:\n * - **Per-entity** (each body integrates itself): iterate the accessor \u2014\n * `for (const inp of this.inputs.get(sid))` (sugar for {@link consume}) \u2014 and\n * sub-integrate one per input. N inputs = N steps for that player, ack lands\n * on the newest applied. The clean default. Iterating consumes ONE AT A TIME,\n * so {@link renderTime} tracks each input (lag comp stays exact per step) and\n * `break` leaves the rest buffered. {@link drain} returns the same set as an\n * array (retainable), but reports only the newest input's {@link renderTime}.\n * - **Shared world** (one solver step advances every body together): you can't\n * replay N inputs as N world steps without over-stepping everyone else, so\n * {@link next} exactly one input per entity per tick (or {@link take} a bounded\n * few and sub-step the solver per input). The ack then === inputs actually\n * simulated; `drain()`-then-apply-latest would silently jump the ack past\n * inputs the server never simulated, snapping the client's reconciler.\n *\n * Returned for unknown sessionIds and rooms without `defineInput()` is a\n * frozen no-op accessor (latest=undefined, drain/next/take/peek=[]/undefined,\n * at=undefined, size=0, clear=no-op).\n *\n * `Idle` is `true` when the room declared `defineInput({ idle })` \u2014 it narrows\n * {@link next} to non-optional `I` (bare calls always yield a frame).\n */\nexport interface InputAccessor<I = any, Idle extends boolean = false> {\n /** Latest decoded input. `undefined` when unknown sessionId or no input declared. */\n readonly latest: I | undefined;\n\n /**\n * Find the buffered snapshot whose `[seqField]` equals `value`. The field\n * name is the Room's `defineInput()` `seqField`. Linear scan \u2014 cheap for\n * typical buffer sizes; not intended for very large rings. Returns\n * `undefined` when no match is buffered (or `seqField` isn't configured).\n *\n * Useful for tick-aligned retrieval (lockstep, rollback).\n */\n at(value: number): I | undefined;\n\n /**\n * The blessed **per-entity** loop: consume each pending input oldest \u2192 newest,\n * ONE AT A TIME. Unlike {@link drain} (which reports only the newest input's\n * stamps up front), iterating updates {@link renderTime}/{@link reckonTime} and\n * advances {@link consumedCount} per yielded input \u2014 so per-step lag comp\n * rewinds to the exact instant of the input being applied:\n *\n * ```ts\n * for (const inp of this.inputs.get(sid)) { // sugar: accessor is iterable\n * applyInput(p, inp, dt);\n * this.collide(sid, p); // renderTime === THIS input's stamp\n * if (!p.alive) break; // break leaves the rest buffered\n * }\n * ```\n *\n * When the buffer is empty and an `idle` policy is in effect (room-level, or\n * per-call `opts.idle`), yields EXACTLY ONE idle frame (not consumed: no ack\n * bump, no {@link renderTime} change, {@link wasIdle} === true) then stops. No\n * policy + empty \u2192 zero iterations. Unknown sessionId / `bufferMaxSize: 0` \u2192\n * empty. `for (const inp of accessor)` is `consume()` with no opts.\n *\n * The returned iterator is POOLED (reused per call, allocation-free). For\n * `for..of` / spread / `Array.from` this is invisible. If you drive it by hand,\n * iterate it once to completion (or call `.return()` / `break`) before the next\n * `consume()`, and read each `.next()` result before the next \u2014 don't retain or\n * compare result objects. (Nesting two `consume()` loops over the SAME channel\n * is handled safely but is meaningless \u2014 both share one cursor.)\n */\n consume(opts?: ConsumeOptions<I>): IterableIterator<I>;\n\n /** Iterate pending (and/or one idle) inputs \u2014 sugar for {@link consume}(). */\n [Symbol.iterator](): IterableIterator<I>;\n\n /**\n * Take everything buffered (oldest \u2192 newest) and clear. Snapshots are safe to retain.\n *\n * With a room-level `defineInput({ idle })` policy the result is TOTAL: an\n * empty tick yields one synthesized \"idle\" frame instead of `[]`, so the sim\n * loop needs no empty-branch (gravity still integrates, action guards\n * naturally no-op on default values). The synthesized frame is the schema's\n * DEFAULTS overlaid with the policy's overrides (see {@link IdleInput}):\n *\n * ```ts\n * // declared once at defineInput({ idle: (ctx) => ({ ... }) }); then simply:\n * for (const f of this.inputs.get(sid).drain()) {\n * stepPlayer(p, f, world); // \u22651 frame, always\n * if (f.fire) tryFire(...); // idle frame: fire=false \u2192 no-op\n * }\n * ```\n *\n * Pass `{ idle }` to override the room policy for this call, or\n * `{ idle: false }` to suppress it.\n *\n * The idle frame is NOT a consumed input: it advances neither the reconcile\n * ack (`consumedCount`) nor {@link renderTime}. It is ONE reused instance per\n * client, refilled on each synthesis \u2014 read it within the tick, don't store\n * it. No synthesis for unknown sessionIds or `bufferMaxSize: 0`.\n */\n drain(opts?: ConsumeOptions<I>): I[];\n\n /**\n * Consume the single oldest buffered input and advance the reconcile ack by\n * exactly one \u2014 the complement to {@link peek}. Returns `undefined`\n * when the buffer is empty.\n *\n * Use this in a **shared-world** loop where one solver step advances every\n * entity together: you can't apply N-inputs-per-player as N world steps without\n * over-stepping the others, so consume one input per entity per fixed step\n * instead. {@link drain} (consume all, ack jumps to newest) is correct only\n * when each entity integrates ITSELF \u2014 see the {@link InputAccessor} docs.\n *\n * `consumedCount`/{@link renderTime} stay exact: the ack reflects only the\n * inputs you actually simulated, and `renderTime` is the stamp of THIS input.\n *\n * With a room-level `defineInput({ idle })` policy (or a per-call `{ idle }`\n * override) an empty tick returns a synthesized frame instead of `undefined`\n * \u2014 same contract as {@link drain}'s. A \"held key keeps moving through a\n * packet gap\" loop:\n *\n * @example\n * ```ts\n * // declared once:\n * inputs = this.defineInput(InputSchema, {\n * idle: ({ latest }) => latest ?? true, // empty tick \u2192 last input verbatim\n * });\n * // per tick:\n * this.setFixedTimestep(() => {\n * for (const [sid, body] of bodies) {\n * applyInputToBody(body, this.inputs.get(sid).next()); // typed I \u2014 never undefined\n * }\n * world.step(); // one step for everyone\n * }, 30);\n * ```\n */\n next(): Idle extends true ? I : I | undefined;\n next(opts: { idle: IdleInput<I> }): I;\n next(opts?: ConsumeOptions<I>): I | undefined;\n\n /**\n * Consume up to `n` oldest buffered inputs (oldest \u2192 newest) and advance the\n * reconcile ack by the count actually taken. Returns fewer than `n` (or `[]`)\n * when the buffer holds fewer. Use for shared-world **sub-stepping** \u2014 apply\n * each taken input as its own solver sub-step within one tick, bounding the\n * sub-steps per tick. {@link next} is the `take(1)` shorthand.\n */\n take(n: number): I[];\n\n /** Read everything buffered without consuming. */\n peek(): I[];\n\n /** Number of snapshots currently buffered. */\n readonly size: number;\n\n /**\n * Cumulative count of this client's inputs CONSUMED so far \u2014 the reconcile ack\n * echoed back to the client (its `lastProcessed`). Advances by one per\n * {@link next}, by the count taken per {@link take}/{@link drain}, by one per\n * yield while iterating {@link consume}, and on overflow-drop; a synthesized\n * idle frame NEVER advances it. Monotonic, so it doubles as a server-side input\n * seq \u2014 gate per-input actions off it (e.g. a fire cooldown counted in inputs)\n * instead of hand-rolling a counter. `0` before the first consume / for the\n * no-op accessor.\n */\n readonly consumedCount: number;\n\n /**\n * `true` when the most recently produced frame was a synthesized idle (vs a\n * real consumed input) \u2014 the \"skip work on idle\" guard. Set per consume\n * call/yield: `true` after a {@link next}/{@link consume}/{@link drain} that\n * fell back to the `idle` policy on an empty buffer, `false` after a real\n * input. Always `false` for {@link take} (never synthesizes) and the no-op\n * accessor. Reads meaningfully right after the consume that produced the frame.\n */\n readonly wasIdle: boolean;\n\n /** Drop all buffered snapshots (also resets the dedupe tracker). */\n clear(): void;\n\n /**\n * Server-clock render timestamp (ms since room start) of the most recently\n * consumed input \u2014 the time the client was rendering the world at when it\n * issued that input. Tracks the consume primitive: {@link drain}/{@link take}\n * report the NEWEST input they consumed; {@link next} and each\n * {@link consume}/`for..of` yield report THAT one input, so per-input loops\n * rewind to the exact instant of the input simulated.\n * `0` until the first render-time-stamped input is consumed. Populated only\n * when the room rewinds a `mode:\"snapshot\"` group (which auto-enables the\n * renderTime stamp) and `bufferMaxSize > 0`. Usually you don't read this\n * directly \u2014 pass the\n * sessionId to `rewind.lastSeenBy(sessionId)` (which resolves this value, clamps\n * it, and falls back to live) for lag-compensated \"what you see is what you\n * hit\" hit registration; use this getter only for a custom rewind time.\n */\n readonly renderTime: number;\n\n /**\n * Reckon-display stamp (server-clock ms) of the most recently consumed input\n * \u2014 the client's serverNow ESTIMATE when it sampled that input, i.e. the\n * exact instant its forward-reckoned (`mode:\"reckon\"`) entities were\n * displayed at. Stamping the display instant DIRECTLY makes the rewind read\n * immune to the client's RTT-estimation error: the client displayed\n * `f(serverNow_est)` and the server reads `f(serverNow_est)` \u2014 the same\n * index into the same recorded timeline, so estimation error cancels (the\n * `maxRewindMs` clamp still bounds spoofing). Same consume semantics as\n * {@link renderTime}; consumed automatically by `rewind.lastSeenBy()` \u2014\n * rarely read directly.\n *\n * Always a usable instant: when no reckon-stamped input has been consumed\n * yet (lag-comp off, client clock unsynced, nothing consumed), resolves to\n * the room clock's current `elapsedTime` \u2014 the fallback rooms previously\n * hand-wrote as `reckonTime > 0 ? reckonTime : now`. `rewind.lastSeenBy()`\n * internally reads the raw stamp, so its midpoint reconstruction still\n * engages for unstamped clients. The no-op accessor (unknown session / no\n * `defineInput()`) returns `0`.\n */\n readonly reckonTime: number;\n}\n\n/**\n * Returned by `Room.defineInput()`. Assign it to `this.inputs`, then call\n * `room.inputs.get(sessionId)` per tick to read each client's input stream \u2014\n * the same per-session lookup verb as `room.clients.get(sessionId)`.\n *\n * The fixed-step metadata lives HERE (one per room), not on the per-client\n * {@link InputAccessor} \u2014 these values are identical for every client, so they'd\n * be pure duplication per connection. `.get()` keeps the accessor and this\n * metadata cleanly separated.\n */\nexport type InputAPI<I = any, Idle extends boolean = false> = {\n /**\n * The input accessor for `sessionId` \u2014 the per-client stream you iterate /\n * `.next()` / `.drain()`. Returns a frozen no-op accessor for unknown sessions.\n * Mirrors `room.clients.get(sessionId)`.\n */\n get(sessionId: string): InputAccessor<I, Idle>;\n /**\n * Server-advertised fixed step rate in **Hz** \u2014 from `defineInput`'s\n * `tickRate`/`stepMs`/`stepSeconds`, or derived from `setTimestep` \u2014\n * the same value cascaded to predicting clients. `undefined` when no fixed\n * step is advertised.\n */\n readonly tickRate?: number;\n /**\n * The fixed step as **seconds** (`1/tickRate`): the dt to integrate one input\n * with, bit-identical to the client's prediction dt. Pass it to your physics\n * step (`applyInput(p, cmd, level, room.inputs.stepSeconds)`) so server and\n * client share one timestep instead of each keeping a constant that can drift.\n * `undefined` when no rate is advertised.\n */\n readonly stepSeconds?: number;\n /** The fixed step as **milliseconds** (`1000/tickRate`). `undefined` when no rate. */\n readonly stepMs?: number;\n /** Physics sub-steps per input tick (\u2265 1; `1` unless declared via\n * `setFixedTimestep(..., { subSteps })` / `defineInput`). */\n readonly subSteps: number;\n /** The physics sub-step as **seconds** (`stepSeconds / subSteps`) \u2014 the engine\n * dt when sub-stepping; equals {@link stepSeconds} when `subSteps` is 1.\n * `undefined` when no rate is advertised. */\n readonly subStepSeconds?: number;\n /** The physics sub-step as **milliseconds** (`stepMs / subSteps`). `undefined`\n * when no rate is advertised. */\n readonly subStepMs?: number;\n};\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|