@irtio/client 0.6.0 → 0.7.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-TE66EDBB.js → chunk-DABQDR3S.js} +245 -32
- package/dist/{chunk-6J6PKFUS.js → chunk-XWVXZRBS.js} +4 -0
- package/dist/index.d.ts +337 -32
- package/dist/index.js +181 -9
- package/dist/{physics-S5LHL3HK.js → physics-RT5T36P5.js} +26 -3
- package/dist/{physics2d-ZV2IRRRZ.js → physics2d-HOFMWPZV.js} +51 -2
- package/package.json +3 -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-XWVXZRBS.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-RT5T36P5.js").then(({ PhysicsPredictor }) => {
|
|
1051
1121
|
if (this.left) return;
|
|
1052
1122
|
attach(
|
|
1053
1123
|
new PhysicsPredictor(
|
|
@@ -1062,7 +1132,7 @@ var Session = class {
|
|
|
1062
1132
|
});
|
|
1063
1133
|
} else if (physics2d && hasPhysics) {
|
|
1064
1134
|
this.predictionRequested = true;
|
|
1065
|
-
void import("./physics2d-
|
|
1135
|
+
void import("./physics2d-HOFMWPZV.js").then(({ Physics2dPredictor }) => {
|
|
1066
1136
|
if (this.left) return;
|
|
1067
1137
|
attach(
|
|
1068
1138
|
new Physics2dPredictor(
|
|
@@ -1102,6 +1172,9 @@ var Session = class {
|
|
|
1102
1172
|
* whatever a `SCHEMA` frame replaced it with. Read instead of `options.schema` everywhere, so a
|
|
1103
1173
|
* swapped session announces its *current* hash when it reconnects rather than the one it was
|
|
1104
1174
|
* born with — otherwise a resume after a swap would be refused as a mismatch.
|
|
1175
|
+
*
|
|
1176
|
+
* D70 made it internally readable rather than private: `buildMessages` reads the declared
|
|
1177
|
+
* shapes off it when the room object is built. Still not on the public `Room` type.
|
|
1105
1178
|
*/
|
|
1106
1179
|
schema;
|
|
1107
1180
|
/** D50: how many times this session has swapped schema. Test seam and a diagnostic. */
|
|
@@ -1165,6 +1238,20 @@ var Session = class {
|
|
|
1165
1238
|
* `INTERNAL_SESSION`, the same route the D50 tests use for the session itself.
|
|
1166
1239
|
*/
|
|
1167
1240
|
voiceListeners = /* @__PURE__ */ new Set();
|
|
1241
|
+
/**
|
|
1242
|
+
* D70: typed-message listeners, by wire index, in their own map for the same structural reason
|
|
1243
|
+
* `voiceListeners` is its own set: a raw `onMessage` callback must never be handed a typed
|
|
1244
|
+
* frame and a typed callback must never be handed raw bytes. Splitting the registries makes
|
|
1245
|
+
* that a fact about the shape of the code rather than a filter somewhere in the fan-out.
|
|
1246
|
+
*/
|
|
1247
|
+
typedListeners = /* @__PURE__ */ new Map();
|
|
1248
|
+
/** D70: one warning per session for dropped typed messages — a flood must stay one line. */
|
|
1249
|
+
typedDropWarned = false;
|
|
1250
|
+
/**
|
|
1251
|
+
* D70: what `room.stats.messages` reads. `dropped` is the one worth watching: it counts typed
|
|
1252
|
+
* frames this client could not decode, which is a peer on a schema this one does not have.
|
|
1253
|
+
*/
|
|
1254
|
+
messageCounts = { sent: 0, received: 0, dropped: 0 };
|
|
1168
1255
|
cancelFlush;
|
|
1169
1256
|
cancelPing;
|
|
1170
1257
|
cancelRetry;
|
|
@@ -1628,15 +1715,62 @@ var Session = class {
|
|
|
1628
1715
|
if (pong.serverTick > this.tick) this.tick = pong.serverTick;
|
|
1629
1716
|
}
|
|
1630
1717
|
onMsg(payload) {
|
|
1631
|
-
|
|
1718
|
+
let msg;
|
|
1719
|
+
try {
|
|
1720
|
+
msg = decodeMsg(payload);
|
|
1721
|
+
} catch (err) {
|
|
1722
|
+
this.dropTypedMessage(err);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1632
1725
|
const bytes = msg.payload.slice();
|
|
1633
1726
|
if (msg.target.kind === "voice") {
|
|
1634
1727
|
for (const cb of [...this.voiceListeners]) cb(bytes);
|
|
1635
1728
|
return;
|
|
1636
1729
|
}
|
|
1637
1730
|
const from = msg.target.kind === "client" ? msg.target.clientId : "server";
|
|
1731
|
+
if (msg.typed) {
|
|
1732
|
+
this.deliverTyped(msg.typed.index, from, bytes);
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
this.messageCounts.received++;
|
|
1638
1736
|
for (const cb of [...this.messageListeners]) cb(from, bytes);
|
|
1639
1737
|
}
|
|
1738
|
+
/**
|
|
1739
|
+
* D70: decode one typed payload against the current schema and hand it to that shape's
|
|
1740
|
+
* listeners.
|
|
1741
|
+
*
|
|
1742
|
+
* Everything a hostile peer can do here ends in the same place: a counter and a dropped frame.
|
|
1743
|
+
* An index past the schema's list, a truncated payload, an oversize `str`, an over-max `list`,
|
|
1744
|
+
* an unknown enum member, or a well-formed value of the wrong shape — each is caught, none
|
|
1745
|
+
* throws out of the frame loop, none closes the socket, and the next frame is delivered
|
|
1746
|
+
* normally. The `console.warn` fires once per session so a flood stays one line.
|
|
1747
|
+
*/
|
|
1748
|
+
deliverTyped(index, from, bytes) {
|
|
1749
|
+
const desc = this.schema?.messages?.[index];
|
|
1750
|
+
if (!desc) {
|
|
1751
|
+
this.dropTypedMessage(new Error(`no message with index ${index} in this schema`));
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
let value;
|
|
1755
|
+
try {
|
|
1756
|
+
value = decodeFields(desc.fields, bytes);
|
|
1757
|
+
} catch (err) {
|
|
1758
|
+
this.dropTypedMessage(err);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
this.messageCounts.received++;
|
|
1762
|
+
const set = this.typedListeners.get(index);
|
|
1763
|
+
if (!set) return;
|
|
1764
|
+
for (const cb of [...set]) cb(from, value);
|
|
1765
|
+
}
|
|
1766
|
+
dropTypedMessage(err) {
|
|
1767
|
+
this.messageCounts.dropped++;
|
|
1768
|
+
if (this.typedDropWarned) return;
|
|
1769
|
+
this.typedDropWarned = true;
|
|
1770
|
+
console.warn(
|
|
1771
|
+
`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.`
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1640
1774
|
// -------------------------------------------------------------------------
|
|
1641
1775
|
// Timers
|
|
1642
1776
|
// -------------------------------------------------------------------------
|
|
@@ -1707,7 +1841,7 @@ var Session = class {
|
|
|
1707
1841
|
let encoded;
|
|
1708
1842
|
try {
|
|
1709
1843
|
desc = this.descOf(name);
|
|
1710
|
-
encoded =
|
|
1844
|
+
encoded = encodeFields2(desc.params, params);
|
|
1711
1845
|
} catch (err) {
|
|
1712
1846
|
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
1713
1847
|
}
|
|
@@ -1728,7 +1862,10 @@ var Session = class {
|
|
|
1728
1862
|
reject,
|
|
1729
1863
|
cancelTimeout
|
|
1730
1864
|
});
|
|
1731
|
-
this.send(
|
|
1865
|
+
this.send(
|
|
1866
|
+
FrameType.CALL,
|
|
1867
|
+
encodeCall({ reqId, rpcId: desc.index, clientTick: this.tick, params: encoded })
|
|
1868
|
+
);
|
|
1732
1869
|
});
|
|
1733
1870
|
}
|
|
1734
1871
|
async requestOwnership(entity, id) {
|
|
@@ -1802,7 +1939,7 @@ var Session = class {
|
|
|
1802
1939
|
replyOk(desc, reqId, result) {
|
|
1803
1940
|
let bytes;
|
|
1804
1941
|
try {
|
|
1805
|
-
bytes = desc.returns ?
|
|
1942
|
+
bytes = desc.returns ? encodeFields2(desc.returns, result ?? {}) : new Uint8Array(0);
|
|
1806
1943
|
} catch (err) {
|
|
1807
1944
|
this.replyError(reqId, err instanceof Error ? err.message : String(err));
|
|
1808
1945
|
return;
|
|
@@ -1824,6 +1961,7 @@ var Session = class {
|
|
|
1824
1961
|
// -------------------------------------------------------------------------
|
|
1825
1962
|
message(target, bytes) {
|
|
1826
1963
|
const wire = target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
|
|
1964
|
+
this.messageCounts.sent++;
|
|
1827
1965
|
this.send(FrameType.MSG, encodeMsg({ target: wire, payload: bytes }));
|
|
1828
1966
|
}
|
|
1829
1967
|
onMessage(cb) {
|
|
@@ -1832,6 +1970,27 @@ var Session = class {
|
|
|
1832
1970
|
this.messageListeners.delete(cb);
|
|
1833
1971
|
};
|
|
1834
1972
|
}
|
|
1973
|
+
/**
|
|
1974
|
+
* D70: send one typed message. `payload` is already `encodeFields`'d by the caller
|
|
1975
|
+
* (`buildMessages`), which is where the shape and the throw on a bad value belong.
|
|
1976
|
+
*/
|
|
1977
|
+
typedMessage(index, target, payload) {
|
|
1978
|
+
const wire = target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
|
|
1979
|
+
this.messageCounts.sent++;
|
|
1980
|
+
this.send(FrameType.MSG, encodeMsg({ target: wire, payload, typed: { index } }));
|
|
1981
|
+
}
|
|
1982
|
+
/** D70: subscribe to one message shape by wire index. Raw listeners never see these frames. */
|
|
1983
|
+
onTypedMessage(index, cb) {
|
|
1984
|
+
let set = this.typedListeners.get(index);
|
|
1985
|
+
if (!set) {
|
|
1986
|
+
set = /* @__PURE__ */ new Set();
|
|
1987
|
+
this.typedListeners.set(index, set);
|
|
1988
|
+
}
|
|
1989
|
+
set.add(cb);
|
|
1990
|
+
return () => {
|
|
1991
|
+
set?.delete(cb);
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1835
1994
|
/**
|
|
1836
1995
|
* D51: write one voice signaling message. Internal — reached only through `INTERNAL_SESSION`,
|
|
1837
1996
|
* so it is not on the `Room` type and a game cannot call it.
|
|
@@ -2403,6 +2562,8 @@ async function joinRoom(schema, options = {}) {
|
|
|
2403
2562
|
}
|
|
2404
2563
|
function makeRoom(session) {
|
|
2405
2564
|
const call = callProxy(session);
|
|
2565
|
+
const messages = buildMessages(session.schema, session);
|
|
2566
|
+
const stats = { messages: session.messageCounts };
|
|
2406
2567
|
const room = {
|
|
2407
2568
|
get me() {
|
|
2408
2569
|
return session.me;
|
|
@@ -2446,6 +2607,8 @@ function makeRoom(session) {
|
|
|
2446
2607
|
return session.predictor?.ready ?? false;
|
|
2447
2608
|
},
|
|
2448
2609
|
predicts: (collection, id) => session.predictor?.has(collection, id) ?? false,
|
|
2610
|
+
// ---- M6 lane D: proxies ----
|
|
2611
|
+
proxied: (collection, id) => session.predictor?.hasProxy(collection, id) ?? false,
|
|
2449
2612
|
get stats() {
|
|
2450
2613
|
return session.predictor?.stats ?? emptyPredictionStats();
|
|
2451
2614
|
}
|
|
@@ -2455,6 +2618,8 @@ function makeRoom(session) {
|
|
|
2455
2618
|
requestOwnership: (entity, id) => session.requestOwnership(entity, id),
|
|
2456
2619
|
message: (target, bytes) => session.message(target, bytes),
|
|
2457
2620
|
onMessage: (cb) => session.onMessage(cb),
|
|
2621
|
+
messages,
|
|
2622
|
+
stats,
|
|
2458
2623
|
on: (event, cb) => session.on(event, cb),
|
|
2459
2624
|
flush: () => session.flush(),
|
|
2460
2625
|
leave: () => session.leave()
|
|
@@ -2468,7 +2633,11 @@ async function joinRelay(options = {}) {
|
|
|
2468
2633
|
const fromLocation = !explicitRoom && currentLocation() !== void 0;
|
|
2469
2634
|
const roomId = explicitRoom ? roomIdFrom(options.room ?? "") : fromLocation ? roomIdFromLocation() : "";
|
|
2470
2635
|
const session = new Session({
|
|
2471
|
-
|
|
2636
|
+
// ---- M6 lane C: typed messages ----
|
|
2637
|
+
// D70: a relay join may now bring the project's deployed schema. This is a type change and a
|
|
2638
|
+
// hash change, not a transport change: the session already chose between the builder's schema
|
|
2639
|
+
// and `relaySchema`, and already put `schema.hash8` or `RELAY_HASH8` in the HELLO.
|
|
2640
|
+
schema: options.schema,
|
|
2472
2641
|
url,
|
|
2473
2642
|
key: options.key ?? resolveKey(void 0, void 0, url),
|
|
2474
2643
|
roomId,
|
|
@@ -2505,6 +2674,8 @@ async function joinRelay(options = {}) {
|
|
|
2505
2674
|
},
|
|
2506
2675
|
message: (target, bytes) => session.message(target, bytes),
|
|
2507
2676
|
onMessage: (cb) => session.onMessage(cb),
|
|
2677
|
+
messages: buildMessages(session.schema, session),
|
|
2678
|
+
stats: { messages: session.messageCounts },
|
|
2508
2679
|
on: (event, cb) => session.on(event, cb),
|
|
2509
2680
|
leave: () => session.leave()
|
|
2510
2681
|
};
|
|
@@ -2524,6 +2695,7 @@ export {
|
|
|
2524
2695
|
IdentityError,
|
|
2525
2696
|
MAX_IDENTITY_RETRY_WAIT_MS,
|
|
2526
2697
|
MAX_PREDICTED_BODIES,
|
|
2698
|
+
MAX_PROXY_BODIES,
|
|
2527
2699
|
MatchError,
|
|
2528
2700
|
PING_INTERVAL_MS,
|
|
2529
2701
|
PREDICTION_EPSILON,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Predictor
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-DABQDR3S.js";
|
|
4
|
+
import "./chunk-XWVXZRBS.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
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Predictor
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-DABQDR3S.js";
|
|
4
|
+
import "./chunk-XWVXZRBS.js";
|
|
5
5
|
|
|
6
6
|
// src/physics2d.ts
|
|
7
7
|
import {
|
|
@@ -47,6 +47,8 @@ var MatterAdapter = class {
|
|
|
47
47
|
};
|
|
48
48
|
/** `applyChannel2d`'s target shape, likewise reused. */
|
|
49
49
|
channels = { x: 0, y: 0, qz: 0, qw: 1, vx: 0, vy: 0, wz: 0 };
|
|
50
|
+
/** `moveKinematic`'s position argument, reused: it runs per proxy per step (D71). */
|
|
51
|
+
moveTarget = { x: 0, y: 0 };
|
|
50
52
|
get timestep() {
|
|
51
53
|
return this.options.timestep;
|
|
52
54
|
}
|
|
@@ -95,6 +97,53 @@ var MatterAdapter = class {
|
|
|
95
97
|
matter.Composite.remove(engine2.world, pinned ? [body, ...pinned] : [body]);
|
|
96
98
|
this.constraints.delete(body);
|
|
97
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* D71: matter.js has no kinematic body type. Static is the only kind it never integrates and
|
|
102
|
+
* never moves from an impulse, so that is what a proxy is.
|
|
103
|
+
*
|
|
104
|
+
* `Body.setStatic` also overwrites the surface properties the factory set — `friction` to 1 and
|
|
105
|
+
* `restitution` to 0, on every part, and on a body that was *already* static it does that without
|
|
106
|
+
* even stashing the old values in `_original`. A proxy has to be the server's body rather than a
|
|
107
|
+
* stickier copy of it (matter pairs friction as the minimum of the two, so a proxy at friction 1
|
|
108
|
+
* is only invisible while the other body is the grippier one), so the factory's values are
|
|
109
|
+
* snapshotted here and put back. Mass and inertia keep the infinities that make it immovable.
|
|
110
|
+
*/
|
|
111
|
+
makeKinematic(handle) {
|
|
112
|
+
const matter = this.matter;
|
|
113
|
+
if (!matter) return;
|
|
114
|
+
const body = handle;
|
|
115
|
+
const parts = body.parts;
|
|
116
|
+
const surfaces = parts.map((p) => ({ friction: p.friction, restitution: p.restitution }));
|
|
117
|
+
matter.Body.setStatic(body, true);
|
|
118
|
+
for (let i = 0; i < parts.length; i++) {
|
|
119
|
+
const part = parts[i];
|
|
120
|
+
const surface = surfaces[i];
|
|
121
|
+
part.friction = surface.friction;
|
|
122
|
+
part.restitution = surface.restitution;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `updateVelocity: true` is the whole mechanism. It leaves `positionPrev` exactly one move behind
|
|
127
|
+
* the new position, and matter's resolver reads a body's velocity as precisely that difference
|
|
128
|
+
* (`Resolver.solveVelocity`: `position - positionPrev`), so a predicted body resting on the proxy
|
|
129
|
+
* is carried by friction the way it is on the server. Given the same pose twice the difference is
|
|
130
|
+
* zero and the proxy is at rest, which is what holds it still through a rebase's re-steps.
|
|
131
|
+
*
|
|
132
|
+
* The third parameter is real in matter-js 0.20 (`src/body/Body.js`) and missing from
|
|
133
|
+
* `@types/matter-js@0.20.2` on both setters, which is what the two casts are for.
|
|
134
|
+
*/
|
|
135
|
+
moveKinematic(handle, pose) {
|
|
136
|
+
const matter = this.matter;
|
|
137
|
+
if (!matter) return;
|
|
138
|
+
const body = handle;
|
|
139
|
+
const setPosition = matter.Body.setPosition;
|
|
140
|
+
const setAngle = matter.Body.setAngle;
|
|
141
|
+
const to = this.moveTarget;
|
|
142
|
+
to.x = pose.t.x;
|
|
143
|
+
to.y = pose.t.y;
|
|
144
|
+
setPosition(body, to, true);
|
|
145
|
+
setAngle(body, angleFrom2d(pose.r.z, pose.r.w), true);
|
|
146
|
+
}
|
|
98
147
|
step() {
|
|
99
148
|
if (!this.matter || !this.engine) return;
|
|
100
149
|
this.matter.Engine.update(this.engine, this.stepMs);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "irtio client SDK: joinRoom, owned-write batching, corrections, typed RPCs, presence, reconnection",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"mediasoup-client": "^3.23.1",
|
|
23
|
-
"@irtio/protocol": "0.
|
|
24
|
-
"@irtio/schema": "0.
|
|
23
|
+
"@irtio/protocol": "0.7.0",
|
|
24
|
+
"@irtio/schema": "0.7.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"@dimforge/rapier3d-compat": ">=0.20.0",
|