@genex-ai/multiplayer 0.10.1 → 0.10.2

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 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 (hit-tests). `objects.ids()` lists them.
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
@@ -34,8 +34,9 @@ type PlayerView<TState> = {
34
34
  name: string;
35
35
  /** Transport is live now. False while this reserved seat is inside reconnect grace. */
36
36
  connected: boolean;
37
- /** State to render — for remote players this is **auto-smoothed** (interpolated);
38
- * for yourself it's live. Numeric fields are lerped; everything else snaps. */
37
+ /** RENDER ONLY — for remote players this is **auto-smoothed** (interpolated) and delayed;
38
+ * for yourself it's live. Numeric fields are lerped; everything else snaps. Use `stateRaw`
39
+ * for every gameplay read (hits, reach, pickups, flags, physics adoption). */
39
40
  state: TState;
40
41
  /** The latest raw snapshot (never smoothed/delayed) — use for hit-tests / logic. */
41
42
  stateRaw: TState;
@@ -49,8 +50,8 @@ type ObjectView<TState> = {
49
50
  owner: string | undefined;
50
51
  /** True if you own it (so you should simulate + `objects.set` it). */
51
52
  isMine: boolean;
52
- /** State to render — **auto-smoothed** like a remote player when someone else owns it;
53
- * live (your last `set`) when it's yours. */
53
+ /** RENDER ONLY — **auto-smoothed** and delayed like a remote player when someone else owns it;
54
+ * live (your last `set`) when it's yours. Use `stateRaw` for every gameplay read. */
54
55
  state: TState;
55
56
  /** The latest raw snapshot (never smoothed/delayed) — for hit-tests / logic. */
56
57
  stateRaw: TState;
package/dist/index.js CHANGED
@@ -208,6 +208,7 @@ function attachSession(client, initialRoom) {
208
208
  const objEpochs = /* @__PURE__ */ new Map();
209
209
  const objRaw = /* @__PURE__ */ new Map();
210
210
  const localObjState = /* @__PURE__ */ new Map();
211
+ const warnedUnownedWrites = /* @__PURE__ */ new Set();
211
212
  const pendingBridge = /* @__PURE__ */ new Map();
212
213
  let hostId;
213
214
  let objectControlVersion = 0;
@@ -215,6 +216,17 @@ function attachSession(client, initialRoom) {
215
216
  const pendingControls = /* @__PURE__ */ new Map();
216
217
  const testSkew = typeof globalThis !== "undefined" && globalThis.__GENEX_MP_TEST_SKEW__ || 0;
217
218
  const now = testSkew ? () => nowMs() + testSkew : nowMs;
219
+ const rejectUnownedWrite = (operation, id) => {
220
+ if (objOwners.get(id) === room.sessionId) return false;
221
+ const warningKey = `${operation}:${id}`;
222
+ if (!warnedUnownedWrites.has(warningKey)) {
223
+ warnedUnownedWrites.add(warningKey);
224
+ console.warn(
225
+ `[multiplayer] objects.${operation}('${id}') ignored because this session does not own the object \u2014 await objects.claimConfirmed('${id}') and check accepted before publishing`
226
+ );
227
+ }
228
+ return true;
229
+ };
218
230
  const pushSnap = (id, state, sendTime) => {
219
231
  const arrival = now();
220
232
  const t = sendTime ?? arrival;
@@ -801,12 +813,14 @@ function attachSession(client, initialRoom) {
801
813
  },
802
814
  set(id, state) {
803
815
  const key = String(id);
816
+ if (rejectUnownedWrite("set", key)) return;
804
817
  const clean = sanitizeFinite(state);
805
818
  if (objOwners.get(key) === room.sessionId) localObjState.set(key, clean);
806
819
  send0("obj", { id: key, d: clean, t: now() });
807
820
  },
808
821
  snap(id, state) {
809
822
  const key = String(id);
823
+ if (rejectUnownedWrite("snap", key)) return;
810
824
  const clean = sanitizeFinite(state);
811
825
  if (objOwners.get(key) === room.sessionId) localObjState.set(key, clean);
812
826
  send0(OBJECT_SNAP_MSG, { id: key, d: clean, t: now() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/multiplayer",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "The multiplayer client wrapper + shared room schema for genex.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",