@irtio/client 0.6.0 → 0.8.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/{chunk-6J6PKFUS.js → chunk-5Z4DHUA3.js} +5 -0
- package/dist/{chunk-TE66EDBB.js → chunk-XFD6WYQW.js} +251 -39
- package/dist/index.d.ts +705 -37
- package/dist/index.js +436 -11
- package/dist/{physics-S5LHL3HK.js → physics-4SGKNBAC.js} +26 -3
- package/dist/physics-rapier2d-TBOAJMVM.js +186 -0
- package/dist/{physics2d-ZV2IRRRZ.js → physics2d-CNSEWKP4.js} +51 -2
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -2,12 +2,13 @@ import {
|
|
|
2
2
|
ClientStore,
|
|
3
3
|
E_CONNECT_FAILED,
|
|
4
4
|
MAX_PREDICTED_BODIES,
|
|
5
|
+
MAX_PROXY_BODIES,
|
|
5
6
|
PREDICTION_EPSILON,
|
|
6
7
|
RESIM_DEPTH,
|
|
7
8
|
SMOOTHING_HALF_LIFE_MS,
|
|
8
9
|
SMOOTHING_SNAP_UNITS,
|
|
9
10
|
emptyPredictionStats
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-5Z4DHUA3.js";
|
|
11
12
|
|
|
12
13
|
// src/index.ts
|
|
13
14
|
import { EMPTY_PROFILE as EMPTY_PROFILE2 } from "@irtio/protocol";
|
|
@@ -495,6 +496,23 @@ async function errorFrom(res) {
|
|
|
495
496
|
return new IdentityError(code, message);
|
|
496
497
|
}
|
|
497
498
|
|
|
499
|
+
// src/messages.ts
|
|
500
|
+
import { encodeFields } from "@irtio/schema";
|
|
501
|
+
function buildMessages(schema, session) {
|
|
502
|
+
const out = {};
|
|
503
|
+
for (const desc of schema?.messages ?? []) {
|
|
504
|
+
out[desc.name] = {
|
|
505
|
+
send(target, value) {
|
|
506
|
+
session.typedMessage(desc.index, target, encodeFields(desc.fields, value));
|
|
507
|
+
},
|
|
508
|
+
on(cb) {
|
|
509
|
+
return session.onTypedMessage(desc.index, cb);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
|
|
498
516
|
// src/session.ts
|
|
499
517
|
import {
|
|
500
518
|
FrameType,
|
|
@@ -529,7 +547,7 @@ import {
|
|
|
529
547
|
decodeDelta,
|
|
530
548
|
decodeDeltaFrom,
|
|
531
549
|
decodeFields,
|
|
532
|
-
encodeFields,
|
|
550
|
+
encodeFields as encodeFields2,
|
|
533
551
|
schemaFromCanonical
|
|
534
552
|
} from "@irtio/schema";
|
|
535
553
|
|
|
@@ -764,6 +782,57 @@ var RenderStore = class {
|
|
|
764
782
|
const alpha = span > 0 ? (renderT - a.t) / span : 1;
|
|
765
783
|
return lerpRecord(desc, a.value, b.value, Math.min(1, Math.max(0, alpha)));
|
|
766
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* M6 lane D (D71): the body-channel values the renderer is drawing for one instance right now,
|
|
787
|
+
* written into `into`. `true` when there is a pose to draw at all.
|
|
788
|
+
*
|
|
789
|
+
* This is what a kinematic proxy is moved to before every local step, and it deliberately does
|
|
790
|
+
* **not** go through `get`. `get` asks the predictor first — which would recurse, since it calls
|
|
791
|
+
* `frame()` — and for an owned or predicted instance it answers out of the local world, which is
|
|
792
|
+
* the one answer a proxy must never be given (a proxy driven by the local world is a body driving
|
|
793
|
+
* itself). So this is the D20 buffer and nothing else: the authoritative pose interpolated at
|
|
794
|
+
* `interpDelayMs` behind arrival, held at the newest delta and never extrapolated past it.
|
|
795
|
+
*
|
|
796
|
+
* Allocation-free, and only the physics channels: it runs once per proxy per frame.
|
|
797
|
+
*
|
|
798
|
+
* Two things `get` does are left out on purpose. `starved` is not counted, because that number
|
|
799
|
+
* belongs to the render path and a physics read landing in it would double it. And a buffer whose
|
|
800
|
+
* entity has aged out is not `gc`'d here, because a read that drives physics must not decide when
|
|
801
|
+
* the render path's buffers die.
|
|
802
|
+
*/
|
|
803
|
+
drawn(desc, id, into) {
|
|
804
|
+
const physics = desc.physics;
|
|
805
|
+
if (!physics) return false;
|
|
806
|
+
const coll = this.store.plain[desc.name];
|
|
807
|
+
const fill = (value) => {
|
|
808
|
+
if (value === void 0) return false;
|
|
809
|
+
for (const [, field] of physics.channels) {
|
|
810
|
+
const v = value[field];
|
|
811
|
+
if (typeof v === "number") into[field] = v;
|
|
812
|
+
}
|
|
813
|
+
return true;
|
|
814
|
+
};
|
|
815
|
+
if (!desc.interpolate) return fill(coll.get(id));
|
|
816
|
+
const buffer = this.buffers.get(desc.name)?.get(id);
|
|
817
|
+
if (!buffer || buffer.frames.length === 0) return fill(coll.get(id));
|
|
818
|
+
const renderT = this.renderTime();
|
|
819
|
+
if (buffer.removedAt !== void 0 && renderT >= buffer.removedAt) return false;
|
|
820
|
+
this.prune(buffer, renderT);
|
|
821
|
+
const frames = buffer.frames;
|
|
822
|
+
const a = frames[0];
|
|
823
|
+
if (renderT < a.t) return false;
|
|
824
|
+
const b = frames[1];
|
|
825
|
+
if (!b) return fill(a.value);
|
|
826
|
+
const span = b.t - a.t;
|
|
827
|
+
const alpha = span > 0 ? Math.min(1, Math.max(0, (renderT - a.t) / span)) : 1;
|
|
828
|
+
for (const [, field] of physics.channels) {
|
|
829
|
+
const av = a.value[field];
|
|
830
|
+
if (typeof av !== "number") continue;
|
|
831
|
+
const bv = b.value[field];
|
|
832
|
+
into[field] = typeof bv === "number" ? av + (bv - av) * alpha : av;
|
|
833
|
+
}
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
767
836
|
/** Is `collection[id]` visible at the render clock? */
|
|
768
837
|
has(desc, id) {
|
|
769
838
|
const coll = this.store.plain[desc.name];
|
|
@@ -1038,6 +1107,7 @@ var Session = class {
|
|
|
1038
1107
|
if (this.left) return;
|
|
1039
1108
|
this.predictor = predictor;
|
|
1040
1109
|
this.render.attachPredictor(predictor);
|
|
1110
|
+
predictor.setDrawnReader((desc, id, into) => this.render.drawn(desc, id, into));
|
|
1041
1111
|
if (this.joined) {
|
|
1042
1112
|
predictor.reset();
|
|
1043
1113
|
void predictor.start();
|
|
@@ -1047,7 +1117,7 @@ var Session = class {
|
|
|
1047
1117
|
const physics2d = options.physics2d;
|
|
1048
1118
|
if (physics && hasPhysics) {
|
|
1049
1119
|
this.predictionRequested = true;
|
|
1050
|
-
void import("./physics-
|
|
1120
|
+
void import("./physics-4SGKNBAC.js").then(({ PhysicsPredictor }) => {
|
|
1051
1121
|
if (this.left) return;
|
|
1052
1122
|
attach(
|
|
1053
1123
|
new PhysicsPredictor(
|
|
@@ -1060,15 +1130,32 @@ var Session = class {
|
|
|
1060
1130
|
)
|
|
1061
1131
|
);
|
|
1062
1132
|
});
|
|
1063
|
-
} else if (physics2d && hasPhysics) {
|
|
1133
|
+
} else if (physics2d !== void 0 && physics2d.engine === "rapier2d" && hasPhysics) {
|
|
1134
|
+
const rapier2d = physics2d;
|
|
1135
|
+
this.predictionRequested = true;
|
|
1136
|
+
void import("./physics-rapier2d-TBOAJMVM.js").then(({ Rapier2dPredictor }) => {
|
|
1137
|
+
if (this.left) return;
|
|
1138
|
+
attach(
|
|
1139
|
+
new Rapier2dPredictor(
|
|
1140
|
+
this.ext,
|
|
1141
|
+
this.store,
|
|
1142
|
+
rapier2d,
|
|
1143
|
+
() => this.me,
|
|
1144
|
+
() => this.rtt,
|
|
1145
|
+
() => this.tickIntervalMs
|
|
1146
|
+
)
|
|
1147
|
+
);
|
|
1148
|
+
});
|
|
1149
|
+
} else if (physics2d !== void 0 && physics2d.engine !== "rapier2d" && hasPhysics) {
|
|
1150
|
+
const matter2d = physics2d;
|
|
1064
1151
|
this.predictionRequested = true;
|
|
1065
|
-
void import("./physics2d-
|
|
1152
|
+
void import("./physics2d-CNSEWKP4.js").then(({ Physics2dPredictor }) => {
|
|
1066
1153
|
if (this.left) return;
|
|
1067
1154
|
attach(
|
|
1068
1155
|
new Physics2dPredictor(
|
|
1069
1156
|
this.ext,
|
|
1070
1157
|
this.store,
|
|
1071
|
-
|
|
1158
|
+
matter2d,
|
|
1072
1159
|
() => this.me,
|
|
1073
1160
|
() => this.rtt,
|
|
1074
1161
|
() => this.tickIntervalMs
|
|
@@ -1102,6 +1189,9 @@ var Session = class {
|
|
|
1102
1189
|
* whatever a `SCHEMA` frame replaced it with. Read instead of `options.schema` everywhere, so a
|
|
1103
1190
|
* swapped session announces its *current* hash when it reconnects rather than the one it was
|
|
1104
1191
|
* born with — otherwise a resume after a swap would be refused as a mismatch.
|
|
1192
|
+
*
|
|
1193
|
+
* D70 made it internally readable rather than private: `buildMessages` reads the declared
|
|
1194
|
+
* shapes off it when the room object is built. Still not on the public `Room` type.
|
|
1105
1195
|
*/
|
|
1106
1196
|
schema;
|
|
1107
1197
|
/** D50: how many times this session has swapped schema. Test seam and a diagnostic. */
|
|
@@ -1165,6 +1255,20 @@ var Session = class {
|
|
|
1165
1255
|
* `INTERNAL_SESSION`, the same route the D50 tests use for the session itself.
|
|
1166
1256
|
*/
|
|
1167
1257
|
voiceListeners = /* @__PURE__ */ new Set();
|
|
1258
|
+
/**
|
|
1259
|
+
* D70: typed-message listeners, by wire index, in their own map for the same structural reason
|
|
1260
|
+
* `voiceListeners` is its own set: a raw `onMessage` callback must never be handed a typed
|
|
1261
|
+
* frame and a typed callback must never be handed raw bytes. Splitting the registries makes
|
|
1262
|
+
* that a fact about the shape of the code rather than a filter somewhere in the fan-out.
|
|
1263
|
+
*/
|
|
1264
|
+
typedListeners = /* @__PURE__ */ new Map();
|
|
1265
|
+
/** D70: one warning per session for dropped typed messages — a flood must stay one line. */
|
|
1266
|
+
typedDropWarned = false;
|
|
1267
|
+
/**
|
|
1268
|
+
* D70: what `room.stats.messages` reads. `dropped` is the one worth watching: it counts typed
|
|
1269
|
+
* frames this client could not decode, which is a peer on a schema this one does not have.
|
|
1270
|
+
*/
|
|
1271
|
+
messageCounts = { sent: 0, received: 0, dropped: 0 };
|
|
1168
1272
|
cancelFlush;
|
|
1169
1273
|
cancelPing;
|
|
1170
1274
|
cancelRetry;
|
|
@@ -1628,15 +1732,62 @@ var Session = class {
|
|
|
1628
1732
|
if (pong.serverTick > this.tick) this.tick = pong.serverTick;
|
|
1629
1733
|
}
|
|
1630
1734
|
onMsg(payload) {
|
|
1631
|
-
|
|
1735
|
+
let msg;
|
|
1736
|
+
try {
|
|
1737
|
+
msg = decodeMsg(payload);
|
|
1738
|
+
} catch (err) {
|
|
1739
|
+
this.dropTypedMessage(err);
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1632
1742
|
const bytes = msg.payload.slice();
|
|
1633
1743
|
if (msg.target.kind === "voice") {
|
|
1634
1744
|
for (const cb of [...this.voiceListeners]) cb(bytes);
|
|
1635
1745
|
return;
|
|
1636
1746
|
}
|
|
1637
1747
|
const from = msg.target.kind === "client" ? msg.target.clientId : "server";
|
|
1748
|
+
if (msg.typed) {
|
|
1749
|
+
this.deliverTyped(msg.typed.index, from, bytes);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
this.messageCounts.received++;
|
|
1638
1753
|
for (const cb of [...this.messageListeners]) cb(from, bytes);
|
|
1639
1754
|
}
|
|
1755
|
+
/**
|
|
1756
|
+
* D70: decode one typed payload against the current schema and hand it to that shape's
|
|
1757
|
+
* listeners.
|
|
1758
|
+
*
|
|
1759
|
+
* Everything a hostile peer can do here ends in the same place: a counter and a dropped frame.
|
|
1760
|
+
* An index past the schema's list, a truncated payload, an oversize `str`, an over-max `list`,
|
|
1761
|
+
* an unknown enum member, or a well-formed value of the wrong shape — each is caught, none
|
|
1762
|
+
* throws out of the frame loop, none closes the socket, and the next frame is delivered
|
|
1763
|
+
* normally. The `console.warn` fires once per session so a flood stays one line.
|
|
1764
|
+
*/
|
|
1765
|
+
deliverTyped(index, from, bytes) {
|
|
1766
|
+
const desc = this.schema?.messages?.[index];
|
|
1767
|
+
if (!desc) {
|
|
1768
|
+
this.dropTypedMessage(new Error(`no message with index ${index} in this schema`));
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
let value;
|
|
1772
|
+
try {
|
|
1773
|
+
value = decodeFields(desc.fields, bytes);
|
|
1774
|
+
} catch (err) {
|
|
1775
|
+
this.dropTypedMessage(err);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
this.messageCounts.received++;
|
|
1779
|
+
const set = this.typedListeners.get(index);
|
|
1780
|
+
if (!set) return;
|
|
1781
|
+
for (const cb of [...set]) cb(from, value);
|
|
1782
|
+
}
|
|
1783
|
+
dropTypedMessage(err) {
|
|
1784
|
+
this.messageCounts.dropped++;
|
|
1785
|
+
if (this.typedDropWarned) return;
|
|
1786
|
+
this.typedDropWarned = true;
|
|
1787
|
+
console.warn(
|
|
1788
|
+
`irtio: dropped a typed message this client could not read (${err instanceof Error ? err.message : String(err)}). Read room.stats.messages.dropped for the count; further drops are silent.`
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1640
1791
|
// -------------------------------------------------------------------------
|
|
1641
1792
|
// Timers
|
|
1642
1793
|
// -------------------------------------------------------------------------
|
|
@@ -1707,7 +1858,7 @@ var Session = class {
|
|
|
1707
1858
|
let encoded;
|
|
1708
1859
|
try {
|
|
1709
1860
|
desc = this.descOf(name);
|
|
1710
|
-
encoded =
|
|
1861
|
+
encoded = encodeFields2(desc.params, params);
|
|
1711
1862
|
} catch (err) {
|
|
1712
1863
|
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
1713
1864
|
}
|
|
@@ -1728,7 +1879,10 @@ var Session = class {
|
|
|
1728
1879
|
reject,
|
|
1729
1880
|
cancelTimeout
|
|
1730
1881
|
});
|
|
1731
|
-
this.send(
|
|
1882
|
+
this.send(
|
|
1883
|
+
FrameType.CALL,
|
|
1884
|
+
encodeCall({ reqId, rpcId: desc.index, clientTick: this.tick, params: encoded })
|
|
1885
|
+
);
|
|
1732
1886
|
});
|
|
1733
1887
|
}
|
|
1734
1888
|
async requestOwnership(entity, id) {
|
|
@@ -1802,7 +1956,7 @@ var Session = class {
|
|
|
1802
1956
|
replyOk(desc, reqId, result) {
|
|
1803
1957
|
let bytes;
|
|
1804
1958
|
try {
|
|
1805
|
-
bytes = desc.returns ?
|
|
1959
|
+
bytes = desc.returns ? encodeFields2(desc.returns, result ?? {}) : new Uint8Array(0);
|
|
1806
1960
|
} catch (err) {
|
|
1807
1961
|
this.replyError(reqId, err instanceof Error ? err.message : String(err));
|
|
1808
1962
|
return;
|
|
@@ -1824,6 +1978,7 @@ var Session = class {
|
|
|
1824
1978
|
// -------------------------------------------------------------------------
|
|
1825
1979
|
message(target, bytes) {
|
|
1826
1980
|
const wire = target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
|
|
1981
|
+
this.messageCounts.sent++;
|
|
1827
1982
|
this.send(FrameType.MSG, encodeMsg({ target: wire, payload: bytes }));
|
|
1828
1983
|
}
|
|
1829
1984
|
onMessage(cb) {
|
|
@@ -1832,6 +1987,27 @@ var Session = class {
|
|
|
1832
1987
|
this.messageListeners.delete(cb);
|
|
1833
1988
|
};
|
|
1834
1989
|
}
|
|
1990
|
+
/**
|
|
1991
|
+
* D70: send one typed message. `payload` is already `encodeFields`'d by the caller
|
|
1992
|
+
* (`buildMessages`), which is where the shape and the throw on a bad value belong.
|
|
1993
|
+
*/
|
|
1994
|
+
typedMessage(index, target, payload) {
|
|
1995
|
+
const wire = target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
|
|
1996
|
+
this.messageCounts.sent++;
|
|
1997
|
+
this.send(FrameType.MSG, encodeMsg({ target: wire, payload, typed: { index } }));
|
|
1998
|
+
}
|
|
1999
|
+
/** D70: subscribe to one message shape by wire index. Raw listeners never see these frames. */
|
|
2000
|
+
onTypedMessage(index, cb) {
|
|
2001
|
+
let set = this.typedListeners.get(index);
|
|
2002
|
+
if (!set) {
|
|
2003
|
+
set = /* @__PURE__ */ new Set();
|
|
2004
|
+
this.typedListeners.set(index, set);
|
|
2005
|
+
}
|
|
2006
|
+
set.add(cb);
|
|
2007
|
+
return () => {
|
|
2008
|
+
set?.delete(cb);
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
1835
2011
|
/**
|
|
1836
2012
|
* D51: write one voice signaling message. Internal — reached only through `INTERNAL_SESSION`,
|
|
1837
2013
|
* so it is not on the `Room` type and a game cannot call it.
|
|
@@ -2032,6 +2208,233 @@ function isRoomFull(err) {
|
|
|
2032
2208
|
const message = err instanceof Error ? err.message : String(err);
|
|
2033
2209
|
return message.startsWith("E_ROOM_FULL");
|
|
2034
2210
|
}
|
|
2211
|
+
async function findPublic(project, options = {}) {
|
|
2212
|
+
const controlUrl = (options.controlUrl ?? DEFAULT_CONTROL_URL).replace(/\/+$/, "");
|
|
2213
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
2214
|
+
let res;
|
|
2215
|
+
try {
|
|
2216
|
+
res = await fetchImpl(`${controlUrl}/match`, {
|
|
2217
|
+
method: "POST",
|
|
2218
|
+
headers: { "content-type": "application/json" },
|
|
2219
|
+
body: JSON.stringify({
|
|
2220
|
+
project,
|
|
2221
|
+
mode: "public",
|
|
2222
|
+
...options.queue !== void 0 ? { queue: options.queue } : {},
|
|
2223
|
+
...options.identity !== void 0 ? { identity: options.identity } : {},
|
|
2224
|
+
...options.exclude !== void 0 ? { exclude: options.exclude } : {}
|
|
2225
|
+
})
|
|
2226
|
+
});
|
|
2227
|
+
} catch (err) {
|
|
2228
|
+
throw new MatchError(
|
|
2229
|
+
"E_MATCH_UNREACHABLE",
|
|
2230
|
+
`cannot reach the matchmaker at ${controlUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
const text = await res.text().catch(() => "");
|
|
2234
|
+
let body = {};
|
|
2235
|
+
try {
|
|
2236
|
+
body = JSON.parse(text);
|
|
2237
|
+
} catch {
|
|
2238
|
+
}
|
|
2239
|
+
if (!res.ok) {
|
|
2240
|
+
throw new MatchError(
|
|
2241
|
+
typeof body.code === "string" ? body.code : `E_HTTP_${res.status}`,
|
|
2242
|
+
typeof body.message === "string" ? body.message : `match failed with status ${res.status}`
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
if (body.status !== "matched" || typeof body.room !== "string") {
|
|
2246
|
+
throw new MatchError("E_MATCH_MALFORMED", "the matchmaker answered something unexpected");
|
|
2247
|
+
}
|
|
2248
|
+
return {
|
|
2249
|
+
room: body.room,
|
|
2250
|
+
queue: typeof body.queue === "string" ? body.queue : "default",
|
|
2251
|
+
size: typeof body.size === "number" ? body.size : 0,
|
|
2252
|
+
created: body.created === true
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
async function joinPublic(schema, options = {}) {
|
|
2256
|
+
const project = schema.project;
|
|
2257
|
+
const key = options.key ?? (typeof project === "string" ? project : void 0);
|
|
2258
|
+
if (key === void 0 || key === "") {
|
|
2259
|
+
throw new MatchError(
|
|
2260
|
+
"E_MATCH_NO_PROJECT",
|
|
2261
|
+
"joinPublic needs a project key: build your schema with `irtio` so it carries one, or pass { key }"
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
const identity = options.identity === true ? new Identity({
|
|
2265
|
+
project: key,
|
|
2266
|
+
controlUrl: options.controlUrl,
|
|
2267
|
+
fetch: options.fetch
|
|
2268
|
+
}) : void 0;
|
|
2269
|
+
const ask = async (exclude) => findPublic(key, {
|
|
2270
|
+
...options.queue !== void 0 ? { queue: options.queue } : {},
|
|
2271
|
+
...options.controlUrl !== void 0 ? { controlUrl: options.controlUrl } : {},
|
|
2272
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {},
|
|
2273
|
+
...identity !== void 0 ? { identity: await identity.ensure() } : {},
|
|
2274
|
+
...exclude !== void 0 ? { exclude } : {}
|
|
2275
|
+
});
|
|
2276
|
+
const ticket = await ask();
|
|
2277
|
+
try {
|
|
2278
|
+
return await joinRoom(schema, { ...options, room: ticket.room });
|
|
2279
|
+
} catch (err) {
|
|
2280
|
+
if (!isRetryablePublicJoin(err)) throw err;
|
|
2281
|
+
const second = await ask(ticket.room);
|
|
2282
|
+
return joinRoom(schema, { ...options, room: second.room });
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
function isRetryablePublicJoin(err) {
|
|
2286
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2287
|
+
return message.startsWith("E_ROOM_FULL") || message.startsWith("E_ROOM_NOT_FOUND");
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// src/lobby.ts
|
|
2291
|
+
var LOBBY_STATE = "irtLobby";
|
|
2292
|
+
var LOBBY_MEMBERS = "irtLobbyMembers";
|
|
2293
|
+
var LOBBY_POLL_MS = 120;
|
|
2294
|
+
function hasLobby(room) {
|
|
2295
|
+
const state = room?.state;
|
|
2296
|
+
return typeof state === "object" && state !== null && state[LOBBY_STATE] !== void 0 && state[LOBBY_MEMBERS] !== void 0;
|
|
2297
|
+
}
|
|
2298
|
+
function lobbyOf(room, options = {}) {
|
|
2299
|
+
if (!hasLobby(room)) return void 0;
|
|
2300
|
+
const r = room;
|
|
2301
|
+
const singleton = () => r.state[LOBBY_STATE] ?? {};
|
|
2302
|
+
const members = () => r.state[LOBBY_MEMBERS];
|
|
2303
|
+
const players = () => r.clients.map((c) => ({
|
|
2304
|
+
clientId: c.clientId,
|
|
2305
|
+
ready: members().get(c.clientId)?.ready === true,
|
|
2306
|
+
connected: c.connected !== false,
|
|
2307
|
+
me: c.clientId === r.me
|
|
2308
|
+
}));
|
|
2309
|
+
const signature = () => {
|
|
2310
|
+
const s = singleton();
|
|
2311
|
+
let out = `${s.phase ?? ""}|${s.public === true ? 1 : 0}|${s.readyUi === true ? 1 : 0}|${s.capacity ?? 0}`;
|
|
2312
|
+
for (const p of players()) out += `|${p.clientId}${p.ready ? 1 : 0}${p.connected ? 1 : 0}`;
|
|
2313
|
+
return out;
|
|
2314
|
+
};
|
|
2315
|
+
return {
|
|
2316
|
+
get phase() {
|
|
2317
|
+
return singleton().phase === "started" ? "started" : "lobby";
|
|
2318
|
+
},
|
|
2319
|
+
get players() {
|
|
2320
|
+
return players();
|
|
2321
|
+
},
|
|
2322
|
+
get public() {
|
|
2323
|
+
return singleton().public === true;
|
|
2324
|
+
},
|
|
2325
|
+
get readyUi() {
|
|
2326
|
+
return singleton().readyUi === true;
|
|
2327
|
+
},
|
|
2328
|
+
get capacity() {
|
|
2329
|
+
return singleton().capacity ?? 0;
|
|
2330
|
+
},
|
|
2331
|
+
ready(value) {
|
|
2332
|
+
const mine = members().get(r.me);
|
|
2333
|
+
if (!mine) return;
|
|
2334
|
+
mine.ready = value === true;
|
|
2335
|
+
r.flush();
|
|
2336
|
+
},
|
|
2337
|
+
...options.setPublic !== void 0 ? { setPublic: options.setPublic } : {},
|
|
2338
|
+
on(event, cb) {
|
|
2339
|
+
if (event !== "change") return () => {
|
|
2340
|
+
};
|
|
2341
|
+
let last = signature();
|
|
2342
|
+
const sample = () => {
|
|
2343
|
+
const next = signature();
|
|
2344
|
+
if (next === last) return;
|
|
2345
|
+
last = next;
|
|
2346
|
+
cb();
|
|
2347
|
+
};
|
|
2348
|
+
const offClients = r.on("clients", sample);
|
|
2349
|
+
const timer = setInterval(sample, LOBBY_POLL_MS);
|
|
2350
|
+
timer.unref?.();
|
|
2351
|
+
return () => {
|
|
2352
|
+
offClients();
|
|
2353
|
+
clearInterval(timer);
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function attachable(room, view) {
|
|
2359
|
+
return {
|
|
2360
|
+
get id() {
|
|
2361
|
+
return room.id;
|
|
2362
|
+
},
|
|
2363
|
+
get link() {
|
|
2364
|
+
return room.link;
|
|
2365
|
+
},
|
|
2366
|
+
get status() {
|
|
2367
|
+
return room.status;
|
|
2368
|
+
},
|
|
2369
|
+
get clients() {
|
|
2370
|
+
return room.clients;
|
|
2371
|
+
},
|
|
2372
|
+
get maxClients() {
|
|
2373
|
+
return room.maxClients;
|
|
2374
|
+
},
|
|
2375
|
+
get rtt() {
|
|
2376
|
+
return room.rtt;
|
|
2377
|
+
},
|
|
2378
|
+
on: (event, cb) => room.on(event, cb),
|
|
2379
|
+
...view !== void 0 ? { lobby: view } : {}
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
function attachLobby(element, schema, options = {}) {
|
|
2383
|
+
let detachRoom;
|
|
2384
|
+
let busy = false;
|
|
2385
|
+
const fail = (err) => {
|
|
2386
|
+
element.removeAttribute("searching");
|
|
2387
|
+
if (options.onError) {
|
|
2388
|
+
options.onError(err);
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
queueMicrotask(() => {
|
|
2392
|
+
throw err;
|
|
2393
|
+
});
|
|
2394
|
+
};
|
|
2395
|
+
const landed = (room) => {
|
|
2396
|
+
element.removeAttribute("searching");
|
|
2397
|
+
const view = lobbyOf(room, {
|
|
2398
|
+
...options.setPublic !== void 0 ? { setPublic: (value) => options.setPublic?.(room, value) } : {}
|
|
2399
|
+
});
|
|
2400
|
+
detachRoom = element.attach(
|
|
2401
|
+
attachable(room, view)
|
|
2402
|
+
);
|
|
2403
|
+
options.onRoom?.(room);
|
|
2404
|
+
};
|
|
2405
|
+
const run = async (make) => {
|
|
2406
|
+
if (busy) return;
|
|
2407
|
+
busy = true;
|
|
2408
|
+
try {
|
|
2409
|
+
landed(await make());
|
|
2410
|
+
} catch (err) {
|
|
2411
|
+
fail(err);
|
|
2412
|
+
} finally {
|
|
2413
|
+
busy = false;
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
const onPrivate = () => {
|
|
2417
|
+
void run(() => joinRoom(schema, { ...options.join ?? {} }));
|
|
2418
|
+
};
|
|
2419
|
+
const onQuickMatch = (event) => {
|
|
2420
|
+
const detail = event.detail;
|
|
2421
|
+
element.setAttribute("searching", "");
|
|
2422
|
+
void run(
|
|
2423
|
+
() => joinPublic(schema, {
|
|
2424
|
+
...options.join ?? {},
|
|
2425
|
+
...options.match ?? {},
|
|
2426
|
+
...detail?.queue !== void 0 ? { queue: detail.queue } : {}
|
|
2427
|
+
})
|
|
2428
|
+
);
|
|
2429
|
+
};
|
|
2430
|
+
element.addEventListener("private", onPrivate);
|
|
2431
|
+
element.addEventListener("quickmatch", onQuickMatch);
|
|
2432
|
+
return () => {
|
|
2433
|
+
element.removeEventListener("private", onPrivate);
|
|
2434
|
+
element.removeEventListener("quickmatch", onQuickMatch);
|
|
2435
|
+
detachRoom?.();
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2035
2438
|
|
|
2036
2439
|
// src/voice.ts
|
|
2037
2440
|
import {
|
|
@@ -2403,6 +2806,8 @@ async function joinRoom(schema, options = {}) {
|
|
|
2403
2806
|
}
|
|
2404
2807
|
function makeRoom(session) {
|
|
2405
2808
|
const call = callProxy(session);
|
|
2809
|
+
const messages = buildMessages(session.schema, session);
|
|
2810
|
+
const stats = { messages: session.messageCounts };
|
|
2406
2811
|
const room = {
|
|
2407
2812
|
get me() {
|
|
2408
2813
|
return session.me;
|
|
@@ -2446,6 +2851,8 @@ function makeRoom(session) {
|
|
|
2446
2851
|
return session.predictor?.ready ?? false;
|
|
2447
2852
|
},
|
|
2448
2853
|
predicts: (collection, id) => session.predictor?.has(collection, id) ?? false,
|
|
2854
|
+
// ---- M6 lane D: proxies ----
|
|
2855
|
+
proxied: (collection, id) => session.predictor?.hasProxy(collection, id) ?? false,
|
|
2449
2856
|
get stats() {
|
|
2450
2857
|
return session.predictor?.stats ?? emptyPredictionStats();
|
|
2451
2858
|
}
|
|
@@ -2455,6 +2862,8 @@ function makeRoom(session) {
|
|
|
2455
2862
|
requestOwnership: (entity, id) => session.requestOwnership(entity, id),
|
|
2456
2863
|
message: (target, bytes) => session.message(target, bytes),
|
|
2457
2864
|
onMessage: (cb) => session.onMessage(cb),
|
|
2865
|
+
messages,
|
|
2866
|
+
stats,
|
|
2458
2867
|
on: (event, cb) => session.on(event, cb),
|
|
2459
2868
|
flush: () => session.flush(),
|
|
2460
2869
|
leave: () => session.leave()
|
|
@@ -2468,7 +2877,11 @@ async function joinRelay(options = {}) {
|
|
|
2468
2877
|
const fromLocation = !explicitRoom && currentLocation() !== void 0;
|
|
2469
2878
|
const roomId = explicitRoom ? roomIdFrom(options.room ?? "") : fromLocation ? roomIdFromLocation() : "";
|
|
2470
2879
|
const session = new Session({
|
|
2471
|
-
|
|
2880
|
+
// ---- M6 lane C: typed messages ----
|
|
2881
|
+
// D70: a relay join may now bring the project's deployed schema. This is a type change and a
|
|
2882
|
+
// hash change, not a transport change: the session already chose between the builder's schema
|
|
2883
|
+
// and `relaySchema`, and already put `schema.hash8` or `RELAY_HASH8` in the HELLO.
|
|
2884
|
+
schema: options.schema,
|
|
2472
2885
|
url,
|
|
2473
2886
|
key: options.key ?? resolveKey(void 0, void 0, url),
|
|
2474
2887
|
roomId,
|
|
@@ -2505,6 +2918,8 @@ async function joinRelay(options = {}) {
|
|
|
2505
2918
|
},
|
|
2506
2919
|
message: (target, bytes) => session.message(target, bytes),
|
|
2507
2920
|
onMessage: (cb) => session.onMessage(cb),
|
|
2921
|
+
messages: buildMessages(session.schema, session),
|
|
2922
|
+
stats: { messages: session.messageCounts },
|
|
2508
2923
|
on: (event, cb) => session.on(event, cb),
|
|
2509
2924
|
leave: () => session.leave()
|
|
2510
2925
|
};
|
|
@@ -2522,8 +2937,12 @@ export {
|
|
|
2522
2937
|
INTERNAL_VOICE_TRACKS,
|
|
2523
2938
|
Identity,
|
|
2524
2939
|
IdentityError,
|
|
2940
|
+
LOBBY_MEMBERS,
|
|
2941
|
+
LOBBY_POLL_MS,
|
|
2942
|
+
LOBBY_STATE,
|
|
2525
2943
|
MAX_IDENTITY_RETRY_WAIT_MS,
|
|
2526
2944
|
MAX_PREDICTED_BODIES,
|
|
2945
|
+
MAX_PROXY_BODIES,
|
|
2527
2946
|
MatchError,
|
|
2528
2947
|
PING_INTERVAL_MS,
|
|
2529
2948
|
PREDICTION_EPSILON,
|
|
@@ -2532,14 +2951,20 @@ export {
|
|
|
2532
2951
|
SMOOTHING_HALF_LIFE_MS,
|
|
2533
2952
|
SMOOTHING_SNAP_UNITS,
|
|
2534
2953
|
Session,
|
|
2954
|
+
attachLobby,
|
|
2955
|
+
attachable,
|
|
2535
2956
|
createParty,
|
|
2536
2957
|
defaultScheduler,
|
|
2537
2958
|
findMatch,
|
|
2959
|
+
findPublic,
|
|
2960
|
+
hasLobby,
|
|
2538
2961
|
identityStorageKey,
|
|
2962
|
+
joinPublic,
|
|
2539
2963
|
joinRelay,
|
|
2540
2964
|
joinRoom,
|
|
2541
2965
|
joinVoice,
|
|
2542
2966
|
linkForUrl,
|
|
2967
|
+
lobbyOf,
|
|
2543
2968
|
matchRoom,
|
|
2544
2969
|
resolveUrl,
|
|
2545
2970
|
roomIdFrom,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Predictor
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-XFD6WYQW.js";
|
|
4
|
+
import "./chunk-5Z4DHUA3.js";
|
|
5
5
|
|
|
6
6
|
// src/physics.ts
|
|
7
7
|
function readPose(body, into) {
|
|
@@ -100,6 +100,25 @@ var RapierAdapter = class {
|
|
|
100
100
|
removeBody(body) {
|
|
101
101
|
this.world?.removeRigidBody(body);
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* D71: a proxy is a `KinematicPositionBased` body — driven by the position it is told to be at
|
|
105
|
+
* next, never by a force, an impulse or a contact. Rapier derives the velocity its contacts see
|
|
106
|
+
* from the move itself, which is what carries a predicted body standing on a moving platform.
|
|
107
|
+
*
|
|
108
|
+
* The body is *switched* rather than built kinematic, so its collider, mass properties and axis
|
|
109
|
+
* locks are the ones the shared factory produced. A proxy has to be the server's body, and the
|
|
110
|
+
* only way to keep that true is to run the same factory call the predicted path runs.
|
|
111
|
+
*/
|
|
112
|
+
makeKinematic(handle) {
|
|
113
|
+
const rapier = this.rapier;
|
|
114
|
+
if (!rapier) return;
|
|
115
|
+
handle.setBodyType(rapier.RigidBodyType.KinematicPositionBased, true);
|
|
116
|
+
}
|
|
117
|
+
moveKinematic(handle, pose) {
|
|
118
|
+
const body = handle;
|
|
119
|
+
body.setNextKinematicTranslation(pose.t);
|
|
120
|
+
body.setNextKinematicRotation(pose.r);
|
|
121
|
+
}
|
|
103
122
|
step() {
|
|
104
123
|
this.world?.step();
|
|
105
124
|
}
|
|
@@ -191,6 +210,10 @@ var PhysicsPredictor = class extends Predictor {
|
|
|
191
210
|
super(ext, store, new RapierAdapter(options), options, meOf, rttOf, tickIntervalOf, log);
|
|
192
211
|
}
|
|
193
212
|
};
|
|
213
|
+
function createRapierAdapter(options) {
|
|
214
|
+
return new RapierAdapter(options);
|
|
215
|
+
}
|
|
194
216
|
export {
|
|
195
|
-
PhysicsPredictor
|
|
217
|
+
PhysicsPredictor,
|
|
218
|
+
createRapierAdapter
|
|
196
219
|
};
|