@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/contract.ts
|
|
2
2
|
var E_CONNECT_FAILED = "E_CONNECT_FAILED";
|
|
3
3
|
var MAX_PREDICTED_BODIES = 64;
|
|
4
|
+
var MAX_PROXY_BODIES = 256;
|
|
4
5
|
var PREDICTION_EPSILON = 0.05;
|
|
5
6
|
var SMOOTHING_HALF_LIFE_MS = 70;
|
|
6
7
|
var SMOOTHING_SNAP_UNITS = 4;
|
|
@@ -10,8 +11,11 @@ function emptyPredictionStats() {
|
|
|
10
11
|
resimSteps: 0,
|
|
11
12
|
rebases: 0,
|
|
12
13
|
snaps: 0,
|
|
14
|
+
leadClamped: 0,
|
|
13
15
|
suppressed: 0,
|
|
14
16
|
overCap: 0,
|
|
17
|
+
proxies: 0,
|
|
18
|
+
absent: 0,
|
|
15
19
|
lastResimMicros: 0,
|
|
16
20
|
smoothing: 0,
|
|
17
21
|
stampGap: 0,
|
|
@@ -620,6 +624,7 @@ var ClientStore = class {
|
|
|
620
624
|
export {
|
|
621
625
|
E_CONNECT_FAILED,
|
|
622
626
|
MAX_PREDICTED_BODIES,
|
|
627
|
+
MAX_PROXY_BODIES,
|
|
623
628
|
PREDICTION_EPSILON,
|
|
624
629
|
SMOOTHING_HALF_LIFE_MS,
|
|
625
630
|
SMOOTHING_SNAP_UNITS,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MAX_PREDICTED_BODIES,
|
|
3
|
+
MAX_PROXY_BODIES,
|
|
3
4
|
PREDICTION_EPSILON,
|
|
4
5
|
RESIM_DEPTH,
|
|
5
6
|
SMOOTHING_HALF_LIFE_MS,
|
|
6
7
|
SMOOTHING_SNAP_UNITS
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-5Z4DHUA3.js";
|
|
8
9
|
|
|
9
10
|
// src/predictor.ts
|
|
10
11
|
var MAX_FREE_STEPS_PER_FRAME = 5;
|
|
@@ -132,8 +133,11 @@ var Predictor = class _Predictor {
|
|
|
132
133
|
resimSteps: 0,
|
|
133
134
|
rebases: 0,
|
|
134
135
|
snaps: 0,
|
|
136
|
+
leadClamped: 0,
|
|
135
137
|
suppressed: 0,
|
|
136
138
|
overCap: 0,
|
|
139
|
+
proxies: 0,
|
|
140
|
+
absent: 0,
|
|
137
141
|
lastResimMicros: 0,
|
|
138
142
|
smoothing: 0,
|
|
139
143
|
stampGap: 0,
|
|
@@ -146,11 +150,19 @@ var Predictor = class _Predictor {
|
|
|
146
150
|
/** Seconds per step, fixed for the life of the world. */
|
|
147
151
|
timestepSeconds;
|
|
148
152
|
bodies = /* @__PURE__ */ new Map();
|
|
153
|
+
/** D71: kinematic proxies, keyed like `bodies`. The two maps are disjoint by construction. */
|
|
154
|
+
proxies = /* @__PURE__ */ new Map();
|
|
149
155
|
/** Physics-backed entity collections, in schema order. D50: not `readonly`, see `swapSchema`. */
|
|
150
156
|
collections;
|
|
151
157
|
warned = /* @__PURE__ */ new Set();
|
|
152
158
|
/** Highest over-cap count warned about per collection, so growth re-warns and noise does not. */
|
|
153
159
|
overCapHigh = /* @__PURE__ */ new Map();
|
|
160
|
+
/** The same, for the proxy cap: `absent` is the count a game has to act on (D71-d). */
|
|
161
|
+
absentHigh = /* @__PURE__ */ new Map();
|
|
162
|
+
/** D71: where a proxy goes. Wired by the session; see {@link DrawnReader}. */
|
|
163
|
+
drawnReader;
|
|
164
|
+
/** `drawnReader`'s output buffer, reused: it is called once per proxy per frame. */
|
|
165
|
+
drawnChannels = {};
|
|
154
166
|
/** Authority arrived since the last frame: rebase before free-running. */
|
|
155
167
|
authorityDirty = false;
|
|
156
168
|
accumulatorMs = 0;
|
|
@@ -227,8 +239,17 @@ var Predictor = class _Predictor {
|
|
|
227
239
|
);
|
|
228
240
|
for (const entry of this.bodies.values()) this.adapter.removeBody(entry.body);
|
|
229
241
|
this.bodies.clear();
|
|
242
|
+
for (const entry of this.proxies.values()) this.adapter.removeBody(entry.body);
|
|
243
|
+
this.proxies.clear();
|
|
230
244
|
this.reset();
|
|
231
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* D71: tell the predictor where its proxies go. The session calls this once, with a reader over
|
|
248
|
+
* the D20 interpolation buffer; without it a proxy holds the pose it was built at.
|
|
249
|
+
*/
|
|
250
|
+
setDrawnReader(reader) {
|
|
251
|
+
this.drawnReader = reader;
|
|
252
|
+
}
|
|
232
253
|
get epsilon() {
|
|
233
254
|
return this.tuning.epsilon ?? PREDICTION_EPSILON;
|
|
234
255
|
}
|
|
@@ -260,6 +281,8 @@ var Predictor = class _Predictor {
|
|
|
260
281
|
}
|
|
261
282
|
free() {
|
|
262
283
|
this.bodies.clear();
|
|
284
|
+
this.proxies.clear();
|
|
285
|
+
this.stats.proxies = 0;
|
|
263
286
|
this.adapter.free();
|
|
264
287
|
this.worldReady = false;
|
|
265
288
|
}
|
|
@@ -330,10 +353,23 @@ var Predictor = class _Predictor {
|
|
|
330
353
|
this.stats.lastStampTick = stampTick;
|
|
331
354
|
this.stats.lastAppliedTick = appliedTick;
|
|
332
355
|
}
|
|
333
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* Does the local world currently **simulate** `collection[id]`?
|
|
358
|
+
*
|
|
359
|
+
* Deliberately still false for a proxied instance (D71). Every caller of this asks it to decide
|
|
360
|
+
* whether the local world is the better answer than authority — the render read path, the
|
|
361
|
+
* correction classifier, `room.prediction.predicts` — and for a proxy it is not: a proxy is
|
|
362
|
+
* authority, one interpolation delay old, put into the world so that other bodies can touch it.
|
|
363
|
+
* Reading it back would be a round trip through the physics engine to learn what the render
|
|
364
|
+
* buffer already said. {@link proxied} is the question about proxies.
|
|
365
|
+
*/
|
|
334
366
|
has(collection, id) {
|
|
335
367
|
return this.bodies.has(key(collection, id));
|
|
336
368
|
}
|
|
369
|
+
/** D71: does `collection[id]` have a kinematic proxy in the local world right now? */
|
|
370
|
+
hasProxy(collection, id) {
|
|
371
|
+
return this.proxies.has(key(collection, id));
|
|
372
|
+
}
|
|
337
373
|
/**
|
|
338
374
|
* Is a correction's every value within the suppression tolerance of the prediction it judges?
|
|
339
375
|
* Position (and rotation) channels compare against `epsilon` directly; velocity and angular
|
|
@@ -630,52 +666,168 @@ var Predictor = class _Predictor {
|
|
|
630
666
|
// Membership
|
|
631
667
|
// -------------------------------------------------------------------------
|
|
632
668
|
/**
|
|
633
|
-
* Mirrors the local world
|
|
634
|
-
* instance it owns
|
|
635
|
-
*
|
|
636
|
-
*
|
|
669
|
+
* Mirrors the local world onto the instances this client can see: a **simulated** body for every
|
|
670
|
+
* physics instance it owns plus non-owned instances of `predicted: true` collections up to
|
|
671
|
+
* `maxPredictedBodies`, and a **kinematic proxy** (D71) for everything else that has a body
|
|
672
|
+
* factory and has not opted out with `proxy: false`, up to `maxProxyBodies`.
|
|
637
673
|
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
674
|
+
* Slot order is collection order then insertion order — the same iteration guarantee the server
|
|
675
|
+
* states — so which instances fall over either cap is deterministic. There is no relevance
|
|
676
|
+
* policy and no distance ordering: the ninth crate spawned is still the ninth crate, it is just
|
|
677
|
+
* now solid rather than missing.
|
|
678
|
+
*
|
|
679
|
+
* Three transitions happen here, and each is a removal before a creation so the world never holds
|
|
680
|
+
* two colliders for one id:
|
|
681
|
+
*
|
|
682
|
+
* - **Promotion.** A proxied instance of a predicted collection finds a free slot (a predicted
|
|
683
|
+
* body was removed, or the cap was raised). `createBody` applies the authority record, velocity
|
|
684
|
+
* channels included, so the body carries on rather than starting from rest.
|
|
685
|
+
* - **Demotion.** A simulated instance loses its slot to an earlier-collection newcomer. That is
|
|
686
|
+
* what this loop already did before proxies — it stopped marking the body live and the sweep
|
|
687
|
+
* below removed it — and now it becomes a proxy at the drawn pose in the same frame. It costs
|
|
688
|
+
* one visible pose step: the body was a lead ahead of authority and the proxy is an
|
|
689
|
+
* interpolation delay behind it.
|
|
690
|
+
* - **Removal.** An instance the server removed, or one that left this client's area of interest
|
|
691
|
+
* (D23), loses whichever of the two it had, the same frame.
|
|
692
|
+
*
|
|
693
|
+
* What is left over — over the proxy cap, `proxy: false`, or no factory — has no collider in the
|
|
694
|
+
* local world at all, is counted in `stats.absent`, and is the only case a predicted body still
|
|
695
|
+
* falls through.
|
|
641
696
|
*/
|
|
642
697
|
reconcileBodies() {
|
|
643
698
|
if (!this.worldReady) return;
|
|
644
699
|
const me = this.meOf();
|
|
645
700
|
const live = /* @__PURE__ */ new Set();
|
|
701
|
+
const liveProxies = /* @__PURE__ */ new Set();
|
|
646
702
|
let nonOwned = 0;
|
|
647
703
|
let overCap = 0;
|
|
704
|
+
let absent = 0;
|
|
648
705
|
const overCapBy = /* @__PURE__ */ new Map();
|
|
706
|
+
const cappedBy = /* @__PURE__ */ new Map();
|
|
649
707
|
const cap = this.tuning.maxPredictedBodies ?? MAX_PREDICTED_BODIES;
|
|
708
|
+
const proxyCap = this.tuning.maxProxyBodies ?? MAX_PROXY_BODIES;
|
|
650
709
|
for (const desc of this.collections) {
|
|
651
710
|
const coll = this.store.plainCollection(desc.name);
|
|
652
711
|
for (const id of coll.ids()) {
|
|
712
|
+
const k = key(desc.name, id);
|
|
653
713
|
const owned = coll.ownerOf(id) === me && me !== "";
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (nonOwned
|
|
714
|
+
let simulate = owned;
|
|
715
|
+
if (!owned && desc.predicted) {
|
|
716
|
+
if (nonOwned < cap) {
|
|
717
|
+
simulate = true;
|
|
718
|
+
nonOwned++;
|
|
719
|
+
} else {
|
|
657
720
|
overCap++;
|
|
658
721
|
overCapBy.set(desc.name, (overCapBy.get(desc.name) ?? 0) + 1);
|
|
659
|
-
continue;
|
|
660
722
|
}
|
|
661
|
-
nonOwned++;
|
|
662
723
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
724
|
+
if (simulate) {
|
|
725
|
+
live.add(k);
|
|
726
|
+
if (this.bodies.has(k)) continue;
|
|
727
|
+
this.removeProxy(k);
|
|
728
|
+
const record2 = coll.get(id);
|
|
729
|
+
if (record2) this.createBody(desc, id, record2, owned);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (!desc.proxy || !this.adapter.hasFactory(desc.name)) {
|
|
733
|
+
absent++;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (liveProxies.size >= proxyCap) {
|
|
737
|
+
absent++;
|
|
738
|
+
cappedBy.set(desc.name, (cappedBy.get(desc.name) ?? 0) + 1);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
liveProxies.add(k);
|
|
742
|
+
if (this.proxies.has(k)) continue;
|
|
743
|
+
this.removePredicted(k);
|
|
666
744
|
const record = coll.get(id);
|
|
667
|
-
if (record) this.
|
|
745
|
+
if (record) this.createProxy(desc, id, record);
|
|
668
746
|
}
|
|
669
747
|
}
|
|
670
748
|
this.stats.overCap = overCap;
|
|
671
|
-
this.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
this.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
this.
|
|
678
|
-
|
|
749
|
+
this.stats.absent = absent;
|
|
750
|
+
this.warnCaps(cap, proxyCap, overCapBy, cappedBy);
|
|
751
|
+
for (const k of [...this.bodies.keys()]) {
|
|
752
|
+
if (!live.has(k)) this.removePredicted(k);
|
|
753
|
+
}
|
|
754
|
+
for (const k of [...this.proxies.keys()]) {
|
|
755
|
+
if (!liveProxies.has(k)) this.removeProxy(k);
|
|
756
|
+
}
|
|
757
|
+
this.stats.proxies = this.proxies.size;
|
|
758
|
+
this.refreshProxyTargets();
|
|
759
|
+
}
|
|
760
|
+
/** Drops one simulated body and everything the loop keeps per body. Safe on a missing key. */
|
|
761
|
+
removePredicted(k) {
|
|
762
|
+
const entry = this.bodies.get(k);
|
|
763
|
+
if (!entry) return;
|
|
764
|
+
this.bodies.delete(k);
|
|
765
|
+
this.authorityTicks.delete(k);
|
|
766
|
+
this.predictedTicks.delete(k);
|
|
767
|
+
this.history.delete(k);
|
|
768
|
+
this.adapter.removeBody(entry.body);
|
|
769
|
+
}
|
|
770
|
+
/** Drops one proxy. Safe on a missing key. */
|
|
771
|
+
removeProxy(k) {
|
|
772
|
+
const entry = this.proxies.get(k);
|
|
773
|
+
if (!entry) return;
|
|
774
|
+
this.proxies.delete(k);
|
|
775
|
+
this.adapter.removeBody(entry.body);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* D71: builds one kinematic proxy through the **same factory call** the simulated path uses, so
|
|
779
|
+
* the collider is the server's collider by construction rather than by a second description of
|
|
780
|
+
* it, then hands it to the adapter to be made kinematic.
|
|
781
|
+
*
|
|
782
|
+
* The authority record is applied first — a proxy appears where the server last said it was, not
|
|
783
|
+
* at the factory's origin — and the drawn pose takes over on the next `refreshProxyTargets`.
|
|
784
|
+
*/
|
|
785
|
+
createProxy(desc, id, record) {
|
|
786
|
+
if (!this.worldReady) return;
|
|
787
|
+
const body = this.adapter.createBody(
|
|
788
|
+
desc,
|
|
789
|
+
id,
|
|
790
|
+
record,
|
|
791
|
+
(k, message) => this.warnOnce(k, message)
|
|
792
|
+
);
|
|
793
|
+
if (!body) return;
|
|
794
|
+
const physics = desc.physics;
|
|
795
|
+
if (physics) this.adapter.applyRecord(body, physics.channels, record);
|
|
796
|
+
this.adapter.makeKinematic(body);
|
|
797
|
+
const entry = { desc, id, body, target: emptyPose(), targeted: false };
|
|
798
|
+
this.adapter.readPose(body, entry.target);
|
|
799
|
+
this.proxies.set(key(desc.name, id), entry);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Reads every proxy's drawn pose once per frame, and only once.
|
|
803
|
+
*
|
|
804
|
+
* Once, because a rebase re-steps the world up to `MAX_LEAD` times against a render clock that
|
|
805
|
+
* has not moved: asking again per step would return the same answer at a real cost. And once
|
|
806
|
+
* *because* of D71's rule — a proxy holds one pose for every re-step of a rebase. It cannot run
|
|
807
|
+
* ahead of the newest delta because the reader never extrapolates, and it cannot run ahead of the
|
|
808
|
+
* frame because it is only ever asked here.
|
|
809
|
+
*
|
|
810
|
+
* A proxy with nothing drawn (a delta gap, an entity not yet at the render clock) keeps the pose
|
|
811
|
+
* it had. That is the hold, and it is the difference between a crate that stays solid through a
|
|
812
|
+
* stall and a crate that slides off across the level.
|
|
813
|
+
*/
|
|
814
|
+
refreshProxyTargets() {
|
|
815
|
+
const reader = this.drawnReader;
|
|
816
|
+
if (!reader || this.proxies.size === 0) return;
|
|
817
|
+
const channels = this.drawnChannels;
|
|
818
|
+
for (const entry of this.proxies.values()) {
|
|
819
|
+
const physics = entry.desc.physics;
|
|
820
|
+
if (!physics) continue;
|
|
821
|
+
for (const [, field] of physics.channels) channels[field] = Number.NaN;
|
|
822
|
+
if (!reader(entry.desc, entry.id, channels)) continue;
|
|
823
|
+
poseFromRecord(physics.channels, channels, entry.target);
|
|
824
|
+
entry.targeted = true;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/** Writes every proxy to its target pose. Runs immediately before each `adapter.step()`. */
|
|
828
|
+
driveProxies() {
|
|
829
|
+
for (const entry of this.proxies.values()) {
|
|
830
|
+
if (entry.targeted) this.adapter.moveKinematic(entry.body, entry.target);
|
|
679
831
|
}
|
|
680
832
|
}
|
|
681
833
|
createBody(desc, id, record, owned) {
|
|
@@ -726,17 +878,29 @@ var Predictor = class _Predictor {
|
|
|
726
878
|
this.bodies.set(key(desc.name, id), entry);
|
|
727
879
|
}
|
|
728
880
|
/**
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
881
|
+
* The two cap notices, both on a new high-water mark per collection: a game that grows past a cap
|
|
882
|
+
* mid-session hears about it, and a count that oscillates around one level does not turn the
|
|
883
|
+
* console into a log.
|
|
884
|
+
*
|
|
885
|
+
* They say different things now, and only the second is an emergency (D71). Over the *prediction*
|
|
886
|
+
* cap an instance is still solid — it has a proxy — it just is not simulated, so a shove does
|
|
887
|
+
* nothing locally until the server agrees. Over the *proxy* cap it is genuinely not there, which
|
|
888
|
+
* is the failure `bugs.md` #3 was about, and the message names both numbers so it is obvious
|
|
889
|
+
* which one to raise.
|
|
733
890
|
*/
|
|
734
|
-
|
|
891
|
+
warnCaps(cap, proxyCap, overCapBy, cappedBy) {
|
|
735
892
|
for (const [name, count] of overCapBy) {
|
|
736
893
|
if (count <= (this.overCapHigh.get(name) ?? 0)) continue;
|
|
737
894
|
this.overCapHigh.set(name, count);
|
|
738
895
|
this.log(
|
|
739
|
-
`irtio: ${count} ${name} instance(s) over the ${cap}-body prediction cap. They
|
|
896
|
+
`irtio: ${count} ${name} instance(s) over the ${cap}-body prediction cap. They keep a kinematic proxy in the local world, so predicted bodies still collide with them, but they are not simulated ahead: pushing one does nothing locally until the server says so. Raise maxPredictedBodies for anything a player shoves (the cost is a world step per lead tick, not a per-body-per-frame cost); leave it for anything a player only stands on or is blocked by.`
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
for (const [name, count] of cappedBy) {
|
|
900
|
+
if (count <= (this.absentHigh.get(name) ?? 0)) continue;
|
|
901
|
+
this.absentHigh.set(name, count);
|
|
902
|
+
this.log(
|
|
903
|
+
`irtio: ${count} ${name} instance(s) over the ${proxyCap}-body proxy cap (maxProxyBodies ${proxyCap}, maxPredictedBodies ${cap}). They are ABSENT from the local world, not interpolated into it: predicted bodies pass through them until the next correction snaps them back. Raise maxProxyBodies, or set proxy: false on the collections nothing collides with so the budget goes to the ones that matter.`
|
|
740
904
|
);
|
|
741
905
|
}
|
|
742
906
|
}
|
|
@@ -788,8 +952,8 @@ var Predictor = class _Predictor {
|
|
|
788
952
|
* written, so plain state holds exactly what the server said), then re-step the world by the
|
|
789
953
|
* client's lead, applying to each re-stepped tick the intent that was in force *at that tick*:
|
|
790
954
|
* the newest buffered unjudged write stamped at or before it, or the baseline (the newest
|
|
791
|
-
* judged write) before the first of them. Bounded by `MAX_LEAD`: an outrun lead
|
|
792
|
-
*
|
|
955
|
+
* judged write) before the first of them. Bounded by `MAX_LEAD`: an outrun lead re-steps that
|
|
956
|
+
* many and counts (`stats.leadClamped`, bugs.md #71).
|
|
793
957
|
*/
|
|
794
958
|
rebase() {
|
|
795
959
|
if (!this.worldReady) return;
|
|
@@ -803,7 +967,9 @@ var Predictor = class _Predictor {
|
|
|
803
967
|
const k = key(entry.desc.name, entry.id);
|
|
804
968
|
this.predictedTicks.set(k, Math.max(this.authorityTick, this.authorityTicks.get(k) ?? 0));
|
|
805
969
|
}
|
|
806
|
-
const
|
|
970
|
+
const wanted = this.leadTicks();
|
|
971
|
+
const lead = Math.min(wanted, MAX_LEAD);
|
|
972
|
+
if (wanted > MAX_LEAD) this.stats.leadClamped++;
|
|
807
973
|
if (this.lastLead !== void 0 && lead !== this.lastLead && this.gapMeasured) {
|
|
808
974
|
const delta = lead - this.lastLead;
|
|
809
975
|
this.stampGap = Math.min(RESIM_DEPTH, Math.max(0, this.stampGap + delta));
|
|
@@ -812,10 +978,6 @@ var Predictor = class _Predictor {
|
|
|
812
978
|
}
|
|
813
979
|
this.lastLead = lead;
|
|
814
980
|
this.headTick = this.authorityTick;
|
|
815
|
-
if (lead > MAX_LEAD) {
|
|
816
|
-
this.stats.snaps++;
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
981
|
const replays = /* @__PURE__ */ new Map();
|
|
820
982
|
for (const entry of this.bodies.values()) {
|
|
821
983
|
if (!entry.owned) continue;
|
|
@@ -840,6 +1002,7 @@ var Predictor = class _Predictor {
|
|
|
840
1002
|
}
|
|
841
1003
|
return r.inForce;
|
|
842
1004
|
});
|
|
1005
|
+
this.driveProxies();
|
|
843
1006
|
this.adapter.step();
|
|
844
1007
|
this.recordStep();
|
|
845
1008
|
this.headTick++;
|
|
@@ -906,6 +1069,7 @@ var Predictor = class _Predictor {
|
|
|
906
1069
|
while (this.accumulatorMs >= dtMs) {
|
|
907
1070
|
this.accumulatorMs -= dtMs;
|
|
908
1071
|
this.applyIntents(() => void 0);
|
|
1072
|
+
this.driveProxies();
|
|
909
1073
|
this.adapter.step();
|
|
910
1074
|
this.recordStep();
|
|
911
1075
|
this.headTick++;
|
|
@@ -925,6 +1089,11 @@ var Predictor = class _Predictor {
|
|
|
925
1089
|
* local body of a collection, owned or not, run after the whole intent pass rather than
|
|
926
1090
|
* interleaved with it. It is what gives a non-owned predicted body its gravity on an engine
|
|
927
1091
|
* whose world has none of its own.
|
|
1092
|
+
*
|
|
1093
|
+
* Both passes walk `this.bodies`, which is why neither ever reaches a proxy (D71): a proxy is not
|
|
1094
|
+
* simulated, so an intent hook or a per-step gravity force on it would be a force on a body that
|
|
1095
|
+
* cannot move, applied to a pose that is going to be overwritten before the next step anyway. The
|
|
1096
|
+
* isolation is structural rather than a filter, which is the version that cannot rot.
|
|
928
1097
|
*/
|
|
929
1098
|
applyIntents(frameFor) {
|
|
930
1099
|
for (const entry of this.bodies.values()) {
|
|
@@ -958,6 +1127,49 @@ var Predictor = class _Predictor {
|
|
|
958
1127
|
return desc.fields[idx]?.type.kind === "f32";
|
|
959
1128
|
}
|
|
960
1129
|
};
|
|
1130
|
+
function poseFromRecord(channels, record, into) {
|
|
1131
|
+
for (const [channel, field] of channels) {
|
|
1132
|
+
const raw = record[field];
|
|
1133
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) continue;
|
|
1134
|
+
switch (channel) {
|
|
1135
|
+
case "x":
|
|
1136
|
+
case "y":
|
|
1137
|
+
case "z":
|
|
1138
|
+
into.t[channel] = raw;
|
|
1139
|
+
break;
|
|
1140
|
+
case "qx":
|
|
1141
|
+
into.r.x = raw;
|
|
1142
|
+
break;
|
|
1143
|
+
case "qy":
|
|
1144
|
+
into.r.y = raw;
|
|
1145
|
+
break;
|
|
1146
|
+
case "qz":
|
|
1147
|
+
into.r.z = raw;
|
|
1148
|
+
break;
|
|
1149
|
+
case "qw":
|
|
1150
|
+
into.r.w = raw;
|
|
1151
|
+
break;
|
|
1152
|
+
case "vx":
|
|
1153
|
+
into.v.x = raw;
|
|
1154
|
+
break;
|
|
1155
|
+
case "vy":
|
|
1156
|
+
into.v.y = raw;
|
|
1157
|
+
break;
|
|
1158
|
+
case "vz":
|
|
1159
|
+
into.v.z = raw;
|
|
1160
|
+
break;
|
|
1161
|
+
case "wx":
|
|
1162
|
+
into.w.x = raw;
|
|
1163
|
+
break;
|
|
1164
|
+
case "wy":
|
|
1165
|
+
into.w.y = raw;
|
|
1166
|
+
break;
|
|
1167
|
+
case "wz":
|
|
1168
|
+
into.w.z = raw;
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
961
1173
|
function channelValue(channel, t, r, v, w) {
|
|
962
1174
|
switch (channel) {
|
|
963
1175
|
case "x":
|