@replicast/core 0.1.2 → 1.0.0-rc.10
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/decorators/entity.d.ts +43 -0
- package/dist/decorators/entity.d.ts.map +1 -0
- package/dist/decorators/entity.js +75 -0
- package/dist/decorators/entity.js.map +1 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/metadata.d.ts +9 -6
- package/dist/decorators/metadata.d.ts.map +1 -1
- package/dist/decorators/metadata.js +20 -13
- package/dist/decorators/metadata.js.map +1 -1
- package/dist/decorators/store.d.ts +5 -8
- package/dist/decorators/store.d.ts.map +1 -1
- package/dist/decorators/store.js +10 -11
- package/dist/decorators/store.js.map +1 -1
- package/dist/decorators/system.d.ts +7 -10
- package/dist/decorators/system.d.ts.map +1 -1
- package/dist/decorators/system.js +15 -23
- package/dist/decorators/system.js.map +1 -1
- package/dist/entity/base-entity.d.ts +19 -0
- package/dist/entity/base-entity.d.ts.map +1 -0
- package/dist/entity/base-entity.js +18 -0
- package/dist/entity/base-entity.js.map +1 -0
- package/dist/entity/fields.d.ts +21 -0
- package/dist/entity/fields.d.ts.map +1 -0
- package/dist/entity/fields.js +67 -0
- package/dist/entity/fields.js.map +1 -0
- package/dist/entity/registry.d.ts +53 -0
- package/dist/entity/registry.d.ts.map +1 -0
- package/dist/entity/registry.js +167 -0
- package/dist/entity/registry.js.map +1 -0
- package/dist/entity/replicator.d.ts +40 -0
- package/dist/entity/replicator.d.ts.map +1 -0
- package/dist/entity/replicator.js +82 -0
- package/dist/entity/replicator.js.map +1 -0
- package/dist/entity/types.d.ts +29 -0
- package/dist/entity/types.d.ts.map +1 -0
- package/dist/entity/types.js +4 -0
- package/dist/entity/types.js.map +1 -0
- package/dist/entity/visibility.d.ts +11 -0
- package/dist/entity/visibility.d.ts.map +1 -0
- package/dist/entity/visibility.js +10 -0
- package/dist/entity/visibility.js.map +1 -0
- package/dist/entity/world.d.ts +67 -0
- package/dist/entity/world.d.ts.map +1 -0
- package/dist/entity/world.js +183 -0
- package/dist/entity/world.js.map +1 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/types.d.ts +2 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/store/base-store.d.ts +24 -4
- package/dist/store/base-store.d.ts.map +1 -1
- package/dist/store/base-store.js +47 -20
- package/dist/store/base-store.js.map +1 -1
- package/dist/transport/app.d.ts +53 -0
- package/dist/transport/app.d.ts.map +1 -0
- package/dist/transport/app.js +53 -0
- package/dist/transport/app.js.map +1 -0
- package/dist/transport/client.d.ts +14 -1
- package/dist/transport/client.d.ts.map +1 -1
- package/dist/transport/client.js +58 -13
- package/dist/transport/client.js.map +1 -1
- package/dist/transport/codec.d.ts +7 -0
- package/dist/transport/codec.d.ts.map +1 -0
- package/dist/transport/codec.js +14 -0
- package/dist/transport/codec.js.map +1 -0
- package/dist/transport/context-browser.d.ts +6 -0
- package/dist/transport/context-browser.d.ts.map +1 -0
- package/dist/transport/context-browser.js +11 -0
- package/dist/transport/context-browser.js.map +1 -0
- package/dist/transport/context.d.ts +5 -0
- package/dist/transport/context.d.ts.map +1 -0
- package/dist/transport/context.js +9 -0
- package/dist/transport/context.js.map +1 -0
- package/dist/transport/protocol.d.ts +19 -1
- package/dist/transport/protocol.d.ts.map +1 -1
- package/dist/transport/protocol.js.map +1 -1
- package/dist/transport/server.d.ts +24 -1
- package/dist/transport/server.d.ts.map +1 -1
- package/dist/transport/server.js +188 -25
- package/dist/transport/server.js.map +1 -1
- package/dist/transport/ws-browser.d.ts +1 -1
- package/dist/transport/ws-browser.d.ts.map +1 -1
- package/dist/transport/ws-browser.js +1 -0
- package/dist/transport/ws-browser.js.map +1 -1
- package/package.json +10 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/entity/registry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEzE,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CACnF;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAA;IAClC,gGAAgG;IAChG,WAAW,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAChC,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB;AAQD,KAAK,QAAQ,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAA;AAE9D;;;;GAIG;AACH,qBAAa,oBAAoB;IAanB,OAAO,CAAC,OAAO;IAZ3B,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,gBAAgB,CAAqC;IAC7D,OAAO,CAAC,WAAW,CAAwB;IAC3C,OAAO,CAAC,WAAW,CAA2D;IAC9E;wGACoG;IACpG,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,OAAO,CAAI;gBAEC,OAAO,EAAE,2BAA2B;IAqBxD,GAAG,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS;IAIrC,GAAG,IAAI,MAAM,EAAE;IAIf,iGAAiG;IACjG,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE;IASzD,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,eAAe;IAIvB,+FAA+F;IAC/F,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAIxF,sFAAsF;IACtF,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAI1F,gGAAgG;IAChG,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAK3C,oEAAoE;IACpE,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAsCjC,yFAAyF;IACzF,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAO7D,0FAA0F;IAC1F,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,MAAM;CASf"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { getEntityHooks, getEntityName } from '../decorators/entity.js';
|
|
2
|
+
import { isEmptySync } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The client's mirror of the entities it can currently see. Creates the same class on `enter`,
|
|
5
|
+
* applies `update` patches in place, and calls `@ClientSpawn` / `@ClientDespawn` as entities join
|
|
6
|
+
* and leave this viewer's set.
|
|
7
|
+
*/
|
|
8
|
+
export class ClientEntityRegistry {
|
|
9
|
+
options;
|
|
10
|
+
entities = new Map();
|
|
11
|
+
infoByName = new Map();
|
|
12
|
+
infoByCtor = new Map();
|
|
13
|
+
spawnListeners = new Map();
|
|
14
|
+
despawnListeners = new Map();
|
|
15
|
+
subscribers = new Set();
|
|
16
|
+
byTypeCache = new Map();
|
|
17
|
+
/** Per-type version, bumped only for types actually touched by a sync — keeps an update to one
|
|
18
|
+
* type from invalidating every other type's `byType()` cache (and its `useEntities()` callers). */
|
|
19
|
+
typeVersions = new Map();
|
|
20
|
+
version = 0;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
for (const Type of options.types) {
|
|
24
|
+
const proto = Type.prototype;
|
|
25
|
+
const hooks = getEntityHooks(proto);
|
|
26
|
+
const name = getEntityName(Type);
|
|
27
|
+
const existing = this.infoByName.get(name);
|
|
28
|
+
if (existing && existing.Type !== Type) {
|
|
29
|
+
throw new Error(`[replicast] two @Entity classes are both named "${name}" — give one an explicit @Entity({ name }) option`);
|
|
30
|
+
}
|
|
31
|
+
const info = {
|
|
32
|
+
Type,
|
|
33
|
+
spawnHooks: Object.keys(hooks).filter(m => hooks[m] === 'clientSpawn'),
|
|
34
|
+
despawnHooks: Object.keys(hooks).filter(m => hooks[m] === 'clientDespawn'),
|
|
35
|
+
};
|
|
36
|
+
this.infoByName.set(name, info);
|
|
37
|
+
this.infoByCtor.set(Type, info);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
get(id) {
|
|
41
|
+
return this.entities.get(id);
|
|
42
|
+
}
|
|
43
|
+
all() {
|
|
44
|
+
return [...this.entities.values()];
|
|
45
|
+
}
|
|
46
|
+
/** Entities of one type. The array is referentially stable until that type's entities change. */
|
|
47
|
+
byType(Type) {
|
|
48
|
+
const version = this.typeVersions.get(Type) ?? 0;
|
|
49
|
+
const cached = this.byTypeCache.get(Type);
|
|
50
|
+
if (cached?.version === version)
|
|
51
|
+
return cached.list;
|
|
52
|
+
const list = this.all().filter(e => e instanceof Type);
|
|
53
|
+
this.byTypeCache.set(Type, { version, list });
|
|
54
|
+
return list;
|
|
55
|
+
}
|
|
56
|
+
touchType(entity) {
|
|
57
|
+
const Type = Object.getPrototypeOf(entity).constructor;
|
|
58
|
+
this.typeVersions.set(Type, (this.typeVersions.get(Type) ?? 0) + 1);
|
|
59
|
+
}
|
|
60
|
+
hasAuthorityFor(entity) {
|
|
61
|
+
return entity['ownerId'] != null && entity['ownerId'] === this.options.getPlayerId();
|
|
62
|
+
}
|
|
63
|
+
/** Called when an entity of `Type` enters this viewer's set, after its fields are assigned. */
|
|
64
|
+
onSpawn(Type, listener) {
|
|
65
|
+
return addListener(this.spawnListeners, Type, listener);
|
|
66
|
+
}
|
|
67
|
+
/** Called when an entity of `Type` leaves this viewer's set, before it is dropped. */
|
|
68
|
+
onDespawn(Type, listener) {
|
|
69
|
+
return addListener(this.despawnListeners, Type, listener);
|
|
70
|
+
}
|
|
71
|
+
/** Fires once per applied sync, after the whole batch is in place. For game loops and React. */
|
|
72
|
+
subscribe(listener) {
|
|
73
|
+
this.subscribers.add(listener);
|
|
74
|
+
return () => { this.subscribers.delete(listener); };
|
|
75
|
+
}
|
|
76
|
+
/** Applies one `{ enter, update, leave }` delta from the server. */
|
|
77
|
+
applySync(sync) {
|
|
78
|
+
if (isEmptySync(sync))
|
|
79
|
+
return;
|
|
80
|
+
for (const { type, id, fields } of sync.enter) {
|
|
81
|
+
const info = this.infoByName.get(type);
|
|
82
|
+
if (!info) {
|
|
83
|
+
console.warn(`[replicast] received an unknown entity type "${type}" — register it in createApp({ entities })`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const entity = new info.Type();
|
|
87
|
+
entity['id'] = id;
|
|
88
|
+
entity['isServer'] = false;
|
|
89
|
+
if (this.options.client)
|
|
90
|
+
entity['__wsClient'] = this.options.client;
|
|
91
|
+
Object.assign(entity, fields);
|
|
92
|
+
entity['hasAuthority'] = this.hasAuthorityFor(entity);
|
|
93
|
+
this.entities.set(id, entity);
|
|
94
|
+
this.touchType(entity);
|
|
95
|
+
callHooks(entity, info.spawnHooks);
|
|
96
|
+
notify(this.spawnListeners, entity);
|
|
97
|
+
}
|
|
98
|
+
for (const { id, fields } of sync.update) {
|
|
99
|
+
const entity = this.entities.get(id);
|
|
100
|
+
if (entity) {
|
|
101
|
+
Object.assign(entity, fields);
|
|
102
|
+
// ownerId is a replicated built-in and can change on an already-mirrored entity
|
|
103
|
+
// (e.g. a ship changes hands) — hasAuthority must track it, not just the initial enter.
|
|
104
|
+
entity['hasAuthority'] = this.hasAuthorityFor(entity);
|
|
105
|
+
this.touchType(entity);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const id of sync.leave)
|
|
109
|
+
this.remove(id);
|
|
110
|
+
this.version++;
|
|
111
|
+
for (const listener of this.subscribers)
|
|
112
|
+
listener();
|
|
113
|
+
}
|
|
114
|
+
/** Dispatches a server-pushed `@Client` / `@Multicast` call onto the mirrored entity. */
|
|
115
|
+
applyRpc(id, method, args) {
|
|
116
|
+
const entity = this.entities.get(id);
|
|
117
|
+
const methodName = method.split('.')[1];
|
|
118
|
+
if (!entity || !methodName || typeof entity[methodName] !== 'function')
|
|
119
|
+
return;
|
|
120
|
+
entity[methodName].apply(entity, args);
|
|
121
|
+
}
|
|
122
|
+
/** Despawns every mirror — used when the connection drops so a reconnect starts clean. */
|
|
123
|
+
clear() {
|
|
124
|
+
if (this.entities.size === 0)
|
|
125
|
+
return;
|
|
126
|
+
for (const id of [...this.entities.keys()])
|
|
127
|
+
this.remove(id);
|
|
128
|
+
this.version++;
|
|
129
|
+
for (const listener of this.subscribers)
|
|
130
|
+
listener();
|
|
131
|
+
}
|
|
132
|
+
remove(id) {
|
|
133
|
+
const entity = this.entities.get(id);
|
|
134
|
+
if (!entity)
|
|
135
|
+
return;
|
|
136
|
+
const info = this.infoByCtor.get(Object.getPrototypeOf(entity).constructor);
|
|
137
|
+
callHooks(entity, info?.despawnHooks ?? []);
|
|
138
|
+
this.entities.delete(id);
|
|
139
|
+
this.touchType(entity);
|
|
140
|
+
notify(this.despawnListeners, entity);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function addListener(map, Type, listener) {
|
|
144
|
+
let set = map.get(Type);
|
|
145
|
+
if (!set) {
|
|
146
|
+
set = new Set();
|
|
147
|
+
map.set(Type, set);
|
|
148
|
+
}
|
|
149
|
+
set.add(listener);
|
|
150
|
+
return () => { set.delete(listener); };
|
|
151
|
+
}
|
|
152
|
+
function notify(map, entity) {
|
|
153
|
+
for (const [Type, listeners] of map) {
|
|
154
|
+
if (!(entity instanceof Type))
|
|
155
|
+
continue;
|
|
156
|
+
for (const listener of listeners)
|
|
157
|
+
listener(entity);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function callHooks(entity, methods) {
|
|
161
|
+
for (const method of methods) {
|
|
162
|
+
const fn = entity[method];
|
|
163
|
+
if (typeof fn === 'function')
|
|
164
|
+
fn.call(entity);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/entity/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FieldPlan } from './fields.js';
|
|
2
|
+
import type { VisibleTo } from './visibility.js';
|
|
3
|
+
import type { EntityId, EntitySync, EntityWorldView } from './types.js';
|
|
4
|
+
/** One live entity as the replicator needs to see it: identity, wire type, field plan, scoping. */
|
|
5
|
+
export interface ReplicationEntry {
|
|
6
|
+
id: EntityId;
|
|
7
|
+
entity: object;
|
|
8
|
+
type: string;
|
|
9
|
+
plan: FieldPlan;
|
|
10
|
+
visibleTo?: VisibleTo;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Tracks, per client, which entities that client has been sent and the fields it last saw, and turns
|
|
14
|
+
* the current world into that client's `{ enter, update, leave }` delta.
|
|
15
|
+
*
|
|
16
|
+
* Cost is O(clients x entities) per flush — deliberate for now; a spatial index hook comes later.
|
|
17
|
+
*/
|
|
18
|
+
export declare class EntityReplicator {
|
|
19
|
+
/** playerId → entityId → last-sent field snapshot. */
|
|
20
|
+
private views;
|
|
21
|
+
/** True when `playerId` currently has `id` in their set (i.e. as of the last flush). */
|
|
22
|
+
hasInView(playerId: string, id: EntityId): boolean;
|
|
23
|
+
/** The clients that currently see `id` — the audience for a `@Multicast`. */
|
|
24
|
+
viewersOf(id: EntityId): string[];
|
|
25
|
+
/** Forget a disconnected client so they re-enter from scratch on reconnect. */
|
|
26
|
+
dropViewer(playerId: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Records `id` as already sent to `playerId` with `fields`, without waiting for the next flush —
|
|
29
|
+
* for a caller that must guarantee delivery order (an `@Client`/`@Multicast` send that needs the
|
|
30
|
+
* recipient's mirror to exist before the rpc arrives). The next real `flush()` then sees this id
|
|
31
|
+
* already in `view` and sends only a diff, never a duplicate `enter`.
|
|
32
|
+
*/
|
|
33
|
+
recordEnter(playerId: string, id: EntityId, fields: Record<string, unknown>): void;
|
|
34
|
+
/**
|
|
35
|
+
* Builds one client's delta and records it as sent. An entity is visible when the client owns it
|
|
36
|
+
* (owners always see their own entities) or the type's `visibleTo` hook allows it.
|
|
37
|
+
*/
|
|
38
|
+
computeSync(playerId: string, entries: Iterable<ReplicationEntry>, world: EntityWorldView): EntitySync;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=replicator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replicator.d.ts","sourceRoot":"","sources":["../../src/entity/replicator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEvE,mGAAmG;AACnG,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,QAAQ,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,SAAS,CAAA;IACf,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AAED;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,sDAAsD;IACtD,OAAO,CAAC,KAAK,CAA4D;IAEzE,wFAAwF;IACxF,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,OAAO;IAIlD,6EAA6E;IAC7E,SAAS,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,EAAE;IAQjC,+EAA+E;IAC/E,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIlC;;;;;OAKG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAMlF;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,KAAK,EAAE,eAAe,GAAG,UAAU;CAmCvG"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { snapshotFields, diffFields, ownerIdField } from './fields.js';
|
|
2
|
+
import { isEntityVisible } from './visibility.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tracks, per client, which entities that client has been sent and the fields it last saw, and turns
|
|
5
|
+
* the current world into that client's `{ enter, update, leave }` delta.
|
|
6
|
+
*
|
|
7
|
+
* Cost is O(clients x entities) per flush — deliberate for now; a spatial index hook comes later.
|
|
8
|
+
*/
|
|
9
|
+
export class EntityReplicator {
|
|
10
|
+
/** playerId → entityId → last-sent field snapshot. */
|
|
11
|
+
views = new Map();
|
|
12
|
+
/** True when `playerId` currently has `id` in their set (i.e. as of the last flush). */
|
|
13
|
+
hasInView(playerId, id) {
|
|
14
|
+
return this.views.get(playerId)?.has(id) ?? false;
|
|
15
|
+
}
|
|
16
|
+
/** The clients that currently see `id` — the audience for a `@Multicast`. */
|
|
17
|
+
viewersOf(id) {
|
|
18
|
+
const viewers = [];
|
|
19
|
+
for (const [playerId, view] of this.views) {
|
|
20
|
+
if (view.has(id))
|
|
21
|
+
viewers.push(playerId);
|
|
22
|
+
}
|
|
23
|
+
return viewers;
|
|
24
|
+
}
|
|
25
|
+
/** Forget a disconnected client so they re-enter from scratch on reconnect. */
|
|
26
|
+
dropViewer(playerId) {
|
|
27
|
+
this.views.delete(playerId);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Records `id` as already sent to `playerId` with `fields`, without waiting for the next flush —
|
|
31
|
+
* for a caller that must guarantee delivery order (an `@Client`/`@Multicast` send that needs the
|
|
32
|
+
* recipient's mirror to exist before the rpc arrives). The next real `flush()` then sees this id
|
|
33
|
+
* already in `view` and sends only a diff, never a duplicate `enter`.
|
|
34
|
+
*/
|
|
35
|
+
recordEnter(playerId, id, fields) {
|
|
36
|
+
let view = this.views.get(playerId);
|
|
37
|
+
if (!view) {
|
|
38
|
+
view = new Map();
|
|
39
|
+
this.views.set(playerId, view);
|
|
40
|
+
}
|
|
41
|
+
view.set(id, fields);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds one client's delta and records it as sent. An entity is visible when the client owns it
|
|
45
|
+
* (owners always see their own entities) or the type's `visibleTo` hook allows it.
|
|
46
|
+
*/
|
|
47
|
+
computeSync(playerId, entries, world) {
|
|
48
|
+
let view = this.views.get(playerId);
|
|
49
|
+
if (!view) {
|
|
50
|
+
view = new Map();
|
|
51
|
+
this.views.set(playerId, view);
|
|
52
|
+
}
|
|
53
|
+
const sync = { enter: [], update: [], leave: [] };
|
|
54
|
+
const stillVisible = new Set();
|
|
55
|
+
for (const { id, entity, type, plan, visibleTo } of entries) {
|
|
56
|
+
const isOwner = entity[ownerIdField] === playerId;
|
|
57
|
+
if (!isEntityVisible(isOwner, playerId, entity, visibleTo, world))
|
|
58
|
+
continue;
|
|
59
|
+
stillVisible.add(id);
|
|
60
|
+
const fields = snapshotFields(entity, plan, isOwner);
|
|
61
|
+
const previous = view.get(id);
|
|
62
|
+
if (previous === undefined) {
|
|
63
|
+
sync.enter.push({ type, id, fields });
|
|
64
|
+
view.set(id, fields);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const patch = diffFields(previous, fields);
|
|
68
|
+
if (Object.keys(patch).length > 0) {
|
|
69
|
+
sync.update.push({ id, fields: patch });
|
|
70
|
+
view.set(id, fields);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const id of view.keys()) {
|
|
74
|
+
if (!stillVisible.has(id)) {
|
|
75
|
+
sync.leave.push(id);
|
|
76
|
+
view.delete(id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return sync;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=replicator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replicator.js","sourceRoot":"","sources":["../../src/entity/replicator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Server-assigned entity id. Numeric so it stays compact on the msgpack wire. */
|
|
2
|
+
export type EntityId = number;
|
|
3
|
+
/** Any class usable as a replicated entity type: it must be constructible with no arguments. */
|
|
4
|
+
export type EntityConstructor<T extends object = object> = new () => T;
|
|
5
|
+
/** Read-only view of the server world handed to `visibleTo` / `canCall` hooks. */
|
|
6
|
+
export interface EntityWorldView {
|
|
7
|
+
get(id: EntityId): object | undefined;
|
|
8
|
+
byType<T extends object>(Type: EntityConstructor<T>): T[];
|
|
9
|
+
all(): object[];
|
|
10
|
+
}
|
|
11
|
+
/** One entity entering a client's view: the full replicated field set for that client. */
|
|
12
|
+
export interface EntityEnter {
|
|
13
|
+
type: string;
|
|
14
|
+
id: EntityId;
|
|
15
|
+
fields: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
/** One entity already in view whose replicated fields changed. */
|
|
18
|
+
export interface EntityUpdate {
|
|
19
|
+
id: EntityId;
|
|
20
|
+
fields: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/** A single client's per-flush entity delta. */
|
|
23
|
+
export interface EntitySync {
|
|
24
|
+
enter: EntityEnter[];
|
|
25
|
+
update: EntityUpdate[];
|
|
26
|
+
leave: EntityId[];
|
|
27
|
+
}
|
|
28
|
+
export declare function isEmptySync(sync: EntitySync): boolean;
|
|
29
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/entity/types.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE7B,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,UAAU,CAAC,CAAA;AAEtE,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAA;IACrC,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAA;IACzD,GAAG,IAAI,MAAM,EAAE,CAAA;CAChB;AAED,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,QAAQ,CAAA;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,QAAQ,CAAA;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,gDAAgD;AAChD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,MAAM,EAAE,YAAY,EAAE,CAAA;IACtB,KAAK,EAAE,QAAQ,EAAE,CAAA;CAClB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAErD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/entity/types.ts"],"names":[],"mappings":"AAiCA,MAAM,UAAU,WAAW,CAAC,IAAgB;IAC1C,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAA;AACvF,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EntityWorldView } from './types.js';
|
|
2
|
+
/** Scopes replication and RPC visibility for an `@Entity` type. Default (undefined): visible to everyone. */
|
|
3
|
+
export type VisibleTo = (viewerId: string, entity: object, world: EntityWorldView) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Whether `viewerId` can see `entity`: an owner always can; everyone else defers to `visibleTo`
|
|
6
|
+
* (no hook means visible to everyone). Shared by replication (who gets `enter`/`update`) and by
|
|
7
|
+
* `@Server` RPC authorisation (`EntityWorld.invoke`), so a caller can never act on an entity the
|
|
8
|
+
* server would not otherwise show them.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isEntityVisible(isOwner: boolean, viewerId: string, entity: object, visibleTo: VisibleTo | undefined, world: EntityWorldView): boolean;
|
|
11
|
+
//# sourceMappingURL=visibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visibility.d.ts","sourceRoot":"","sources":["../../src/entity/visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,6GAA6G;AAC7G,MAAM,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,KAAK,OAAO,CAAA;AAE7F;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,KAAK,EAAE,eAAe,GACrB,OAAO,CAET"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `viewerId` can see `entity`: an owner always can; everyone else defers to `visibleTo`
|
|
3
|
+
* (no hook means visible to everyone). Shared by replication (who gets `enter`/`update`) and by
|
|
4
|
+
* `@Server` RPC authorisation (`EntityWorld.invoke`), so a caller can never act on an entity the
|
|
5
|
+
* server would not otherwise show them.
|
|
6
|
+
*/
|
|
7
|
+
export function isEntityVisible(isOwner, viewerId, entity, visibleTo, world) {
|
|
8
|
+
return isOwner || !visibleTo || visibleTo(viewerId, entity, world);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=visibility.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visibility.js","sourceRoot":"","sources":["../../src/entity/visibility.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAgB,EAChB,QAAgB,EAChB,MAAc,EACd,SAAgC,EAChC,KAAsB;IAEtB,OAAO,OAAO,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;AACpE,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EntityConstructor, EntityId, EntitySync, EntityWorldView } from './types.js';
|
|
2
|
+
/** How the world reaches connected clients. Injected so the world stays testable without sockets. */
|
|
3
|
+
export interface EntityTransport {
|
|
4
|
+
/** Push one entity-scoped `@Client` / `@Multicast` call to a client. */
|
|
5
|
+
sendRpc(playerId: string, id: EntityId, method: string, args: unknown[]): void;
|
|
6
|
+
/** Push one client their `{ enter, update, leave }` delta. */
|
|
7
|
+
sendSync(playerId: string, sync: EntitySync): void;
|
|
8
|
+
/** The currently connected players. */
|
|
9
|
+
players(): string[];
|
|
10
|
+
}
|
|
11
|
+
export interface EntityWorldOptions {
|
|
12
|
+
/** Entity classes registered up front (also auto-registered lazily on first spawn). */
|
|
13
|
+
types?: EntityConstructor<object>[];
|
|
14
|
+
transport?: EntityTransport;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Server-side entity registry: spawns entities, runs their `@ServerTick`, authorises their
|
|
18
|
+
* `@Server` calls, and flushes each client's interest-scoped delta.
|
|
19
|
+
*/
|
|
20
|
+
export declare class EntityWorld implements EntityWorldView {
|
|
21
|
+
private entities;
|
|
22
|
+
private infoByCtor;
|
|
23
|
+
private replicator;
|
|
24
|
+
private transport;
|
|
25
|
+
private nextId;
|
|
26
|
+
constructor(options?: EntityWorldOptions);
|
|
27
|
+
private register;
|
|
28
|
+
private infoOf;
|
|
29
|
+
/** Registers an entity, assigns its id and owner, and wires its server → client dispatch. */
|
|
30
|
+
spawn<T extends object>(entity: T, options?: {
|
|
31
|
+
ownerId?: string | null;
|
|
32
|
+
}): T;
|
|
33
|
+
despawn(id: EntityId): void;
|
|
34
|
+
get(id: EntityId): object | undefined;
|
|
35
|
+
byType<T extends object>(Type: EntityConstructor<T>): T[];
|
|
36
|
+
all(): object[];
|
|
37
|
+
/** Forget a disconnected client's view so they re-enter from scratch on reconnect. */
|
|
38
|
+
dropViewer(playerId: string): void;
|
|
39
|
+
/** Runs every entity's `@ServerTick` methods. Called by the server's fixed-step scheduler. */
|
|
40
|
+
tick(dt: number): void;
|
|
41
|
+
/** Computes and sends every connected client's delta. Called at the replication rate. */
|
|
42
|
+
flush(): void;
|
|
43
|
+
private replicationEntries;
|
|
44
|
+
/**
|
|
45
|
+
* Runs a client-initiated `@Server` call on one entity after checking the caller is allowed.
|
|
46
|
+
* Rejects on an unknown entity, a method that is not `@Server` on that type, an entity the caller
|
|
47
|
+
* cannot see, or a failed `canCall` — `canCall` alone would otherwise let a `canCall: 'anyone'`
|
|
48
|
+
* entity be driven by a client the `visibleTo` hook was written to hide it from entirely.
|
|
49
|
+
*/
|
|
50
|
+
invoke(callerId: string, id: EntityId, method: string, args: unknown[]): Promise<unknown>;
|
|
51
|
+
private canCall;
|
|
52
|
+
/**
|
|
53
|
+
* Replaces `@Client` / `@Multicast` methods on this instance with sends, so the server stays
|
|
54
|
+
* independent of the Vite transform (plain tsx/node works) and tick code can call them directly.
|
|
55
|
+
*
|
|
56
|
+
* Both recompute visibility live against `this.transport.players()` rather than asking the
|
|
57
|
+
* replicator who was already sent an `enter` for this id, because `flush()` runs on its own timer:
|
|
58
|
+
* an entity spawned and called on within the same tick would otherwise have no recorded viewers yet
|
|
59
|
+
* and the rpc would reach nobody. That alone isn't sufficient though — a recipient who is newly
|
|
60
|
+
* visible but has not yet had a flush also has no client-side mirror to run the rpc against, so
|
|
61
|
+
* `sendToRecipient` first catches that one recipient up with a synthetic `enter` (recorded into the
|
|
62
|
+
* replicator so the next real flush sends a diff, not a duplicate enter) before sending the rpc, to
|
|
63
|
+
* guarantee enter-before-rpc per `(player, entity)`.
|
|
64
|
+
*/
|
|
65
|
+
private bindClientDispatch;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=world.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/entity/world.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAE1F,qGAAqG;AACrG,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAC9E,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IAClD,uCAAuC;IACvC,OAAO,IAAI,MAAM,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,uFAAuF;IACvF,KAAK,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAA;IACnC,SAAS,CAAC,EAAE,eAAe,CAAA;CAC5B;AAeD;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,MAAM,CAAI;gBAEN,OAAO,GAAE,kBAAuB;IAK5C,OAAO,CAAC,QAAQ;IAuBhB,OAAO,CAAC,MAAM;IAId,6FAA6F;IAC7F,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAO,GAAG,CAAC;IAahF,OAAO,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI;IAI3B,GAAG,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS;IAIrC,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE;IAQzD,GAAG,IAAI,MAAM,EAAE;IAIf,sFAAsF;IACtF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIlC,8FAA8F;IAC9F,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAQtB,yFAAyF;IACzF,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,kBAAkB;IAS1B;;;;;OAKG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAmB/F,OAAO,CAAC,OAAO;IAOf;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,kBAAkB;CA2B3B"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { getMethodRoles } from '../decorators/metadata.js';
|
|
2
|
+
import { getEntityHooks, getEntityName, getEntityOptions, isEntityClass } from '../decorators/entity.js';
|
|
3
|
+
import { fieldPlanOf, ownerIdField, snapshotFields } from './fields.js';
|
|
4
|
+
import { EntityReplicator } from './replicator.js';
|
|
5
|
+
import { isEntityVisible } from './visibility.js';
|
|
6
|
+
import { isEmptySync } from './types.js';
|
|
7
|
+
const noopTransport = {
|
|
8
|
+
sendRpc() { }, sendSync() { }, players: () => [],
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Server-side entity registry: spawns entities, runs their `@ServerTick`, authorises their
|
|
12
|
+
* `@Server` calls, and flushes each client's interest-scoped delta.
|
|
13
|
+
*/
|
|
14
|
+
export class EntityWorld {
|
|
15
|
+
entities = new Map();
|
|
16
|
+
infoByCtor = new Map();
|
|
17
|
+
replicator = new EntityReplicator();
|
|
18
|
+
transport;
|
|
19
|
+
nextId = 1;
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.transport = options.transport ?? noopTransport;
|
|
22
|
+
for (const Type of options.types ?? [])
|
|
23
|
+
this.register(Type);
|
|
24
|
+
}
|
|
25
|
+
register(Type) {
|
|
26
|
+
const existing = this.infoByCtor.get(Type);
|
|
27
|
+
if (existing)
|
|
28
|
+
return existing;
|
|
29
|
+
if (!isEntityClass(Type)) {
|
|
30
|
+
throw new Error(`${Type.name || 'class'} is not decorated with @Entity`);
|
|
31
|
+
}
|
|
32
|
+
const proto = Type.prototype;
|
|
33
|
+
const hooks = getEntityHooks(proto);
|
|
34
|
+
const roles = getMethodRoles(proto);
|
|
35
|
+
const info = {
|
|
36
|
+
name: getEntityName(Type),
|
|
37
|
+
plan: fieldPlanOf(Type),
|
|
38
|
+
options: getEntityOptions(Type) ?? {},
|
|
39
|
+
serverTicks: Object.keys(hooks).filter(m => hooks[m] === 'serverTick'),
|
|
40
|
+
clientMethods: Object.fromEntries(Object.entries(roles).filter(([, r]) => r === 'client' || r === 'multicast')),
|
|
41
|
+
serverMethods: new Set(Object.keys(roles).filter(m => roles[m] === 'server')),
|
|
42
|
+
};
|
|
43
|
+
this.infoByCtor.set(Type, info);
|
|
44
|
+
return info;
|
|
45
|
+
}
|
|
46
|
+
infoOf(entity) {
|
|
47
|
+
return this.register(Object.getPrototypeOf(entity).constructor);
|
|
48
|
+
}
|
|
49
|
+
/** Registers an entity, assigns its id and owner, and wires its server → client dispatch. */
|
|
50
|
+
spawn(entity, options = {}) {
|
|
51
|
+
const info = this.infoOf(entity);
|
|
52
|
+
const id = this.nextId++;
|
|
53
|
+
const target = entity;
|
|
54
|
+
target['id'] = id;
|
|
55
|
+
target['ownerId'] = options.ownerId ?? null;
|
|
56
|
+
target['isServer'] = true;
|
|
57
|
+
target['hasAuthority'] = true;
|
|
58
|
+
this.entities.set(id, entity);
|
|
59
|
+
this.bindClientDispatch(entity, id, info);
|
|
60
|
+
return entity;
|
|
61
|
+
}
|
|
62
|
+
despawn(id) {
|
|
63
|
+
this.entities.delete(id);
|
|
64
|
+
}
|
|
65
|
+
get(id) {
|
|
66
|
+
return this.entities.get(id);
|
|
67
|
+
}
|
|
68
|
+
byType(Type) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const entity of this.entities.values()) {
|
|
71
|
+
if (entity instanceof Type)
|
|
72
|
+
out.push(entity);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
all() {
|
|
77
|
+
return [...this.entities.values()];
|
|
78
|
+
}
|
|
79
|
+
/** Forget a disconnected client's view so they re-enter from scratch on reconnect. */
|
|
80
|
+
dropViewer(playerId) {
|
|
81
|
+
this.replicator.dropViewer(playerId);
|
|
82
|
+
}
|
|
83
|
+
/** Runs every entity's `@ServerTick` methods. Called by the server's fixed-step scheduler. */
|
|
84
|
+
tick(dt) {
|
|
85
|
+
for (const entity of [...this.entities.values()]) {
|
|
86
|
+
for (const method of this.infoOf(entity).serverTicks) {
|
|
87
|
+
entity[method].call(entity, dt);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Computes and sends every connected client's delta. Called at the replication rate. */
|
|
92
|
+
flush() {
|
|
93
|
+
const entries = this.replicationEntries();
|
|
94
|
+
for (const playerId of this.transport.players()) {
|
|
95
|
+
const sync = this.replicator.computeSync(playerId, entries, this);
|
|
96
|
+
if (!isEmptySync(sync))
|
|
97
|
+
this.transport.sendSync(playerId, sync);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
replicationEntries() {
|
|
101
|
+
const entries = [];
|
|
102
|
+
for (const [id, entity] of this.entities) {
|
|
103
|
+
const info = this.infoOf(entity);
|
|
104
|
+
entries.push({ id, entity, type: info.name, plan: info.plan, visibleTo: info.options.visibleTo });
|
|
105
|
+
}
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Runs a client-initiated `@Server` call on one entity after checking the caller is allowed.
|
|
110
|
+
* Rejects on an unknown entity, a method that is not `@Server` on that type, an entity the caller
|
|
111
|
+
* cannot see, or a failed `canCall` — `canCall` alone would otherwise let a `canCall: 'anyone'`
|
|
112
|
+
* entity be driven by a client the `visibleTo` hook was written to hide it from entirely.
|
|
113
|
+
*/
|
|
114
|
+
async invoke(callerId, id, method, args) {
|
|
115
|
+
const entity = this.entities.get(id);
|
|
116
|
+
if (!entity)
|
|
117
|
+
throw new Error(`Unknown entity ${id}`);
|
|
118
|
+
const info = this.infoOf(entity);
|
|
119
|
+
const [typeName, methodName] = method.split('.');
|
|
120
|
+
if (typeName !== info.name || !methodName || !info.serverMethods.has(methodName)) {
|
|
121
|
+
throw new Error(`Unknown entity method: ${method}`);
|
|
122
|
+
}
|
|
123
|
+
const isOwner = entity[ownerIdField] === callerId;
|
|
124
|
+
if (!isEntityVisible(isOwner, callerId, entity, info.options.visibleTo, this)) {
|
|
125
|
+
// Same message as a missing id: a caller with no visibility shouldn't learn the entity exists.
|
|
126
|
+
throw new Error(`Unknown entity ${id}`);
|
|
127
|
+
}
|
|
128
|
+
if (!this.canCall(callerId, entity, info)) {
|
|
129
|
+
throw new Error(`Not allowed: ${method}`);
|
|
130
|
+
}
|
|
131
|
+
return entity[methodName].apply(entity, args);
|
|
132
|
+
}
|
|
133
|
+
canCall(callerId, entity, info) {
|
|
134
|
+
const rule = info.options.canCall ?? 'owner';
|
|
135
|
+
if (rule === 'anyone')
|
|
136
|
+
return true;
|
|
137
|
+
if (rule === 'owner')
|
|
138
|
+
return entity['ownerId'] === callerId;
|
|
139
|
+
return rule(callerId, entity, this);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Replaces `@Client` / `@Multicast` methods on this instance with sends, so the server stays
|
|
143
|
+
* independent of the Vite transform (plain tsx/node works) and tick code can call them directly.
|
|
144
|
+
*
|
|
145
|
+
* Both recompute visibility live against `this.transport.players()` rather than asking the
|
|
146
|
+
* replicator who was already sent an `enter` for this id, because `flush()` runs on its own timer:
|
|
147
|
+
* an entity spawned and called on within the same tick would otherwise have no recorded viewers yet
|
|
148
|
+
* and the rpc would reach nobody. That alone isn't sufficient though — a recipient who is newly
|
|
149
|
+
* visible but has not yet had a flush also has no client-side mirror to run the rpc against, so
|
|
150
|
+
* `sendToRecipient` first catches that one recipient up with a synthetic `enter` (recorded into the
|
|
151
|
+
* replicator so the next real flush sends a diff, not a duplicate enter) before sending the rpc, to
|
|
152
|
+
* guarantee enter-before-rpc per `(player, entity)`.
|
|
153
|
+
*/
|
|
154
|
+
bindClientDispatch(entity, id, info) {
|
|
155
|
+
const sendToRecipient = (playerId, isOwner, key, args) => {
|
|
156
|
+
if (!this.replicator.hasInView(playerId, id)) {
|
|
157
|
+
const fields = snapshotFields(entity, info.plan, isOwner);
|
|
158
|
+
this.replicator.recordEnter(playerId, id, fields);
|
|
159
|
+
this.transport.sendSync(playerId, { enter: [{ type: info.name, id, fields }], update: [], leave: [] });
|
|
160
|
+
}
|
|
161
|
+
this.transport.sendRpc(playerId, id, key, args);
|
|
162
|
+
};
|
|
163
|
+
for (const [methodName, role] of Object.entries(info.clientMethods)) {
|
|
164
|
+
const key = `${info.name}.${methodName}`;
|
|
165
|
+
const send = role === 'client'
|
|
166
|
+
? (...args) => {
|
|
167
|
+
const ownerId = entity['ownerId'];
|
|
168
|
+
if (typeof ownerId === 'string')
|
|
169
|
+
sendToRecipient(ownerId, true, key, args);
|
|
170
|
+
}
|
|
171
|
+
: (...args) => {
|
|
172
|
+
for (const playerId of this.transport.players()) {
|
|
173
|
+
const isOwner = entity[ownerIdField] === playerId;
|
|
174
|
+
if (isEntityVisible(isOwner, playerId, entity, info.options.visibleTo, this)) {
|
|
175
|
+
sendToRecipient(playerId, isOwner, key, args);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
Object.defineProperty(entity, methodName, { value: send, configurable: true, writable: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=world.js.map
|