@colyseus/schema 5.0.6 → 5.0.9
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/Reflection.d.ts +53 -0
- package/build/Schema.d.ts +26 -1
- package/build/annotations.d.ts +16 -1
- package/build/codegen/cli.cjs +103 -64
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/types.d.ts +0 -1
- package/build/decoder/Decoder.d.ts +28 -0
- package/build/decoder/Resync.d.ts +61 -0
- package/build/encoder/ChangeTree.d.ts +15 -0
- package/build/encoder/Encoder.d.ts +13 -0
- package/build/encoder/Pool.d.ts +62 -0
- package/build/encoder/Root.d.ts +5 -4
- package/build/encoder/StateView.d.ts +12 -3
- package/build/index.cjs +1211 -284
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +4 -1
- package/build/index.js +1211 -284
- package/build/index.mjs +1207 -285
- package/build/index.mjs.map +1 -1
- package/build/input/InputDecoder.d.ts +9 -4
- package/build/input/InputEncoder.d.ts +44 -53
- package/build/input/index.cjs +102 -7321
- package/build/input/index.cjs.map +1 -1
- package/build/input/index.mjs +97 -7316
- package/build/input/index.mjs.map +1 -1
- package/build/types/HelperTypes.d.ts +3 -0
- package/build/types/builder.d.ts +57 -4
- package/build/types/custom/ArraySchema.d.ts +11 -1
- package/build/types/custom/CollectionSchema.d.ts +9 -1
- package/build/types/custom/MapSchema.d.ts +9 -1
- package/build/types/custom/SetSchema.d.ts +9 -1
- package/build/types/custom/StreamSchema.d.ts +2 -1
- package/build/types/quantize.d.ts +102 -0
- package/build/types/symbols.d.ts +15 -0
- package/package.json +8 -1
- package/src/Metadata.ts +34 -9
- package/src/Reflection.ts +49 -11
- package/src/Schema.ts +64 -2
- package/src/annotations.ts +157 -51
- package/src/bench_churn.ts +121 -0
- package/src/codegen/languages/haxe.ts +0 -16
- package/src/codegen/parser.ts +69 -11
- package/src/codegen/types.ts +45 -63
- package/src/decoder/DecodeOperation.ts +46 -4
- package/src/decoder/Decoder.ts +48 -0
- package/src/decoder/ReferenceTracker.ts +9 -5
- package/src/decoder/Resync.ts +170 -0
- package/src/encoder/ChangeTree.ts +59 -0
- package/src/encoder/Encoder.ts +53 -12
- package/src/encoder/Pool.ts +90 -0
- package/src/encoder/Root.ts +22 -32
- package/src/encoder/StateView.ts +101 -50
- package/src/encoder/changeTree/liveIteration.ts +4 -0
- package/src/encoder/changeTree/treeAttachment.ts +28 -24
- package/src/index.ts +12 -1
- package/src/input/InputDecoder.ts +11 -5
- package/src/input/InputEncoder.ts +85 -126
- package/src/types/HelperTypes.ts +7 -0
- package/src/types/TypeContext.ts +7 -0
- package/src/types/builder.ts +62 -5
- package/src/types/custom/ArraySchema.ts +70 -12
- package/src/types/custom/CollectionSchema.ts +36 -1
- package/src/types/custom/MapSchema.ts +50 -1
- package/src/types/custom/SetSchema.ts +36 -1
- package/src/types/custom/StreamSchema.ts +7 -0
- package/src/types/quantize.ts +173 -0
- package/src/types/symbols.ts +16 -0
- package/build/encoder/RefIdAllocator.d.ts +0 -35
- package/src/encoder/RefIdAllocator.ts +0 -68
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { Metadata } from "./Metadata.js";
|
|
|
49
49
|
export {
|
|
50
50
|
type,
|
|
51
51
|
deprecated,
|
|
52
|
+
defineTypes,
|
|
52
53
|
owned,
|
|
53
54
|
unreliable,
|
|
54
55
|
transient,
|
|
@@ -79,6 +80,7 @@ export { getRawChangesCallback } from "./decoder/strategy/RawChanges.js";
|
|
|
79
80
|
|
|
80
81
|
export { Encoder } from "./encoder/Encoder.js";
|
|
81
82
|
export { Root } from "./encoder/Root.js";
|
|
83
|
+
export { createPool, type SchemaPool, type PoolOptions } from "./encoder/Pool.js";
|
|
82
84
|
export { encodeSchemaOperation, encodeArray, encodeKeyValueOperation, encodeMapEntry, encodeIndexedEntry } from "./encoder/EncodeOperation.js";
|
|
83
85
|
export { ChangeTree, type Ref, type IRef } from "./encoder/ChangeTree.js";
|
|
84
86
|
export { StateView } from "./encoder/StateView.js";
|
|
@@ -86,4 +88,13 @@ export { StateView } from "./encoder/StateView.js";
|
|
|
86
88
|
export { Decoder } from "./decoder/Decoder.js";
|
|
87
89
|
export { decodeSchemaOperation, decodeKeyValueOperation } from "./decoder/DecodeOperation.js";
|
|
88
90
|
|
|
89
|
-
export { OPERATION } from "./encoding/spec.js";
|
|
91
|
+
export { OPERATION } from "./encoding/spec.js";
|
|
92
|
+
|
|
93
|
+
// Re-exported for `@colyseus/schema/input` — that subpath bundle is built
|
|
94
|
+
// as a thin wrapper that imports identity-bearing modules from here at
|
|
95
|
+
// runtime, so it needs `getEncodeDescriptor` available on the public surface.
|
|
96
|
+
export { getEncodeDescriptor, type EncodeDescriptor } from "./encoder/EncodeDescriptor.js";
|
|
97
|
+
|
|
98
|
+
// Symbols used by InputEncoder/InputDecoder via the runtime-externalized
|
|
99
|
+
// `@colyseus/schema` import in `build/input/index.mjs`.
|
|
100
|
+
export { $numFields, $values } from "./types/symbols.js";
|
|
@@ -34,21 +34,27 @@ export class InputDecoder<T extends Schema = any> {
|
|
|
34
34
|
/**
|
|
35
35
|
* Walk a multi-input (unreliable) packet, decoding each length-framed
|
|
36
36
|
* input into the bound instance in order and invoking `onInput` after
|
|
37
|
-
* each decode. `onInput` receives the bound instance
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* each decode. `onInput` receives the bound instance and the framework
|
|
38
|
+
* **seq** of that input (decoded from the packet's base seq + position) —
|
|
39
|
+
* the receiver dedupes the redundancy ring on it (monotonic: drop seq ≤
|
|
40
|
+
* last seen). Reads of the instance must be synchronous; downstream code
|
|
41
|
+
* should apply the input to game state, not retain the reference.
|
|
42
|
+
*
|
|
43
|
+
* Packet layout: `[baseSeq][len][body]…[len][body]` (oldest→newest);
|
|
44
|
+
* slot *i* has `seq = baseSeq + i`.
|
|
40
45
|
*
|
|
41
46
|
* Returns the number of inputs decoded.
|
|
42
47
|
*/
|
|
43
|
-
decodeAll(bytes: Uint8Array, onInput: (instance: T,
|
|
48
|
+
decodeAll(bytes: Uint8Array, onInput: (instance: T, seq: number) => void): number {
|
|
44
49
|
const it = this._it;
|
|
45
50
|
it.offset = 0;
|
|
51
|
+
const baseSeq = decode.number(bytes, it); // packet-level base seq (oldest slot)
|
|
46
52
|
let count = 0;
|
|
47
53
|
while (it.offset < bytes.length) {
|
|
48
54
|
const len = decode.number(bytes, it);
|
|
49
55
|
const end = it.offset + len;
|
|
50
56
|
this._decoder.decode(bytes.subarray(it.offset, end));
|
|
51
|
-
onInput(this.instance, count);
|
|
57
|
+
onInput(this.instance, baseSeq + count); // slot seq by position
|
|
52
58
|
it.offset = end;
|
|
53
59
|
count++;
|
|
54
60
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { OPERATION } from "../encoding/spec.js";
|
|
2
1
|
import { $numFields, $values, $changes } from "../types/symbols.js";
|
|
3
2
|
import { encode } from "../encoding/encode.js";
|
|
4
3
|
import { Encoder } from "../encoder/Encoder.js";
|
|
@@ -26,31 +25,6 @@ export interface InputEncoderOptions {
|
|
|
26
25
|
* reliable mode (always exactly one input per packet).
|
|
27
26
|
*/
|
|
28
27
|
historySize?: number;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* When `true`, `encode()` emits only fields that changed since the
|
|
32
|
-
* previous call. First call (or first after `reset()`) still emits
|
|
33
|
-
* a full snapshot since there's no baseline to diff against.
|
|
34
|
-
*
|
|
35
|
-
* **Reliable mode:** returns an empty `Uint8Array` when nothing
|
|
36
|
-
* changed — caller can skip sending.
|
|
37
|
-
*
|
|
38
|
-
* **Unreliable mode:** no-change ticks don't push a new ring slot
|
|
39
|
-
* (avoids bloating the ring with empties); the existing ring is
|
|
40
|
-
* still re-emitted for redundancy. Wire ops use absolute values so
|
|
41
|
-
* cross-packet re-application is per-field idempotent.
|
|
42
|
-
*
|
|
43
|
-
* Decoder side is unchanged in either case: `(index|ADD)` wire ops
|
|
44
|
-
* apply to the bound instance; fields absent from a packet stay at
|
|
45
|
-
* their previously decoded value.
|
|
46
|
-
*/
|
|
47
|
-
delta?: boolean;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Override the reliable-mode output buffer. Default: 256 bytes,
|
|
51
|
-
* auto-grown on overflow.
|
|
52
|
-
*/
|
|
53
|
-
buffer?: Uint8Array;
|
|
54
28
|
}
|
|
55
29
|
|
|
56
30
|
const DEFAULT_SLOT_SIZE = 256;
|
|
@@ -60,17 +34,23 @@ const LENGTH_PREFIX_WORST_CASE = 5; // varint max for a uint32 length.
|
|
|
60
34
|
* Bound single-struct encoder for client→server input packets. Holds a
|
|
61
35
|
* reference to a Schema instance and produces wire-compatible bytes.
|
|
62
36
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
37
|
+
* Always delta-encodes: each `encode()` emits only the fields that changed
|
|
38
|
+
* since the previous call (via the setter-populated ChangeTree dirty set),
|
|
39
|
+
* wrapping a standard {@link Encoder}. The first call (or first after
|
|
40
|
+
* {@link reset}) emits a full snapshot, since there's no baseline to diff
|
|
41
|
+
* against.
|
|
65
42
|
*
|
|
66
|
-
* **
|
|
67
|
-
* `
|
|
68
|
-
*
|
|
69
|
-
*
|
|
43
|
+
* **Reliable mode** emits one delta per `encode()` — or an empty
|
|
44
|
+
* `Uint8Array` when nothing changed (the caller decides whether to send a
|
|
45
|
+
* body-less keep-alive or skip the tick). The bytes decode cleanly through
|
|
46
|
+
* the standard {@link Decoder}.
|
|
70
47
|
*
|
|
71
|
-
* **
|
|
72
|
-
* and emits
|
|
73
|
-
*
|
|
48
|
+
* **Unreliable mode** pushes each delta onto a ring buffer of size
|
|
49
|
+
* `historySize` and emits the last N in one packet, each framed with a
|
|
50
|
+
* varint length prefix. A no-change tick still pushes an (empty,
|
|
51
|
+
* carry-forward) slot so every tick gets a consecutive framework seq. Use
|
|
52
|
+
* {@link InputDecoder.decodeAll} to walk the framed packet. Wire ops use
|
|
53
|
+
* absolute values, so re-applying a redundant slot is per-field idempotent.
|
|
74
54
|
*
|
|
75
55
|
* Flat primitive fields only. Nested Schema / collection fields throw
|
|
76
56
|
* at construction.
|
|
@@ -79,32 +59,30 @@ export class InputEncoder<T extends Schema = any> {
|
|
|
79
59
|
readonly instance: T;
|
|
80
60
|
readonly mode: InputMode;
|
|
81
61
|
readonly historySize: number;
|
|
82
|
-
readonly delta: boolean;
|
|
83
62
|
|
|
84
63
|
private readonly _desc: EncodeDescriptor;
|
|
85
64
|
private readonly _numFields: number;
|
|
86
65
|
|
|
87
|
-
//
|
|
88
|
-
// not `readonly` — `_encodeFull()` may swap in a larger buffer.
|
|
89
|
-
private _buffer: Uint8Array;
|
|
90
|
-
private readonly _it = { offset: 0 };
|
|
91
|
-
|
|
92
|
-
// Unreliable-mode ring state. `_outBuffer` is where the concatenated
|
|
93
|
-
// packet lives; `_slots` / `_slotLens` hold each snapshot's bytes.
|
|
66
|
+
// Unreliable-mode ring: `_slots`/`_slotLens` hold each snapshot; `_outBuffer` holds the concatenated packet.
|
|
94
67
|
private _slots?: Uint8Array[];
|
|
95
68
|
private readonly _slotLens?: number[];
|
|
96
69
|
private _slotHead: number = 0;
|
|
97
70
|
private _slotCount: number = 0;
|
|
98
71
|
private _outBuffer?: Uint8Array;
|
|
72
|
+
// Monotonic per-tick input seq (unreliable only): ++ per pushed slot. The
|
|
73
|
+
// packet carries the OLDEST slot's seq once; the decoder derives each slot's
|
|
74
|
+
// seq by position (slots are consecutive — every tick pushes). This is the
|
|
75
|
+
// framework-owned seq that drives server-side dedupe of the redundancy ring
|
|
76
|
+
// WITHOUT the user adding a seq field. Monotonic across reset() so a
|
|
77
|
+
// reconnect that reuses the server buffer doesn't replay already-seen seqs.
|
|
78
|
+
private _seq: number = 0;
|
|
99
79
|
|
|
100
|
-
// Delta
|
|
101
|
-
|
|
102
|
-
private readonly _encoder?: Encoder<T>;
|
|
80
|
+
// Delta delegate; setters populate its ChangeTree, `encode()` drains dirty fields.
|
|
81
|
+
private readonly _encoder: Encoder<T>;
|
|
103
82
|
|
|
104
83
|
constructor(instance: T, options: InputEncoderOptions = {}) {
|
|
105
84
|
this.instance = instance;
|
|
106
85
|
this.mode = options.mode ?? "reliable";
|
|
107
|
-
this.delta = options.delta ?? false;
|
|
108
86
|
this.historySize = this.mode === "unreliable"
|
|
109
87
|
? Math.max(1, options.historySize ?? 3)
|
|
110
88
|
: 1;
|
|
@@ -124,8 +102,6 @@ export class InputEncoder<T extends Schema = any> {
|
|
|
124
102
|
}
|
|
125
103
|
}
|
|
126
104
|
|
|
127
|
-
this._buffer = options.buffer ?? new Uint8Array(DEFAULT_SLOT_SIZE);
|
|
128
|
-
|
|
129
105
|
if (this.mode === "unreliable") {
|
|
130
106
|
this._slots = new Array(this.historySize);
|
|
131
107
|
this._slotLens = new Array(this.historySize).fill(0);
|
|
@@ -137,113 +113,93 @@ export class InputEncoder<T extends Schema = any> {
|
|
|
137
113
|
);
|
|
138
114
|
}
|
|
139
115
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// Tracking stays enabled so setters keep populating it.
|
|
144
|
-
this._encoder = new Encoder<T>(instance);
|
|
145
|
-
} else {
|
|
146
|
-
// Full mode reads `$values` directly; setter-side tracking
|
|
147
|
-
// is pure overhead, so pause it.
|
|
148
|
-
instance.pauseTracking();
|
|
149
|
-
}
|
|
116
|
+
// Wrap a standard Encoder; keep tracking on so setters keep populating
|
|
117
|
+
// the dirty set we drain each encode().
|
|
118
|
+
this._encoder = new Encoder<T>(instance);
|
|
150
119
|
}
|
|
151
120
|
|
|
152
121
|
/**
|
|
153
|
-
*
|
|
122
|
+
* The framework input seq of the most recently encoded tick (unreliable
|
|
123
|
+
* mode): monotonic, ++ per `encode()`, kept across {@link reset}. `0` in
|
|
124
|
+
* reliable mode (which sequences inputs implicitly by message count). The
|
|
125
|
+
* client keys its reconciliation replay ring by this so the server's
|
|
126
|
+
* seq-value ack lines up across packet loss.
|
|
127
|
+
*/
|
|
128
|
+
get seq(): number { return this._seq; }
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Encode the bound instance's delta. Returns a subarray of an internal
|
|
154
132
|
* buffer — copy if retaining across calls.
|
|
155
133
|
*
|
|
156
|
-
* Output shape by
|
|
157
|
-
* - `reliable
|
|
158
|
-
* - `
|
|
159
|
-
*
|
|
160
|
-
* -
|
|
161
|
-
* length-framed per slot.
|
|
162
|
-
* - `unreliable` + delta: ring of last `historySize` deltas. No-
|
|
163
|
-
* change ticks don't push a new slot but still re-emit the ring.
|
|
164
|
-
* Empty only until the first change has been pushed.
|
|
134
|
+
* Output shape by mode:
|
|
135
|
+
* - `reliable`: only changed fields, or empty when nothing changed.
|
|
136
|
+
* - `unreliable`: ring of the last `historySize` deltas, length-framed
|
|
137
|
+
* per slot. A no-change tick pushes an empty (carry-forward) slot but
|
|
138
|
+
* still re-emits the ring. Empty only until the first slot is pushed.
|
|
165
139
|
*
|
|
166
140
|
* Buffers auto-grow on overflow; a one-time `console.warn` is
|
|
167
141
|
* emitted the first time it happens.
|
|
168
142
|
*/
|
|
169
143
|
encode(): Uint8Array {
|
|
170
|
-
const blob = this.
|
|
144
|
+
const blob = this._produceDelta();
|
|
171
145
|
return this.mode === "reliable" ? blob : this._pushAndEmitRing(blob);
|
|
172
146
|
}
|
|
173
147
|
|
|
174
148
|
/**
|
|
175
149
|
* Reset the encoder's internal state:
|
|
176
|
-
* -
|
|
177
|
-
* -
|
|
178
|
-
*
|
|
150
|
+
* - Drops the unreliable ring buffer.
|
|
151
|
+
* - Re-marks every currently populated field as dirty, so the next
|
|
152
|
+
* `encode()` emits a fresh full snapshot.
|
|
179
153
|
*
|
|
180
154
|
* Useful on disconnect / reconnect / scene transitions.
|
|
181
155
|
*/
|
|
182
156
|
reset(): void {
|
|
183
157
|
this._slotHead = 0;
|
|
184
158
|
this._slotCount = 0;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
tree.markDirty(i);
|
|
192
|
-
}
|
|
159
|
+
this._encoder.discardChanges();
|
|
160
|
+
const tree = this.instance[$changes];
|
|
161
|
+
const values = this.instance[$values];
|
|
162
|
+
for (let i = 0; i <= this._numFields; i++) {
|
|
163
|
+
if (values[i] === undefined || values[i] === null) continue;
|
|
164
|
+
tree.markDirty(i);
|
|
193
165
|
}
|
|
194
166
|
}
|
|
195
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Copy the bound instance's field values into `target` (a same-type instance)
|
|
170
|
+
* in place — no allocation, no `Object.keys` (cf. `Schema#assign`) and no
|
|
171
|
+
* `clone()`. For buffering a snapshot of the just-sent input into a reused
|
|
172
|
+
* slot (e.g. a client reconciliation/replay ring) without the transport
|
|
173
|
+
* having to reach into schema internals. The codec owns this because it owns
|
|
174
|
+
* the field representation. The in-place / alloc-free cousin of `clone()`,
|
|
175
|
+
* and the inverse direction of `Schema#assign(source)`.
|
|
176
|
+
*/
|
|
177
|
+
copyInto(target: T): void {
|
|
178
|
+
const src = (this.instance as any)[$values];
|
|
179
|
+
const dst = (target as any)[$values];
|
|
180
|
+
for (let i = 0; i <= this._numFields; i++) dst[i] = src[i];
|
|
181
|
+
}
|
|
182
|
+
|
|
196
183
|
// ────────────────────────────────────────────────────────────────────
|
|
197
|
-
//
|
|
198
|
-
// current instance. Caller routes by mode.
|
|
184
|
+
// Delta producer — one diff of the instance; caller routes by mode.
|
|
199
185
|
// ────────────────────────────────────────────────────────────────────
|
|
200
186
|
|
|
201
|
-
/** Write every populated primitive field into `_buffer`. */
|
|
202
|
-
private _produceFull(): Uint8Array {
|
|
203
|
-
let buf = this._buffer;
|
|
204
|
-
const it = this._it;
|
|
205
|
-
this._writeFields(buf, it);
|
|
206
|
-
if (it.offset > buf.byteLength) {
|
|
207
|
-
buf = this._buffer = InputEncoder._grow(buf, it.offset, "reliable encode");
|
|
208
|
-
this._writeFields(buf, it);
|
|
209
|
-
}
|
|
210
|
-
return buf.subarray(0, it.offset);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
187
|
/** Delegate to the wrapped Encoder, then clear its dirty set. */
|
|
214
188
|
private _produceDelta(): Uint8Array {
|
|
215
|
-
const bytes = this._encoder
|
|
216
|
-
this._encoder
|
|
189
|
+
const bytes = this._encoder.encode();
|
|
190
|
+
this._encoder.discardChanges();
|
|
217
191
|
return bytes;
|
|
218
192
|
}
|
|
219
193
|
|
|
220
|
-
/** Emit every populated field as `(index|ADD)` + value. */
|
|
221
|
-
private _writeFields(buf: Uint8Array, it: { offset: number }): void {
|
|
222
|
-
const values = (this.instance as any)[$values];
|
|
223
|
-
const encoders = this._desc.encoders;
|
|
224
|
-
it.offset = 0;
|
|
225
|
-
for (let i = 0; i <= this._numFields; i++) {
|
|
226
|
-
const value = values[i];
|
|
227
|
-
if (value === undefined || value === null) continue;
|
|
228
|
-
buf[it.offset++] = (i | OPERATION.ADD) & 255;
|
|
229
|
-
(encoders[i] as (b: Uint8Array, v: any, it: any) => void)(buf, value, it);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
194
|
// ────────────────────────────────────────────────────────────────────
|
|
234
|
-
// Ring — push blob to
|
|
235
|
-
// into the output buffer, return the framed packet.
|
|
195
|
+
// Ring — push blob to current slot, concat oldest→newest, return framed packet.
|
|
236
196
|
// ────────────────────────────────────────────────────────────────────
|
|
237
197
|
|
|
238
198
|
private _pushAndEmitRing(blob: Uint8Array): Uint8Array {
|
|
239
|
-
//
|
|
240
|
-
// the
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return this._emitRing();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Copy blob into the current slot, growing it if needed.
|
|
199
|
+
// Push a slot EVERY tick (even an empty delta) so ring seqs stay
|
|
200
|
+
// consecutive: the packet carries ONE base seq and the decoder derives
|
|
201
|
+
// each slot's seq by position. An empty slot decodes as carry-forward.
|
|
202
|
+
this._seq++;
|
|
247
203
|
let slot = this._slots![this._slotHead];
|
|
248
204
|
if (blob.length > slot.byteLength) {
|
|
249
205
|
slot = this._slots![this._slotHead] = InputEncoder._grow(
|
|
@@ -259,8 +215,12 @@ export class InputEncoder<T extends Schema = any> {
|
|
|
259
215
|
}
|
|
260
216
|
|
|
261
217
|
private _emitRing(): Uint8Array {
|
|
262
|
-
//
|
|
263
|
-
|
|
218
|
+
// Packet layout: [baseSeq][len][slot]…[len][slot] (oldest→newest).
|
|
219
|
+
// baseSeq = the oldest slot's seq; consecutive slots ⇒ slot i has seq baseSeq + i.
|
|
220
|
+
const baseSeq = this._seq - this._slotCount + 1;
|
|
221
|
+
|
|
222
|
+
// Upper bound: baseSeq varint + sum of slot byte counts + per-slot varint length.
|
|
223
|
+
let needed = LENGTH_PREFIX_WORST_CASE;
|
|
264
224
|
for (let i = 0; i < this._slotCount; i++) {
|
|
265
225
|
needed += this._slotLens![i] + LENGTH_PREFIX_WORST_CASE;
|
|
266
226
|
}
|
|
@@ -271,6 +231,7 @@ export class InputEncoder<T extends Schema = any> {
|
|
|
271
231
|
}
|
|
272
232
|
|
|
273
233
|
const outIt = { offset: 0 };
|
|
234
|
+
encode.number(out, baseSeq, outIt); // packet-level base seq
|
|
274
235
|
const oldest = (this._slotHead - this._slotCount + this.historySize) % this.historySize;
|
|
275
236
|
for (let i = 0; i < this._slotCount; i++) {
|
|
276
237
|
const idx = (oldest + i) % this.historySize;
|
|
@@ -283,9 +244,7 @@ export class InputEncoder<T extends Schema = any> {
|
|
|
283
244
|
}
|
|
284
245
|
|
|
285
246
|
// ────────────────────────────────────────────────────────────────────
|
|
286
|
-
// Buffer growth.
|
|
287
|
-
// but `it.offset` still advances — callers detect overflow via the
|
|
288
|
-
// `offset > byteLength` check and re-encode into the grown buffer.
|
|
247
|
+
// Buffer growth. Writes past `byteLength` silently drop but `it.offset` still advances, so callers detect overflow via `offset > byteLength` and re-encode into the grown buffer.
|
|
289
248
|
// ────────────────────────────────────────────────────────────────────
|
|
290
249
|
|
|
291
250
|
private static _warned = false;
|
|
@@ -295,7 +254,7 @@ export class InputEncoder<T extends Schema = any> {
|
|
|
295
254
|
InputEncoder._warned = true;
|
|
296
255
|
console.warn(
|
|
297
256
|
`@colyseus/schema/input: InputEncoder buffer overflow in ${where}. ` +
|
|
298
|
-
`Growing to ${newSize} bytes
|
|
257
|
+
`Growing to ${newSize} bytes.`
|
|
299
258
|
);
|
|
300
259
|
}
|
|
301
260
|
return new Uint8Array(newSize);
|
package/src/types/HelperTypes.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { $resyncPrune } from "./symbols.js";
|
|
1
2
|
import type { Definition, DefinitionType, PrimitiveType, RawPrimitiveType } from "../annotations.js";
|
|
2
3
|
import type { Schema } from "../Schema.js";
|
|
3
4
|
import type { ArraySchema } from "./custom/ArraySchema.js";
|
|
@@ -20,6 +21,12 @@ export interface Collection<K = any, V = any, IT = V> {
|
|
|
20
21
|
[Symbol.iterator](): IterableIterator<IT>;
|
|
21
22
|
forEach(callback: Function): void;
|
|
22
23
|
entries(): IterableIterator<[K, V]>;
|
|
24
|
+
/** See {@link $resyncPrune} — every collection kind must declare its resync-sweep semantics. */
|
|
25
|
+
[$resyncPrune](
|
|
26
|
+
visited: Set<number | string>,
|
|
27
|
+
prune: (value: V, identity: number | string) => void,
|
|
28
|
+
keep: (value: V) => void,
|
|
29
|
+
): void;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
export type InferValueType<T> =
|
package/src/types/TypeContext.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Metadata } from "../Metadata.js";
|
|
2
2
|
import { Schema } from "../Schema.js";
|
|
3
3
|
import { $streamFieldIndexes, $viewFieldIndexes } from "./symbols.js";
|
|
4
|
+
import { isQuantizedType } from "./quantize.js";
|
|
4
5
|
|
|
5
6
|
export class TypeContext {
|
|
6
7
|
types: { [id: number]: typeof Schema; } = {};
|
|
@@ -125,6 +126,12 @@ export class TypeContext {
|
|
|
125
126
|
continue;
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
// Quantized fields are scalar — their object `type` only carries the
|
|
130
|
+
// descriptor, there's no child Schema to discover.
|
|
131
|
+
if (isQuantizedType(fieldType)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
if (typeof (fieldType) === "function") {
|
|
129
136
|
this.discoverTypes(fieldType as typeof Schema, klass, index, parentHasViewTag || fieldHasViewTag);
|
|
130
137
|
|
package/src/types/builder.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { DefinitionType, RawPrimitiveType } from "../annotations.js";
|
|
|
8
8
|
import type { InferValueType, Constructor } from "./HelperTypes.js";
|
|
9
9
|
import { $builder } from "./symbols.js";
|
|
10
10
|
import { ARRAY_STREAM_NOT_SUPPORTED } from "../encoder/streaming.js";
|
|
11
|
+
import { resolveQuantize, type QuantizeOptions } from "./quantize.js";
|
|
11
12
|
|
|
12
13
|
type CollectionKind = "array" | "map" | "set" | "collection";
|
|
13
14
|
|
|
@@ -89,8 +90,23 @@ export class FieldBuilder<
|
|
|
89
90
|
this._type = type;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Provide a default value for this field.
|
|
95
|
+
*
|
|
96
|
+
* Pass a **factory function** `() => T` to build a FRESH value per instance
|
|
97
|
+
* (invoked once per construction) instead of sharing a single default — the
|
|
98
|
+
* clean way to default a ref to a plain custom class, or any field that must
|
|
99
|
+
* not share a mutable default across instances:
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* acc: t.ref(GunAccuracy).noSync().default(() => new GunAccuracy()),
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* Schema fields are never function-typed, so a function is always treated as a
|
|
106
|
+
* factory. A non-function value is shared (and cloned per instance if it is
|
|
107
|
+
* clone-able, e.g. a Schema/collection).
|
|
108
|
+
*/
|
|
109
|
+
default(value: T | (() => T)): FieldBuilder<T, true, IsOptional> {
|
|
94
110
|
this._default = value;
|
|
95
111
|
this._hasDefault = true;
|
|
96
112
|
return this as unknown as FieldBuilder<T, true, IsOptional>;
|
|
@@ -359,12 +375,27 @@ type RefHasDefault<C> =
|
|
|
359
375
|
: true;
|
|
360
376
|
|
|
361
377
|
interface RefFactory {
|
|
362
|
-
<C extends Constructor
|
|
378
|
+
<C extends Constructor>(ctor: C): FieldBuilder<InstanceType<C>, RefHasDefault<C>, false>;
|
|
363
379
|
}
|
|
364
380
|
|
|
365
|
-
const refFactory: RefFactory = (<C extends Constructor
|
|
381
|
+
const refFactory: RefFactory = (<C extends Constructor>(ctor: C) =>
|
|
366
382
|
new FieldBuilder<InstanceType<C>>(ctor as unknown as DefinitionType)) as RefFactory;
|
|
367
383
|
|
|
384
|
+
/**
|
|
385
|
+
* A bounded float carried on the wire as a fixed-width unsigned integer. App code
|
|
386
|
+
* reads/writes the FLOAT; the wire carries the quantized int and the field only
|
|
387
|
+
* ever yields `dequant(q)`, so client predict and server sim read the same value
|
|
388
|
+
* (no full-precision path to leak ⇒ no shot misprediction). See
|
|
389
|
+
* {@link QuantizeOptions} for the precision/wire trade-offs.
|
|
390
|
+
*
|
|
391
|
+
* yaw: t.quantized({ min: 0, max: TWO_PI, mode: "wrap" }), // 16-bit
|
|
392
|
+
* pitch: t.quantized({ min: -PITCH_LIMIT, max: PITCH_LIMIT }), // clamp (default)
|
|
393
|
+
* throttle: t.quantized({ min: 0, max: 1, bits: 8 }), // 1 byte
|
|
394
|
+
*/
|
|
395
|
+
function quantizedFactory(opts: QuantizeOptions): FieldBuilder<number> {
|
|
396
|
+
return new FieldBuilder<number>({ quantized: resolveQuantize(opts) } as unknown as DefinitionType);
|
|
397
|
+
}
|
|
398
|
+
|
|
368
399
|
export const t = Object.freeze({
|
|
369
400
|
// Primitives
|
|
370
401
|
string: primitive<string>("string"),
|
|
@@ -383,11 +414,37 @@ export const t = Object.freeze({
|
|
|
383
414
|
bigint64: primitive<bigint>("bigint64"),
|
|
384
415
|
biguint64: primitive<bigint>("biguint64"),
|
|
385
416
|
|
|
386
|
-
/**
|
|
417
|
+
/**
|
|
418
|
+
* Reference to a Schema subtype — `t.array(Item)` usually reads better, but
|
|
419
|
+
* this is available when a plain ref is needed.
|
|
420
|
+
*
|
|
421
|
+
* The target may also be a **non-Schema custom class**. A synced ref still
|
|
422
|
+
* requires it to be encodable — a `Schema` subclass, or a class retrofitted
|
|
423
|
+
* with `Metadata.setFields(...)` (both carry `[Symbol.metadata]`); a bare
|
|
424
|
+
* custom class is rejected at `schema()` time. A `.noSync()` (local-only)
|
|
425
|
+
* field accepts ANY zero-arg class and auto-instantiates one per parent.
|
|
426
|
+
*/
|
|
387
427
|
ref: refFactory,
|
|
388
428
|
array: arrayFactory,
|
|
389
429
|
map: mapFactory,
|
|
390
430
|
set: setFactory,
|
|
391
431
|
collection: collectionFactory,
|
|
392
432
|
stream: streamFactory,
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* A bounded float quantized to a fixed-width unsigned int on the wire — half
|
|
436
|
+
* (or a quarter) the bytes of a `float32` at a precision you pick. The field
|
|
437
|
+
* reads/writes the float and only ever yields `dequant(q)`. {@see QuantizeOptions}
|
|
438
|
+
*/
|
|
439
|
+
quantized: quantizedFactory,
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Sugar for a full-circle wrapping angle in radians:
|
|
443
|
+
* `t.quantized({ min: 0, max: 2π, mode: "wrap", bits })` (default 16-bit,
|
|
444
|
+
* ~0.0055°/step). Any input angle is range-reduced into `[0, 2π)`. Render note:
|
|
445
|
+
* lerp interpolated remotes shortest-arc (`attach({ angle: true })`) — the
|
|
446
|
+
* wrap fixes the WIRE seam, not interpolation (see {@link QuantizeOptions.mode}).
|
|
447
|
+
*/
|
|
448
|
+
angle: (opts?: { bits?: 8 | 16 | 32 }) =>
|
|
449
|
+
quantizedFactory({ min: 0, max: Math.PI * 2, mode: "wrap", bits: opts?.bits ?? 16 }),
|
|
393
450
|
});
|