@irtio/server 0.6.0 → 0.7.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 +153 -7
- package/dist/index.js +12 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnySchema, RoleOf, VisibleKeys, SchemaDefs, EntityDef, ReadonlyCollection, DeepReadonly, InferFields, Owned, SingletonDef, PhysicsKeys, InstanceOf, State, ServerCallProxy, SchemaRpc, BroadcastProxy, OwnableKeys, Implementations, ServerRpcs } from '@irtio/schema';
|
|
1
|
+
import { AnySchema, RoleOf, VisibleKeys, SchemaDefs, EntityDef, ReadonlyCollection, DeepReadonly, InferFields, Owned, SingletonDef, PhysicsKeys, InstanceOf, State, ServerMessageChannels, ServerCallProxy, SchemaRpc, BroadcastProxy, OwnableKeys, MessageNames, MessageValue, Implementations, ServerRpcs } from '@irtio/schema';
|
|
2
2
|
import RAPIER from '@dimforge/rapier3d-compat';
|
|
3
3
|
import * as MATTER from 'matter-js';
|
|
4
4
|
|
|
@@ -195,6 +195,7 @@ declare function choose<B extends Behaviour>(behaviours: readonly B[]): B | unde
|
|
|
195
195
|
type RapierModule = typeof RAPIER;
|
|
196
196
|
type RapierWorld = RAPIER.World;
|
|
197
197
|
type RapierRigidBody = RAPIER.RigidBody;
|
|
198
|
+
type RapierCollider = RAPIER.Collider;
|
|
198
199
|
type RapierRigidBodyDesc = RAPIER.RigidBodyDesc;
|
|
199
200
|
type RapierColliderDesc = RAPIER.ColliderDesc;
|
|
200
201
|
type MatterModule = typeof MATTER;
|
|
@@ -253,6 +254,50 @@ interface RapierPhysicsConfig<S extends AnySchema> {
|
|
|
253
254
|
*/
|
|
254
255
|
setup?(world: RapierWorld, rapier: RapierModule, room: Room<S>): void;
|
|
255
256
|
readonly bodies: BodyFactories<S>;
|
|
257
|
+
/** D72: how many ticks of body poses to keep for `room.rewind`. See {@link HISTORY_DOC}. */
|
|
258
|
+
readonly history?: number;
|
|
259
|
+
}
|
|
260
|
+
/** D72: the deepest history a room may declare, in ticks. One second at the maximum tick rate. */
|
|
261
|
+
declare const HISTORY_MAX_TICKS = 240;
|
|
262
|
+
/**
|
|
263
|
+
* D72: what `room.rewind(tick, fn)` hands `fn`.
|
|
264
|
+
*
|
|
265
|
+
* Exactly one of `rapier` and `matter` is present, decided by the room's engine.
|
|
266
|
+
*
|
|
267
|
+
* What a rewound query **can** see: every tracked body's pose at that tick, and the world's static
|
|
268
|
+
* geometry as it stands now. What it **cannot**: colliders as they were then (shapes are not
|
|
269
|
+
* historied, only poses), joints and contacts, bodies created since that tick, and anything the
|
|
270
|
+
* caller had not been told about yet.
|
|
271
|
+
*/
|
|
272
|
+
interface RewindView {
|
|
273
|
+
/** The tick actually answered from, after clamping. */
|
|
274
|
+
readonly tick: number;
|
|
275
|
+
/** The tick that was asked for, unclamped, so a room can log or refuse the difference. */
|
|
276
|
+
readonly requested: number;
|
|
277
|
+
/**
|
|
278
|
+
* `true` when `requested` fell outside the buffer and was pulled to its nearest edge. Not an
|
|
279
|
+
* error: the oldest pose the room still holds is the honest answer to "further back than I
|
|
280
|
+
* remember", and a room that would rather refuse a clamped answer can read this and do so.
|
|
281
|
+
*/
|
|
282
|
+
readonly clamped: boolean;
|
|
283
|
+
/** rapier3d rooms: a scratch world to `castRay` / `castShape` / `intersectionsWithShape` on. */
|
|
284
|
+
readonly rapier?: {
|
|
285
|
+
readonly world: RapierWorld;
|
|
286
|
+
/** Which entity a collider in the scratch world belongs to; `undefined` for static geometry. */
|
|
287
|
+
who(collider: RapierCollider): {
|
|
288
|
+
readonly collection: string;
|
|
289
|
+
readonly id: string;
|
|
290
|
+
} | undefined;
|
|
291
|
+
};
|
|
292
|
+
/** matter2d rooms: the body array `Matter.Query.ray/point/region/collides` takes. */
|
|
293
|
+
readonly matter?: {
|
|
294
|
+
readonly bodies: readonly MatterBody[];
|
|
295
|
+
/** Which entity a body in the array belongs to; `undefined` for the world's static bodies. */
|
|
296
|
+
who(body: MatterBody): {
|
|
297
|
+
readonly collection: string;
|
|
298
|
+
readonly id: string;
|
|
299
|
+
} | undefined;
|
|
300
|
+
};
|
|
256
301
|
}
|
|
257
302
|
/**
|
|
258
303
|
* D57: the matter-flavoured counterpart to `@irtio/client`'s `ClientIntent2dHook` — one step of
|
|
@@ -305,6 +350,8 @@ interface Matter2dPhysicsConfig<S extends AnySchema> {
|
|
|
305
350
|
* with no declared intents, which is the check the type cannot make.
|
|
306
351
|
*/
|
|
307
352
|
readonly intents?: Readonly<Partial<Record<PhysicsKeys<S> & string, Matter2dIntentHook>>>;
|
|
353
|
+
/** D72: how many ticks of body poses to keep for `room.rewind`. See {@link HISTORY_DOC}. */
|
|
354
|
+
readonly history?: number;
|
|
308
355
|
}
|
|
309
356
|
type PhysicsConfig<S extends AnySchema> = RapierPhysicsConfig<S> | Matter2dPhysicsConfig<S>;
|
|
310
357
|
/**
|
|
@@ -351,6 +398,17 @@ interface PhysicsRoomApi<S extends AnySchema> {
|
|
|
351
398
|
type MessageTarget = 'all' | string | {
|
|
352
399
|
readonly role: string;
|
|
353
400
|
};
|
|
401
|
+
/**
|
|
402
|
+
* D70: what `onMessage`'s sixth argument carries for a typed message, and nothing at all for a
|
|
403
|
+
* raw one. A discriminated union over the declared names, so `if (typed?.name === 'emote')`
|
|
404
|
+
* narrows `typed.value` to that shape.
|
|
405
|
+
*/
|
|
406
|
+
type TypedMessage<S> = {
|
|
407
|
+
[K in MessageNames<S>]: {
|
|
408
|
+
readonly name: K;
|
|
409
|
+
readonly value: MessageValue<S, K>;
|
|
410
|
+
};
|
|
411
|
+
}[MessageNames<S>];
|
|
354
412
|
interface ClientInfo {
|
|
355
413
|
readonly clientId: string;
|
|
356
414
|
readonly role: string;
|
|
@@ -372,6 +430,16 @@ interface Room<S extends AnySchema = AnySchema> {
|
|
|
372
430
|
/** Seeded, recorded for replay. */
|
|
373
431
|
random(): number;
|
|
374
432
|
send(target: MessageTarget, bytes: Uint8Array): void;
|
|
433
|
+
/**
|
|
434
|
+
* D70: `room.messages.<name>.send(target, value)` — one of the shapes the schema declares,
|
|
435
|
+
* encoded for you. `{}` on a schema that declares none, so game code can be written before the
|
|
436
|
+
* schema has anything to say.
|
|
437
|
+
*
|
|
438
|
+
* There is no `on` here: a room observes messages through `onMessage`, which is also where it
|
|
439
|
+
* can drop one. A second way in would be a second place to see a message the room had already
|
|
440
|
+
* declined.
|
|
441
|
+
*/
|
|
442
|
+
readonly messages: ServerMessageChannels<S, MessageTarget>;
|
|
375
443
|
setRole(clientId: string, role: RoleOf<S> & string): void;
|
|
376
444
|
kick(clientId: string, reason?: string): void;
|
|
377
445
|
close(reason?: string): void;
|
|
@@ -435,6 +503,38 @@ interface Room<S extends AnySchema = AnySchema> {
|
|
|
435
503
|
* reading `room.physics` here.
|
|
436
504
|
*/
|
|
437
505
|
readonly physics2d: Matter2dRoomApi<S>;
|
|
506
|
+
/**
|
|
507
|
+
* D72: run `fn` against the world as it stood at `tick`.
|
|
508
|
+
*
|
|
509
|
+
* A shot fired at 200 ms of latency was aimed at where the target was drawn on the shooter's
|
|
510
|
+
* screen, a round trip ago. `ctx.clientTick` says which tick that was; this puts every tracked
|
|
511
|
+
* body back where it was then, on a scratch world, and lets the room's own query answer there:
|
|
512
|
+
*
|
|
513
|
+
* ```ts
|
|
514
|
+
* rpc: {
|
|
515
|
+
* fire(state, { dx, dy, dz }, ctx) {
|
|
516
|
+
* const { rapier, world } = ctx.room.physics;
|
|
517
|
+
* const from = ctx.room.physics.body('players', ctx.clientId)?.translation();
|
|
518
|
+
* if (!from) return { hit: false };
|
|
519
|
+
* return ctx.room.rewind(ctx.clientTick ?? ctx.tick, (past) => {
|
|
520
|
+
* const hit = past.rapier?.world.castRay(new rapier.Ray(from, { x: dx, y: dy, z: dz }), 100, true);
|
|
521
|
+
* return { hit: hit ? past.rapier?.who(hit.collider) !== undefined : false };
|
|
522
|
+
* });
|
|
523
|
+
* },
|
|
524
|
+
* }
|
|
525
|
+
* ```
|
|
526
|
+
*
|
|
527
|
+
* `fn` runs synchronously and whatever it returns is returned. The live world is never touched:
|
|
528
|
+
* this is stored poses on a second world, not a re-simulation, and nothing a player can see is
|
|
529
|
+
* stepped. A tick outside the buffer is clamped to its nearest edge and `past.clamped` says so,
|
|
530
|
+
* which is a fallback rather than an error.
|
|
531
|
+
*
|
|
532
|
+
* Throws when the room declares no `physics.history`, when the buffer is still empty (a room
|
|
533
|
+
* that has just woken), and when called from inside another `rewind`.
|
|
534
|
+
*
|
|
535
|
+
* @see irt.io/docs/concepts/lag-compensation
|
|
536
|
+
*/
|
|
537
|
+
rewind<T>(tick: number, fn: (past: RewindView) => T): T;
|
|
438
538
|
/**
|
|
439
539
|
* D44: spawn a scripted NPC. `config.brain.script` names an entry in the room definition's
|
|
440
540
|
* `npcs` map; the session it opens is an ordinary client session — it appears in
|
|
@@ -703,6 +803,19 @@ interface RoomRatings {
|
|
|
703
803
|
readonly deviation?: number;
|
|
704
804
|
}): Promise<void>;
|
|
705
805
|
}
|
|
806
|
+
/** D69-c: what a submit may say about the board beyond the score. One field, and it is optional. */
|
|
807
|
+
interface LeaderboardSubmitOptions {
|
|
808
|
+
/**
|
|
809
|
+
* Which cohort of a bucketed board this score belongs to: a region, a platform, a league —
|
|
810
|
+
* whatever your game means by it. 1 to 64 characters of `a-z 0-9 . _ -`.
|
|
811
|
+
*
|
|
812
|
+
* Required on a board configured with `buckets: true`, and refused on a board that is not
|
|
813
|
+
* (`E_LB_BUCKET_REQUIRED` / `E_LB_NO_BUCKETS`). Neither is guessed at: a bucket that was
|
|
814
|
+
* silently dropped would put a cohort's scores on the wrong ranking, and a read that silently
|
|
815
|
+
* merged every cohort would be a ranking nobody asked for.
|
|
816
|
+
*/
|
|
817
|
+
readonly bucket?: string;
|
|
818
|
+
}
|
|
706
819
|
interface RoomLeaderboard {
|
|
707
820
|
/**
|
|
708
821
|
* Post `score` for `playerId` on `board`.
|
|
@@ -710,10 +823,15 @@ interface RoomLeaderboard {
|
|
|
710
823
|
* `playerId` must be a player currently in this room — `ctx.playerId` is the id to pass, and
|
|
711
824
|
* anything else rejects with `E_LB_NOT_IN_ROOM`. Board names are 1-64 of `a-z 0-9 . _ -`.
|
|
712
825
|
* Rejections name what they hit: `E_LB_BAD_BOARD`, `E_LB_BAD_SCORE`, `E_LB_NOT_IN_ROOM`,
|
|
713
|
-
* `E_LB_PROJECT_FULL`, `E_LB_UNAVAILABLE
|
|
714
|
-
* continuation runs as its own
|
|
826
|
+
* `E_LB_PROJECT_FULL`, `E_LB_UNAVAILABLE`, `E_LB_BAD_BUCKET`, `E_LB_BUCKET_REQUIRED`,
|
|
827
|
+
* `E_LB_NO_BUCKETS`. Like every promise-returning room API, the continuation runs as its own
|
|
828
|
+
* event between ticks.
|
|
829
|
+
*
|
|
830
|
+
* D69: there is no period argument and there will not be one. A rotating board's current period
|
|
831
|
+
* is computed on the control plane from its own clock at the moment the score lands, so a room
|
|
832
|
+
* needs no change to start rotating and cannot write into a period that has closed.
|
|
715
833
|
*/
|
|
716
|
-
submit(board: string, playerId: string, score: number): Promise<void>;
|
|
834
|
+
submit(board: string, playerId: string, score: number, options?: LeaderboardSubmitOptions): Promise<void>;
|
|
717
835
|
}
|
|
718
836
|
interface Ctx<S extends AnySchema = AnySchema> {
|
|
719
837
|
readonly clientId: string;
|
|
@@ -730,6 +848,23 @@ interface Ctx<S extends AnySchema = AnySchema> {
|
|
|
730
848
|
readonly name: string;
|
|
731
849
|
/** Server tick this join/call/write is applied at. */
|
|
732
850
|
readonly tick: number;
|
|
851
|
+
/**
|
|
852
|
+
* D72: RPCs only. The newest authoritative tick the caller had applied when it sent the `CALL`,
|
|
853
|
+
* which is the tick whose world it was looking at. `undefined` for a join, a write, or a `CALL`
|
|
854
|
+
* from a client that sends no stamp.
|
|
855
|
+
*
|
|
856
|
+
* It is what `room.rewind` is meant to be given:
|
|
857
|
+
*
|
|
858
|
+
* ```ts
|
|
859
|
+
* const hit = ctx.room.rewind(ctx.clientTick ?? ctx.tick, (past) => …);
|
|
860
|
+
* ```
|
|
861
|
+
*
|
|
862
|
+
* **It is a number a client chose.** The history depth bounds how far into the past a lie can
|
|
863
|
+
* reach and `rewind` clamps anything outside the buffer, but a room that wants to distrust it
|
|
864
|
+
* compares it with `ctx.tick` and refuses a gap it does not like. See
|
|
865
|
+
* irt.io/docs/concepts/lag-compensation.
|
|
866
|
+
*/
|
|
867
|
+
readonly clientTick: number | undefined;
|
|
733
868
|
/** Joins only: a resumed session. */
|
|
734
869
|
readonly reconnecting: boolean;
|
|
735
870
|
readonly room: Room<S>;
|
|
@@ -878,8 +1013,19 @@ interface RoomConfigBase<S extends AnySchema> {
|
|
|
878
1013
|
readonly validate?: Validators<S>;
|
|
879
1014
|
/** Implements the built-in `requestOwnership` RPC. Default: grant if unowned. */
|
|
880
1015
|
onOwnershipRequest?(state: State<S>, entity: OwnableKeys<S> & string, id: string, ctx: Ctx<S>): boolean;
|
|
881
|
-
/**
|
|
882
|
-
|
|
1016
|
+
/**
|
|
1017
|
+
* Peer messages, raw and typed alike; return `false` to drop.
|
|
1018
|
+
*
|
|
1019
|
+
* `bytes` is what the peer sent: opaque for `room.message(target, bytes)`, and the encoded
|
|
1020
|
+
* payload for a typed message. D70 adds the sixth argument: present, with the declared name and
|
|
1021
|
+
* the decoded value, exactly when the message was typed. The decode happens **before** this
|
|
1022
|
+
* handler runs, so a malformed payload never arrives here at all — it is counted and dropped —
|
|
1023
|
+
* and `typed.value` is always a well-formed value of a shape this schema declares.
|
|
1024
|
+
*
|
|
1025
|
+
* Returning `false` drops a typed message exactly as it drops a raw one. That is the room's
|
|
1026
|
+
* veto, and it is the only one it needs.
|
|
1027
|
+
*/
|
|
1028
|
+
onMessage?(state: State<S>, from: string, target: MessageTarget, bytes: Uint8Array, ctx: Ctx<S>, typed?: TypedMessage<S>): boolean | undefined | void;
|
|
883
1029
|
}
|
|
884
1030
|
type RoomConfig<S extends AnySchema> = RoomConfigBase<S> & RpcConfig<S>;
|
|
885
1031
|
/** Config with defaults filled and `rpc` always present. */
|
|
@@ -961,4 +1107,4 @@ declare function defineRoom<S extends AnySchema>(schema: S, config: RoomConfig<S
|
|
|
961
1107
|
/** Type guard for what a bundle's default export should be. */
|
|
962
1108
|
declare function isRoomDefinition(v: unknown): v is RoomDefinition;
|
|
963
1109
|
|
|
964
|
-
export { type Behaviour, type BodyFactories, type BodySpec, type BusConfig, type BusEvent, type BusMessage, type ClientInfo, type Ctx, DEFAULTS, type LeaveReason, MAX_AWAKE_MAX, MEMORY_MB_MAX, MEMORY_MB_MIN, type Matter2dBodyFactories, type Matter2dBodySpec, type Matter2dIntentHook, type Matter2dPhysicsConfig, type Matter2dRoomApi, type MatterBody, type MatterConstraint, type MatterEngine, type MatterModule, type MessageTarget, type Npc, type NpcCollection, type NpcConfig, type NpcHandle, type NpcRoom, type NpcScript, type NpcScriptBrain, type NpcState, type PatrolStep, type PhysicsConfig, type PhysicsRoomApi, type PlayerKv, RETENTION_MAX_MS, RETENTION_MIN_MS, RETENTION_RE, ROOM_DEFINITION_VERSION, type RapierColliderDesc, type RapierModule, type RapierPhysicsConfig, type RapierRigidBody, type RapierRigidBodyDesc, type RapierWorld, type ResolvedRoomConfig, type Room, type RoomBackfill, type RoomBus, type RoomConfig, type RoomConfigBase, type RoomDefinition, type RoomLeaderboard, type RoomMode, type RoomRatingResult, type RoomRatings, type RpcImplementations, type TimerHandle, type Validators, type Vec2, type Vector2, type Vector3, type WanderState, arrive, choose, defineRoom, flee, isRoomDefinition, newWander, patrol, seek, wander };
|
|
1110
|
+
export { type Behaviour, type BodyFactories, type BodySpec, type BusConfig, type BusEvent, type BusMessage, type ClientInfo, type Ctx, DEFAULTS, HISTORY_MAX_TICKS, type LeaderboardSubmitOptions, type LeaveReason, MAX_AWAKE_MAX, MEMORY_MB_MAX, MEMORY_MB_MIN, type Matter2dBodyFactories, type Matter2dBodySpec, type Matter2dIntentHook, type Matter2dPhysicsConfig, type Matter2dRoomApi, type MatterBody, type MatterConstraint, type MatterEngine, type MatterModule, type MessageTarget, type Npc, type NpcCollection, type NpcConfig, type NpcHandle, type NpcRoom, type NpcScript, type NpcScriptBrain, type NpcState, type PatrolStep, type PhysicsConfig, type PhysicsRoomApi, type PlayerKv, RETENTION_MAX_MS, RETENTION_MIN_MS, RETENTION_RE, ROOM_DEFINITION_VERSION, type RapierCollider, type RapierColliderDesc, type RapierModule, type RapierPhysicsConfig, type RapierRigidBody, type RapierRigidBodyDesc, type RapierWorld, type ResolvedRoomConfig, type RewindView, type Room, type RoomBackfill, type RoomBus, type RoomConfig, type RoomConfigBase, type RoomDefinition, type RoomLeaderboard, type RoomMode, type RoomRatingResult, type RoomRatings, type RpcImplementations, type TimerHandle, type TypedMessage, type Validators, type Vec2, type Vector2, type Vector3, type WanderState, arrive, choose, defineRoom, flee, isRoomDefinition, newWander, patrol, seek, wander };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/physics.ts
|
|
2
|
+
var HISTORY_MAX_TICKS = 240;
|
|
3
|
+
|
|
1
4
|
// src/npc.ts
|
|
2
5
|
var ZERO = { x: 0, y: 0 };
|
|
3
6
|
function scaleTo(dx, dy, speed) {
|
|
@@ -221,6 +224,14 @@ function checkPhysics(schema, config, mode) {
|
|
|
221
224
|
`defineRoom: physics.timestep must be a positive number of seconds, got ${String(physics.timestep)}`
|
|
222
225
|
);
|
|
223
226
|
}
|
|
227
|
+
const history = physics.history;
|
|
228
|
+
if (history !== void 0) {
|
|
229
|
+
if (!Number.isInteger(history) || history < 0 || history > HISTORY_MAX_TICKS) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`defineRoom: physics.history must be an integer in 0..${HISTORY_MAX_TICKS} ticks (0 or absent means no history and no cost), got ${String(history)}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
224
235
|
assertSyncHandler(physics.setup, "physics.setup");
|
|
225
236
|
const bodies = physics.bodies ?? {};
|
|
226
237
|
for (const [k, fn] of Object.entries(bodies)) assertSyncHandler(fn, `physics.bodies.${k}`);
|
|
@@ -265,6 +276,7 @@ function isRoomDefinition(v) {
|
|
|
265
276
|
}
|
|
266
277
|
export {
|
|
267
278
|
DEFAULTS,
|
|
279
|
+
HISTORY_MAX_TICKS,
|
|
268
280
|
MAX_AWAKE_MAX,
|
|
269
281
|
MEMORY_MB_MAX,
|
|
270
282
|
MEMORY_MB_MIN,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "irtio room-file API: defineRoom, handler and ctx types",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@irtio/schema": "0.
|
|
23
|
+
"@irtio/schema": "0.7.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"@dimforge/rapier3d-compat": ">=0.20.0",
|