@irtio/schema 0.10.1 → 1.0.0
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/dist/index.d.ts +115 -1
- package/dist/index.js +205 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -200,6 +200,80 @@ declare function applyChannel2d(channel: PhysicsBodyChannel, value: number, targ
|
|
|
200
200
|
wz: number;
|
|
201
201
|
}): void;
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* M6 lane K (D77): the declarative ownership policies a collection may declare.
|
|
205
|
+
*
|
|
206
|
+
* A relay room with state has no room file, so the three things a rung-1 room file exists for —
|
|
207
|
+
* add one record per player, remove it when they go, and decide contested handovers — have to be
|
|
208
|
+
* declarations instead. These are those declarations, compiled the way `physics` is compiled: a
|
|
209
|
+
* literal in the schema, a descriptor on the collection, and a canonical fragment that is emitted
|
|
210
|
+
* **only when declared**, so every schema written before this keeps the hash it had.
|
|
211
|
+
*
|
|
212
|
+
* They are server behaviour, not rendering behaviour, which is why they join the hash at all: a
|
|
213
|
+
* client and a room that disagree about who may take a crate disagree about the world, the way
|
|
214
|
+
* they would about `physics`, and not the way they would about `interpolate`.
|
|
215
|
+
*
|
|
216
|
+
* Nothing here reads state. Compilation is pure and the descriptors are plain data, so the same
|
|
217
|
+
* declaration means the same thing in a coded room, in the relay host, and in a test.
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
/** How a contested record changes hands. `free | closer | ask | never` (D77). */
|
|
221
|
+
type OwnershipTransfer = 'free' | 'closer' | 'ask' | 'never';
|
|
222
|
+
/** What happens to a held record when its hold runs out. */
|
|
223
|
+
type OwnershipLeaveAction = 'release' | 'nearest' | 'random' | 'remove';
|
|
224
|
+
/** The fields `closer` and `nearest` read positions from. Same shape `spatial-grid` uses. */
|
|
225
|
+
interface OwnershipPosition {
|
|
226
|
+
/** A numeric field of this collection: the object's x. */
|
|
227
|
+
readonly x: string;
|
|
228
|
+
/** A numeric field of this collection: the object's y. */
|
|
229
|
+
readonly y: string;
|
|
230
|
+
/** The entity collection holding one record per player, keyed by client id. */
|
|
231
|
+
readonly players: string;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Hold, then act. When a player's presence is removed, every record they own is held for `hold`
|
|
235
|
+
* (still theirs, still visible, unwritable and untakeable by anyone else); when the hold expires,
|
|
236
|
+
* `then` fires. `hold: 0` — the default — makes `then` fire at once.
|
|
237
|
+
*/
|
|
238
|
+
interface OwnershipLeave {
|
|
239
|
+
/** `'5m'`, `'30s'`, `'2h'`, or milliseconds as a number. Default `0`. */
|
|
240
|
+
readonly hold?: string | number;
|
|
241
|
+
/** Default `'release'`, except on a `perPlayer` collection where it is `'remove'`. */
|
|
242
|
+
readonly then?: OwnershipLeaveAction;
|
|
243
|
+
}
|
|
244
|
+
interface OwnershipOptions {
|
|
245
|
+
/** Default `'free'`'s opposite: with no `ownership` declared at all the old rule applies. */
|
|
246
|
+
readonly transfer?: OwnershipTransfer;
|
|
247
|
+
/** `closer`'s flap guard: the requester must be nearer by at least this many world units. */
|
|
248
|
+
readonly by?: number;
|
|
249
|
+
/** Required by `transfer: 'closer'` and by `onLeave.then: 'nearest'`. */
|
|
250
|
+
readonly position?: OwnershipPosition;
|
|
251
|
+
readonly onLeave?: OwnershipLeave;
|
|
252
|
+
}
|
|
253
|
+
/** The compiled form. `holdMs` is milliseconds, always; `then` is always resolved. */
|
|
254
|
+
interface OwnershipDesc {
|
|
255
|
+
readonly transfer: OwnershipTransfer;
|
|
256
|
+
readonly by: number;
|
|
257
|
+
readonly position: OwnershipPosition | undefined;
|
|
258
|
+
readonly holdMs: number;
|
|
259
|
+
readonly then: OwnershipLeaveAction;
|
|
260
|
+
}
|
|
261
|
+
/** A hold window, in milliseconds. Accepts `'5m'`-style strings and plain millisecond numbers. */
|
|
262
|
+
declare function parseHold(at: string, raw: string | number | undefined): number;
|
|
263
|
+
/**
|
|
264
|
+
* Compiles one collection's `ownership` declaration, refusing the combinations that cannot mean
|
|
265
|
+
* anything. Cross-collection checks (does `position.players` name an entity?) happen in
|
|
266
|
+
* `defineSchema`, once every collection is compiled.
|
|
267
|
+
*/
|
|
268
|
+
declare function compileOwnership(name: string, kind: 'entity' | 'singleton', fields: readonly FieldDesc[], options: OwnershipOptions, perPlayer: boolean): OwnershipDesc;
|
|
269
|
+
/**
|
|
270
|
+
* The canonical fragment. `hold` is emitted in milliseconds so the round trip through
|
|
271
|
+
* `schemaFromCanonical` is exact whichever spelling the author used.
|
|
272
|
+
*/
|
|
273
|
+
declare function canonicalOwnership(o: OwnershipDesc): Record<string, unknown>;
|
|
274
|
+
/** Reads a canonical ownership fragment back (`schemaFromCanonical`). */
|
|
275
|
+
declare function ownershipFromCanonical(at: string, raw: unknown): OwnershipOptions;
|
|
276
|
+
|
|
203
277
|
/**
|
|
204
278
|
* `entity`, `singleton`, `server`, `client`, `defineSchema`, and the compiled schema
|
|
205
279
|
* (ordered collection descriptors, sorted RPC table, canonical form, hash).
|
|
@@ -269,6 +343,27 @@ interface EntityOptions {
|
|
|
269
343
|
* world. Requires `physics:` on the room config; the runtime rejects the pairing otherwise.
|
|
270
344
|
*/
|
|
271
345
|
readonly physics?: EntityPhysics;
|
|
346
|
+
/**
|
|
347
|
+
* D77: one record per joined client, id = the client id, owned by them. The runtime adds it on
|
|
348
|
+
* join and applies `ownership.onLeave` when the player goes, which is what replaces a room
|
|
349
|
+
* file's `onJoin`/`onLeave` pair. Defaults its leave rule to `{ hold: 0, then: 'remove' }` and
|
|
350
|
+
* accepts no other `then`, because a per-player record cannot belong to someone else.
|
|
351
|
+
*
|
|
352
|
+
* Server behaviour, so it is part of the canonical form and the hash — unlike `predicted`.
|
|
353
|
+
*/
|
|
354
|
+
readonly perPlayer?: boolean;
|
|
355
|
+
/**
|
|
356
|
+
* D77: clients may `add` records to this collection (the adder becomes the owner, with the id
|
|
357
|
+
* it chose) and the owner may `remove` one. Off by default, which is what every collection
|
|
358
|
+
* written before this is: `add` and `remove` from a client are refused by name.
|
|
359
|
+
*/
|
|
360
|
+
readonly clientCreate?: boolean;
|
|
361
|
+
/**
|
|
362
|
+
* D77: how a contested record changes hands, and what happens to it when its owner leaves.
|
|
363
|
+
* Absent means the old rule and nothing else: `requestOwnership` grants only when the record is
|
|
364
|
+
* unowned, and a leaving player's records keep their (now absent) owner.
|
|
365
|
+
*/
|
|
366
|
+
readonly ownership?: OwnershipOptions;
|
|
272
367
|
}
|
|
273
368
|
interface EntityDef<F extends Fields = Fields, O extends EntityOptions = EntityOptions> {
|
|
274
369
|
readonly kind: 'entity';
|
|
@@ -323,6 +418,12 @@ declare function client<P extends Fields = {}, R extends Fields = Fields>(spec:
|
|
|
323
418
|
}): RpcDef<'client', P, R>;
|
|
324
419
|
/** RPC names a builder may not declare (built-ins live in `@irtio/protocol`). */
|
|
325
420
|
declare const RESERVED_RPC_NAMES: readonly string[];
|
|
421
|
+
/**
|
|
422
|
+
* The prefix irtio's built-in RPC names carry (`$ownership.ask`), refused to a builder here and to
|
|
423
|
+
* a caller in `@irtio/protocol`'s `isReservedRpcName`. Stated twice because the dependency runs
|
|
424
|
+
* schema → protocol and never the other way.
|
|
425
|
+
*/
|
|
426
|
+
declare const RESERVED_RPC_PREFIX = "$";
|
|
326
427
|
/** Collection names a builder may not declare (the runtime merges built-in state under them). */
|
|
327
428
|
declare const RESERVED_COLLECTION_NAMES: readonly string[];
|
|
328
429
|
interface FieldDesc {
|
|
@@ -351,6 +452,12 @@ interface CollectionDesc {
|
|
|
351
452
|
readonly proxy: boolean;
|
|
352
453
|
/** D22 body/intent field split, compiled. `undefined` for ordinary collections. In the hash. */
|
|
353
454
|
readonly physics: PhysicsDesc | undefined;
|
|
455
|
+
/** D77: one record per joined client, owned by them. In the hash. */
|
|
456
|
+
readonly perPlayer: boolean;
|
|
457
|
+
/** D77: clients may add records here and their owner may remove them. In the hash. */
|
|
458
|
+
readonly clientCreate: boolean;
|
|
459
|
+
/** D77: the compiled transfer and leave rules. `undefined` when none were declared. In the hash. */
|
|
460
|
+
readonly ownership: OwnershipDesc | undefined;
|
|
354
461
|
}
|
|
355
462
|
interface RpcDesc {
|
|
356
463
|
readonly name: string;
|
|
@@ -1009,5 +1116,12 @@ interface SchemaChange {
|
|
|
1009
1116
|
}
|
|
1010
1117
|
/** Compares two compiled schemas and returns every classified change, sorted by path then code. */
|
|
1011
1118
|
declare function diffSchemas(oldSchema: AnySchema, newSchema: AnySchema): SchemaChange[];
|
|
1119
|
+
/**
|
|
1120
|
+
* True when every breaking change in `changes` lives under the RPC table (`rpc` / `rpc.*`
|
|
1121
|
+
* paths) and there is at least one. Such a change breaks connected clients (the version fence
|
|
1122
|
+
* reconnects them) but touches nothing a snapshot stores, so a deploy needs `--allow-breaking`
|
|
1123
|
+
* and no migration: a chain step without one decodes and re-encodes losslessly.
|
|
1124
|
+
*/
|
|
1125
|
+
declare function breakingIsWireOnly(changes: readonly SchemaChange[]): boolean;
|
|
1012
1126
|
|
|
1013
|
-
export { type AddOptions, type AnyDef, type AnyMessageMap, type AnyRpc, type AnySchema, type AnyType, type Body2dState, type BodyFieldsOf, type BroadcastProxy, ByteReader, ByteWriter, CANONICAL_VERSION, type ClientCallProxy, type ClientImplementations, type ClientRpcs, type ClientState, type Collection, type CollectionDesc, type CollectionDirty, type DecodedSnapshot, type DeepReadonly, type DeepWritable, type DefMap, type Delta, type DeltaCollection, type DeltaOp, type DirtySet, EntityCollection, type EntityDef, type EntityOptions, type EntityPhysics, type FieldDesc, type FieldMask, type Fields, type FromCanonicalOptions, type GridDesc, type GridOptions, type Header, ID_MAX_BYTES, type Implementations, type Infer, type InferFields, type InitFields, type InitOf, type InstanceOf, type Kind, MAX_MESSAGES, type MessageChannels, type MessageDesc, type MessageMap, type MessageNames, type MessageValue, type MessagesOf, type OwnableKeys, type Owned, PHYSICS_BODY_CHANNELS, type PhysicsBodyChannel, type PhysicsBodyMap, type PhysicsDesc, type PhysicsKeys, type PlainState, RESERVED_COLLECTION_NAMES, RESERVED_RPC_NAMES, type ReadonlyBodyFields, type ReadonlyCollection, type RecordDirty, type RoleOf, type RpcDef, type RpcDesc, type RpcDirection, type RpcMap, type RpcParams, type RpcReturns, type RpcSpec, SERVER_OWNER, SINGLETON_ID, type Schema, type SchemaChange, type SchemaDefs, type SchemaIssue, type SchemaMessages, type SchemaOptions, type SchemaRoles, type SchemaRpc, type ServerCallProxy, type ServerMessageChannels, type ServerOwnedKeys, type ServerRpcs, type SingletonDef, type SnapshotOptions, type State, type Tracked, type Type, type TypeDesc, type ValueSink, type Visibility, type VisibleKeys, type WalkEvents, angleFrom2d, applyChannel2d, applyDelta, bool, bytesEqual, canonicalPhysics, canonicalType, channelOf2d, client, cloneValue, collectionDirty, compilePhysics, computeDirty, createDirtySet, createFieldMask, createState, decodeDelta, decodeDeltaFrom, decodeFields, decodeSnapshot, defaultRecord, defaultValue, defineSchema, describeType, diffSchemas, encodeDelta, encodeFields, encodeSnapshot, entity, enumOf, estimateSize, f32, f64, filterDirty, frozenProxy, i32, isDirtyEmpty, isWhole, list, markAdd, markField, markOwner, markPath, markRemove, mergeDirty, mergeMask, normalizeRecord, normalizeValue, physicsFromCanonical, readValue, ref, schemaFromCanonical, server, sha256, sha256Hex, singleton, stableStringify, str, struct, toHex, track, u16, u32, u8, validateForDeploy, validateValue, walkDelta, walkSnapshot, writeValue };
|
|
1127
|
+
export { type AddOptions, type AnyDef, type AnyMessageMap, type AnyRpc, type AnySchema, type AnyType, type Body2dState, type BodyFieldsOf, type BroadcastProxy, ByteReader, ByteWriter, CANONICAL_VERSION, type ClientCallProxy, type ClientImplementations, type ClientRpcs, type ClientState, type Collection, type CollectionDesc, type CollectionDirty, type DecodedSnapshot, type DeepReadonly, type DeepWritable, type DefMap, type Delta, type DeltaCollection, type DeltaOp, type DirtySet, EntityCollection, type EntityDef, type EntityOptions, type EntityPhysics, type FieldDesc, type FieldMask, type Fields, type FromCanonicalOptions, type GridDesc, type GridOptions, type Header, ID_MAX_BYTES, type Implementations, type Infer, type InferFields, type InitFields, type InitOf, type InstanceOf, type Kind, MAX_MESSAGES, type MessageChannels, type MessageDesc, type MessageMap, type MessageNames, type MessageValue, type MessagesOf, type OwnableKeys, type Owned, type OwnershipDesc, type OwnershipLeave, type OwnershipLeaveAction, type OwnershipOptions, type OwnershipPosition, type OwnershipTransfer, PHYSICS_BODY_CHANNELS, type PhysicsBodyChannel, type PhysicsBodyMap, type PhysicsDesc, type PhysicsKeys, type PlainState, RESERVED_COLLECTION_NAMES, RESERVED_RPC_NAMES, RESERVED_RPC_PREFIX, type ReadonlyBodyFields, type ReadonlyCollection, type RecordDirty, type RoleOf, type RpcDef, type RpcDesc, type RpcDirection, type RpcMap, type RpcParams, type RpcReturns, type RpcSpec, SERVER_OWNER, SINGLETON_ID, type Schema, type SchemaChange, type SchemaDefs, type SchemaIssue, type SchemaMessages, type SchemaOptions, type SchemaRoles, type SchemaRpc, type ServerCallProxy, type ServerMessageChannels, type ServerOwnedKeys, type ServerRpcs, type SingletonDef, type SnapshotOptions, type State, type Tracked, type Type, type TypeDesc, type ValueSink, type Visibility, type VisibleKeys, type WalkEvents, angleFrom2d, applyChannel2d, applyDelta, bool, breakingIsWireOnly, bytesEqual, canonicalOwnership, canonicalPhysics, canonicalType, channelOf2d, client, cloneValue, collectionDirty, compileOwnership, compilePhysics, computeDirty, createDirtySet, createFieldMask, createState, decodeDelta, decodeDeltaFrom, decodeFields, decodeSnapshot, defaultRecord, defaultValue, defineSchema, describeType, diffSchemas, encodeDelta, encodeFields, encodeSnapshot, entity, enumOf, estimateSize, f32, f64, filterDirty, frozenProxy, i32, isDirtyEmpty, isWhole, list, markAdd, markField, markOwner, markPath, markRemove, mergeDirty, mergeMask, normalizeRecord, normalizeValue, ownershipFromCanonical, parseHold, physicsFromCanonical, readValue, ref, schemaFromCanonical, server, sha256, sha256Hex, singleton, stableStringify, str, struct, toHex, track, u16, u32, u8, validateForDeploy, validateValue, walkDelta, walkSnapshot, writeValue };
|
package/dist/index.js
CHANGED
|
@@ -369,6 +369,141 @@ function applyChannel2d(channel, value, target) {
|
|
|
369
369
|
}
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
// src/policy.ts
|
|
373
|
+
var DURATION_RE = /^(\d+)(ms|s|m|h|d)$/;
|
|
374
|
+
var UNIT_MS = {
|
|
375
|
+
ms: 1,
|
|
376
|
+
s: 1e3,
|
|
377
|
+
m: 6e4,
|
|
378
|
+
h: 36e5,
|
|
379
|
+
d: 864e5
|
|
380
|
+
};
|
|
381
|
+
function parseHold(at, raw) {
|
|
382
|
+
if (raw === void 0) return 0;
|
|
383
|
+
if (typeof raw === "number") {
|
|
384
|
+
if (!Number.isFinite(raw) || raw < 0 || !Number.isInteger(raw)) {
|
|
385
|
+
throw new Error(`${at}: ownership.onLeave.hold must be a non-negative whole ms, got ${raw}`);
|
|
386
|
+
}
|
|
387
|
+
return raw;
|
|
388
|
+
}
|
|
389
|
+
const m = DURATION_RE.exec(raw);
|
|
390
|
+
if (!m) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`${at}: ownership.onLeave.hold must be a duration like '30s', '5m', '2h' or milliseconds, got ${JSON.stringify(raw)}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return Number(m[1]) * UNIT_MS[m[2]];
|
|
396
|
+
}
|
|
397
|
+
var NUMERIC = /* @__PURE__ */ new Set(["u8", "u16", "u32", "i32", "f32", "f64"]);
|
|
398
|
+
function compileOwnership(name, kind, fields, options, perPlayer) {
|
|
399
|
+
const at = name;
|
|
400
|
+
if (kind !== "entity") {
|
|
401
|
+
throw new Error(`${at}: ownership is entity-only \u2014 a singleton has no per-record owner`);
|
|
402
|
+
}
|
|
403
|
+
const transfer = options.transfer ?? (perPlayer ? "never" : "free");
|
|
404
|
+
if (!["free", "closer", "ask", "never"].includes(transfer)) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`${at}: ownership.transfer must be 'free', 'closer', 'ask' or 'never', got ${JSON.stringify(
|
|
407
|
+
transfer
|
|
408
|
+
)}`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const then = options.onLeave?.then ?? (perPlayer ? "remove" : "release");
|
|
412
|
+
if (!["release", "nearest", "random", "remove"].includes(then)) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
`${at}: ownership.onLeave.then must be 'release', 'nearest', 'random' or 'remove', got ` + JSON.stringify(then)
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (perPlayer && then !== "remove") {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`${at}: a perPlayer collection only accepts onLeave.then: 'remove' \u2014 a per-player record cannot belong to someone else`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const holdMs = parseHold(at, options.onLeave?.hold);
|
|
423
|
+
const position = options.position;
|
|
424
|
+
if ((transfer === "closer" || then === "nearest") && position === void 0) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`${at}: ownership.position: { x, y, players } is required by transfer: 'closer' and by onLeave.then: 'nearest' \u2014 there is no other way to know who is nearer`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (options.by !== void 0) {
|
|
430
|
+
if (transfer !== "closer") {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`${at}: ownership.by is the 'closer' flap guard and means nothing without it`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
if (!Number.isFinite(options.by) || options.by < 0) {
|
|
436
|
+
throw new Error(`${at}: ownership.by must be a non-negative finite number`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (position !== void 0) {
|
|
440
|
+
for (const axis of ["x", "y"]) {
|
|
441
|
+
const field = fields.find((f) => f.name === position[axis]);
|
|
442
|
+
if (!field || !NUMERIC.has(field.type.kind)) {
|
|
443
|
+
throw new Error(`${at}: ownership.position.${axis} must name a numeric field of ${at}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (typeof position.players !== "string" || position.players === "") {
|
|
447
|
+
throw new Error(`${at}: ownership.position.players must name an entity collection`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
transfer,
|
|
452
|
+
by: options.by ?? 0,
|
|
453
|
+
position: position ? { x: position.x, y: position.y, players: position.players } : void 0,
|
|
454
|
+
holdMs,
|
|
455
|
+
then
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function canonicalOwnership(o) {
|
|
459
|
+
return {
|
|
460
|
+
transfer: o.transfer,
|
|
461
|
+
...o.by !== 0 ? { by: o.by } : {},
|
|
462
|
+
...o.position ? { position: { x: o.position.x, y: o.position.y, players: o.position.players } } : {},
|
|
463
|
+
// `then` is the owner-decided name for the leave action in the D77 spec, and this is plain
|
|
464
|
+
// data that is never awaited.
|
|
465
|
+
// biome-ignore lint/suspicious/noThenProperty: see above
|
|
466
|
+
onLeave: { hold: o.holdMs, then: o.then }
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function ownershipFromCanonical(at, raw) {
|
|
470
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
471
|
+
throw new Error(`${at}: ownership must be an object`);
|
|
472
|
+
}
|
|
473
|
+
const o = raw;
|
|
474
|
+
const transfer = o.transfer;
|
|
475
|
+
if (typeof transfer !== "string") throw new Error(`${at}: ownership.transfer must be a string`);
|
|
476
|
+
const leaveRaw = o.onLeave;
|
|
477
|
+
if (typeof leaveRaw !== "object" || leaveRaw === null || Array.isArray(leaveRaw)) {
|
|
478
|
+
throw new Error(`${at}: ownership.onLeave must be an object`);
|
|
479
|
+
}
|
|
480
|
+
const leave = leaveRaw;
|
|
481
|
+
if (typeof leave.hold !== "number")
|
|
482
|
+
throw new Error(`${at}: ownership.onLeave.hold must be a ms number`);
|
|
483
|
+
if (typeof leave.then !== "string")
|
|
484
|
+
throw new Error(`${at}: ownership.onLeave.then must be a string`);
|
|
485
|
+
let position;
|
|
486
|
+
if (o.position !== void 0 && o.position !== null) {
|
|
487
|
+
const p = o.position;
|
|
488
|
+
if (typeof p.x !== "string" || typeof p.y !== "string" || typeof p.players !== "string") {
|
|
489
|
+
throw new Error(`${at}: ownership.position needs string x, y and players`);
|
|
490
|
+
}
|
|
491
|
+
position = { x: p.x, y: p.y, players: p.players };
|
|
492
|
+
}
|
|
493
|
+
const by = o.by;
|
|
494
|
+
if (by !== void 0 && typeof by !== "number")
|
|
495
|
+
throw new Error(`${at}: ownership.by must be a number`);
|
|
496
|
+
return {
|
|
497
|
+
transfer,
|
|
498
|
+
...by !== void 0 ? { by } : {},
|
|
499
|
+
...position !== void 0 ? { position } : {},
|
|
500
|
+
// `then` is the owner-decided name for the leave action in the D77 spec, and this is plain
|
|
501
|
+
// data that is never awaited.
|
|
502
|
+
// biome-ignore lint/suspicious/noThenProperty: see above
|
|
503
|
+
onLeave: { hold: leave.hold, then: leave.then }
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
372
507
|
// src/sha256.ts
|
|
373
508
|
var K = new Uint32Array([
|
|
374
509
|
1116352408,
|
|
@@ -541,6 +676,7 @@ function makeRpc(direction, spec) {
|
|
|
541
676
|
return { kind: "rpc", direction, params, returns: spec.returns };
|
|
542
677
|
}
|
|
543
678
|
var RESERVED_RPC_NAMES = ["requestOwnership"];
|
|
679
|
+
var RESERVED_RPC_PREFIX = "$";
|
|
544
680
|
var RESERVED_COLLECTION_NAMES = ["clients"];
|
|
545
681
|
var MAX_MESSAGES = 256;
|
|
546
682
|
function defineSchema(defs, options = {}) {
|
|
@@ -565,6 +701,19 @@ function defineSchema(defs, options = {}) {
|
|
|
565
701
|
`${name}: proxy needs physics \u2014 only body-backed collections get a body in a client's local world`
|
|
566
702
|
);
|
|
567
703
|
}
|
|
704
|
+
const perPlayer = o.perPlayer === true;
|
|
705
|
+
if (perPlayer && def.kind !== "entity") {
|
|
706
|
+
throw new Error(`${name}: perPlayer is entity-only \u2014 a singleton has no per-player record`);
|
|
707
|
+
}
|
|
708
|
+
if (o.clientCreate === true && def.kind !== "entity") {
|
|
709
|
+
throw new Error(`${name}: clientCreate is entity-only \u2014 a singleton is never added`);
|
|
710
|
+
}
|
|
711
|
+
if ((perPlayer || o.clientCreate === true || o.ownership) && o.serverOwned === true) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`${name}: perPlayer, clientCreate and ownership all describe client ownership, which a serverOwned collection does not have`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const ownership = o.ownership || perPlayer ? compileOwnership(name, def.kind, fields, o.ownership ?? {}, perPlayer) : void 0;
|
|
568
717
|
return {
|
|
569
718
|
name,
|
|
570
719
|
index,
|
|
@@ -572,6 +721,9 @@ function defineSchema(defs, options = {}) {
|
|
|
572
721
|
fields,
|
|
573
722
|
fieldIndex: new Map(fields.map((f) => [f.name, f.index])),
|
|
574
723
|
serverOwned: o.serverOwned === true,
|
|
724
|
+
perPlayer,
|
|
725
|
+
clientCreate: o.clientCreate === true,
|
|
726
|
+
ownership,
|
|
575
727
|
visibility: o.visibility ?? "all",
|
|
576
728
|
roles: o.roles ? [...o.roles] : void 0,
|
|
577
729
|
grid: o.grid ? {
|
|
@@ -591,12 +743,37 @@ function defineSchema(defs, options = {}) {
|
|
|
591
743
|
for (const c of collections) {
|
|
592
744
|
for (const f of c.fields) checkRefs(f.type, entityNames, `${c.name}.${f.name}`);
|
|
593
745
|
}
|
|
746
|
+
for (const c of collections) {
|
|
747
|
+
const players = c.ownership?.position?.players;
|
|
748
|
+
if (players === void 0) continue;
|
|
749
|
+
const target = collections.find((o) => o.name === players);
|
|
750
|
+
if (!target || target.kind !== "entity") {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`${c.name}: ownership.position.players ${JSON.stringify(players)} does not name an entity collection in this schema`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
const position = c.ownership?.position;
|
|
756
|
+
if (!position) continue;
|
|
757
|
+
for (const axis of ["x", "y"]) {
|
|
758
|
+
const field = target.fields.find((f) => f.name === position[axis]);
|
|
759
|
+
if (!field || !["u8", "u16", "u32", "i32", "f32", "f64"].includes(field.type.kind)) {
|
|
760
|
+
throw new Error(
|
|
761
|
+
`${c.name}: ownership.position.${axis} must name a numeric field of BOTH ${c.name} and ${players} \u2014 the comparison is between an object and a player`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
594
766
|
const rpcMap = options.rpc ?? {};
|
|
595
767
|
for (const n of Object.keys(rpcMap)) {
|
|
596
768
|
assertFieldName(n);
|
|
597
769
|
if (RESERVED_RPC_NAMES.includes(n)) {
|
|
598
770
|
throw new Error(`RPC name ${JSON.stringify(n)} is reserved by irtio (built-in RPC)`);
|
|
599
771
|
}
|
|
772
|
+
if (n.startsWith(RESERVED_RPC_PREFIX)) {
|
|
773
|
+
throw new Error(
|
|
774
|
+
`RPC name ${JSON.stringify(n)} starts with ${JSON.stringify(RESERVED_RPC_PREFIX)}, which is reserved for irtio's built-in RPCs \u2014 pick another name`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
600
777
|
}
|
|
601
778
|
const rpcs = Object.keys(rpcMap).sort().map((name, index) => {
|
|
602
779
|
const r = rpcMap[name];
|
|
@@ -738,7 +915,17 @@ function canonicalize(s) {
|
|
|
738
915
|
} : void 0,
|
|
739
916
|
// Emitted only when declared: `stableStringify` drops `undefined`, so every schema written
|
|
740
917
|
// before D22 keeps the byte-identical canonical form — and therefore the hash — it had.
|
|
741
|
-
physics: c.physics ? canonicalPhysics(c.physics) : void 0
|
|
918
|
+
physics: c.physics ? canonicalPhysics(c.physics) : void 0,
|
|
919
|
+
// ---- M6 lane K: stateful relay ----
|
|
920
|
+
// D77: the same only-when-declared rule as `grid`, `physics` and `messages`, for the same
|
|
921
|
+
// reason — every schema written before ownership policies existed keeps the byte-identical
|
|
922
|
+
// canonical form, and therefore the hash, it had. They are in the hash when declared because
|
|
923
|
+
// they are server behaviour: a client and a room that disagree about who may take a crate
|
|
924
|
+
// disagree about the world.
|
|
925
|
+
perPlayer: c.perPlayer ? true : void 0,
|
|
926
|
+
clientCreate: c.clientCreate ? true : void 0,
|
|
927
|
+
ownership: c.ownership ? canonicalOwnership(c.ownership) : void 0
|
|
928
|
+
// ---- end M6 lane K ----
|
|
742
929
|
// Deliberately absent, and this is the list: `interpolate` (D20), `predicted` (D21) and
|
|
743
930
|
// `proxy` (D71) are all decisions about what one client does with a body it was sent, not
|
|
744
931
|
// about the shape of the world. A room and a client that disagree about any of them still
|
|
@@ -997,12 +1184,18 @@ function entityOptions(e, name) {
|
|
|
997
1184
|
});
|
|
998
1185
|
const physics = e.physics === void 0 || e.physics === null ? void 0 : physicsFromCanonical(`schemaFromCanonical: ${name}`, e.physics);
|
|
999
1186
|
const grid = e.grid === void 0 || e.grid === null ? void 0 : gridFromCanonical(name, objectAt(e, "grid"));
|
|
1187
|
+
const ownership = e.ownership === void 0 || e.ownership === null ? void 0 : ownershipFromCanonical(`schemaFromCanonical: ${name}`, e.ownership);
|
|
1000
1188
|
return {
|
|
1001
1189
|
serverOwned: e.serverOwned === true,
|
|
1002
1190
|
visibility,
|
|
1003
1191
|
...roles !== void 0 ? { roles } : {},
|
|
1004
1192
|
...grid !== void 0 ? { grid } : {},
|
|
1005
|
-
...physics !== void 0 ? { physics } : {}
|
|
1193
|
+
...physics !== void 0 ? { physics } : {},
|
|
1194
|
+
// ---- M6 lane K: stateful relay ----
|
|
1195
|
+
...e.perPlayer === true ? { perPlayer: true } : {},
|
|
1196
|
+
...e.clientCreate === true ? { clientCreate: true } : {},
|
|
1197
|
+
...ownership !== void 0 ? { ownership } : {}
|
|
1198
|
+
// ---- end M6 lane K ----
|
|
1006
1199
|
};
|
|
1007
1200
|
}
|
|
1008
1201
|
function gridFromCanonical(name, grid) {
|
|
@@ -2597,6 +2790,10 @@ function diffSchemas(oldSchema, newSchema) {
|
|
|
2597
2790
|
);
|
|
2598
2791
|
return changes;
|
|
2599
2792
|
}
|
|
2793
|
+
function breakingIsWireOnly(changes) {
|
|
2794
|
+
const breaking = changes.filter((c) => c.kind === "breaking");
|
|
2795
|
+
return breaking.length > 0 && breaking.every((c) => c.path === "rpc" || c.path.startsWith("rpc."));
|
|
2796
|
+
}
|
|
2600
2797
|
function diffCollections(oldCollections, newCollections, changes) {
|
|
2601
2798
|
const oldByName = new Map(oldCollections.map((c) => [c.name, c]));
|
|
2602
2799
|
const newByName = new Map(newCollections.map((c) => [c.name, c]));
|
|
@@ -3009,19 +3206,23 @@ export {
|
|
|
3009
3206
|
PHYSICS_BODY_CHANNELS,
|
|
3010
3207
|
RESERVED_COLLECTION_NAMES,
|
|
3011
3208
|
RESERVED_RPC_NAMES,
|
|
3209
|
+
RESERVED_RPC_PREFIX,
|
|
3012
3210
|
SERVER_OWNER,
|
|
3013
3211
|
SINGLETON_ID,
|
|
3014
3212
|
angleFrom2d,
|
|
3015
3213
|
applyChannel2d,
|
|
3016
3214
|
applyDelta,
|
|
3017
3215
|
bool,
|
|
3216
|
+
breakingIsWireOnly,
|
|
3018
3217
|
bytesEqual,
|
|
3218
|
+
canonicalOwnership,
|
|
3019
3219
|
canonicalPhysics,
|
|
3020
3220
|
canonicalType,
|
|
3021
3221
|
channelOf2d,
|
|
3022
3222
|
client,
|
|
3023
3223
|
cloneValue,
|
|
3024
3224
|
collectionDirty,
|
|
3225
|
+
compileOwnership,
|
|
3025
3226
|
compilePhysics,
|
|
3026
3227
|
computeDirty,
|
|
3027
3228
|
createDirtySet,
|
|
@@ -3059,6 +3260,8 @@ export {
|
|
|
3059
3260
|
mergeMask,
|
|
3060
3261
|
normalizeRecord,
|
|
3061
3262
|
normalizeValue,
|
|
3263
|
+
ownershipFromCanonical,
|
|
3264
|
+
parseHold,
|
|
3062
3265
|
physicsFromCanonical,
|
|
3063
3266
|
readValue,
|
|
3064
3267
|
ref,
|