@genex-ai/multiplayer 0.2.6 → 0.4.1
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/README.md +47 -4
- package/dist/index.d.ts +57 -3
- package/dist/index.js +170 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,42 @@ room.shared.get('round'); // 3
|
|
|
57
57
|
room.shared.keys(); // ['round', ...]
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
## `objects` — shared objects nobody owns until claimed
|
|
61
|
+
|
|
62
|
+
For a **ball, a puck, an NPC** — a moving thing that isn't any one player. Exactly one client
|
|
63
|
+
**owns** an object at a time (the server enforces it); the owner simulates it and `set`s it, and
|
|
64
|
+
everyone else reads it **auto-smoothed**, on the same interpolation as remote players. Ownership
|
|
65
|
+
survives the owner leaving — the object is reassigned to the room host.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
if (iKicked) room.objects.claim('ball'); // take ownership (last claim wins)
|
|
69
|
+
|
|
70
|
+
if (room.objects.get('ball')?.isMine) {
|
|
71
|
+
room.objects.set('ball', simulateBall()); // only the owner's writes land
|
|
72
|
+
}
|
|
73
|
+
const ball = room.objects.get('ball'); // { owner, isMine, state, stateRaw }
|
|
74
|
+
drawBall(ball.state); // smoothed for everyone (live for the owner)
|
|
75
|
+
|
|
76
|
+
room.objects.release('ball'); // give it up (stays until reclaimed)
|
|
77
|
+
room.objects.remove('bullet'); // destroy transient objects when done
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Keep object state **flat** (top-level numbers + a 4-number quaternion) so it smooths, same as a
|
|
81
|
+
player. `state` is smoothed; `stateRaw` is the raw latest (hit-tests). `objects.ids()` lists them.
|
|
82
|
+
|
|
83
|
+
## `isHost` / `host` — the elected room authority
|
|
84
|
+
|
|
85
|
+
One client is the **host** (the first joiner still present, re-elected automatically when they
|
|
86
|
+
leave). Use it to pick a single writer for scores/rounds or a single simulator for NPCs.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
if (room.isHost) room.shared.set('round', nextRound); // only the host advances the round
|
|
90
|
+
room.on('host', (id) => {}); // host changed (migration)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`isHost`/`host` settle within the first patch after `connect()` — read them in your loop (or react
|
|
94
|
+
to `on('host')`), not once at startup.
|
|
95
|
+
|
|
60
96
|
## `on` — events
|
|
61
97
|
|
|
62
98
|
```ts
|
|
@@ -64,6 +100,8 @@ room.on('join', (id, state) => {}); // a player appeared (also fires for YOU
|
|
|
64
100
|
room.on('change', (id, state) => {}); // a player updated their me-state
|
|
65
101
|
room.on('leave', (id) => {}); // a player left
|
|
66
102
|
room.on('shared', (key, value) => {}); // a shared key changed
|
|
103
|
+
room.on('object', (id) => {}); // an object's owner changed (handoff)
|
|
104
|
+
room.on('host', (id) => {}); // the room host changed
|
|
67
105
|
room.on('hit', (payload) => {}); // a custom event someone sent (see `send`)
|
|
68
106
|
```
|
|
69
107
|
|
|
@@ -143,13 +181,18 @@ onKey('right', () => { me.x += 1; });
|
|
|
143
181
|
|
|
144
182
|
## Persistent worlds (optional)
|
|
145
183
|
|
|
146
|
-
The relay is in-memory — room state is gone when everyone leaves or the server restarts. To persist world state across restarts,
|
|
184
|
+
The relay is in-memory — room state is gone when everyone leaves or the server restarts. To persist world state across restarts, use `@genex-ai/embed-sdk`'s state helpers (they own auth tokens, guest handling, and write-conflict decoding):
|
|
147
185
|
|
|
148
186
|
```ts
|
|
187
|
+
import { loadWorldState, saveWorldState } from '@genex-ai/embed-sdk';
|
|
188
|
+
|
|
149
189
|
// load on boot
|
|
150
|
-
const { data } = await
|
|
190
|
+
const { data, version } = await loadWorldState();
|
|
151
191
|
initWorld(data ?? defaultWorld());
|
|
152
192
|
|
|
153
|
-
// save
|
|
154
|
-
|
|
193
|
+
// save (from ONE authority — the elected host — debounced, with ifVersion so
|
|
194
|
+
// races surface as { conflict: true } instead of silently clobbering)
|
|
195
|
+
if (room.isHost) await saveWorldState(world, { ifVersion: version });
|
|
155
196
|
```
|
|
197
|
+
|
|
198
|
+
The relay prefers signed-in players when electing the host (guests cannot write saves), so host-driven persistence works whenever any account is in the room. Per-player progression belongs in `savePlayerState()` (each player's own slot), never in the shared world blob.
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,20 @@ type ConnectOptions = {
|
|
|
7
7
|
/** Game id — everyone who passes the same id shares a room; different ids are
|
|
8
8
|
* fully isolated. Use your project slug (or any stable per-game string). */
|
|
9
9
|
room: string;
|
|
10
|
-
/** Display name for this player
|
|
10
|
+
/** Display name for this player. The server prefers the verified identity's
|
|
11
|
+
* name (from `auth`) when present; this is the display fallback. */
|
|
11
12
|
name?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Verified-identity credential — REQUIRED to actually join: the relay
|
|
15
|
+
* rejects tokenless joins (there is no anonymous play). Pass
|
|
16
|
+
* `genexEmbed.getColyseusAuth()` AFTER `await genexEmbed.waitForAuth()`
|
|
17
|
+
* (from `@genex-ai/embed-sdk`), and read it fresh at every connect() call —
|
|
18
|
+
* never cache the object across reconnects (tokens rotate every ~10 min).
|
|
19
|
+
* NEVER log this value.
|
|
20
|
+
*/
|
|
21
|
+
auth?: {
|
|
22
|
+
embedToken: string;
|
|
23
|
+
};
|
|
12
24
|
/** Extra join options passed to the room. */
|
|
13
25
|
options?: Record<string, unknown>;
|
|
14
26
|
};
|
|
@@ -21,6 +33,19 @@ type PlayerView<TState> = {
|
|
|
21
33
|
/** The latest raw snapshot (never smoothed/delayed) — use for hit-tests / logic. */
|
|
22
34
|
stateRaw: TState;
|
|
23
35
|
};
|
|
36
|
+
/** A shared object (a ball, a puck, an NPC) owned by exactly one client at a time. */
|
|
37
|
+
type ObjectView<TState> = {
|
|
38
|
+
id: string;
|
|
39
|
+
/** Session id of the current owner, or `undefined` if unowned. */
|
|
40
|
+
owner: string | undefined;
|
|
41
|
+
/** True if you own it (so you should simulate + `objects.set` it). */
|
|
42
|
+
isMine: boolean;
|
|
43
|
+
/** State to render — **auto-smoothed** like a remote player when someone else owns it;
|
|
44
|
+
* live (your last `set`) when it's yours. */
|
|
45
|
+
state: TState;
|
|
46
|
+
/** The latest raw snapshot (never smoothed/delayed) — for hit-tests / logic. */
|
|
47
|
+
stateRaw: TState;
|
|
48
|
+
};
|
|
24
49
|
type Session<TState extends Record<string, unknown>> = {
|
|
25
50
|
/** Your own session id. */
|
|
26
51
|
readonly id: string;
|
|
@@ -45,12 +70,41 @@ type Session<TState extends Record<string, unknown>> = {
|
|
|
45
70
|
/** All shared keys currently set. */
|
|
46
71
|
keys(): string[];
|
|
47
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Shared objects nobody owns until claimed — a ball, a puck, an NPC. Exactly one client
|
|
75
|
+
* owns an object at a time (server-enforced); the owner simulates it and `set`s it, and
|
|
76
|
+
* everyone else reads it **auto-smoothed**, on the same interpolation as remote players.
|
|
77
|
+
* Ownership survives an owner leaving: the object is reassigned to the room host.
|
|
78
|
+
*/
|
|
79
|
+
readonly objects: {
|
|
80
|
+
/** Take ownership (last claim wins — call it when you kick/touch the object). */
|
|
81
|
+
claim(id: string): void;
|
|
82
|
+
/** Give up ownership (leaves the object unowned until someone claims it). */
|
|
83
|
+
release(id: string): void;
|
|
84
|
+
/** Destroy the object everywhere (owner-only) — use for transient objects (bullets, pickups). */
|
|
85
|
+
remove(id: string): void;
|
|
86
|
+
/** Publish the object's state — only takes effect while you own it. Full object each call. */
|
|
87
|
+
set(id: string, state: Record<string, unknown>): void;
|
|
88
|
+
/** Read an object, or `undefined` if it doesn't exist yet. */
|
|
89
|
+
get<T extends Record<string, unknown> = Record<string, unknown>>(id: string): ObjectView<T> | undefined;
|
|
90
|
+
/** Ids of every object seen so far. */
|
|
91
|
+
ids(): string[];
|
|
92
|
+
};
|
|
93
|
+
/** True if you are the elected room host (first joiner still present). Settles within the
|
|
94
|
+
* first state patch after connect — read it in your loop or react to `on('host')`, not once. */
|
|
95
|
+
readonly isHost: boolean;
|
|
96
|
+
/** Session id of the current room host, or `undefined` until the first host patch lands. */
|
|
97
|
+
readonly host: string | undefined;
|
|
48
98
|
/** A player appeared/updated. **Also fires for you** on connect — filter `id === room.id`. */
|
|
49
99
|
on(event: 'join' | 'change', cb: (id: string, state: TState) => void): () => void;
|
|
50
100
|
/** A player left. */
|
|
51
101
|
on(event: 'leave', cb: (id: string) => void): () => void;
|
|
52
|
-
/** A shared key changed (
|
|
102
|
+
/** A shared key changed (reserved `__*` keys are hidden). For live values just read `shared.get`. */
|
|
53
103
|
on(event: 'shared', cb: (key: string, value: unknown) => void): () => void;
|
|
104
|
+
/** An object's owner or state changed (fires with its id). */
|
|
105
|
+
on(event: 'object', cb: (id: string) => void): () => void;
|
|
106
|
+
/** The room host changed (fires with the new host's id, or `undefined`). */
|
|
107
|
+
on(event: 'host', cb: (host: string | undefined) => void): () => void;
|
|
54
108
|
/** A custom event another client sent via `send(type, ...)`. */
|
|
55
109
|
on(event: string & {}, cb: (payload: unknown) => void): () => void;
|
|
56
110
|
/** Fire-and-forget event to all OTHER clients (not stored in state). */
|
|
@@ -75,4 +129,4 @@ type Session<TState extends Record<string, unknown>> = {
|
|
|
75
129
|
*/
|
|
76
130
|
declare function connect<TState extends Record<string, unknown> = never>(opts: ConnectOptions): Promise<Session<TState>>;
|
|
77
131
|
|
|
78
|
-
export { type ConnectOptions, type PlayerView, type Session, connect };
|
|
132
|
+
export { type ConnectOptions, type ObjectView, type PlayerView, type Session, connect };
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,10 @@ var CLOCK_WIN = 80;
|
|
|
10
10
|
var SMOOTH_TAU_POS = 30;
|
|
11
11
|
var SMOOTH_TAU_ROT = 70;
|
|
12
12
|
var SEND_TIME_KEY = "__t";
|
|
13
|
+
var RESERVED_PREFIX = "__";
|
|
14
|
+
var OBJ_PREFIX = "__obj:";
|
|
15
|
+
var HOST_KEY = "__host";
|
|
16
|
+
var MP_PROTOCOL = 1;
|
|
13
17
|
function parseData(s) {
|
|
14
18
|
if (!s) return { state: {} };
|
|
15
19
|
try {
|
|
@@ -123,7 +127,10 @@ async function connect(opts) {
|
|
|
123
127
|
const room = await client.joinOrCreate(RELAY_ROOM_TYPE, {
|
|
124
128
|
gameId: opts.room,
|
|
125
129
|
name: opts.name,
|
|
126
|
-
|
|
130
|
+
embedToken: opts.auth?.embedToken,
|
|
131
|
+
...opts.options,
|
|
132
|
+
__v: MP_PROTOCOL
|
|
133
|
+
// tells the relay this client hides reserved `__` keys (see MP_PROTOCOL)
|
|
127
134
|
});
|
|
128
135
|
const buffers = /* @__PURE__ */ new Map();
|
|
129
136
|
const lastStamp = /* @__PURE__ */ new Map();
|
|
@@ -131,8 +138,13 @@ async function connect(opts) {
|
|
|
131
138
|
const offsetWin = /* @__PURE__ */ new Map();
|
|
132
139
|
const smoothed = /* @__PURE__ */ new Map();
|
|
133
140
|
const smoothedAt = /* @__PURE__ */ new Map();
|
|
134
|
-
const
|
|
135
|
-
|
|
141
|
+
const intervalsById = /* @__PURE__ */ new Map();
|
|
142
|
+
const autoDelayById = /* @__PURE__ */ new Map();
|
|
143
|
+
let localState;
|
|
144
|
+
const objOwners = /* @__PURE__ */ new Map();
|
|
145
|
+
const objRaw = /* @__PURE__ */ new Map();
|
|
146
|
+
const localObjState = /* @__PURE__ */ new Map();
|
|
147
|
+
let hostId;
|
|
136
148
|
const pushSnap = (id, state, sendTime) => {
|
|
137
149
|
const arrival = nowMs();
|
|
138
150
|
const t = sendTime ?? arrival;
|
|
@@ -148,10 +160,13 @@ async function connect(opts) {
|
|
|
148
160
|
if (prev != null) {
|
|
149
161
|
const gap = t - prev;
|
|
150
162
|
if (gap > 5 && gap < 1e3) {
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
let iv = intervalsById.get(id);
|
|
164
|
+
if (!iv) intervalsById.set(id, iv = []);
|
|
165
|
+
iv.push(gap);
|
|
166
|
+
if (iv.length > 30) iv.shift();
|
|
167
|
+
const target = Math.min(INTERP_MAX_MS, Math.max(INTERP_MIN_MS, INTERP_RATIO * median(iv)));
|
|
168
|
+
const cur = autoDelayById.get(id) ?? 100;
|
|
169
|
+
autoDelayById.set(id, cur + (target - cur) * INTERP_EASE);
|
|
155
170
|
}
|
|
156
171
|
}
|
|
157
172
|
let buf = buffers.get(id);
|
|
@@ -160,9 +175,37 @@ async function connect(opts) {
|
|
|
160
175
|
const cutoff = t - 1500;
|
|
161
176
|
while (buf.length > 2 && buf[0].t < cutoff) buf.shift();
|
|
162
177
|
};
|
|
178
|
+
const sampleSmoothed = (streamKey, localNow) => {
|
|
179
|
+
const buf = buffers.get(streamKey);
|
|
180
|
+
const autoDelay = autoDelayById.get(streamKey) ?? 100;
|
|
181
|
+
const renderTime = localNow - (clockOffsets.get(streamKey) ?? 0) - autoDelay;
|
|
182
|
+
const sampled = buf && sampleBuffer(buf, renderTime);
|
|
183
|
+
if (!sampled) return void 0;
|
|
184
|
+
const prev = smoothed.get(streamKey);
|
|
185
|
+
let out;
|
|
186
|
+
if (prev) {
|
|
187
|
+
const dt = Math.min(250, Math.max(0, localNow - (smoothedAt.get(streamKey) ?? localNow)));
|
|
188
|
+
out = easeState(prev, sampled, 1 - Math.exp(-dt / SMOOTH_TAU_POS), 1 - Math.exp(-dt / SMOOTH_TAU_ROT));
|
|
189
|
+
} else {
|
|
190
|
+
out = sampled;
|
|
191
|
+
}
|
|
192
|
+
smoothed.set(streamKey, out);
|
|
193
|
+
smoothedAt.set(streamKey, localNow);
|
|
194
|
+
return out;
|
|
195
|
+
};
|
|
196
|
+
const clearStream = (streamKey) => {
|
|
197
|
+
buffers.delete(streamKey);
|
|
198
|
+
lastStamp.delete(streamKey);
|
|
199
|
+
clockOffsets.delete(streamKey);
|
|
200
|
+
offsetWin.delete(streamKey);
|
|
201
|
+
smoothed.delete(streamKey);
|
|
202
|
+
smoothedAt.delete(streamKey);
|
|
203
|
+
intervalsById.delete(streamKey);
|
|
204
|
+
autoDelayById.delete(streamKey);
|
|
205
|
+
};
|
|
163
206
|
const emitters = /* @__PURE__ */ new Map();
|
|
164
207
|
const bridged = /* @__PURE__ */ new Set();
|
|
165
|
-
const reserved = /* @__PURE__ */ new Set(["join", "leave", "change", "shared"]);
|
|
208
|
+
const reserved = /* @__PURE__ */ new Set(["join", "leave", "change", "shared", "object", "host"]);
|
|
166
209
|
const emit = (event, ...args) => emitters.get(event)?.forEach((cb) => cb(...args));
|
|
167
210
|
const $ = getStateCallbacks(room);
|
|
168
211
|
$(room.state).players.onAdd((player, id) => {
|
|
@@ -177,14 +220,60 @@ async function connect(opts) {
|
|
|
177
220
|
});
|
|
178
221
|
$(room.state).players.onRemove((_player, id) => {
|
|
179
222
|
emit("leave", id);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
223
|
+
clearStream(id);
|
|
224
|
+
});
|
|
225
|
+
const lastShared = /* @__PURE__ */ new Map();
|
|
226
|
+
const handleShared = (value, key) => {
|
|
227
|
+
if (lastShared.get(key) === value) return;
|
|
228
|
+
lastShared.set(key, value);
|
|
229
|
+
if (key === HOST_KEY) {
|
|
230
|
+
const next = parseValue(value) || void 0;
|
|
231
|
+
if (next !== hostId) {
|
|
232
|
+
hostId = next;
|
|
233
|
+
emit("host", hostId);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (key.startsWith(OBJ_PREFIX)) {
|
|
238
|
+
const id = key.slice(OBJ_PREFIX.length);
|
|
239
|
+
const blob = parseValue(value);
|
|
240
|
+
if (!blob || typeof blob !== "object") return;
|
|
241
|
+
const owner = typeof blob.o === "string" && blob.o ? blob.o : void 0;
|
|
242
|
+
const ownerChanged = owner !== objOwners.get(id);
|
|
243
|
+
if (ownerChanged) {
|
|
244
|
+
if (owner) objOwners.set(id, owner);
|
|
245
|
+
else objOwners.delete(id);
|
|
246
|
+
if (owner !== room.sessionId) localObjState.delete(id);
|
|
247
|
+
}
|
|
248
|
+
if (blob.d && typeof blob.d === "object" && Object.keys(blob.d).length > 0) {
|
|
249
|
+
objRaw.set(id, blob.d);
|
|
250
|
+
if (owner !== room.sessionId) pushSnap(key, blob.d, blob.t);
|
|
251
|
+
}
|
|
252
|
+
if (ownerChanged) emit("object", id);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
emit("shared", key, parseValue(value));
|
|
256
|
+
};
|
|
257
|
+
$(room.state).shared.onAdd(handleShared);
|
|
258
|
+
$(room.state).shared.onChange(handleShared);
|
|
259
|
+
$(room.state).shared.onRemove((_value, key) => {
|
|
260
|
+
lastShared.delete(key);
|
|
261
|
+
if (key === HOST_KEY) {
|
|
262
|
+
if (hostId !== void 0) {
|
|
263
|
+
hostId = void 0;
|
|
264
|
+
emit("host", void 0);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (key.startsWith(OBJ_PREFIX)) {
|
|
269
|
+
const id = key.slice(OBJ_PREFIX.length);
|
|
270
|
+
clearStream(key);
|
|
271
|
+
objRaw.delete(id);
|
|
272
|
+
objOwners.delete(id);
|
|
273
|
+
localObjState.delete(id);
|
|
274
|
+
emit("object", id);
|
|
275
|
+
}
|
|
186
276
|
});
|
|
187
|
-
$(room.state).shared.onAdd((value, key) => emit("shared", key, parseValue(value)));
|
|
188
277
|
const on = ((event, cb) => {
|
|
189
278
|
let set = emitters.get(event);
|
|
190
279
|
if (!set) emitters.set(event, set = /* @__PURE__ */ new Set());
|
|
@@ -205,42 +294,92 @@ async function connect(opts) {
|
|
|
205
294
|
room.state?.players?.forEach((p, id) => {
|
|
206
295
|
const { state: latest } = parseData(p.data);
|
|
207
296
|
let state = latest;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const prev = smoothed.get(id);
|
|
214
|
-
if (prev) {
|
|
215
|
-
const dt = Math.min(250, Math.max(0, localNow - (smoothedAt.get(id) ?? localNow)));
|
|
216
|
-
state = easeState(prev, sampled, 1 - Math.exp(-dt / SMOOTH_TAU_POS), 1 - Math.exp(-dt / SMOOTH_TAU_ROT));
|
|
217
|
-
} else {
|
|
218
|
-
state = sampled;
|
|
219
|
-
}
|
|
220
|
-
smoothed.set(id, state);
|
|
221
|
-
smoothedAt.set(id, localNow);
|
|
297
|
+
let raw = latest;
|
|
298
|
+
if (id === room.sessionId) {
|
|
299
|
+
if (localState) {
|
|
300
|
+
state = localState;
|
|
301
|
+
raw = localState;
|
|
222
302
|
}
|
|
303
|
+
} else {
|
|
304
|
+
state = sampleSmoothed(id, localNow) ?? latest;
|
|
223
305
|
}
|
|
224
|
-
out.set(id, { id, name: p.name, state, stateRaw:
|
|
306
|
+
out.set(id, { id, name: p.name, state, stateRaw: raw });
|
|
225
307
|
});
|
|
226
308
|
return out;
|
|
227
309
|
},
|
|
228
310
|
me: {
|
|
229
311
|
set(state) {
|
|
312
|
+
localState = { ...state };
|
|
230
313
|
room.send("state", JSON.stringify({ ...state, [SEND_TIME_KEY]: nowMs() }));
|
|
231
314
|
}
|
|
232
315
|
},
|
|
233
316
|
shared: {
|
|
234
317
|
get(key) {
|
|
318
|
+
if (key.startsWith(RESERVED_PREFIX)) return void 0;
|
|
235
319
|
return parseValue(room.state?.shared?.get(key));
|
|
236
320
|
},
|
|
237
321
|
set(key, value) {
|
|
322
|
+
if (key.startsWith(RESERVED_PREFIX)) return;
|
|
238
323
|
room.send("shared", { key, value: JSON.stringify(value) });
|
|
239
324
|
},
|
|
240
325
|
keys() {
|
|
241
|
-
return room.state?.shared ? Array.from(room.state.shared.keys()) : [];
|
|
326
|
+
return room.state?.shared ? Array.from(room.state.shared.keys()).filter((k) => !k.startsWith(RESERVED_PREFIX)) : [];
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
objects: {
|
|
330
|
+
claim(id) {
|
|
331
|
+
const key = String(id);
|
|
332
|
+
objOwners.set(key, room.sessionId);
|
|
333
|
+
room.send("claim", { id: key });
|
|
334
|
+
},
|
|
335
|
+
release(id) {
|
|
336
|
+
const key = String(id);
|
|
337
|
+
localObjState.delete(key);
|
|
338
|
+
if (objOwners.get(key) === room.sessionId) objOwners.delete(key);
|
|
339
|
+
room.send("release", { id: key });
|
|
340
|
+
},
|
|
341
|
+
remove(id) {
|
|
342
|
+
const key = String(id);
|
|
343
|
+
if (objOwners.get(key) === room.sessionId) {
|
|
344
|
+
localObjState.delete(key);
|
|
345
|
+
objRaw.delete(key);
|
|
346
|
+
objOwners.delete(key);
|
|
347
|
+
clearStream(OBJ_PREFIX + key);
|
|
348
|
+
}
|
|
349
|
+
room.send("destroy", { id: key });
|
|
350
|
+
},
|
|
351
|
+
set(id, state) {
|
|
352
|
+
const key = String(id);
|
|
353
|
+
if (objOwners.get(key) === room.sessionId) localObjState.set(key, { ...state });
|
|
354
|
+
room.send("obj", { id: key, d: { ...state } });
|
|
355
|
+
},
|
|
356
|
+
get(id) {
|
|
357
|
+
const key = String(id);
|
|
358
|
+
const owner = objOwners.get(key);
|
|
359
|
+
const mine = localObjState.get(key);
|
|
360
|
+
if (owner === void 0 && !objRaw.has(key) && !mine) return void 0;
|
|
361
|
+
const isMine = owner !== void 0 && owner === room.sessionId;
|
|
362
|
+
let state;
|
|
363
|
+
let raw;
|
|
364
|
+
if (isMine && mine) {
|
|
365
|
+
state = mine;
|
|
366
|
+
raw = mine;
|
|
367
|
+
} else {
|
|
368
|
+
raw = objRaw.get(key) ?? {};
|
|
369
|
+
state = sampleSmoothed(OBJ_PREFIX + key, nowMs()) ?? raw;
|
|
370
|
+
}
|
|
371
|
+
return { id: key, owner, isMine, state, stateRaw: raw };
|
|
372
|
+
},
|
|
373
|
+
ids() {
|
|
374
|
+
return [.../* @__PURE__ */ new Set([...objOwners.keys(), ...objRaw.keys(), ...localObjState.keys()])];
|
|
242
375
|
}
|
|
243
376
|
},
|
|
377
|
+
get isHost() {
|
|
378
|
+
return hostId !== void 0 && hostId === room.sessionId;
|
|
379
|
+
},
|
|
380
|
+
get host() {
|
|
381
|
+
return hostId;
|
|
382
|
+
},
|
|
244
383
|
on,
|
|
245
384
|
send(type, payload) {
|
|
246
385
|
room.send(type, payload);
|