@colyseus/schema 5.0.6 → 5.0.8
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 +8 -1
- package/build/codegen/cli.cjs +74 -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 +1192 -284
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.js +1192 -284
- package/build/index.mjs +1189 -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 +133 -51
- package/src/bench_churn.ts +121 -0
- package/src/codegen/languages/haxe.ts +0 -16
- package/src/codegen/parser.ts +29 -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 +11 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { $resyncPrune } from "./symbols.js";
|
|
1
2
|
import type { PrimitiveType, RawPrimitiveType } from "../annotations.js";
|
|
2
3
|
import type { Schema } from "../Schema.js";
|
|
3
4
|
import type { ArraySchema } from "./custom/ArraySchema.js";
|
|
@@ -12,6 +13,8 @@ export interface Collection<K = any, V = any, IT = V> {
|
|
|
12
13
|
[Symbol.iterator](): IterableIterator<IT>;
|
|
13
14
|
forEach(callback: Function): void;
|
|
14
15
|
entries(): IterableIterator<[K, V]>;
|
|
16
|
+
/** See {@link $resyncPrune} — every collection kind must declare its resync-sweep semantics. */
|
|
17
|
+
[$resyncPrune](visited: Set<number | string>, prune: (value: V, identity: number | string) => void, keep: (value: V) => void): void;
|
|
15
18
|
}
|
|
16
19
|
export type InferValueType<T> = T extends FieldBuilder<infer V> ? V : T extends "string" ? string : T extends "number" ? number : T extends "int8" ? number : T extends "uint8" ? number : T extends "int16" ? number : T extends "uint16" ? number : T extends "int32" ? number : T extends "uint32" ? number : T extends "int64" ? number : T extends "uint64" ? number : T extends "float32" ? number : T extends "float64" ? number : T extends "boolean" ? boolean : T extends {
|
|
17
20
|
type: infer ChildType extends PrimitiveType;
|
package/build/types/builder.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Schema } from "../Schema.js";
|
|
|
7
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
|
+
import { type QuantizeOptions } from "./quantize.js";
|
|
10
11
|
/**
|
|
11
12
|
* Internal record produced by FieldBuilder#toDefinition() and consumed by schema().
|
|
12
13
|
*/
|
|
@@ -70,8 +71,23 @@ export declare class FieldBuilder<T = unknown, HasDefault extends boolean = fals
|
|
|
70
71
|
private _noSync;
|
|
71
72
|
private _streamPriority;
|
|
72
73
|
constructor(type: DefinitionType);
|
|
73
|
-
/**
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Provide a default value for this field.
|
|
76
|
+
*
|
|
77
|
+
* Pass a **factory function** `() => T` to build a FRESH value per instance
|
|
78
|
+
* (invoked once per construction) instead of sharing a single default — the
|
|
79
|
+
* clean way to default a ref to a plain custom class, or any field that must
|
|
80
|
+
* not share a mutable default across instances:
|
|
81
|
+
*
|
|
82
|
+
* ```ts
|
|
83
|
+
* acc: t.ref(GunAccuracy).noSync().default(() => new GunAccuracy()),
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* Schema fields are never function-typed, so a function is always treated as a
|
|
87
|
+
* factory. A non-function value is shared (and cloned per instance if it is
|
|
88
|
+
* clone-able, e.g. a Schema/collection).
|
|
89
|
+
*/
|
|
90
|
+
default(value: T | (() => T)): FieldBuilder<T, true, IsOptional>;
|
|
75
91
|
/** Tag this field with a view tag (DEFAULT_VIEW_TAG when called without arg). */
|
|
76
92
|
view(tag?: number): this;
|
|
77
93
|
/** Mark this field as owned (encoder-side ownership filtering). */
|
|
@@ -217,8 +233,20 @@ type RefHasDefault<C> = C extends {
|
|
|
217
233
|
};
|
|
218
234
|
} ? (P extends readonly [] ? true : false) : true;
|
|
219
235
|
interface RefFactory {
|
|
220
|
-
<C extends Constructor
|
|
236
|
+
<C extends Constructor>(ctor: C): FieldBuilder<InstanceType<C>, RefHasDefault<C>, false>;
|
|
221
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* A bounded float carried on the wire as a fixed-width unsigned integer. App code
|
|
240
|
+
* reads/writes the FLOAT; the wire carries the quantized int and the field only
|
|
241
|
+
* ever yields `dequant(q)`, so client predict and server sim read the same value
|
|
242
|
+
* (no full-precision path to leak ⇒ no shot misprediction). See
|
|
243
|
+
* {@link QuantizeOptions} for the precision/wire trade-offs.
|
|
244
|
+
*
|
|
245
|
+
* yaw: t.quantized({ min: 0, max: TWO_PI, mode: "wrap" }), // 16-bit
|
|
246
|
+
* pitch: t.quantized({ min: -PITCH_LIMIT, max: PITCH_LIMIT }), // clamp (default)
|
|
247
|
+
* throttle: t.quantized({ min: 0, max: 1, bits: 8 }), // 1 byte
|
|
248
|
+
*/
|
|
249
|
+
declare function quantizedFactory(opts: QuantizeOptions): FieldBuilder<number>;
|
|
222
250
|
export declare const t: Readonly<{
|
|
223
251
|
string: PrimitiveFactory<string>;
|
|
224
252
|
number: PrimitiveFactory<number>;
|
|
@@ -235,12 +263,37 @@ export declare const t: Readonly<{
|
|
|
235
263
|
float64: PrimitiveFactory<number>;
|
|
236
264
|
bigint64: PrimitiveFactory<bigint>;
|
|
237
265
|
biguint64: PrimitiveFactory<bigint>;
|
|
238
|
-
/**
|
|
266
|
+
/**
|
|
267
|
+
* Reference to a Schema subtype — `t.array(Item)` usually reads better, but
|
|
268
|
+
* this is available when a plain ref is needed.
|
|
269
|
+
*
|
|
270
|
+
* The target may also be a **non-Schema custom class**. A synced ref still
|
|
271
|
+
* requires it to be encodable — a `Schema` subclass, or a class retrofitted
|
|
272
|
+
* with `Metadata.setFields(...)` (both carry `[Symbol.metadata]`); a bare
|
|
273
|
+
* custom class is rejected at `schema()` time. A `.noSync()` (local-only)
|
|
274
|
+
* field accepts ANY zero-arg class and auto-instantiates one per parent.
|
|
275
|
+
*/
|
|
239
276
|
ref: RefFactory;
|
|
240
277
|
array: ArrayFactory;
|
|
241
278
|
map: MapFactory;
|
|
242
279
|
set: SetFactory;
|
|
243
280
|
collection: CollectionFactory;
|
|
244
281
|
stream: StreamFactory;
|
|
282
|
+
/**
|
|
283
|
+
* A bounded float quantized to a fixed-width unsigned int on the wire — half
|
|
284
|
+
* (or a quarter) the bytes of a `float32` at a precision you pick. The field
|
|
285
|
+
* reads/writes the float and only ever yields `dequant(q)`. {@see QuantizeOptions}
|
|
286
|
+
*/
|
|
287
|
+
quantized: typeof quantizedFactory;
|
|
288
|
+
/**
|
|
289
|
+
* Sugar for a full-circle wrapping angle in radians:
|
|
290
|
+
* `t.quantized({ min: 0, max: 2π, mode: "wrap", bits })` (default 16-bit,
|
|
291
|
+
* ~0.0055°/step). Any input angle is range-reduced into `[0, 2π)`. Render note:
|
|
292
|
+
* lerp interpolated remotes shortest-arc (`attach({ angle: true })`) — the
|
|
293
|
+
* wrap fixes the WIRE seam, not interpolation (see {@link QuantizeOptions.mode}).
|
|
294
|
+
*/
|
|
295
|
+
angle: (opts?: {
|
|
296
|
+
bits?: 8 | 16 | 32;
|
|
297
|
+
}) => FieldBuilder<number, false, false>;
|
|
245
298
|
}>;
|
|
246
299
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $onDecodeEnd, $proxyTarget, $refId } from "../symbols.js";
|
|
1
|
+
import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $onDecodeEnd, $proxyTarget, $refId, $reset, $resyncPrune } from "../symbols.js";
|
|
2
2
|
import type { Schema } from "../../Schema.js";
|
|
3
3
|
import { type IRef, ChangeTree } from "../../encoder/ChangeTree.js";
|
|
4
4
|
import { OPERATION } from "../../encoding/spec.js";
|
|
@@ -14,6 +14,8 @@ export declare class ArraySchema<V = any> implements Array<V>, Collection<number
|
|
|
14
14
|
protected tmpItems: V[];
|
|
15
15
|
protected deletedIndexes: boolean[];
|
|
16
16
|
protected isMovingItems: boolean;
|
|
17
|
+
/** Decode-side: `items` has holes (delete or gap-write) — `$onDecodeEnd` must compact. */
|
|
18
|
+
protected _needsCompaction: boolean;
|
|
17
19
|
static [$encoder]: import("../../encoder/EncodeOperation.js").EncodeOperation<any>;
|
|
18
20
|
static [$decoder]: import("../../decoder/DecodeOperation.js").DecodeOperation<any>;
|
|
19
21
|
/** Integer tag read by `decodeKeyValueOperation` — see `CollectionKind`. */
|
|
@@ -57,6 +59,13 @@ export declare class ArraySchema<V = any> implements Array<V>, Collection<number
|
|
|
57
59
|
protected $deleteAt(index: number, operation?: OPERATION): void;
|
|
58
60
|
protected $setAt(index: number, value: V, operation: OPERATION): void;
|
|
59
61
|
clear(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Pool reset: empty this array and recycle its ChangeTree WITHOUT recording
|
|
64
|
+
* any wire op (the parent field's ADD/DELETE owns the wire). Recurses into
|
|
65
|
+
* ref-type children. Called by Schema.reset when a pooled entity has an
|
|
66
|
+
* array field. The instance must already be detached from the encoder.
|
|
67
|
+
*/
|
|
68
|
+
[$reset](): void;
|
|
60
69
|
/**
|
|
61
70
|
* Combines two or more arrays.
|
|
62
71
|
* @param items Additional items to add to the end of array1.
|
|
@@ -285,6 +294,7 @@ export declare class ArraySchema<V = any> implements Array<V>, Collection<number
|
|
|
285
294
|
[$deleteByIndex](index: number): void;
|
|
286
295
|
protected [$onEncodeEnd](): void;
|
|
287
296
|
protected [$onDecodeEnd](): void;
|
|
297
|
+
[$resyncPrune](visited: Set<number | string>, prune: (value: V, identity: number | string) => void, keep: (value: V) => void): void;
|
|
288
298
|
toArray(): V[];
|
|
289
299
|
toJSON(): any[];
|
|
290
300
|
clone(isDecoding?: boolean): ArraySchema<V>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd, $refId } from "../symbols.js";
|
|
1
|
+
import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd, $refId, $reset, $resyncPrune } from "../symbols.js";
|
|
2
2
|
import { ChangeTree, type IRef } from "../../encoder/ChangeTree.js";
|
|
3
3
|
import { Collection } from "../HelperTypes.js";
|
|
4
4
|
import { type StreamableState } from "../../encoder/streaming.js";
|
|
@@ -55,6 +55,13 @@ export declare class CollectionSchema<V = any> implements Collection<K, V>, IRef
|
|
|
55
55
|
entries(): MapIterator<[number, V]>;
|
|
56
56
|
delete(item: V): boolean;
|
|
57
57
|
clear(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Pool reset: empty this collection and recycle its ChangeTree WITHOUT
|
|
60
|
+
* recording any wire op (the parent field's ADD/DELETE owns the wire).
|
|
61
|
+
* Recurses into ref-type children. Called by Schema.reset when a pooled
|
|
62
|
+
* entity has a collection field. Must already be detached from the encoder.
|
|
63
|
+
*/
|
|
64
|
+
[$reset](): void;
|
|
58
65
|
has(value: V): boolean;
|
|
59
66
|
forEach(callbackfn: (value: V, key: K, collection: CollectionSchema<V>) => void): void;
|
|
60
67
|
values(): MapIterator<V>;
|
|
@@ -69,6 +76,7 @@ export declare class CollectionSchema<V = any> implements Collection<K, V>, IRef
|
|
|
69
76
|
protected getIndex(index: number): number;
|
|
70
77
|
[$getByIndex](index: number): any;
|
|
71
78
|
[$deleteByIndex](index: number): void;
|
|
79
|
+
[$resyncPrune](visited: Set<number | string>, prune: (value: V, identity: number | string) => void, keep: (value: V) => void): void;
|
|
72
80
|
protected [$onEncodeEnd](): void;
|
|
73
81
|
_dropView(viewId: number): void;
|
|
74
82
|
_unregister(): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $refId } from "../symbols.js";
|
|
1
|
+
import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $refId, $reset, $resyncPrune } from "../symbols.js";
|
|
2
2
|
import { ChangeTree, IRef } from "../../encoder/ChangeTree.js";
|
|
3
3
|
import { Collection } from "../HelperTypes.js";
|
|
4
4
|
import { MapJournal } from "../../encoder/MapJournal.js";
|
|
@@ -78,6 +78,13 @@ export declare class MapSchema<V = any, K extends string = string> implements Ma
|
|
|
78
78
|
get(key: K): V | undefined;
|
|
79
79
|
delete(key: K): boolean;
|
|
80
80
|
clear(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Pool reset: empty this map and recycle its ChangeTree WITHOUT recording
|
|
83
|
+
* any wire op (the parent field's ADD/DELETE owns the wire). Recurses into
|
|
84
|
+
* ref-type children. Called by Schema.reset when a pooled entity has a
|
|
85
|
+
* map field. The instance must already be detached from the encoder.
|
|
86
|
+
*/
|
|
87
|
+
[$reset](): void;
|
|
81
88
|
has(key: K): boolean;
|
|
82
89
|
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void): void;
|
|
83
90
|
entries(): MapIterator<[K, V]>;
|
|
@@ -92,6 +99,7 @@ export declare class MapSchema<V = any, K extends string = string> implements Ma
|
|
|
92
99
|
protected getIndex(index: number): K;
|
|
93
100
|
[$getByIndex](index: number): V | undefined;
|
|
94
101
|
[$deleteByIndex](index: number): void;
|
|
102
|
+
[$resyncPrune](visited: Set<number | string>, prune: (value: V, identity: number | string) => void, keep: (value: V) => void): void;
|
|
95
103
|
protected [$onEncodeEnd](): void;
|
|
96
104
|
_dropView(viewId: number): void;
|
|
97
105
|
_unregister(): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd, $refId } from "../symbols.js";
|
|
1
|
+
import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd, $refId, $reset, $resyncPrune } from "../symbols.js";
|
|
2
2
|
import { Collection } from "../HelperTypes.js";
|
|
3
3
|
import { ChangeTree, type IRef } from "../../encoder/ChangeTree.js";
|
|
4
4
|
import { type StreamableState } from "../../encoder/streaming.js";
|
|
@@ -55,6 +55,13 @@ export declare class SetSchema<V = any> implements Collection<number, V>, IRef {
|
|
|
55
55
|
entries(): MapIterator<[number, V]>;
|
|
56
56
|
delete(item: V): boolean;
|
|
57
57
|
clear(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Pool reset: empty this set and recycle its ChangeTree WITHOUT recording
|
|
60
|
+
* any wire op (the parent field's ADD/DELETE owns the wire). Recurses into
|
|
61
|
+
* ref-type children. Called by Schema.reset when a pooled entity has a set
|
|
62
|
+
* field. The instance must already be detached from the encoder.
|
|
63
|
+
*/
|
|
64
|
+
[$reset](): void;
|
|
58
65
|
has(value: V): boolean;
|
|
59
66
|
forEach(callbackfn: (value: V, key: number, collection: SetSchema<V>) => void): void;
|
|
60
67
|
values(): MapIterator<V>;
|
|
@@ -69,6 +76,7 @@ export declare class SetSchema<V = any> implements Collection<number, V>, IRef {
|
|
|
69
76
|
protected getIndex(index: number): number;
|
|
70
77
|
[$getByIndex](index: number): any;
|
|
71
78
|
[$deleteByIndex](index: number): void;
|
|
79
|
+
[$resyncPrune](visited: Set<number | string>, prune: (value: V, identity: number | string) => void, keep: (value: V) => void): void;
|
|
72
80
|
protected [$onEncodeEnd](): void;
|
|
73
81
|
_dropView(viewId: number): void;
|
|
74
82
|
_unregister(): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd, $refId } from "../symbols.js";
|
|
1
|
+
import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd, $refId, $resyncPrune } from "../symbols.js";
|
|
2
2
|
import { ChangeTree, type IRef } from "../../encoder/ChangeTree.js";
|
|
3
3
|
import { type StreamableState } from "../../encoder/streaming.js";
|
|
4
4
|
import type { StateView } from "../../encoder/StateView.js";
|
|
@@ -104,6 +104,7 @@ export declare class StreamSchema<V = any> implements IRef {
|
|
|
104
104
|
protected getIndex(index: number): number;
|
|
105
105
|
[$getByIndex](index: number): V;
|
|
106
106
|
[$deleteByIndex](index: number): void;
|
|
107
|
+
[$resyncPrune](): void;
|
|
107
108
|
protected [$onEncodeEnd](): void;
|
|
108
109
|
toArray(): V[];
|
|
109
110
|
toJSON(): any[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `t.quantized()` — a bounded float encoded as a fixed-width unsigned integer.
|
|
3
|
+
*
|
|
4
|
+
* A float that changes every frame within a known range (look angles, a
|
|
5
|
+
* normalized scalar, a unit-vector component) costs a full `float32` (4 value
|
|
6
|
+
* bytes). Mapping it onto `uint8`/`uint16`/`uint32` halves or quarters that at a
|
|
7
|
+
* precision you choose — e.g. a yaw on `uint16` is ~0.0055°/step, finer than any
|
|
8
|
+
* mouse, for 2 bytes instead of 4.
|
|
9
|
+
*
|
|
10
|
+
* Quantization is LOSSY, but lossy IDENTICALLY on both peers: the field only ever
|
|
11
|
+
* yields `dequant(q)`, so client prediction and server simulation read the SAME
|
|
12
|
+
* value and a predicted hitscan ray bit-matches the server's — the lossiness is
|
|
13
|
+
* vs. the original float, never between the two peers (see the reconciler note in
|
|
14
|
+
* `predict/reconciler.ts`).
|
|
15
|
+
*
|
|
16
|
+
* Portable by construction (a C/C#/Lua client must reproduce it bit-for-bit):
|
|
17
|
+
* rounding is explicit `floor(x + 0.5)` — NOT the language-default round, which
|
|
18
|
+
* disagrees on the `.5` case (JS half-up, C# banker's, C half-away) — and a
|
|
19
|
+
* wrapping range is reduced in the FLOAT domain BEFORE the integer step, so there
|
|
20
|
+
* is no huge-double→int cast (UB in C, throws in C#). All math is float64.
|
|
21
|
+
*/
|
|
22
|
+
export interface QuantizeOptions {
|
|
23
|
+
/** Inclusive lower bound of the value domain. */
|
|
24
|
+
min: number;
|
|
25
|
+
/** Upper bound of the value domain (inclusive when clamped, exclusive when wrapping). */
|
|
26
|
+
max: number;
|
|
27
|
+
/**
|
|
28
|
+
* Wire width in bits — a typed union, NOT a free integer: schema fields are
|
|
29
|
+
* byte-aligned, so only 8/16/32 change the wire (`bits: 13` would still occupy
|
|
30
|
+
* 2 bytes and mislead). Default 16.
|
|
31
|
+
*/
|
|
32
|
+
bits?: 8 | 16 | 32;
|
|
33
|
+
/**
|
|
34
|
+
* Is the value CYCLIC (lives on a circle) or BOUNDED (has hard walls)? That is
|
|
35
|
+
* the whole choice — the wire size is identical either way.
|
|
36
|
+
*
|
|
37
|
+
* `"clamp"` (default) — for a BOUNDED value with hard ends (pitch, a health bar,
|
|
38
|
+
* a 0–1 throttle). A UNORM over `[min, max]` INCLUSIVE: both endpoints are exact
|
|
39
|
+
* + distinct, and out-of-range inputs CLAMP to the nearest end (`0` / `2^bits − 1`).
|
|
40
|
+
* Pitch past the limit stops AT the limit.
|
|
41
|
+
*
|
|
42
|
+
* `"wrap"` — for a CYCLIC value where `min` and `max` are the SAME point (an
|
|
43
|
+
* angle/heading, compass bearing, hue, phase). `[min, max)` over `2^bits` steps
|
|
44
|
+
* with the top folding onto the bottom; any input is range-reduced modulo
|
|
45
|
+
* `(max − min)` first, so a large accumulated or negative angle maps into the
|
|
46
|
+
* domain (yaw keeps spinning, never jams). `min` is exact; `max ≡ min`.
|
|
47
|
+
*
|
|
48
|
+
* Decision rule: does one step past the top return you to the bottom as the SAME
|
|
49
|
+
* physical thing? → `"wrap"`. Is the top a wall you can't cross? → `"clamp"`.
|
|
50
|
+
* Choosing wrong is a real bug: a clamped heading JAMS at the 0/2π seam; a wrapped
|
|
51
|
+
* pitch FLIPS the camera the instant you look a hair too far up.
|
|
52
|
+
*
|
|
53
|
+
* NB (cyclic fields): `"wrap"` fixes the WIRE seam, not rendering — a wrapped
|
|
54
|
+
* field that's interpolated must ALSO be lerped shortest-arc (the SDK predict
|
|
55
|
+
* `attach({ angle: true })`), or it unwinds the long way across the `0 ↔ max` seam.
|
|
56
|
+
*/
|
|
57
|
+
mode?: "clamp" | "wrap";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolved, plain-data quantization descriptor stored on the field metadata as
|
|
61
|
+
* `{ quantized: QuantizeDescriptor }`. Both encode and decode derive `scale` from
|
|
62
|
+
* the same fields, so the two sides — and a future cross-language port — agree by
|
|
63
|
+
* construction. Serializable (no functions) so it can ride schema reflection.
|
|
64
|
+
*/
|
|
65
|
+
export interface QuantizeDescriptor {
|
|
66
|
+
min: number;
|
|
67
|
+
max: number;
|
|
68
|
+
bits: 8 | 16 | 32;
|
|
69
|
+
wrap: boolean;
|
|
70
|
+
/** Wire codec name — `"uint8" | "uint16" | "uint32"`, derived from `bits`. */
|
|
71
|
+
wire: "uint8" | "uint16" | "uint32";
|
|
72
|
+
/** `max − min`, the domain width. */
|
|
73
|
+
range: number;
|
|
74
|
+
/**
|
|
75
|
+
* The integer span used for the scale:
|
|
76
|
+
* - wrapping: `2^bits` (the number of steps; the top step folds onto 0).
|
|
77
|
+
* - clamped: `2^bits − 1` (endpoints inclusive, so the max maps to it).
|
|
78
|
+
*/
|
|
79
|
+
span: number;
|
|
80
|
+
}
|
|
81
|
+
/** Validate options and precompute the wire codec + scale span. */
|
|
82
|
+
export declare function resolveQuantize(opts: QuantizeOptions): QuantizeDescriptor;
|
|
83
|
+
/** Type guard for the `{ quantized: QuantizeDescriptor }` field-type shape. */
|
|
84
|
+
export declare function isQuantizedType(type: any): type is {
|
|
85
|
+
quantized: QuantizeDescriptor;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Float → unsigned integer. Rounding is explicit half-up `floor(x + 0.5)`;
|
|
89
|
+
* wrapping ranges are reduced in the float domain first (no huge→int cast).
|
|
90
|
+
*/
|
|
91
|
+
export declare function quantize(desc: QuantizeDescriptor, value: number): number;
|
|
92
|
+
/** Unsigned integer → float. Correctly-rounded mul/div ⇒ bit-identical across languages. */
|
|
93
|
+
export declare function dequantize(desc: QuantizeDescriptor, q: number): number;
|
|
94
|
+
/**
|
|
95
|
+
* Pre-baked encoder for a quantized field: quantize the (snapped) float held on
|
|
96
|
+
* the instance, then write it with the field's unsigned-int wire codec. Stored in
|
|
97
|
+
* `metadata[$encoders]` so the encode hot path reaches it via the fast lane,
|
|
98
|
+
* exactly like the pre-computed primitive encoders.
|
|
99
|
+
*/
|
|
100
|
+
export declare function makeQuantizedEncoder(desc: QuantizeDescriptor): (bytes: any, value: number, it: any) => void;
|
|
101
|
+
/** Decode a quantized field: read the unsigned-int wire value, then dequantize. */
|
|
102
|
+
export declare function decodeQuantized(desc: QuantizeDescriptor, bytes: any, it: any): number;
|
package/build/types/symbols.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export declare const $decoder = "~decoder";
|
|
|
5
5
|
export declare const $filter = "~filter";
|
|
6
6
|
export declare const $getByIndex = "~getByIndex";
|
|
7
7
|
export declare const $deleteByIndex = "~deleteByIndex";
|
|
8
|
+
/**
|
|
9
|
+
* Resync-sweep hook (see decoder/Resync.ts): prune every entry the rejoin
|
|
10
|
+
* snapshot did not visit. Each collection owns its storage-specific
|
|
11
|
+
* bookkeeping (journal pruning, compaction, item indexes); the generic
|
|
12
|
+
* DELETE/ref/callback bookkeeping arrives via the `prune`/`keep` callbacks.
|
|
13
|
+
*/
|
|
14
|
+
export declare const $resyncPrune = "~resyncPrune";
|
|
8
15
|
/**
|
|
9
16
|
* Used to hold ChangeTree instances whitin the structures.
|
|
10
17
|
*
|
|
@@ -29,6 +36,14 @@ export declare const $proxyTarget: unique symbol;
|
|
|
29
36
|
* (Discards changes for next serialization)
|
|
30
37
|
*/
|
|
31
38
|
export declare const $onEncodeEnd = "~onEncodeEnd";
|
|
39
|
+
/**
|
|
40
|
+
* Optional "reset" method on every poolable Ref (Schema + collections).
|
|
41
|
+
* Empties the instance's backing store and recycles its ChangeTree WITHOUT
|
|
42
|
+
* emitting any wire op, and recurses into ref-type children — so the instance
|
|
43
|
+
* can be returned to a SchemaPool and reused, avoiding the cost of `new`.
|
|
44
|
+
* See encoder/Pool.ts and Schema.reset().
|
|
45
|
+
*/
|
|
46
|
+
export declare const $reset = "~reset";
|
|
32
47
|
/**
|
|
33
48
|
* When decoding, this method is called after the instance is fully decoded
|
|
34
49
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/schema",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.8",
|
|
4
4
|
"description": "Binary state serializer with delta encoding for games",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
"schema-debug": "bin/schema-debug"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
+
"bench": "node bench/run.mjs",
|
|
12
|
+
"bench:compare": "node bench/run.mjs --compare",
|
|
13
|
+
"bench:snapshot": "bash bench/snapshot-build.sh",
|
|
14
|
+
"bench:gate": "node bench/run.mjs --assert --samples 3 --filter gate",
|
|
15
|
+
"profile:cpu": "node bench/profile.mjs --cpu",
|
|
16
|
+
"profile:heap": "node bench/profile.mjs --heap",
|
|
11
17
|
"build": "tsc -p tsconfig.build.json && rollup -c rollup.config.mjs",
|
|
12
18
|
"watch": "tsc -p tsconfig.build.json -w",
|
|
13
19
|
"typecheck:build": "tsc -p tsconfig.build.json --noEmit",
|
|
14
20
|
"test": "tsx --tsconfig tsconfig.test.json ./node_modules/mocha/bin/mocha.js \"test/*.test.ts\" \"test/**/*.test.ts\"",
|
|
21
|
+
"test:exports": "node test/verify-exports.mjs",
|
|
15
22
|
"typecheck": "tsc -p tsconfig.test.json",
|
|
16
23
|
"coverage": "c8 npm run test",
|
|
17
24
|
"generate-test-1": "bin/schema-codegen test-external/PrimitiveTypes.ts --namespace SchemaTest.PrimitiveTypes --output ../colyseus-unity-sdk/Assets/Colyseus/Tests/Editor/ColyseusTests/Schema/PrimitiveTypes",
|
package/src/Metadata.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { $decoder, $descriptors, $encoder, $encoders, $fieldIndexesByViewTag, $n
|
|
|
5
5
|
import { ARRAY_STREAM_NOT_SUPPORTED } from "./encoder/streaming.js";
|
|
6
6
|
import { encode } from "./encoding/encode.js";
|
|
7
7
|
import { TypeContext } from "./types/TypeContext.js";
|
|
8
|
+
import { isQuantizedType, makeQuantizedEncoder, resolveQuantize } from "./types/quantize.js";
|
|
8
9
|
|
|
9
10
|
export type MetadataField = {
|
|
10
11
|
type: DefinitionType,
|
|
@@ -54,6 +55,14 @@ export function getNormalizedType(type: any): DefinitionType {
|
|
|
54
55
|
if (Array.isArray(type)) {
|
|
55
56
|
return { array: getNormalizedType(type[0]) };
|
|
56
57
|
|
|
58
|
+
} else if (isQuantizedType(type)) {
|
|
59
|
+
// `{ quantized: ... }` — a scalar wire type, NOT a collection/ref. Resolve
|
|
60
|
+
// raw options (the `@type({quantized:{min,max}})` path) once; an already-
|
|
61
|
+
// resolved descriptor (the `t.quantized()` builder path) passes through.
|
|
62
|
+
return (typeof (type.quantized as any).wire === "string")
|
|
63
|
+
? type
|
|
64
|
+
: { quantized: resolveQuantize(type.quantized as any) } as any;
|
|
65
|
+
|
|
57
66
|
} else if (typeof (type['type']) !== "undefined") {
|
|
58
67
|
return type['type'];
|
|
59
68
|
|
|
@@ -146,8 +155,9 @@ export const Metadata = {
|
|
|
146
155
|
configurable: true,
|
|
147
156
|
});
|
|
148
157
|
|
|
149
|
-
// if child Ref/complex type, add to -4
|
|
150
|
-
|
|
158
|
+
// if child Ref/complex type, add to -4. Quantized fields are scalar (their
|
|
159
|
+
// `type` is an object only to carry the descriptor) — not refs, so skip them.
|
|
160
|
+
if (typeof (metadata[index].type) !== "string" && !isQuantizedType(metadata[index].type)) {
|
|
151
161
|
if (metadata[$refTypeFieldIndexes] === undefined) {
|
|
152
162
|
Object.defineProperty(metadata, $refTypeFieldIndexes, {
|
|
153
163
|
value: [],
|
|
@@ -216,11 +226,24 @@ export const Metadata = {
|
|
|
216
226
|
|
|
217
227
|
metadata[$viewFieldIndexes].push(index);
|
|
218
228
|
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
// Populate $fieldIndexesByViewTag: for a bitmask tag, register the field
|
|
230
|
+
// index under each individual set bit so that view.add(obj, Tag.ONE) finds
|
|
231
|
+
// fields tagged @view(Tag.ONE|Tag.TWO).
|
|
232
|
+
// Negative tags (i.e. DEFAULT_VIEW_TAG = -1) are stored as-is.
|
|
233
|
+
if (tag < 0) {
|
|
234
|
+
if (!metadata[$fieldIndexesByViewTag][tag]) {
|
|
235
|
+
metadata[$fieldIndexesByViewTag][tag] = [];
|
|
236
|
+
}
|
|
237
|
+
metadata[$fieldIndexesByViewTag][tag].push(index);
|
|
238
|
+
} else {
|
|
239
|
+
for (let bits = tag; bits > 0; bits &= bits - 1) {
|
|
240
|
+
const bit = bits & (-bits); // isolate lowest set bit
|
|
241
|
+
if (!metadata[$fieldIndexesByViewTag][bit]) {
|
|
242
|
+
metadata[$fieldIndexesByViewTag][bit] = [];
|
|
243
|
+
}
|
|
244
|
+
metadata[$fieldIndexesByViewTag][bit].push(index);
|
|
245
|
+
}
|
|
221
246
|
}
|
|
222
|
-
|
|
223
|
-
metadata[$fieldIndexesByViewTag][tag].push(index);
|
|
224
247
|
},
|
|
225
248
|
|
|
226
249
|
setUnreliable(metadata: Metadata, fieldName: string) {
|
|
@@ -358,8 +381,8 @@ export const Metadata = {
|
|
|
358
381
|
Object.defineProperty(target.prototype, fieldName, metadata[$descriptors][fieldName]);
|
|
359
382
|
}
|
|
360
383
|
|
|
361
|
-
// Pre-compute encoder function for primitive types.
|
|
362
|
-
if (typeof normalized === "string") {
|
|
384
|
+
// Pre-compute encoder function for primitive + quantized types.
|
|
385
|
+
if (typeof normalized === "string" || isQuantizedType(normalized)) {
|
|
363
386
|
if (!metadata[$encoders]) {
|
|
364
387
|
Object.defineProperty(metadata, $encoders, {
|
|
365
388
|
value: [],
|
|
@@ -368,7 +391,9 @@ export const Metadata = {
|
|
|
368
391
|
writable: true,
|
|
369
392
|
});
|
|
370
393
|
}
|
|
371
|
-
metadata[$encoders][fieldIndex] = (
|
|
394
|
+
metadata[$encoders][fieldIndex] = (typeof normalized === "string")
|
|
395
|
+
? (encode as any)[normalized]
|
|
396
|
+
: makeQuantizedEncoder((normalized as any).quantized);
|
|
372
397
|
}
|
|
373
398
|
},
|
|
374
399
|
|
package/src/Reflection.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { Schema } from "./Schema.js";
|
|
|
8
8
|
import { t, FieldBuilder } from "./types/builder.js";
|
|
9
9
|
import { ArraySchema } from "./types/custom/ArraySchema.js";
|
|
10
10
|
import { $encodeDescriptor, $numFields } from "./types/symbols.js";
|
|
11
|
+
import { isQuantizedType, resolveQuantize } from "./types/quantize.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Static methods available on Reflection
|
|
@@ -52,10 +53,30 @@ interface ReflectionStatic {
|
|
|
52
53
|
/**
|
|
53
54
|
* Reflection
|
|
54
55
|
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `t.quantized()` field descriptor as it rides the reflection handshake —
|
|
59
|
+
* schema-typed (bit-exact float64 bounds), NOT a string grammar, so every
|
|
60
|
+
* language port decodes it with the schema decoder it already has.
|
|
61
|
+
*/
|
|
62
|
+
export const QuantizedDescriptor = schema({
|
|
63
|
+
min: t.float64(),
|
|
64
|
+
max: t.float64(),
|
|
65
|
+
bits: t.uint8(),
|
|
66
|
+
mode: t.uint8(), // 0 = clamp, 1 = wrap
|
|
67
|
+
}, "QuantizedDescriptor");
|
|
68
|
+
export type QuantizedDescriptor = SchemaType<typeof QuantizedDescriptor>;
|
|
69
|
+
|
|
55
70
|
export const ReflectionField = schema({
|
|
56
71
|
name: t.string(),
|
|
57
72
|
type: t.string(),
|
|
58
73
|
referencedType: t.number(),
|
|
74
|
+
/** Primitive child of a collection (`array`/`map`/... of "string" etc.) —
|
|
75
|
+
* its own slot, replacing the legacy `"array:string"` colon packing. */
|
|
76
|
+
childPrimitive: t.string(),
|
|
77
|
+
/** Set only on `t.quantized()` fields (`.optional()` — no auto-instantiated
|
|
78
|
+
* default; its absence is the "not quantized" signal on decode). */
|
|
79
|
+
quantized: t.ref(QuantizedDescriptor).optional(),
|
|
59
80
|
}, "ReflectionField");
|
|
60
81
|
export type ReflectionField = SchemaType<typeof ReflectionField>;
|
|
61
82
|
|
|
@@ -147,6 +168,18 @@ Reflection.encode = function (encoder: Encoder, it: Iterator = { offset: 0 }) {
|
|
|
147
168
|
if (typeof (field.type) === "string") {
|
|
148
169
|
fieldType = field.type;
|
|
149
170
|
|
|
171
|
+
} else if (isQuantizedType(field.type)) {
|
|
172
|
+
// Params ride as a schema-typed descriptor (bit-exact float64) —
|
|
173
|
+
// no string grammar for the peer (or a language port) to parse.
|
|
174
|
+
const d = field.type.quantized;
|
|
175
|
+
fieldType = "quantized";
|
|
176
|
+
const desc = new QuantizedDescriptor();
|
|
177
|
+
desc.min = d.min;
|
|
178
|
+
desc.max = d.max;
|
|
179
|
+
desc.bits = d.bits;
|
|
180
|
+
desc.mode = d.wrap ? 1 : 0;
|
|
181
|
+
reflectionField.quantized = desc;
|
|
182
|
+
|
|
150
183
|
} else {
|
|
151
184
|
let childTypeSchema: typeof Schema;
|
|
152
185
|
|
|
@@ -161,7 +194,8 @@ Reflection.encode = function (encoder: Encoder, it: Iterator = { offset: 0 }) {
|
|
|
161
194
|
fieldType = Object.keys(field.type)[0];
|
|
162
195
|
|
|
163
196
|
if (typeof (field.type[fieldType as keyof typeof field.type]) === "string") {
|
|
164
|
-
|
|
197
|
+
// primitive child gets its own slot (was packed as "array:string")
|
|
198
|
+
reflectionField.childPrimitive = field.type[fieldType as keyof typeof field.type] as string;
|
|
165
199
|
|
|
166
200
|
} else {
|
|
167
201
|
childTypeSchema = field.type[fieldType as keyof typeof field.type];
|
|
@@ -215,16 +249,20 @@ Reflection.decode = function <T extends Schema = Schema>(bytes: Uint8Array, it?:
|
|
|
215
249
|
reflectionType.fields.forEach((field, i) => {
|
|
216
250
|
const fieldIndex = parentFieldIndex + i;
|
|
217
251
|
|
|
218
|
-
if (field.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
252
|
+
if (field.quantized !== undefined) {
|
|
253
|
+
// Schema-typed descriptor → resolved codec (validation stays in
|
|
254
|
+
// resolveQuantize, same as the builder path).
|
|
255
|
+
const q = field.quantized;
|
|
256
|
+
Metadata.addField(metadata, fieldIndex, field.name, {
|
|
257
|
+
quantized: resolveQuantize({ min: q.min, max: q.max, bits: q.bits as 8 | 16 | 32, mode: q.mode === 1 ? "wrap" : "clamp" }),
|
|
258
|
+
} as any);
|
|
259
|
+
|
|
260
|
+
} else if (field.referencedType !== undefined) {
|
|
261
|
+
const fieldType = field.type;
|
|
262
|
+
// Schema child by type id; a primitive child (referencedType -1)
|
|
263
|
+
// rides its own childPrimitive slot.
|
|
264
|
+
const refType: PrimitiveType = typeContext.get(field.referencedType)
|
|
265
|
+
?? field.childPrimitive as PrimitiveType;
|
|
228
266
|
|
|
229
267
|
if (fieldType === "ref") {
|
|
230
268
|
Metadata.addField(metadata, fieldIndex, field.name, refType);
|