@colyseus/schema 5.0.11 → 5.0.13
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/Metadata.d.ts +20 -12
- package/build/annotations.d.ts +23 -10
- package/build/codegen/cli.cjs +615 -204
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/languages/dart.d.ts +20 -0
- package/build/codegen/types.d.ts +20 -0
- package/build/decoder/Resync.d.ts +3 -3
- package/build/encoder/ChangeTree.d.ts +25 -9
- package/build/encoder/EncodeDescriptor.d.ts +11 -12
- package/build/encoder/StateView.d.ts +26 -2
- package/build/encoder/changeTree/inheritedFlags.d.ts +1 -1
- package/build/encoder/changeTree/parentChain.d.ts +9 -0
- package/build/encoder/streaming.d.ts +7 -0
- package/build/index.cjs +449 -233
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +449 -233
- package/build/index.mjs +448 -232
- package/build/index.mjs.map +1 -1
- package/build/types/builder.d.ts +31 -22
- package/build/types/custom/ArraySchema.d.ts +17 -0
- package/build/types/custom/StreamSchema.d.ts +1 -1
- package/build/types/symbols.d.ts +4 -10
- package/package.json +1 -1
- package/src/Metadata.ts +58 -31
- package/src/annotations.ts +56 -32
- package/src/codegen/api.ts +2 -1
- package/src/codegen/languages/c.ts +21 -3
- package/src/codegen/languages/csharp.ts +7 -1
- package/src/codegen/languages/dart.ts +274 -0
- package/src/codegen/languages/haxe.ts +7 -1
- package/src/codegen/languages/lua.ts +16 -4
- package/src/codegen/languages/ts.ts +5 -0
- package/src/codegen/parser.ts +97 -3
- package/src/codegen/types.ts +24 -0
- package/src/decoder/Resync.ts +8 -8
- package/src/encoder/ChangeRecorder.ts +1 -1
- package/src/encoder/ChangeTree.ts +46 -26
- package/src/encoder/EncodeDescriptor.ts +17 -38
- package/src/encoder/EncodeOperation.ts +3 -1
- package/src/encoder/Encoder.ts +97 -21
- package/src/encoder/Root.ts +18 -20
- package/src/encoder/StateView.ts +102 -12
- package/src/encoder/changeTree/inheritedFlags.ts +10 -10
- package/src/encoder/changeTree/liveIteration.ts +9 -9
- package/src/encoder/changeTree/parentChain.ts +29 -0
- package/src/encoder/streaming.ts +8 -0
- package/src/encoding/spec.ts +1 -1
- package/src/index.ts +2 -2
- package/src/types/builder.ts +35 -31
- package/src/types/custom/ArraySchema.ts +40 -1
- package/src/types/custom/StreamSchema.ts +1 -1
- package/src/types/symbols.ts +4 -11
- package/src/bench_bloat.ts +0 -173
- package/src/bench_churn.ts +0 -121
- package/src/bench_decode.ts +0 -221
- package/src/bench_decode_mem.ts +0 -165
- package/src/bench_encode.ts +0 -108
- package/src/bench_init.ts +0 -150
- package/src/bench_static.ts +0 -109
- package/src/bench_stream.ts +0 -295
- package/src/bench_view_cmp.ts +0 -142
package/build/types/builder.d.ts
CHANGED
|
@@ -16,12 +16,11 @@ export interface BuilderDefinition {
|
|
|
16
16
|
default?: any;
|
|
17
17
|
hasDefault: boolean;
|
|
18
18
|
view?: number;
|
|
19
|
-
owned?: boolean;
|
|
20
19
|
unreliable?: boolean;
|
|
21
|
-
|
|
20
|
+
patchOnly?: boolean;
|
|
22
21
|
deprecated?: boolean;
|
|
23
22
|
deprecatedThrows?: boolean;
|
|
24
|
-
|
|
23
|
+
fullStateOnly?: boolean;
|
|
25
24
|
stream?: boolean;
|
|
26
25
|
optional?: boolean;
|
|
27
26
|
/** Local-only field: typed + initialized, but never registered for sync. */
|
|
@@ -60,12 +59,11 @@ export declare class FieldBuilder<T = unknown, HasDefault extends boolean = fals
|
|
|
60
59
|
private _default;
|
|
61
60
|
private _hasDefault;
|
|
62
61
|
private _view;
|
|
63
|
-
private _owned;
|
|
64
62
|
private _unreliable;
|
|
65
|
-
private
|
|
63
|
+
private _patchOnly;
|
|
66
64
|
private _deprecated;
|
|
67
65
|
private _deprecatedThrows;
|
|
68
|
-
private
|
|
66
|
+
private _fullStateOnly;
|
|
69
67
|
private _stream;
|
|
70
68
|
private _optional;
|
|
71
69
|
private _noSync;
|
|
@@ -90,28 +88,36 @@ export declare class FieldBuilder<T = unknown, HasDefault extends boolean = fals
|
|
|
90
88
|
default(value: T | (() => T)): FieldBuilder<T, true, IsOptional>;
|
|
91
89
|
/** Tag this field with a view tag (DEFAULT_VIEW_TAG when called without arg). */
|
|
92
90
|
view(tag?: number): this;
|
|
93
|
-
/** Mark this field as owned (encoder-side ownership filtering). */
|
|
94
|
-
owned(): this;
|
|
95
91
|
/**
|
|
96
92
|
* Mark this field as unreliable — tick patches emit it on the unreliable
|
|
97
93
|
* transport channel. Still persisted to full-sync snapshots unless also
|
|
98
|
-
* tagged with `.
|
|
94
|
+
* tagged with `.patchOnly()`. Primitive fields only.
|
|
95
|
+
*
|
|
96
|
+
* The field's FIRST value still travels the reliable channel, as part of
|
|
97
|
+
* the owning instance's ADD; only later mutations become unreliable. A
|
|
98
|
+
* decoder cannot apply a write to a ref it has not been told about, so a
|
|
99
|
+
* value emitted ahead of that ADD would be dropped — and lost for good if
|
|
100
|
+
* the field is never written again.
|
|
99
101
|
*/
|
|
100
102
|
unreliable(): this;
|
|
101
103
|
/**
|
|
102
|
-
*
|
|
103
|
-
* (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
104
|
-
* only after its next mutation is emitted on a
|
|
105
|
-
* to `.unreliable()`.
|
|
104
|
+
* Deliver this field on tick patches ONLY — it is never written to a
|
|
105
|
+
* full-state sync (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
106
|
+
* see the field only after its next mutation is emitted on a patch.
|
|
107
|
+
* The mirror of `.fullStateOnly()`, and orthogonal to `.unreliable()`.
|
|
106
108
|
*/
|
|
107
|
-
|
|
109
|
+
patchOnly(): this;
|
|
108
110
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
111
|
+
* Deliver this field in the full state sync ONLY (`encodeAll` /
|
|
112
|
+
* `encodeAllView`) — it never enters a tick patch. A client receives it
|
|
113
|
+
* on join (and again on a resync); writes after that are not tracked.
|
|
114
|
+
* The mirror of `.patchOnly()`.
|
|
115
|
+
*
|
|
116
|
+
* The field itself is NOT frozen — it stays mutable server-side, only
|
|
117
|
+
* its propagation stops. On a stream field (`t.stream(X).fullStateOnly()`)
|
|
118
|
+
* the same rule applies per element: post-add mutations are no-ops.
|
|
113
119
|
*/
|
|
114
|
-
|
|
120
|
+
fullStateOnly(): this;
|
|
115
121
|
/**
|
|
116
122
|
* Mark this field as **local-only** — it is typed and initialized on the
|
|
117
123
|
* instance (so `.default()` and the inferred instance type still apply),
|
|
@@ -121,8 +127,8 @@ export declare class FieldBuilder<T = unknown, HasDefault extends boolean = fals
|
|
|
121
127
|
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
122
128
|
* want on the class for typing convenience without paying any sync cost.
|
|
123
129
|
*
|
|
124
|
-
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
125
|
-
* `.unreliable()`, `.
|
|
130
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
131
|
+
* `.unreliable()`, `.patchOnly()`, `.fullStateOnly()`, `.stream()`) — combining
|
|
126
132
|
* them throws at `schema()` time.
|
|
127
133
|
*
|
|
128
134
|
* ```ts
|
|
@@ -154,9 +160,12 @@ export declare class FieldBuilder<T = unknown, HasDefault extends boolean = fals
|
|
|
154
160
|
* higher return values emit first. Does nothing in broadcast mode
|
|
155
161
|
* (shared `encode()` drains FIFO). Only meaningful on stream fields.
|
|
156
162
|
*
|
|
163
|
+
* `StateView` carries no position of its own — attach whatever the
|
|
164
|
+
* callback needs to sort by (`view` is loosely typed for this).
|
|
165
|
+
*
|
|
157
166
|
* ```ts
|
|
158
167
|
* t.stream(Enemy).priority((view, enemy) =>
|
|
159
|
-
* -
|
|
168
|
+
* -((enemy.x - view.x) ** 2 + (enemy.y - view.y) ** 2)
|
|
160
169
|
* )
|
|
161
170
|
* ```
|
|
162
171
|
*/
|
|
@@ -63,6 +63,23 @@ export declare class ArraySchema<V = any> implements Array<V>, Collection<number
|
|
|
63
63
|
* land on the wrong wire slots.
|
|
64
64
|
*/
|
|
65
65
|
protected $wireIndex(index: number): number;
|
|
66
|
+
/**
|
|
67
|
+
* Re-point children at their wire slot. `ChangeTree._parentIndex` caches
|
|
68
|
+
* the slot a child holds in `tmpItems`, and StateView addresses per-view
|
|
69
|
+
* ADD/DELETE with it — so a reorder that leaves it behind aims those ops
|
|
70
|
+
* at whichever element inherited the slot (issue #231).
|
|
71
|
+
*
|
|
72
|
+
* The filter check is a correctness boundary, not a tunable: StateView is
|
|
73
|
+
* the only reader and reaches the index only through a filtered array
|
|
74
|
+
* (`addParentOf` bails on `hasFilteredFields`, `remove` on the child's
|
|
75
|
+
* `isFiltered`). Everything else stops at the flag read instead of walking
|
|
76
|
+
* its children every tick.
|
|
77
|
+
*
|
|
78
|
+
* Callers name the lowest slot that moved as `from`. Compaction cannot, so
|
|
79
|
+
* it hands over the pre-compaction layout as `staged` and the unchanged
|
|
80
|
+
* prefix is skipped instead. Either way tail churn walks nothing.
|
|
81
|
+
*/
|
|
82
|
+
protected $reindexChildren(from: number, staged?: V[]): void;
|
|
66
83
|
protected $changeAt(index: number, value: V): number | undefined;
|
|
67
84
|
protected $deleteAt(index: number, operation?: OPERATION): void;
|
|
68
85
|
protected $setAt(index: number, value: V, operation: OPERATION): void;
|
|
@@ -11,7 +11,7 @@ import type { Schema } from "../../Schema.js";
|
|
|
11
11
|
* per-client and drained in priority order (callback on StateView) up to
|
|
12
12
|
* `maxPerTick` per encode pass. Field mutations on already-sent elements
|
|
13
13
|
* propagate through the normal reliable channel without consuming the
|
|
14
|
-
* per-tick budget. Chain `.
|
|
14
|
+
* per-tick budget. Chain `.fullStateOnly()` on the field builder to suppress
|
|
15
15
|
* post-add mutation tracking entirely.
|
|
16
16
|
*/
|
|
17
17
|
export declare class StreamSchema<V = any> implements IRef {
|
package/build/types/symbols.d.ts
CHANGED
|
@@ -66,16 +66,10 @@ export declare const $builder = "~builder";
|
|
|
66
66
|
* Metadata
|
|
67
67
|
*/
|
|
68
68
|
export declare const $descriptors = "~descriptors";
|
|
69
|
-
/**
|
|
70
|
-
* Per-class bitmask: bit i set iff field i carries a @view tag.
|
|
71
|
-
* Lazily computed from $viewFieldIndexes on first encode pass.
|
|
72
|
-
* Skips the per-field metadata[i].tag property chase in the hot encode loop.
|
|
73
|
-
*/
|
|
74
|
-
export declare const $filterBitmask = "~__filterBitmask";
|
|
75
69
|
/**
|
|
76
70
|
* Cached per-class encode descriptor: bundles encoder fn, filter fn,
|
|
77
|
-
* metadata, isSchema flag
|
|
78
|
-
* the constructor. Replaces
|
|
71
|
+
* metadata, isSchema flag and the per-field arrays into one object stashed
|
|
72
|
+
* on the constructor. Replaces several separate per-tree property chases /
|
|
79
73
|
* function calls in the encode loop with a single property load.
|
|
80
74
|
*/
|
|
81
75
|
export declare const $encodeDescriptor = "~__encodeDescriptor";
|
|
@@ -85,7 +79,7 @@ export declare const $refTypeFieldIndexes = "~__refTypeFieldIndexes";
|
|
|
85
79
|
export declare const $viewFieldIndexes = "~__viewFieldIndexes";
|
|
86
80
|
export declare const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
|
|
87
81
|
export declare const $unreliableFieldIndexes = "~__unreliableFieldIndexes";
|
|
88
|
-
export declare const $
|
|
89
|
-
export declare const $
|
|
82
|
+
export declare const $patchOnlyFieldIndexes = "~__patchOnlyFieldIndexes";
|
|
83
|
+
export declare const $fullStateOnlyFieldIndexes = "~__fullStateOnlyFieldIndexes";
|
|
90
84
|
export declare const $streamFieldIndexes = "~__streamFieldIndexes";
|
|
91
85
|
export declare const $streamPriorities = "~__streamPriorities";
|
package/package.json
CHANGED
package/src/Metadata.ts
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
import { DefinitionType, getPropertyDescriptor } from "./annotations.js";
|
|
2
2
|
import { Schema } from "./Schema.js";
|
|
3
3
|
import { getType, registeredTypes, TypeDefinition } from "./types/registry.js";
|
|
4
|
-
import { $decoder, $descriptors, $encoder, $encoders, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $
|
|
4
|
+
import { $decoder, $descriptors, $encoder, $encoders, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $fullStateOnlyFieldIndexes, $streamFieldIndexes, $streamPriorities, $track, $patchOnlyFieldIndexes, $unreliableFieldIndexes, $viewFieldIndexes } from "./types/symbols.js";
|
|
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
8
|
import { isQuantizedType, makeQuantizedEncoder, resolveQuantize } from "./types/quantize.js";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Field indexes ride in the low 6 bits of the operation byte
|
|
12
|
+
* (`(index | operation) & 255`), which leaves room for 0..63. Index 63 is
|
|
13
|
+
* given up: `DELETE_AND_ADD | 63` is 255, the same byte the decoder claims
|
|
14
|
+
* as SWITCH_TO_STRUCTURE before any field decoder sees it. Every nullable
|
|
15
|
+
* field can produce that operation (delete-then-set in one tick merges to
|
|
16
|
+
* DELETE_AND_ADD), so the slot is unusable rather than partly usable.
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_FIELDS = 63;
|
|
19
|
+
|
|
10
20
|
export type MetadataField = {
|
|
11
21
|
type: DefinitionType,
|
|
12
22
|
name: string,
|
|
13
23
|
index: number,
|
|
14
24
|
tag?: number,
|
|
15
25
|
unreliable?: boolean,
|
|
16
|
-
|
|
26
|
+
patchOnly?: boolean,
|
|
17
27
|
deprecated?: boolean,
|
|
18
|
-
|
|
19
|
-
static?: boolean,
|
|
28
|
+
fullStateOnly?: boolean,
|
|
20
29
|
stream?: boolean,
|
|
21
30
|
optional?: boolean,
|
|
22
31
|
};
|
|
@@ -27,8 +36,8 @@ export type Metadata =
|
|
|
27
36
|
{ [$fieldIndexesByViewTag]: {[tag: number]: number[]}; } & // field indexes by "view" tag
|
|
28
37
|
{ [$refTypeFieldIndexes]: number[]; } & // all field indexes containing Ref types (Schema, ArraySchema, MapSchema, etc)
|
|
29
38
|
{ [$unreliableFieldIndexes]: number[]; } & // all field indexes tagged with @unreliable
|
|
30
|
-
{ [$
|
|
31
|
-
{ [$
|
|
39
|
+
{ [$patchOnlyFieldIndexes]: number[]; } & // all field indexes tagged with @patchOnly (not persisted to snapshots)
|
|
40
|
+
{ [$fullStateOnlyFieldIndexes]: number[]; } & // all field indexes tagged @fullStateOnly / .fullStateOnly() (not tracked after assignment)
|
|
32
41
|
{ [$streamFieldIndexes]: number[]; } & // all field indexes holding a t.stream(...) collection
|
|
33
42
|
{ [$streamPriorities]: { [field: number]: (view: any, element: any) => number }; } & // per-stream-field priority callback declared at schema definition time
|
|
34
43
|
{ [$encoders]: Array<(bytes: Uint8Array, value: any, it: any) => void>; } & // pre-computed encoder fn per primitive field
|
|
@@ -107,12 +116,14 @@ function isTSEnum(_enum: any) {
|
|
|
107
116
|
export const Metadata = {
|
|
108
117
|
|
|
109
118
|
addField(metadata: any, index: number, name: string, type: DefinitionType, descriptor?: PropertyDescriptor) {
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
// `index` is 0-based, so 62 is the last usable slot — see MAX_FIELDS
|
|
120
|
+
// for why 63 is off limits.
|
|
121
|
+
if (index >= MAX_FIELDS) {
|
|
122
|
+
throw new Error(`Can't define field '${name}'.\nSchema instances may only have up to ${MAX_FIELDS} fields.`);
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
metadata[index] = Object.assign(
|
|
115
|
-
metadata[index] || {}, // avoid overwriting previous field metadata (@
|
|
126
|
+
metadata[index] || {}, // avoid overwriting previous field metadata (@deprecated / @unreliable)
|
|
116
127
|
{
|
|
117
128
|
type: getNormalizedType(type),
|
|
118
129
|
index,
|
|
@@ -278,34 +289,50 @@ export const Metadata = {
|
|
|
278
289
|
metadata[$unreliableFieldIndexes].push(index);
|
|
279
290
|
},
|
|
280
291
|
|
|
281
|
-
|
|
292
|
+
setPatchOnly(metadata: Metadata, fieldName: string) {
|
|
282
293
|
const index = metadata[fieldName];
|
|
283
|
-
|
|
294
|
+
// patchOnly + fullStateOnly are the only two delivery channels —
|
|
295
|
+
// excluding a field from both would silently never reach a client.
|
|
296
|
+
// (The builder validates earlier; this guards the decorator path.)
|
|
297
|
+
if (metadata[index].fullStateOnly) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`field "${fieldName}" cannot be both patchOnly and fullStateOnly — ` +
|
|
300
|
+
`those are the only two delivery channels, so the field would never reach a client.`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
metadata[index].patchOnly = true;
|
|
284
304
|
|
|
285
|
-
if (!metadata[$
|
|
286
|
-
Object.defineProperty(metadata, $
|
|
305
|
+
if (!metadata[$patchOnlyFieldIndexes]) {
|
|
306
|
+
Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
|
|
287
307
|
value: [],
|
|
288
308
|
enumerable: false,
|
|
289
309
|
configurable: true,
|
|
290
310
|
writable: true,
|
|
291
311
|
});
|
|
292
312
|
}
|
|
293
|
-
metadata[$
|
|
313
|
+
metadata[$patchOnlyFieldIndexes].push(index);
|
|
294
314
|
},
|
|
295
315
|
|
|
296
|
-
|
|
316
|
+
setFullStateOnly(metadata: Metadata, fieldName: string) {
|
|
297
317
|
const index = metadata[fieldName];
|
|
298
|
-
|
|
318
|
+
// Mirror of the guard in setPatchOnly — covers both decorator orders.
|
|
319
|
+
if (metadata[index].patchOnly) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`field "${fieldName}" cannot be both patchOnly and fullStateOnly — ` +
|
|
322
|
+
`those are the only two delivery channels, so the field would never reach a client.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
metadata[index].fullStateOnly = true;
|
|
299
326
|
|
|
300
|
-
if (!metadata[$
|
|
301
|
-
Object.defineProperty(metadata, $
|
|
327
|
+
if (!metadata[$fullStateOnlyFieldIndexes]) {
|
|
328
|
+
Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
|
|
302
329
|
value: [],
|
|
303
330
|
enumerable: false,
|
|
304
331
|
configurable: true,
|
|
305
332
|
writable: true,
|
|
306
333
|
});
|
|
307
334
|
}
|
|
308
|
-
metadata[$
|
|
335
|
+
metadata[$fullStateOnlyFieldIndexes].push(index);
|
|
309
336
|
},
|
|
310
337
|
|
|
311
338
|
setStream(metadata: Metadata, fieldName: string) {
|
|
@@ -503,20 +530,20 @@ export const Metadata = {
|
|
|
503
530
|
});
|
|
504
531
|
}
|
|
505
532
|
|
|
506
|
-
// $
|
|
507
|
-
if (parentMetadata[$
|
|
508
|
-
Object.defineProperty(metadata, $
|
|
509
|
-
value: [...parentMetadata[$
|
|
533
|
+
// $patchOnlyFieldIndexes
|
|
534
|
+
if (parentMetadata[$patchOnlyFieldIndexes] !== undefined) {
|
|
535
|
+
Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
|
|
536
|
+
value: [...parentMetadata[$patchOnlyFieldIndexes]],
|
|
510
537
|
enumerable: false,
|
|
511
538
|
configurable: true,
|
|
512
539
|
writable: true,
|
|
513
540
|
});
|
|
514
541
|
}
|
|
515
542
|
|
|
516
|
-
// $
|
|
517
|
-
if (parentMetadata[$
|
|
518
|
-
Object.defineProperty(metadata, $
|
|
519
|
-
value: [...parentMetadata[$
|
|
543
|
+
// $fullStateOnlyFieldIndexes
|
|
544
|
+
if (parentMetadata[$fullStateOnlyFieldIndexes] !== undefined) {
|
|
545
|
+
Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
|
|
546
|
+
value: [...parentMetadata[$fullStateOnlyFieldIndexes]],
|
|
520
547
|
enumerable: false,
|
|
521
548
|
configurable: true,
|
|
522
549
|
writable: true,
|
|
@@ -586,12 +613,12 @@ export const Metadata = {
|
|
|
586
613
|
return metadata?.[$unreliableFieldIndexes]?.includes(index);
|
|
587
614
|
},
|
|
588
615
|
|
|
589
|
-
|
|
590
|
-
return metadata?.[$
|
|
616
|
+
hasPatchOnlyAtIndex(metadata: Metadata, index: number) {
|
|
617
|
+
return metadata?.[$patchOnlyFieldIndexes]?.includes(index);
|
|
591
618
|
},
|
|
592
619
|
|
|
593
|
-
|
|
594
|
-
return metadata?.[$
|
|
620
|
+
hasFullStateOnlyAtIndex(metadata: Metadata, index: number) {
|
|
621
|
+
return metadata?.[$fullStateOnlyFieldIndexes]?.includes(index);
|
|
595
622
|
},
|
|
596
623
|
|
|
597
624
|
hasStreamAtIndex(metadata: Metadata, index: number) {
|
package/src/annotations.ts
CHANGED
|
@@ -37,12 +37,12 @@ export type PrimitiveType = RawPrimitiveType | typeof Schema | object;
|
|
|
37
37
|
// TODO: infer "default" value type correctly.
|
|
38
38
|
export type DefinitionType<T extends PrimitiveType = PrimitiveType> = T
|
|
39
39
|
| T[]
|
|
40
|
-
| { type: T, default?: InferValueType<T>, view?: boolean | number, sync?: boolean
|
|
41
|
-
| { array: T, default?: ArraySchema<InferValueType<T>>, view?: boolean | number, sync?: boolean
|
|
42
|
-
| { map: T, default?: MapSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean
|
|
43
|
-
| { collection: T, default?: CollectionSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean
|
|
44
|
-
| { set: T, default?: SetSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean
|
|
45
|
-
| { stream: T, default?: StreamSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean,
|
|
40
|
+
| { type: T, default?: InferValueType<T>, view?: boolean | number, sync?: boolean }
|
|
41
|
+
| { array: T, default?: ArraySchema<InferValueType<T>>, view?: boolean | number, sync?: boolean }
|
|
42
|
+
| { map: T, default?: MapSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean }
|
|
43
|
+
| { collection: T, default?: CollectionSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean }
|
|
44
|
+
| { set: T, default?: SetSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean }
|
|
45
|
+
| { stream: T, default?: StreamSchema<InferValueType<T>>, view?: boolean | number, sync?: boolean, priority?: (view: any, element: InferValueType<T>) => number };
|
|
46
46
|
|
|
47
47
|
export type Definition = { [field: string]: DefinitionType };
|
|
48
48
|
|
|
@@ -237,27 +237,46 @@ export function view<T> (tag: number = DEFAULT_VIEW_TAG) {
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
/**
|
|
241
|
+
* `@unreliable` — route a field onto the unreliable transport channel, so a
|
|
242
|
+
* dropped update costs one stale value instead of stalling the ordered stream
|
|
243
|
+
* behind a retransmit. Primitive fields only (see `Metadata.setUnreliable`).
|
|
244
|
+
*
|
|
245
|
+
* The field's FIRST value still travels the reliable channel, as part of the
|
|
246
|
+
* owning instance's ADD; only later mutations become unreliable. A decoder
|
|
247
|
+
* cannot apply a write to a ref it has not been told about, so a value emitted
|
|
248
|
+
* ahead of that ADD would be dropped — and lost for good if the field is never
|
|
249
|
+
* written again.
|
|
250
|
+
*/
|
|
245
251
|
export function unreliable<T> (target: T, field: string) {
|
|
246
252
|
const metadata = Metadata.initialize(target.constructor as typeof Schema);
|
|
247
253
|
Metadata.setUnreliable(metadata, field);
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
/**
|
|
251
|
-
* @
|
|
252
|
-
* encodeAllView).
|
|
257
|
+
* @patchOnly — mark a field as not persisted to snapshots (encodeAll /
|
|
258
|
+
* encodeAllView). PatchOnly fields are still emitted on per-tick patches
|
|
253
259
|
* (reliable or unreliable), but late-joining clients won't see them until
|
|
254
260
|
* the next mutation.
|
|
255
261
|
*
|
|
256
262
|
* Orthogonal to @unreliable: a field can be either, both, or neither.
|
|
257
263
|
*/
|
|
258
|
-
export function
|
|
264
|
+
export function patchOnly<T> (target: T, field: string) {
|
|
259
265
|
const metadata = Metadata.initialize(target.constructor as typeof Schema);
|
|
260
|
-
Metadata.
|
|
266
|
+
Metadata.setPatchOnly(metadata, field);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @fullStateOnly — mark a field as delivered in the full state sync only
|
|
271
|
+
* (encodeAll / encodeAllView), never on per-tick patches. Writes after a
|
|
272
|
+
* client has joined are not propagated to it — populate these fields
|
|
273
|
+
* before clients connect (e.g. during onCreate).
|
|
274
|
+
*
|
|
275
|
+
* The exact mirror of @patchOnly — the two are mutually exclusive.
|
|
276
|
+
*/
|
|
277
|
+
export function fullStateOnly<T> (target: T, field: string) {
|
|
278
|
+
const metadata = Metadata.initialize(target.constructor as typeof Schema);
|
|
279
|
+
Metadata.setFullStateOnly(metadata, field);
|
|
261
280
|
}
|
|
262
281
|
|
|
263
282
|
export function type (
|
|
@@ -772,11 +791,10 @@ export function schema<
|
|
|
772
791
|
};
|
|
773
792
|
|
|
774
793
|
const viewTagFields: { [field: string]: number } = {};
|
|
775
|
-
const ownedFields: string[] = [];
|
|
776
794
|
const unreliableFields: string[] = [];
|
|
777
|
-
const
|
|
795
|
+
const patchOnlyFields: string[] = [];
|
|
778
796
|
const deprecatedFields: { [field: string]: boolean } = {};
|
|
779
|
-
const
|
|
797
|
+
const fullStateOnlyFields: string[] = [];
|
|
780
798
|
const streamFields: string[] = [];
|
|
781
799
|
const streamPriorityFields: { [field: string]: (view: any, element: any) => number } = {};
|
|
782
800
|
const optionalFields: string[] = [];
|
|
@@ -791,11 +809,11 @@ export function schema<
|
|
|
791
809
|
// Local-only field: skip metadata registration entirely so it is
|
|
792
810
|
// never encoded/decoded, but still seed its construction default
|
|
793
811
|
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
794
|
-
if (def.view !== undefined || def.
|
|
795
|
-
def.
|
|
812
|
+
if (def.view !== undefined || def.unreliable ||
|
|
813
|
+
def.patchOnly || def.fullStateOnly || def.stream) {
|
|
796
814
|
throw new Error(
|
|
797
815
|
`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
798
|
-
`together with a sync-only modifier (.view/.
|
|
816
|
+
`together with a sync-only modifier (.view/.unreliable/.patchOnly/.fullStateOnly/.stream). ` +
|
|
799
817
|
`A local-only field cannot be synchronized.`
|
|
800
818
|
);
|
|
801
819
|
}
|
|
@@ -803,6 +821,16 @@ export function schema<
|
|
|
803
821
|
continue;
|
|
804
822
|
}
|
|
805
823
|
|
|
824
|
+
// The two delivery channels are exhaustive: excluding a field from
|
|
825
|
+
// both leaves it with nowhere to go — a silent .noSync().
|
|
826
|
+
if (def.patchOnly && def.fullStateOnly) {
|
|
827
|
+
throw new Error(
|
|
828
|
+
`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .patchOnly() ` +
|
|
829
|
+
`together with .fullStateOnly(). Those are the only two delivery channels, ` +
|
|
830
|
+
`so the field would never reach a client — use .noSync() if that is intended.`
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
806
834
|
const normalizedType = getNormalizedType(def.type);
|
|
807
835
|
// A synced ref must be encodable (a Schema, or Metadata.setFields()'d) — reject a bare class.
|
|
808
836
|
if (typeof normalizedType === "function" && !Schema.is(normalizedType)) {
|
|
@@ -814,11 +842,10 @@ export function schema<
|
|
|
814
842
|
fields[fieldName] = normalizedType;
|
|
815
843
|
|
|
816
844
|
if (def.view !== undefined) { viewTagFields[fieldName] = def.view; }
|
|
817
|
-
if (def.owned) { ownedFields.push(fieldName); }
|
|
818
845
|
if (def.unreliable) { unreliableFields.push(fieldName); }
|
|
819
|
-
if (def.
|
|
846
|
+
if (def.patchOnly) { patchOnlyFields.push(fieldName); }
|
|
820
847
|
if (def.deprecated) { deprecatedFields[fieldName] = def.deprecatedThrows; }
|
|
821
|
-
if (def.
|
|
848
|
+
if (def.fullStateOnly) { fullStateOnlyFields.push(fieldName); }
|
|
822
849
|
if (def.stream) { streamFields.push(fieldName); }
|
|
823
850
|
if (def.streamPriority !== undefined) { streamPriorityFields[fieldName] = def.streamPriority; }
|
|
824
851
|
if (def.optional) { optionalFields.push(fieldName); }
|
|
@@ -903,23 +930,20 @@ export function schema<
|
|
|
903
930
|
for (const fieldName in viewTagFields) {
|
|
904
931
|
view(viewTagFields[fieldName])(klass.prototype, fieldName);
|
|
905
932
|
}
|
|
906
|
-
for (const fieldName of ownedFields) {
|
|
907
|
-
owned(klass.prototype, fieldName);
|
|
908
|
-
}
|
|
909
933
|
for (const fieldName of unreliableFields) {
|
|
910
934
|
unreliable(klass.prototype, fieldName);
|
|
911
935
|
}
|
|
912
|
-
for (const fieldName of
|
|
913
|
-
|
|
936
|
+
for (const fieldName of patchOnlyFields) {
|
|
937
|
+
patchOnly(klass.prototype, fieldName);
|
|
914
938
|
}
|
|
915
939
|
for (const fieldName in deprecatedFields) {
|
|
916
940
|
deprecated(deprecatedFields[fieldName])(klass.prototype, fieldName);
|
|
917
941
|
}
|
|
918
942
|
|
|
919
|
-
if (
|
|
943
|
+
if (fullStateOnlyFields.length > 0 || streamFields.length > 0) {
|
|
920
944
|
const metadata = (klass as any)[Symbol.metadata] as Metadata;
|
|
921
|
-
for (const fieldName of
|
|
922
|
-
Metadata.
|
|
945
|
+
for (const fieldName of fullStateOnlyFields) {
|
|
946
|
+
Metadata.setFullStateOnly(metadata, fieldName);
|
|
923
947
|
}
|
|
924
948
|
for (const fieldName of streamFields) {
|
|
925
949
|
Metadata.setStream(metadata, fieldName);
|
package/src/codegen/api.ts
CHANGED
|
@@ -14,8 +14,9 @@ import * as java from "./languages/java.js";
|
|
|
14
14
|
import * as lua from "./languages/lua.js";
|
|
15
15
|
import * as c from "./languages/c.js";
|
|
16
16
|
import * as gdscript from "./languages/gdscript.js";
|
|
17
|
+
import * as dart from "./languages/dart.js";
|
|
17
18
|
|
|
18
|
-
export const generators: Record<string, any> = { csharp, cpp, haxe, ts, js, java, lua, c, gdscript, };
|
|
19
|
+
export const generators: Record<string, any> = { csharp, cpp, haxe, ts, js, java, lua, c, gdscript, dart, };
|
|
19
20
|
|
|
20
21
|
export interface GenerateOptions {
|
|
21
22
|
files: string[],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class, Property, File, getCommentHeader, getInheritanceTree, Context } from "../types.js";
|
|
1
|
+
import { Class, Property, File, getCommentHeader, getInheritanceTree, Context, resolveQuantized } from "../types.js";
|
|
2
2
|
import { GenerateOptions } from "../api.js";
|
|
3
3
|
|
|
4
4
|
export const name = "C";
|
|
@@ -20,6 +20,7 @@ const typeMaps: { [key: string]: string } = {
|
|
|
20
20
|
"uint64": "uint64_t",
|
|
21
21
|
"float32": "float",
|
|
22
22
|
"float64": "double",
|
|
23
|
+
"quantized": "double",
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -42,6 +43,7 @@ const fieldTypeMaps: { [key: string]: string } = {
|
|
|
42
43
|
"ref": "COLYSEUS_FIELD_REF",
|
|
43
44
|
"array": "COLYSEUS_FIELD_ARRAY",
|
|
44
45
|
"map": "COLYSEUS_FIELD_MAP",
|
|
46
|
+
"quantized": "COLYSEUS_FIELD_QUANTIZED",
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
const COMMON_INCLUDES = `#include "colyseus/schema/types.h"
|
|
@@ -202,11 +204,22 @@ function generateFieldsArray(klass: Class, typeName: string, snakeName: string,
|
|
|
202
204
|
return `static const colyseus_field_t ${snakeName}_fields[] = {};`;
|
|
203
205
|
}
|
|
204
206
|
|
|
207
|
+
// one pre-resolved static descriptor per quantized field
|
|
208
|
+
const descriptors = allProperties
|
|
209
|
+
.filter(prop => prop.quantized)
|
|
210
|
+
.map(prop => {
|
|
211
|
+
const q = prop.quantized;
|
|
212
|
+
const { range, span } = resolveQuantized(q);
|
|
213
|
+
return `static const colyseus_quantized_descriptor_t ${snakeName}_${prop.name}_quantized = {${q.min}, ${q.max}, ${range}, ${span}, ${q.bits}, ${q.wrap}};`;
|
|
214
|
+
});
|
|
215
|
+
|
|
205
216
|
const fields = allProperties.map((prop, i) => {
|
|
206
217
|
const fieldType = getFieldType(prop);
|
|
207
218
|
const typeString = getFieldTypeString(prop);
|
|
208
219
|
|
|
209
220
|
let vtableRef = "NULL";
|
|
221
|
+
let childPrimitiveRef = "NULL";
|
|
222
|
+
let quantizedRef = "NULL";
|
|
210
223
|
|
|
211
224
|
if (prop.type === "ref" && prop.childType && !typeMaps[prop.childType]) {
|
|
212
225
|
const childSnake = toSnakeCase(prop.childType);
|
|
@@ -214,12 +227,17 @@ function generateFieldsArray(klass: Class, typeName: string, snakeName: string,
|
|
|
214
227
|
} else if ((prop.type === "array" || prop.type === "map") && prop.childType && !typeMaps[prop.childType]) {
|
|
215
228
|
const childSnake = toSnakeCase(prop.childType);
|
|
216
229
|
vtableRef = `&${childSnake}_vtable`;
|
|
230
|
+
} else if ((prop.type === "array" || prop.type === "map") && prop.childType) {
|
|
231
|
+
// collection of primitives — the decoder strcmp()s this to pick the reader
|
|
232
|
+
childPrimitiveRef = `"${prop.childType}"`;
|
|
233
|
+
} else if (prop.quantized) {
|
|
234
|
+
quantizedRef = `&${snakeName}_${prop.name}_quantized`;
|
|
217
235
|
}
|
|
218
236
|
|
|
219
|
-
return ` {${prop.index}, "${prop.name}", ${fieldType}, "${typeString}", offsetof(${typeName}, ${prop.name}), ${vtableRef},
|
|
237
|
+
return ` {${prop.index}, "${prop.name}", ${fieldType}, "${typeString}", offsetof(${typeName}, ${prop.name}), ${vtableRef}, ${childPrimitiveRef}, ${quantizedRef}}`;
|
|
220
238
|
}).join(",\n");
|
|
221
239
|
|
|
222
|
-
return
|
|
240
|
+
return `${descriptors.length ? descriptors.join("\n") + "\n\n" : ""}static const colyseus_field_t ${snakeName}_fields[] = {
|
|
223
241
|
${fields}
|
|
224
242
|
};`;
|
|
225
243
|
}
|
|
@@ -191,7 +191,13 @@ function generateProperty(prop: Property, indent: string = "") {
|
|
|
191
191
|
let langType: string;
|
|
192
192
|
let initializer = "";
|
|
193
193
|
|
|
194
|
-
if (prop.
|
|
194
|
+
if (prop.quantized) {
|
|
195
|
+
const q = prop.quantized;
|
|
196
|
+
typeArgs += `, QuantizeMin = ${q.min}, QuantizeMax = ${q.max}, QuantizeBits = ${q.bits}, QuantizeWrap = ${q.wrap}`;
|
|
197
|
+
langType = "double";
|
|
198
|
+
initializer = "default(double)";
|
|
199
|
+
|
|
200
|
+
} else if (prop.childType) {
|
|
195
201
|
const isUpcaseFirst = prop.childType.match(/^[A-Z]/);
|
|
196
202
|
|
|
197
203
|
langType = getType(prop);
|