@genex-ai/multiplayer 0.10.1 → 0.11.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/README.md +12 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,13 @@ across reconnects. Never log it.
|
|
|
34
34
|
|
|
35
35
|
Different `room` ids are **fully isolated** — players in game A never see game B.
|
|
36
36
|
|
|
37
|
+
`connect()` joins immediately. Use it for one ongoing drop-in world. If the game has a
|
|
38
|
+
Play/Online/Local/Bots menu, call it only after the online click and call `room.leave()` when the
|
|
39
|
+
player returns to the offline menu. Immediate connect + spawn is valid only when loading the game
|
|
40
|
+
already means joining the always-online world; do not put a fake Play screen in front of a player
|
|
41
|
+
who is already present. Use `matchmake()` below for fresh bounded sessions with queue/quorum/fair
|
|
42
|
+
start/backfill behavior.
|
|
43
|
+
|
|
37
44
|
---
|
|
38
45
|
|
|
39
46
|
## `me` — publish your own state
|
|
@@ -101,7 +108,11 @@ room.objects.snap('ball', { x: 0, y: 1, z: 0, q: [0, 0, 0, 1] });
|
|
|
101
108
|
```
|
|
102
109
|
|
|
103
110
|
Keep object state **flat** (top-level numbers + a 4-number quaternion) so it smooths, same as a
|
|
104
|
-
player. `state` is smoothed; `stateRaw` is the raw latest
|
|
111
|
+
player. `state` is render-only and smoothed; `stateRaw` is the raw latest for every gameplay read
|
|
112
|
+
(hit-tests, reach, pickups, physics adoption, discrete values). `objects.ids()` lists them.
|
|
113
|
+
`objects.set()` and `objects.snap()` are owner-only: SDK 0.10.2+ ignores an unowned call and warns
|
|
114
|
+
once per object/operation. Await `claimConfirmed()` and check `accepted` before an irreversible
|
|
115
|
+
write; legacy `claim(); set()` remains valid because `claim()` is optimistic locally.
|
|
105
116
|
|
|
106
117
|
The relay enforces a short minimum ownership hold to prevent contact thrash. A current-owner
|
|
107
118
|
reassert is a true no-op: it does not extend that hold. Legacy `claim()` remains optimistic for
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ type ConnectOptions = {
|
|
|
9
9
|
/** Regional relay urls (pass `getColyseusUrls()`) — the SDK pings each and connects to the
|
|
10
10
|
* lowest-latency one; a single entry just connects. */
|
|
11
11
|
urls: string[];
|
|
12
|
+
/** Cross-region invite override: connect to THIS exact relay url, skipping latency selection. Two
|
|
13
|
+
* players are only in the same room if they share one region's relay, so an invitee must follow the
|
|
14
|
+
* inviter's region (carried in the invite) instead of picking its own nearest. Omit for normal
|
|
15
|
+
* nearest-region play. */
|
|
16
|
+
url?: string;
|
|
12
17
|
/** Game id — everyone who passes the same id shares a room; different ids are
|
|
13
18
|
* fully isolated. Use your project slug (or any stable per-game string). */
|
|
14
19
|
room: string;
|
|
@@ -34,8 +39,9 @@ type PlayerView<TState> = {
|
|
|
34
39
|
name: string;
|
|
35
40
|
/** Transport is live now. False while this reserved seat is inside reconnect grace. */
|
|
36
41
|
connected: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* for yourself it's live. Numeric fields are lerped; everything else snaps.
|
|
42
|
+
/** RENDER ONLY — for remote players this is **auto-smoothed** (interpolated) and delayed;
|
|
43
|
+
* for yourself it's live. Numeric fields are lerped; everything else snaps. Use `stateRaw`
|
|
44
|
+
* for every gameplay read (hits, reach, pickups, flags, physics adoption). */
|
|
39
45
|
state: TState;
|
|
40
46
|
/** The latest raw snapshot (never smoothed/delayed) — use for hit-tests / logic. */
|
|
41
47
|
stateRaw: TState;
|
|
@@ -49,8 +55,8 @@ type ObjectView<TState> = {
|
|
|
49
55
|
owner: string | undefined;
|
|
50
56
|
/** True if you own it (so you should simulate + `objects.set` it). */
|
|
51
57
|
isMine: boolean;
|
|
52
|
-
/**
|
|
53
|
-
* live (your last `set`) when it's yours. */
|
|
58
|
+
/** RENDER ONLY — **auto-smoothed** and delayed like a remote player when someone else owns it;
|
|
59
|
+
* live (your last `set`) when it's yours. Use `stateRaw` for every gameplay read. */
|
|
54
60
|
state: TState;
|
|
55
61
|
/** The latest raw snapshot (never smoothed/delayed) — for hit-tests / logic. */
|
|
56
62
|
stateRaw: TState;
|
|
@@ -276,6 +282,9 @@ type Matchmaking<TState extends Record<string, unknown>> = {
|
|
|
276
282
|
type MatchmakeOptions = {
|
|
277
283
|
/** Regional relay urls (pass `getColyseusUrls()`) — pings each, connects to the lowest-latency one. */
|
|
278
284
|
urls: string[];
|
|
285
|
+
/** Cross-region override: matchmake on THIS exact relay url, skipping latency selection — an invitee
|
|
286
|
+
* follows the inviter's region so both land on the same relay. Omit for normal nearest-region play. */
|
|
287
|
+
url?: string;
|
|
279
288
|
/** Game id (your project slug/id) — isolates this game's matchmaking. */
|
|
280
289
|
room: string;
|
|
281
290
|
name?: string;
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ var STATE_SNAP_MSG = "__state:snap";
|
|
|
28
28
|
var OBJECT_SNAP_MSG = "__object:snap";
|
|
29
29
|
var MP_PROTOCOL = 2;
|
|
30
30
|
async function relayClient(opts) {
|
|
31
|
+
if (opts.url) return new Client(opts.url);
|
|
31
32
|
if (opts.urls.length > 1) return Client.selectByLatency(opts.urls);
|
|
32
33
|
const url = opts.urls[0];
|
|
33
34
|
if (!url) throw new Error("genex multiplayer: `urls` must contain at least one relay URL");
|
|
@@ -208,6 +209,7 @@ function attachSession(client, initialRoom) {
|
|
|
208
209
|
const objEpochs = /* @__PURE__ */ new Map();
|
|
209
210
|
const objRaw = /* @__PURE__ */ new Map();
|
|
210
211
|
const localObjState = /* @__PURE__ */ new Map();
|
|
212
|
+
const warnedUnownedWrites = /* @__PURE__ */ new Set();
|
|
211
213
|
const pendingBridge = /* @__PURE__ */ new Map();
|
|
212
214
|
let hostId;
|
|
213
215
|
let objectControlVersion = 0;
|
|
@@ -215,6 +217,17 @@ function attachSession(client, initialRoom) {
|
|
|
215
217
|
const pendingControls = /* @__PURE__ */ new Map();
|
|
216
218
|
const testSkew = typeof globalThis !== "undefined" && globalThis.__GENEX_MP_TEST_SKEW__ || 0;
|
|
217
219
|
const now = testSkew ? () => nowMs() + testSkew : nowMs;
|
|
220
|
+
const rejectUnownedWrite = (operation, id) => {
|
|
221
|
+
if (objOwners.get(id) === room.sessionId) return false;
|
|
222
|
+
const warningKey = `${operation}:${id}`;
|
|
223
|
+
if (!warnedUnownedWrites.has(warningKey)) {
|
|
224
|
+
warnedUnownedWrites.add(warningKey);
|
|
225
|
+
console.warn(
|
|
226
|
+
`[multiplayer] objects.${operation}('${id}') ignored because this session does not own the object \u2014 await objects.claimConfirmed('${id}') and check accepted before publishing`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
};
|
|
218
231
|
const pushSnap = (id, state, sendTime) => {
|
|
219
232
|
const arrival = now();
|
|
220
233
|
const t = sendTime ?? arrival;
|
|
@@ -801,12 +814,14 @@ function attachSession(client, initialRoom) {
|
|
|
801
814
|
},
|
|
802
815
|
set(id, state) {
|
|
803
816
|
const key = String(id);
|
|
817
|
+
if (rejectUnownedWrite("set", key)) return;
|
|
804
818
|
const clean = sanitizeFinite(state);
|
|
805
819
|
if (objOwners.get(key) === room.sessionId) localObjState.set(key, clean);
|
|
806
820
|
send0("obj", { id: key, d: clean, t: now() });
|
|
807
821
|
},
|
|
808
822
|
snap(id, state) {
|
|
809
823
|
const key = String(id);
|
|
824
|
+
if (rejectUnownedWrite("snap", key)) return;
|
|
810
825
|
const clean = sanitizeFinite(state);
|
|
811
826
|
if (objOwners.get(key) === room.sessionId) localObjState.set(key, clean);
|
|
812
827
|
send0(OBJECT_SNAP_MSG, { id: key, d: clean, t: now() });
|