@irtio/client 0.7.0 → 0.9.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-XWVXZRBS.js → chunk-5OHONPVG.js} +142 -0
- package/dist/{chunk-DABQDR3S.js → chunk-VZFJXPVF.js} +7 -8
- package/dist/index.d.ts +439 -11
- package/dist/index.js +291 -8
- package/dist/{physics-RT5T36P5.js → physics-D65WIHPR.js} +2 -2
- package/dist/physics-rapier2d-O53E6BHO.js +186 -0
- package/dist/{physics2d-HOFMWPZV.js → physics2d-GIL6YYAY.js} +2 -2
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
SMOOTHING_HALF_LIFE_MS,
|
|
9
9
|
SMOOTHING_SNAP_UNITS,
|
|
10
10
|
emptyPredictionStats
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-5OHONPVG.js";
|
|
12
12
|
|
|
13
13
|
// src/index.ts
|
|
14
14
|
import { EMPTY_PROFILE as EMPTY_PROFILE2 } from "@irtio/protocol";
|
|
@@ -1065,6 +1065,7 @@ var DEFAULT_WRITE_INTERVAL_MS = 50;
|
|
|
1065
1065
|
var PING_INTERVAL_MS = 2e3;
|
|
1066
1066
|
var RTT_ALPHA = 0.3;
|
|
1067
1067
|
var CALL_TIMEOUT_MS = 1e4;
|
|
1068
|
+
var LEAVE_CLOSE_TIMEOUT_MS = 1e3;
|
|
1068
1069
|
var BACKOFF_START_MS = 250;
|
|
1069
1070
|
var BACKOFF_MAX_MS = 5e3;
|
|
1070
1071
|
function isThenable(v) {
|
|
@@ -1117,7 +1118,7 @@ var Session = class {
|
|
|
1117
1118
|
const physics2d = options.physics2d;
|
|
1118
1119
|
if (physics && hasPhysics) {
|
|
1119
1120
|
this.predictionRequested = true;
|
|
1120
|
-
void import("./physics-
|
|
1121
|
+
void import("./physics-D65WIHPR.js").then(({ PhysicsPredictor }) => {
|
|
1121
1122
|
if (this.left) return;
|
|
1122
1123
|
attach(
|
|
1123
1124
|
new PhysicsPredictor(
|
|
@@ -1130,15 +1131,32 @@ var Session = class {
|
|
|
1130
1131
|
)
|
|
1131
1132
|
);
|
|
1132
1133
|
});
|
|
1133
|
-
} else if (physics2d && hasPhysics) {
|
|
1134
|
+
} else if (physics2d !== void 0 && physics2d.engine === "rapier2d" && hasPhysics) {
|
|
1135
|
+
const rapier2d = physics2d;
|
|
1134
1136
|
this.predictionRequested = true;
|
|
1135
|
-
void import("./
|
|
1137
|
+
void import("./physics-rapier2d-O53E6BHO.js").then(({ Rapier2dPredictor }) => {
|
|
1138
|
+
if (this.left) return;
|
|
1139
|
+
attach(
|
|
1140
|
+
new Rapier2dPredictor(
|
|
1141
|
+
this.ext,
|
|
1142
|
+
this.store,
|
|
1143
|
+
rapier2d,
|
|
1144
|
+
() => this.me,
|
|
1145
|
+
() => this.rtt,
|
|
1146
|
+
() => this.tickIntervalMs
|
|
1147
|
+
)
|
|
1148
|
+
);
|
|
1149
|
+
});
|
|
1150
|
+
} else if (physics2d !== void 0 && physics2d.engine !== "rapier2d" && hasPhysics) {
|
|
1151
|
+
const matter2d = physics2d;
|
|
1152
|
+
this.predictionRequested = true;
|
|
1153
|
+
void import("./physics2d-GIL6YYAY.js").then(({ Physics2dPredictor }) => {
|
|
1136
1154
|
if (this.left) return;
|
|
1137
1155
|
attach(
|
|
1138
1156
|
new Physics2dPredictor(
|
|
1139
1157
|
this.ext,
|
|
1140
1158
|
this.store,
|
|
1141
|
-
|
|
1159
|
+
matter2d,
|
|
1142
1160
|
() => this.me,
|
|
1143
1161
|
() => this.rtt,
|
|
1144
1162
|
() => this.tickIntervalMs
|
|
@@ -1362,18 +1380,45 @@ var Session = class {
|
|
|
1362
1380
|
});
|
|
1363
1381
|
this.send(FrameType.HELLO, hello);
|
|
1364
1382
|
}
|
|
1365
|
-
/**
|
|
1383
|
+
/**
|
|
1384
|
+
* Leaves for good: no reconnect, pending calls reject, the socket closes.
|
|
1385
|
+
*
|
|
1386
|
+
* The promise resolves when the socket reports its close (or immediately when there was
|
|
1387
|
+
* nothing open), so `await room.leave()` is a real wait rather than a guessed timeout. A
|
|
1388
|
+
* transport that never reports one is bounded by `LEAVE_CLOSE_TIMEOUT_MS` so the caller cannot
|
|
1389
|
+
* hang on it.
|
|
1390
|
+
*/
|
|
1366
1391
|
leave() {
|
|
1367
|
-
if (this.left) return;
|
|
1392
|
+
if (this.left) return Promise.resolve();
|
|
1368
1393
|
this.left = true;
|
|
1369
1394
|
this.stopTimers();
|
|
1370
1395
|
this.predictor?.free();
|
|
1371
1396
|
this.rejectPending(new Error("irtio: left the room"));
|
|
1372
1397
|
this.setStatus("closed");
|
|
1373
1398
|
if (this.socket && this.joined) this.send(FrameType.LEAVE, new Uint8Array(0));
|
|
1374
|
-
this.socket
|
|
1399
|
+
const socket = this.socket;
|
|
1375
1400
|
this.socket = void 0;
|
|
1376
1401
|
this.socketOpen = false;
|
|
1402
|
+
if (!socket) {
|
|
1403
|
+
return Promise.resolve();
|
|
1404
|
+
}
|
|
1405
|
+
return new Promise((resolve) => {
|
|
1406
|
+
let settled = false;
|
|
1407
|
+
let cancelTimeout;
|
|
1408
|
+
const finish = () => {
|
|
1409
|
+
if (settled) return;
|
|
1410
|
+
settled = true;
|
|
1411
|
+
cancelTimeout?.();
|
|
1412
|
+
resolve();
|
|
1413
|
+
};
|
|
1414
|
+
const previous = socket.onclose;
|
|
1415
|
+
socket.onclose = (info) => {
|
|
1416
|
+
previous?.(info);
|
|
1417
|
+
finish();
|
|
1418
|
+
};
|
|
1419
|
+
socket.close();
|
|
1420
|
+
if (!settled) cancelTimeout = this.scheduler.setTimeout(finish, LEAVE_CLOSE_TIMEOUT_MS);
|
|
1421
|
+
});
|
|
1377
1422
|
}
|
|
1378
1423
|
fatal(error) {
|
|
1379
1424
|
this.left = true;
|
|
@@ -2191,6 +2236,233 @@ function isRoomFull(err) {
|
|
|
2191
2236
|
const message = err instanceof Error ? err.message : String(err);
|
|
2192
2237
|
return message.startsWith("E_ROOM_FULL");
|
|
2193
2238
|
}
|
|
2239
|
+
async function findPublic(project, options = {}) {
|
|
2240
|
+
const controlUrl = (options.controlUrl ?? DEFAULT_CONTROL_URL).replace(/\/+$/, "");
|
|
2241
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
2242
|
+
let res;
|
|
2243
|
+
try {
|
|
2244
|
+
res = await fetchImpl(`${controlUrl}/match`, {
|
|
2245
|
+
method: "POST",
|
|
2246
|
+
headers: { "content-type": "application/json" },
|
|
2247
|
+
body: JSON.stringify({
|
|
2248
|
+
project,
|
|
2249
|
+
mode: "public",
|
|
2250
|
+
...options.queue !== void 0 ? { queue: options.queue } : {},
|
|
2251
|
+
...options.identity !== void 0 ? { identity: options.identity } : {},
|
|
2252
|
+
...options.exclude !== void 0 ? { exclude: options.exclude } : {}
|
|
2253
|
+
})
|
|
2254
|
+
});
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
throw new MatchError(
|
|
2257
|
+
"E_MATCH_UNREACHABLE",
|
|
2258
|
+
`cannot reach the matchmaker at ${controlUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
const text = await res.text().catch(() => "");
|
|
2262
|
+
let body = {};
|
|
2263
|
+
try {
|
|
2264
|
+
body = JSON.parse(text);
|
|
2265
|
+
} catch {
|
|
2266
|
+
}
|
|
2267
|
+
if (!res.ok) {
|
|
2268
|
+
throw new MatchError(
|
|
2269
|
+
typeof body.code === "string" ? body.code : `E_HTTP_${res.status}`,
|
|
2270
|
+
typeof body.message === "string" ? body.message : `match failed with status ${res.status}`
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
if (body.status !== "matched" || typeof body.room !== "string") {
|
|
2274
|
+
throw new MatchError("E_MATCH_MALFORMED", "the matchmaker answered something unexpected");
|
|
2275
|
+
}
|
|
2276
|
+
return {
|
|
2277
|
+
room: body.room,
|
|
2278
|
+
queue: typeof body.queue === "string" ? body.queue : "default",
|
|
2279
|
+
size: typeof body.size === "number" ? body.size : 0,
|
|
2280
|
+
created: body.created === true
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
async function joinPublic(schema, options = {}) {
|
|
2284
|
+
const project = schema.project;
|
|
2285
|
+
const key = options.key ?? (typeof project === "string" ? project : void 0);
|
|
2286
|
+
if (key === void 0 || key === "") {
|
|
2287
|
+
throw new MatchError(
|
|
2288
|
+
"E_MATCH_NO_PROJECT",
|
|
2289
|
+
"joinPublic needs a project key: build your schema with `irtio` so it carries one, or pass { key }"
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
const identity = options.identity === true ? new Identity({
|
|
2293
|
+
project: key,
|
|
2294
|
+
controlUrl: options.controlUrl,
|
|
2295
|
+
fetch: options.fetch
|
|
2296
|
+
}) : void 0;
|
|
2297
|
+
const ask = async (exclude) => findPublic(key, {
|
|
2298
|
+
...options.queue !== void 0 ? { queue: options.queue } : {},
|
|
2299
|
+
...options.controlUrl !== void 0 ? { controlUrl: options.controlUrl } : {},
|
|
2300
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {},
|
|
2301
|
+
...identity !== void 0 ? { identity: await identity.ensure() } : {},
|
|
2302
|
+
...exclude !== void 0 ? { exclude } : {}
|
|
2303
|
+
});
|
|
2304
|
+
const ticket = await ask();
|
|
2305
|
+
try {
|
|
2306
|
+
return await joinRoom(schema, { ...options, room: ticket.room });
|
|
2307
|
+
} catch (err) {
|
|
2308
|
+
if (!isRetryablePublicJoin(err)) throw err;
|
|
2309
|
+
const second = await ask(ticket.room);
|
|
2310
|
+
return joinRoom(schema, { ...options, room: second.room });
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
function isRetryablePublicJoin(err) {
|
|
2314
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2315
|
+
return message.startsWith("E_ROOM_FULL") || message.startsWith("E_ROOM_NOT_FOUND");
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// src/lobby.ts
|
|
2319
|
+
var LOBBY_STATE = "irtLobby";
|
|
2320
|
+
var LOBBY_MEMBERS = "irtLobbyMembers";
|
|
2321
|
+
var LOBBY_POLL_MS = 120;
|
|
2322
|
+
function hasLobby(room) {
|
|
2323
|
+
const state = room?.state;
|
|
2324
|
+
return typeof state === "object" && state !== null && state[LOBBY_STATE] !== void 0 && state[LOBBY_MEMBERS] !== void 0;
|
|
2325
|
+
}
|
|
2326
|
+
function lobbyOf(room, options = {}) {
|
|
2327
|
+
if (!hasLobby(room)) return void 0;
|
|
2328
|
+
const r = room;
|
|
2329
|
+
const singleton = () => r.state[LOBBY_STATE] ?? {};
|
|
2330
|
+
const members = () => r.state[LOBBY_MEMBERS];
|
|
2331
|
+
const players = () => r.clients.map((c) => ({
|
|
2332
|
+
clientId: c.clientId,
|
|
2333
|
+
ready: members().get(c.clientId)?.ready === true,
|
|
2334
|
+
connected: c.connected !== false,
|
|
2335
|
+
me: c.clientId === r.me
|
|
2336
|
+
}));
|
|
2337
|
+
const signature = () => {
|
|
2338
|
+
const s = singleton();
|
|
2339
|
+
let out = `${s.phase ?? ""}|${s.public === true ? 1 : 0}|${s.readyUi === true ? 1 : 0}|${s.capacity ?? 0}`;
|
|
2340
|
+
for (const p of players()) out += `|${p.clientId}${p.ready ? 1 : 0}${p.connected ? 1 : 0}`;
|
|
2341
|
+
return out;
|
|
2342
|
+
};
|
|
2343
|
+
return {
|
|
2344
|
+
get phase() {
|
|
2345
|
+
return singleton().phase === "started" ? "started" : "lobby";
|
|
2346
|
+
},
|
|
2347
|
+
get players() {
|
|
2348
|
+
return players();
|
|
2349
|
+
},
|
|
2350
|
+
get public() {
|
|
2351
|
+
return singleton().public === true;
|
|
2352
|
+
},
|
|
2353
|
+
get readyUi() {
|
|
2354
|
+
return singleton().readyUi === true;
|
|
2355
|
+
},
|
|
2356
|
+
get capacity() {
|
|
2357
|
+
return singleton().capacity ?? 0;
|
|
2358
|
+
},
|
|
2359
|
+
ready(value) {
|
|
2360
|
+
const mine = members().get(r.me);
|
|
2361
|
+
if (!mine) return;
|
|
2362
|
+
mine.ready = value === true;
|
|
2363
|
+
r.flush();
|
|
2364
|
+
},
|
|
2365
|
+
...options.setPublic !== void 0 ? { setPublic: options.setPublic } : {},
|
|
2366
|
+
on(event, cb) {
|
|
2367
|
+
if (event !== "change") return () => {
|
|
2368
|
+
};
|
|
2369
|
+
let last = signature();
|
|
2370
|
+
const sample = () => {
|
|
2371
|
+
const next = signature();
|
|
2372
|
+
if (next === last) return;
|
|
2373
|
+
last = next;
|
|
2374
|
+
cb();
|
|
2375
|
+
};
|
|
2376
|
+
const offClients = r.on("clients", sample);
|
|
2377
|
+
const timer = setInterval(sample, LOBBY_POLL_MS);
|
|
2378
|
+
timer.unref?.();
|
|
2379
|
+
return () => {
|
|
2380
|
+
offClients();
|
|
2381
|
+
clearInterval(timer);
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
function attachable(room, view) {
|
|
2387
|
+
return {
|
|
2388
|
+
get id() {
|
|
2389
|
+
return room.id;
|
|
2390
|
+
},
|
|
2391
|
+
get link() {
|
|
2392
|
+
return room.link;
|
|
2393
|
+
},
|
|
2394
|
+
get status() {
|
|
2395
|
+
return room.status;
|
|
2396
|
+
},
|
|
2397
|
+
get clients() {
|
|
2398
|
+
return room.clients;
|
|
2399
|
+
},
|
|
2400
|
+
get maxClients() {
|
|
2401
|
+
return room.maxClients;
|
|
2402
|
+
},
|
|
2403
|
+
get rtt() {
|
|
2404
|
+
return room.rtt;
|
|
2405
|
+
},
|
|
2406
|
+
on: (event, cb) => room.on(event, cb),
|
|
2407
|
+
...view !== void 0 ? { lobby: view } : {}
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
function attachLobby(element, schema, options = {}) {
|
|
2411
|
+
let detachRoom;
|
|
2412
|
+
let busy = false;
|
|
2413
|
+
const fail = (err) => {
|
|
2414
|
+
element.removeAttribute("searching");
|
|
2415
|
+
if (options.onError) {
|
|
2416
|
+
options.onError(err);
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
queueMicrotask(() => {
|
|
2420
|
+
throw err;
|
|
2421
|
+
});
|
|
2422
|
+
};
|
|
2423
|
+
const landed = (room) => {
|
|
2424
|
+
element.removeAttribute("searching");
|
|
2425
|
+
const view = lobbyOf(room, {
|
|
2426
|
+
...options.setPublic !== void 0 ? { setPublic: (value) => options.setPublic?.(room, value) } : {}
|
|
2427
|
+
});
|
|
2428
|
+
detachRoom = element.attach(
|
|
2429
|
+
attachable(room, view)
|
|
2430
|
+
);
|
|
2431
|
+
options.onRoom?.(room);
|
|
2432
|
+
};
|
|
2433
|
+
const run = async (make) => {
|
|
2434
|
+
if (busy) return;
|
|
2435
|
+
busy = true;
|
|
2436
|
+
try {
|
|
2437
|
+
landed(await make());
|
|
2438
|
+
} catch (err) {
|
|
2439
|
+
fail(err);
|
|
2440
|
+
} finally {
|
|
2441
|
+
busy = false;
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
const onPrivate = () => {
|
|
2445
|
+
void run(() => joinRoom(schema, { ...options.join ?? {} }));
|
|
2446
|
+
};
|
|
2447
|
+
const onQuickMatch = (event) => {
|
|
2448
|
+
const detail = event.detail;
|
|
2449
|
+
element.setAttribute("searching", "");
|
|
2450
|
+
void run(
|
|
2451
|
+
() => joinPublic(schema, {
|
|
2452
|
+
...options.join ?? {},
|
|
2453
|
+
...options.match ?? {},
|
|
2454
|
+
...detail?.queue !== void 0 ? { queue: detail.queue } : {}
|
|
2455
|
+
})
|
|
2456
|
+
);
|
|
2457
|
+
};
|
|
2458
|
+
element.addEventListener("private", onPrivate);
|
|
2459
|
+
element.addEventListener("quickmatch", onQuickMatch);
|
|
2460
|
+
return () => {
|
|
2461
|
+
element.removeEventListener("private", onPrivate);
|
|
2462
|
+
element.removeEventListener("quickmatch", onQuickMatch);
|
|
2463
|
+
detachRoom?.();
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2194
2466
|
|
|
2195
2467
|
// src/voice.ts
|
|
2196
2468
|
import {
|
|
@@ -2621,6 +2893,8 @@ function makeRoom(session) {
|
|
|
2621
2893
|
messages,
|
|
2622
2894
|
stats,
|
|
2623
2895
|
on: (event, cb) => session.on(event, cb),
|
|
2896
|
+
onAdd: ((collection, cb) => session.store.onEntityAdd(collection, cb)),
|
|
2897
|
+
onRemove: ((collection, cb) => session.store.onEntityRemove(collection, cb)),
|
|
2624
2898
|
flush: () => session.flush(),
|
|
2625
2899
|
leave: () => session.leave()
|
|
2626
2900
|
};
|
|
@@ -2693,6 +2967,9 @@ export {
|
|
|
2693
2967
|
INTERNAL_VOICE_TRACKS,
|
|
2694
2968
|
Identity,
|
|
2695
2969
|
IdentityError,
|
|
2970
|
+
LOBBY_MEMBERS,
|
|
2971
|
+
LOBBY_POLL_MS,
|
|
2972
|
+
LOBBY_STATE,
|
|
2696
2973
|
MAX_IDENTITY_RETRY_WAIT_MS,
|
|
2697
2974
|
MAX_PREDICTED_BODIES,
|
|
2698
2975
|
MAX_PROXY_BODIES,
|
|
@@ -2704,14 +2981,20 @@ export {
|
|
|
2704
2981
|
SMOOTHING_HALF_LIFE_MS,
|
|
2705
2982
|
SMOOTHING_SNAP_UNITS,
|
|
2706
2983
|
Session,
|
|
2984
|
+
attachLobby,
|
|
2985
|
+
attachable,
|
|
2707
2986
|
createParty,
|
|
2708
2987
|
defaultScheduler,
|
|
2709
2988
|
findMatch,
|
|
2989
|
+
findPublic,
|
|
2990
|
+
hasLobby,
|
|
2710
2991
|
identityStorageKey,
|
|
2992
|
+
joinPublic,
|
|
2711
2993
|
joinRelay,
|
|
2712
2994
|
joinRoom,
|
|
2713
2995
|
joinVoice,
|
|
2714
2996
|
linkForUrl,
|
|
2997
|
+
lobbyOf,
|
|
2715
2998
|
matchRoom,
|
|
2716
2999
|
resolveUrl,
|
|
2717
3000
|
roomIdFrom,
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Predictor
|
|
3
|
+
} from "./chunk-VZFJXPVF.js";
|
|
4
|
+
import "./chunk-5OHONPVG.js";
|
|
5
|
+
|
|
6
|
+
// src/physics-rapier2d.ts
|
|
7
|
+
import {
|
|
8
|
+
angleFrom2d,
|
|
9
|
+
applyChannel2d,
|
|
10
|
+
channelOf2d
|
|
11
|
+
} from "@irtio/schema";
|
|
12
|
+
var engine;
|
|
13
|
+
var loading;
|
|
14
|
+
async function loadEngine() {
|
|
15
|
+
if (engine) return engine;
|
|
16
|
+
loading ??= (async () => {
|
|
17
|
+
const mod = await import("@dimforge/rapier2d-compat");
|
|
18
|
+
const ns = mod.default ?? mod;
|
|
19
|
+
await ns.init();
|
|
20
|
+
engine = ns;
|
|
21
|
+
return ns;
|
|
22
|
+
})();
|
|
23
|
+
return loading;
|
|
24
|
+
}
|
|
25
|
+
var Rapier2dAdapter = class {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
}
|
|
29
|
+
options;
|
|
30
|
+
engineName = "@dimforge/rapier2d-compat";
|
|
31
|
+
optionName = "physics2d";
|
|
32
|
+
rapier;
|
|
33
|
+
world;
|
|
34
|
+
timestepSeconds = 1 / 30;
|
|
35
|
+
/** Reused by `readPose`, which runs per body per step and must not allocate. */
|
|
36
|
+
state = {
|
|
37
|
+
x: 0,
|
|
38
|
+
y: 0,
|
|
39
|
+
angle: 0,
|
|
40
|
+
vx: 0,
|
|
41
|
+
vy: 0,
|
|
42
|
+
angularVelocity: 0
|
|
43
|
+
};
|
|
44
|
+
/** `applyChannel2d`'s target shape, likewise reused. */
|
|
45
|
+
channels = { x: 0, y: 0, qz: 0, qw: 1, vx: 0, vy: 0, wz: 0 };
|
|
46
|
+
/** `moveKinematic`'s translation argument, reused: it runs per proxy per step (D71). */
|
|
47
|
+
moveTarget = { x: 0, y: 0 };
|
|
48
|
+
get timestep() {
|
|
49
|
+
return this.options.timestep;
|
|
50
|
+
}
|
|
51
|
+
async start(timestepSeconds) {
|
|
52
|
+
const rapier = await loadEngine();
|
|
53
|
+
this.rapier = rapier;
|
|
54
|
+
this.timestepSeconds = timestepSeconds;
|
|
55
|
+
const world = new rapier.World({ x: this.options.gravity.x, y: this.options.gravity.y });
|
|
56
|
+
world.timestep = timestepSeconds;
|
|
57
|
+
if (this.options.setup) this.options.setup(world, rapier);
|
|
58
|
+
this.world = world;
|
|
59
|
+
}
|
|
60
|
+
free() {
|
|
61
|
+
this.world?.free();
|
|
62
|
+
this.world = void 0;
|
|
63
|
+
}
|
|
64
|
+
hasFactory(collection) {
|
|
65
|
+
return this.options.bodies?.[collection] !== void 0;
|
|
66
|
+
}
|
|
67
|
+
createBody(desc, id, record, _warn) {
|
|
68
|
+
const world = this.world;
|
|
69
|
+
const rapier = this.rapier;
|
|
70
|
+
const factory = this.options.bodies?.[desc.name];
|
|
71
|
+
if (!world || !rapier || !factory) return void 0;
|
|
72
|
+
const spec = factory(rapier, record, id);
|
|
73
|
+
if (!spec || !spec.body) return void 0;
|
|
74
|
+
const body = world.createRigidBody(spec.body);
|
|
75
|
+
for (const collider of spec.colliders ?? []) world.createCollider(collider, body);
|
|
76
|
+
return body;
|
|
77
|
+
}
|
|
78
|
+
removeBody(body) {
|
|
79
|
+
this.world?.removeRigidBody(body);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* D71: a proxy is a `KinematicPositionBased` body, exactly as in 3D — driven by the position it
|
|
83
|
+
* is told to be at next, never by a force, an impulse or a contact, with Rapier deriving the
|
|
84
|
+
* velocity its contacts see from the move itself. The body is *switched* rather than built
|
|
85
|
+
* kinematic, so its collider and mass properties are the ones the shared factory produced.
|
|
86
|
+
*/
|
|
87
|
+
makeKinematic(handle) {
|
|
88
|
+
const rapier = this.rapier;
|
|
89
|
+
if (!rapier) return;
|
|
90
|
+
handle.setBodyType(rapier.RigidBodyType.KinematicPositionBased, true);
|
|
91
|
+
}
|
|
92
|
+
moveKinematic(handle, pose) {
|
|
93
|
+
const body = handle;
|
|
94
|
+
const to = this.moveTarget;
|
|
95
|
+
to.x = pose.t.x;
|
|
96
|
+
to.y = pose.t.y;
|
|
97
|
+
body.setNextKinematicTranslation(to);
|
|
98
|
+
body.setNextKinematicRotation(angleFrom2d(pose.r.z, pose.r.w));
|
|
99
|
+
}
|
|
100
|
+
step() {
|
|
101
|
+
this.world?.step();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Server record → body state, through `applyChannel2d` so the plane-to-channel mapping stays
|
|
105
|
+
* written once, and in `Rapier2dRuntime.applyRecordToBody`'s order — translation, rotation,
|
|
106
|
+
* linear velocity, spin — so the two sides write the same body the same way. Every setter takes
|
|
107
|
+
* `wakeUp = true`: a rebase writes state a sleeping body would otherwise ignore.
|
|
108
|
+
*/
|
|
109
|
+
applyRecord(handle, channels, record) {
|
|
110
|
+
const body = handle;
|
|
111
|
+
const t = this.channels;
|
|
112
|
+
const pos = body.translation();
|
|
113
|
+
const vel = body.linvel();
|
|
114
|
+
const angle = body.rotation();
|
|
115
|
+
t.x = pos.x;
|
|
116
|
+
t.y = pos.y;
|
|
117
|
+
t.qz = Math.sin(angle / 2);
|
|
118
|
+
t.qw = Math.cos(angle / 2);
|
|
119
|
+
t.vx = vel.x;
|
|
120
|
+
t.vy = vel.y;
|
|
121
|
+
t.wz = body.angvel();
|
|
122
|
+
for (const [channel, field] of channels) {
|
|
123
|
+
const raw = record[field];
|
|
124
|
+
if (typeof raw !== "number") continue;
|
|
125
|
+
applyChannel2d(channel, raw, t);
|
|
126
|
+
}
|
|
127
|
+
body.setTranslation({ x: t.x, y: t.y }, true);
|
|
128
|
+
body.setRotation(angleFrom2d(t.qz, t.qw), true);
|
|
129
|
+
body.setLinvel({ x: t.vx, y: t.vy }, true);
|
|
130
|
+
body.setAngvel(t.wz, true);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Body state → a 3D pose, through `channelOf2d`: `t = (x, y, 0)`, `r` a quaternion about Z,
|
|
134
|
+
* `v = (vx, vy, 0)`, `w = (0, 0, spin)`.
|
|
135
|
+
*/
|
|
136
|
+
readPose(handle, into) {
|
|
137
|
+
const body = handle;
|
|
138
|
+
const s = this.state;
|
|
139
|
+
const pos = body.translation();
|
|
140
|
+
const vel = body.linvel();
|
|
141
|
+
s.x = pos.x;
|
|
142
|
+
s.y = pos.y;
|
|
143
|
+
s.angle = body.rotation();
|
|
144
|
+
s.vx = vel.x;
|
|
145
|
+
s.vy = vel.y;
|
|
146
|
+
s.angularVelocity = body.angvel();
|
|
147
|
+
const b = s;
|
|
148
|
+
into.t.x = channelOf2d("x", b);
|
|
149
|
+
into.t.y = channelOf2d("y", b);
|
|
150
|
+
into.t.z = channelOf2d("z", b);
|
|
151
|
+
into.r.x = channelOf2d("qx", b);
|
|
152
|
+
into.r.y = channelOf2d("qy", b);
|
|
153
|
+
into.r.z = channelOf2d("qz", b);
|
|
154
|
+
into.r.w = channelOf2d("qw", b);
|
|
155
|
+
into.v.x = channelOf2d("vx", b);
|
|
156
|
+
into.v.y = channelOf2d("vy", b);
|
|
157
|
+
into.v.z = channelOf2d("vz", b);
|
|
158
|
+
into.w.x = channelOf2d("wx", b);
|
|
159
|
+
into.w.y = channelOf2d("wy", b);
|
|
160
|
+
into.w.z = channelOf2d("wz", b);
|
|
161
|
+
}
|
|
162
|
+
applyIntent(collection, body, instance) {
|
|
163
|
+
const hook = this.options.intents?.[collection];
|
|
164
|
+
const world = this.world;
|
|
165
|
+
const rapier = this.rapier;
|
|
166
|
+
if (!hook || !world || !rapier) return;
|
|
167
|
+
hook(body, instance, rapier, world, this.timestepSeconds);
|
|
168
|
+
}
|
|
169
|
+
// No `settle`. The world applies gravity; see the module docblock.
|
|
170
|
+
/** Rapier velocities are per second, so one tick of error is `epsilon` when `dv = eps / dt`. */
|
|
171
|
+
velocityTolerance(epsilon, timestepSeconds) {
|
|
172
|
+
return epsilon / timestepSeconds;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var Rapier2dPredictor = class extends Predictor {
|
|
176
|
+
constructor(ext, store, options, meOf, rttOf, tickIntervalOf, log = (m) => console.warn(m)) {
|
|
177
|
+
super(ext, store, new Rapier2dAdapter(options), options, meOf, rttOf, tickIntervalOf, log);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
function createRapier2dAdapter(options) {
|
|
181
|
+
return new Rapier2dAdapter(options);
|
|
182
|
+
}
|
|
183
|
+
export {
|
|
184
|
+
Rapier2dPredictor,
|
|
185
|
+
createRapier2dAdapter
|
|
186
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "irtio client SDK: joinRoom, owned-write batching, corrections, typed RPCs, presence, reconnection",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,14 +20,18 @@
|
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"mediasoup-client": "^3.23.1",
|
|
23
|
-
"@irtio/protocol": "0.
|
|
24
|
-
"@irtio/schema": "0.
|
|
23
|
+
"@irtio/protocol": "0.9.0",
|
|
24
|
+
"@irtio/schema": "0.9.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
+
"@dimforge/rapier2d-compat": ">=0.20.0",
|
|
27
28
|
"@dimforge/rapier3d-compat": ">=0.20.0",
|
|
28
29
|
"matter-js": ">=0.20.0"
|
|
29
30
|
},
|
|
30
31
|
"peerDependenciesMeta": {
|
|
32
|
+
"@dimforge/rapier2d-compat": {
|
|
33
|
+
"optional": true
|
|
34
|
+
},
|
|
31
35
|
"@dimforge/rapier3d-compat": {
|
|
32
36
|
"optional": true
|
|
33
37
|
},
|
|
@@ -39,6 +43,7 @@
|
|
|
39
43
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
40
44
|
"@types/matter-js": "0.20.2",
|
|
41
45
|
"matter-js": "0.20.0",
|
|
46
|
+
"@dimforge/rapier2d-compat": "0.20.0",
|
|
42
47
|
"@irtio/sfu": "0.0.0"
|
|
43
48
|
},
|
|
44
49
|
"scripts": {
|