@genex-ai/multiplayer 0.17.6 → 0.17.8
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/PROTOCOL.md +101 -30
- package/README.md +35 -14
- package/WORLD-GLTF.md +109 -0
- package/WORLD-SCENE.md +135 -0
- package/dist/authority/fast-udp.d.ts +4 -2
- package/dist/authority/fast-udp.js +8 -46
- package/dist/authority/fast-webtransport.d.ts +26 -4
- package/dist/authority/fast-webtransport.js +12 -115
- package/dist/authority/index.d.ts +379 -41
- package/dist/authority/index.js +247 -47
- package/dist/authority/module.d.ts +2 -1
- package/dist/authority/module.js +1 -1
- package/dist/bridge-authority.d.ts +3 -2
- package/dist/bridge-authority.global.js +10 -9
- package/dist/bridge-authority.js +11 -6
- package/dist/bridge.global.js +5 -5
- package/dist/bridge.js +3 -3
- package/dist/chunk-2NGXXQDS.js +1916 -0
- package/dist/{chunk-ZSWF4VCN.js → chunk-CSQEEANB.js} +78 -2
- package/dist/{chunk-UX3F6YTV.js → chunk-FPWP22Y7.js} +1 -1
- package/dist/{chunk-5JGRFT2N.js → chunk-NFMGIKFZ.js} +879 -1898
- package/dist/{chunk-K3SF5VDS.js → chunk-OD6NRSX2.js} +3 -1
- package/dist/{chunk-7QVOIHJF.js → chunk-QY3QQNDJ.js} +2 -2
- package/dist/{chunk-Y6XHXDIK.js → chunk-QZOCUQJT.js} +820 -537
- package/dist/chunk-TSYEWPVO.js +44 -0
- package/dist/chunk-TVTVIMIH.js +675 -0
- package/dist/chunk-YWQ3ALYY.js +69 -0
- package/dist/{fast-client-CZ41v-oB.d.ts → fast-client--A88q0kk.d.ts} +85 -4
- package/dist/index.js +3 -3
- package/dist/level-build-CbRqrYJq.d.ts +140 -0
- package/dist/{module-DqtFb5wA.d.ts → module-CSpXrzFc.d.ts} +151 -400
- package/dist/{physics-predict-49udN3a1.d.ts → physics-predict-C7ZB1gDG.d.ts} +18 -3
- package/dist/protocol.d.ts +87 -2
- package/dist/protocol.js +15 -1
- package/dist/shot-world-D22tqOUT.d.ts +485 -0
- package/dist/world-fit/index.d.ts +549 -0
- package/dist/world-fit/index.js +4763 -0
- package/dist/world-fit/three.d.ts +77 -0
- package/dist/world-fit/three.js +140 -0
- package/package.json +15 -3
|
@@ -5,8 +5,12 @@ import {
|
|
|
5
5
|
joinOptions,
|
|
6
6
|
useSdkReconnection,
|
|
7
7
|
waitForFirstState
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-FPWP22Y7.js";
|
|
9
9
|
import {
|
|
10
|
+
udpFastOpener
|
|
11
|
+
} from "./chunk-YWQ3ALYY.js";
|
|
12
|
+
import {
|
|
13
|
+
AuthorityJoinMethod,
|
|
10
14
|
BODY_CORRECTION_TAU_MS,
|
|
11
15
|
BODY_EXTRAPOLATE_BACK_TICKS,
|
|
12
16
|
BODY_EXTRAPOLATE_MAX_MS,
|
|
@@ -16,11 +20,12 @@ import {
|
|
|
16
20
|
ContactDrawing,
|
|
17
21
|
ContactPush,
|
|
18
22
|
ContactShapeKind,
|
|
19
|
-
|
|
23
|
+
FAST_BIND_REV,
|
|
20
24
|
FAST_HANDSHAKE_MS,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
FAST_KEEPALIVE_BIND_MS,
|
|
26
|
+
FAST_RETURN_PROOF,
|
|
27
|
+
FAST_RETURN_STABLE_MS,
|
|
28
|
+
FastFallbackReason,
|
|
24
29
|
FastMessage,
|
|
25
30
|
HeadFriction,
|
|
26
31
|
InputRepeat,
|
|
@@ -30,48 +35,59 @@ import {
|
|
|
30
35
|
LiftPathMode,
|
|
31
36
|
LiftShapeKind,
|
|
32
37
|
MAX_PROJECTILE_G,
|
|
38
|
+
MotionRecordKind,
|
|
33
39
|
NO_LIFTS,
|
|
34
40
|
PHYSICS_PREDICT_MAX_ANGULAR,
|
|
35
41
|
PHYSICS_PREDICT_MAX_LINEAR,
|
|
36
42
|
PHYSICS_PRESET_LENGTH_UNITS,
|
|
37
43
|
PhysicsPredictRange,
|
|
38
|
-
Preset,
|
|
39
44
|
TopPolicy,
|
|
40
|
-
|
|
45
|
+
blankEntityMotion,
|
|
46
|
+
blankSeatEcho,
|
|
47
|
+
chooseFastSocket,
|
|
41
48
|
dequantizeBodyValue,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
firstpersonWorld,
|
|
45
|
-
hullOps,
|
|
46
|
-
inputRedundancyTicks,
|
|
49
|
+
fastRetryDelayMs,
|
|
50
|
+
isFastBind,
|
|
47
51
|
isProjectile,
|
|
48
|
-
|
|
52
|
+
nextCodeRegion,
|
|
53
|
+
openFastClient,
|
|
54
|
+
planAuthorityJoin,
|
|
49
55
|
presetInputRepeat,
|
|
50
56
|
projectileAt,
|
|
51
|
-
psincos,
|
|
52
57
|
quantizeBodyValue,
|
|
53
|
-
receiveFragment,
|
|
54
|
-
resolvePresetParams,
|
|
55
58
|
sanitizeInput,
|
|
56
59
|
seqAheadMax,
|
|
57
60
|
serverTick,
|
|
58
61
|
tickInstant,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
uplinkMaxBytes
|
|
62
|
-
} from "./chunk-5JGRFT2N.js";
|
|
62
|
+
validRoomCode
|
|
63
|
+
} from "./chunk-NFMGIKFZ.js";
|
|
63
64
|
import {
|
|
64
65
|
RemotePredict
|
|
65
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-OD6NRSX2.js";
|
|
67
|
+
import {
|
|
68
|
+
EMPTY_WORLD,
|
|
69
|
+
Preset,
|
|
70
|
+
definitionOf,
|
|
71
|
+
firstpersonWorld,
|
|
72
|
+
hullOps,
|
|
73
|
+
posedWorld,
|
|
74
|
+
psincos,
|
|
75
|
+
resolvePresetParams,
|
|
76
|
+
unsignedZero
|
|
77
|
+
} from "./chunk-2NGXXQDS.js";
|
|
66
78
|
import {
|
|
67
79
|
AUTHORITY_PROTOCOL_VERSION,
|
|
80
|
+
AUTHORITY_SDK_LABEL,
|
|
68
81
|
AuthorityCaps,
|
|
69
82
|
AuthorityJoinOption,
|
|
70
83
|
AuthorityTiming,
|
|
71
84
|
ClientReconnection,
|
|
85
|
+
CloseCode,
|
|
72
86
|
JoinError,
|
|
73
|
-
RoomType
|
|
74
|
-
|
|
87
|
+
RoomType,
|
|
88
|
+
ServerMessage,
|
|
89
|
+
authorityCloseCodeOf
|
|
90
|
+
} from "./chunk-CSQEEANB.js";
|
|
75
91
|
|
|
76
92
|
// src/authority/regions.ts
|
|
77
93
|
var AUTHORITY_REFUSALS_PER_REGION = 2;
|
|
@@ -120,6 +136,20 @@ function capacityWalk(regionCount, delays) {
|
|
|
120
136
|
}
|
|
121
137
|
return attempts;
|
|
122
138
|
}
|
|
139
|
+
var RefusalKind = {
|
|
140
|
+
/** A full region, or a process shutting down that a sibling answers: the schedule, on this region. */
|
|
141
|
+
Capacity: "capacity",
|
|
142
|
+
/** A dead path, with other regions healthy: the next region at once. */
|
|
143
|
+
NextRegion: "next_region",
|
|
144
|
+
/** The server's answer: final. */
|
|
145
|
+
Final: "final"
|
|
146
|
+
};
|
|
147
|
+
var CAPACITY_REFUSALS = /* @__PURE__ */ new Set([503, 4213]);
|
|
148
|
+
var DEAD_PATH_REFUSALS = /* @__PURE__ */ new Set([502, 504, 520, 521, 522, 523, 524, 525, 526, 530]);
|
|
149
|
+
function refusalKind(code) {
|
|
150
|
+
if (typeof code !== "number" || code <= 0 || DEAD_PATH_REFUSALS.has(code)) return RefusalKind.NextRegion;
|
|
151
|
+
return CAPACITY_REFUSALS.has(code) ? RefusalKind.Capacity : RefusalKind.Final;
|
|
152
|
+
}
|
|
123
153
|
function nextAttempt(state, regionCount, delays) {
|
|
124
154
|
const refused = state.refused + 1;
|
|
125
155
|
if (refused >= AUTHORITY_REFUSALS_PER_REGION && state.region + 1 < regionCount) {
|
|
@@ -192,6 +222,187 @@ async function probeLatencies(plans, options = {}) {
|
|
|
192
222
|
return results;
|
|
193
223
|
}
|
|
194
224
|
|
|
225
|
+
// src/authority/rejoin.ts
|
|
226
|
+
var REJOIN = {
|
|
227
|
+
/** Waits before each attempt; past the end the last one repeats. */
|
|
228
|
+
scheduleMs: [0, 250, 500, 1e3, 2e3, 4e3],
|
|
229
|
+
/** Each wait adds up to this much, by chance, so a room's seats do not arrive at once. */
|
|
230
|
+
jitterMs: 250,
|
|
231
|
+
/** How long a seat keeps trying: long enough for a roll's next process to come up on a full node. */
|
|
232
|
+
budgetMs: 18e4,
|
|
233
|
+
/** How long a seat stays with its own region before it tries the next one. */
|
|
234
|
+
regionStayMs: 6e4,
|
|
235
|
+
/** How long a public seat looks for its room's successor before it joins the game's pool. */
|
|
236
|
+
successorMs: 9e4
|
|
237
|
+
};
|
|
238
|
+
var SUCCESSOR_OPEN_MS = 12e4;
|
|
239
|
+
var RejoinWay = { Room: "room", Successor: "successor", Pool: "pool" };
|
|
240
|
+
var RejoinStop = { Final: "final", Budget: "budget" };
|
|
241
|
+
var COMES_BACK = /* @__PURE__ */ new Set([
|
|
242
|
+
CloseCode.GOING_AWAY,
|
|
243
|
+
CloseCode.NO_STATUS,
|
|
244
|
+
CloseCode.ABNORMAL,
|
|
245
|
+
1011,
|
|
246
|
+
1012,
|
|
247
|
+
1013,
|
|
248
|
+
CloseCode.SERVER_SHUTDOWN,
|
|
249
|
+
CloseCode.FAILED_TO_RECONNECT,
|
|
250
|
+
CloseCode.MAY_TRY_RECONNECT
|
|
251
|
+
]);
|
|
252
|
+
var FINAL_REFUSALS = /* @__PURE__ */ new Set([
|
|
253
|
+
JoinError.BAD_OPTION,
|
|
254
|
+
JoinError.NOT_ENTITLED,
|
|
255
|
+
JoinError.FORBIDDEN,
|
|
256
|
+
JoinError.MODULE_UNAVAILABLE,
|
|
257
|
+
JoinError.PROTOCOL_UNSUPPORTED
|
|
258
|
+
]);
|
|
259
|
+
function comesBackAfter(code) {
|
|
260
|
+
return COMES_BACK.has(code);
|
|
261
|
+
}
|
|
262
|
+
function rejoinStep(input) {
|
|
263
|
+
const { attempt, elapsedMs, refusal, lostCode, closed, regions, jitter } = input;
|
|
264
|
+
if (refusal !== null && FINAL_REFUSALS.has(refusal)) return { stop: RejoinStop.Final };
|
|
265
|
+
if (elapsedMs >= REJOIN.budgetMs) return { stop: RejoinStop.Budget };
|
|
266
|
+
const last = REJOIN.scheduleMs.length - 1;
|
|
267
|
+
const base = REJOIN.scheduleMs[Math.min(attempt, last)];
|
|
268
|
+
const waitMs = base + Math.floor(jitter * REJOIN.jitterMs);
|
|
269
|
+
if (attempt === 0 && lostCode !== CloseCode.SERVER_SHUTDOWN) return { waitMs, region: 0, way: RejoinWay.Room };
|
|
270
|
+
const region = regions < 1 ? 0 : Math.floor(elapsedMs / REJOIN.regionStayMs) % regions;
|
|
271
|
+
const way = closed || elapsedMs < REJOIN.successorMs ? RejoinWay.Successor : RejoinWay.Pool;
|
|
272
|
+
return { waitMs, region, way };
|
|
273
|
+
}
|
|
274
|
+
function validSuccessorOf(value) {
|
|
275
|
+
return validRoomCode(value);
|
|
276
|
+
}
|
|
277
|
+
function planSuccessorJoin(input) {
|
|
278
|
+
return {
|
|
279
|
+
...input.matchKey === "" ? {} : { matchKey: input.matchKey },
|
|
280
|
+
successorOf: input.lostRoomId,
|
|
281
|
+
...input.closed ? { successorClosed: true } : {}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/authority/session-rejoin.ts
|
|
286
|
+
var RejoinEvent = { Draining: "draining", Rejoining: "rejoining", Rejoined: "rejoined" };
|
|
287
|
+
var DISCONNECT = "disconnect";
|
|
288
|
+
var UNKNOWN_CLOSE_MS = -1;
|
|
289
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
290
|
+
var refusalOf = (err) => {
|
|
291
|
+
const code = err !== null && typeof err === "object" ? err.code : void 0;
|
|
292
|
+
return typeof code === "number" ? code : null;
|
|
293
|
+
};
|
|
294
|
+
function survivingSession(first, deps) {
|
|
295
|
+
const now = deps.now ?? Date.now;
|
|
296
|
+
const random = deps.random ?? Math.random;
|
|
297
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
298
|
+
const input = { ...first.input };
|
|
299
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
300
|
+
let current = first;
|
|
301
|
+
let left = false;
|
|
302
|
+
let physicsHash = null;
|
|
303
|
+
let world = null;
|
|
304
|
+
const fire = (event, ...payload) => {
|
|
305
|
+
for (const handler of listeners.get(event) ?? []) handler(...payload);
|
|
306
|
+
};
|
|
307
|
+
const forward = (session, event) => {
|
|
308
|
+
if (event === DISCONNECT || event === RejoinEvent.Draining || event === RejoinEvent.Rejoining || event === RejoinEvent.Rejoined) return;
|
|
309
|
+
session.on(event, (...payload) => {
|
|
310
|
+
if (session === current) fire(event, ...payload);
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
const rejoin = async (lostCode, lostReason) => {
|
|
314
|
+
const startedAt = now();
|
|
315
|
+
const lost = { roomId: current.code, matchKey: current.matchKey, closed: deps.closed };
|
|
316
|
+
const regions = deps.targets.length;
|
|
317
|
+
let attempt = 0;
|
|
318
|
+
let refusal = null;
|
|
319
|
+
while (!left) {
|
|
320
|
+
const step = rejoinStep({ attempt, elapsedMs: now() - startedAt, refusal, lostCode, closed: deps.closed, regions, jitter: random() });
|
|
321
|
+
if ("stop" in step) {
|
|
322
|
+
fire(DISCONNECT, lostCode, lostReason);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
attempt += 1;
|
|
326
|
+
fire(RejoinEvent.Rejoining, { attempt, code: lostCode, waitMs: step.waitMs });
|
|
327
|
+
await sleep(step.waitMs);
|
|
328
|
+
if (left) return;
|
|
329
|
+
try {
|
|
330
|
+
const next = await deps.join(deps.targets[step.region], step.way, lost);
|
|
331
|
+
if (left) {
|
|
332
|
+
await next.leave().catch(() => {
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
adopt(next);
|
|
337
|
+
fire(RejoinEvent.Rejoined, { code: next.code, url: next.url });
|
|
338
|
+
return;
|
|
339
|
+
} catch (err) {
|
|
340
|
+
refusal = refusalOf(err);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const adopt = (session) => {
|
|
345
|
+
current = session;
|
|
346
|
+
if (physicsHash !== null) session.setPhysics(physicsHash);
|
|
347
|
+
if (world !== null) session.setWorld(world);
|
|
348
|
+
for (const event of listeners.keys()) forward(session, event);
|
|
349
|
+
watch(session);
|
|
350
|
+
};
|
|
351
|
+
const watch = (session) => {
|
|
352
|
+
session.on(ServerMessage.SERVER_RESTART, (notice) => {
|
|
353
|
+
if (session !== current) return;
|
|
354
|
+
const drainMs = notice?.drainMs;
|
|
355
|
+
fire(RejoinEvent.Draining, { closesInMs: typeof drainMs === "number" ? drainMs : UNKNOWN_CLOSE_MS });
|
|
356
|
+
});
|
|
357
|
+
session.on(DISCONNECT, (code, reason) => {
|
|
358
|
+
if (session !== current || left) return;
|
|
359
|
+
if (comesBackAfter(code)) void rejoin(code, reason);
|
|
360
|
+
else fire(DISCONNECT, code, reason);
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
watch(first);
|
|
364
|
+
const own = {
|
|
365
|
+
input,
|
|
366
|
+
frame(frameNow) {
|
|
367
|
+
Object.assign(current.input, input);
|
|
368
|
+
return current.frame(frameNow);
|
|
369
|
+
},
|
|
370
|
+
setWorld(next) {
|
|
371
|
+
const hash = current.setWorld(next);
|
|
372
|
+
world = next;
|
|
373
|
+
return hash;
|
|
374
|
+
},
|
|
375
|
+
setPhysics(hash) {
|
|
376
|
+
physicsHash = hash;
|
|
377
|
+
return current.setPhysics(hash);
|
|
378
|
+
},
|
|
379
|
+
on(event, handler) {
|
|
380
|
+
let set = listeners.get(event);
|
|
381
|
+
if (set === void 0) {
|
|
382
|
+
set = /* @__PURE__ */ new Set();
|
|
383
|
+
listeners.set(event, set);
|
|
384
|
+
forward(current, event);
|
|
385
|
+
}
|
|
386
|
+
const fn = handler;
|
|
387
|
+
set.add(fn);
|
|
388
|
+
return () => {
|
|
389
|
+
set.delete(fn);
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
async leave() {
|
|
393
|
+
left = true;
|
|
394
|
+
await current.leave();
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
return new Proxy(first, {
|
|
398
|
+
get(_target, key) {
|
|
399
|
+
if (Object.hasOwn(own, key)) return own[key];
|
|
400
|
+
const value = current[key];
|
|
401
|
+
return typeof value === "function" ? value.bind(current) : value;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
195
406
|
// src/authority/physics-draw.ts
|
|
196
407
|
var IDENTITY = [0, 0, 0, 1];
|
|
197
408
|
function quatMul(a, b) {
|
|
@@ -289,85 +500,127 @@ var GRID_SIDE = 32;
|
|
|
289
500
|
var GRID_CELL_MIN = 64;
|
|
290
501
|
var GRID_WIDE = 64;
|
|
291
502
|
var GRID_QUERY_CELLS = 16;
|
|
503
|
+
var GRID_MERGED_WIDE = 64;
|
|
292
504
|
var UNSTICK_SOLIDS = 4;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
[0, 0, -1, -(z0 - h)]
|
|
302
|
-
];
|
|
303
|
-
}
|
|
304
|
-
function grownRamp(ramp, r, h) {
|
|
505
|
+
var BOUNDS_STRIDE = 6;
|
|
506
|
+
var PLANE_STRIDE = 4;
|
|
507
|
+
var BOX_PLANES = 6;
|
|
508
|
+
var RAMP_PLANES = 7;
|
|
509
|
+
var RAMP_STRIDE = RAMP_PLANES * PLANE_STRIDE;
|
|
510
|
+
var BOX_NORMALS = [[-1, 0, 0], [1, 0, 0], [0, -1, 0], [0, 1, 0], [0, 0, 1], [0, 0, -1]];
|
|
511
|
+
var BOX_OFFSETS = [0, 3, 1, 4, 5, 2];
|
|
512
|
+
function writeRamp(out, o, ramp, r, h) {
|
|
305
513
|
const [x0, y0, z0, x1, y1, z1, axis, lo, hi] = ramp;
|
|
306
514
|
const m0 = axis === 0 ? x0 : y0;
|
|
307
515
|
const slope = (hi - lo) / ((axis === 0 ? x1 : y1) - m0);
|
|
308
516
|
const k = 1 / Math.sqrt(slope * slope + 1);
|
|
309
517
|
const e = (lo - slope * m0 + r * Math.abs(slope)) * k;
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
function solidOf(planes, src, r, h) {
|
|
314
|
-
return { planes, min: [src[0] - r, src[1] - r, src[2] - h], max: [src[3] + r, src[4] + r, src[5]] };
|
|
518
|
+
const planes = [1, 0, 0, x1 + r, -1, 0, 0, -x0 + r, 0, 1, 0, y1 + r, 0, -1, 0, -y0 + r, axis === 0 ? -slope * k : 0, axis === 0 ? 0 : -slope * k, k, e, 0, 0, 1, z1, 0, 0, -1, -z0 + h];
|
|
519
|
+
out.set(planes, o);
|
|
315
520
|
}
|
|
316
521
|
function compileContactWorld(spec, r, h, skin) {
|
|
317
|
-
const boxes =
|
|
318
|
-
const ramps =
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
522
|
+
const boxes = spec?.boxes ?? [];
|
|
523
|
+
const ramps = spec?.ramps ?? [];
|
|
524
|
+
const count = boxes.length + ramps.length;
|
|
525
|
+
const bounds = new Float64Array(count * BOUNDS_STRIDE);
|
|
526
|
+
const rampPlanes = new Float64Array(ramps.length * RAMP_STRIDE);
|
|
527
|
+
for (let i = 0; i < count; i++) {
|
|
528
|
+
const src = i < boxes.length ? boxes[i] : ramps[i - boxes.length];
|
|
529
|
+
const o = i * BOUNDS_STRIDE;
|
|
530
|
+
bounds[o] = src[0] - r;
|
|
531
|
+
bounds[o + 1] = src[1] - r;
|
|
532
|
+
bounds[o + 2] = src[2] - h;
|
|
533
|
+
bounds[o + 3] = src[3] + r;
|
|
534
|
+
bounds[o + 4] = src[4] + r;
|
|
535
|
+
bounds[o + 5] = src[5];
|
|
536
|
+
if (i >= boxes.length) writeRamp(rampPlanes, (i - boxes.length) * RAMP_STRIDE, src, r, h);
|
|
537
|
+
}
|
|
538
|
+
const world = { count, boxCount: boxes.length, bounds, rampPlanes, skin };
|
|
539
|
+
const grid = gridOf(world);
|
|
540
|
+
return grid === null ? world : { ...world, grid };
|
|
541
|
+
}
|
|
542
|
+
function placeable(b, i) {
|
|
543
|
+
const o = i * BOUNDS_STRIDE;
|
|
544
|
+
return Number.isFinite(b[o]) && Number.isFinite(b[o + 3]) && Number.isFinite(b[o + 1]) && Number.isFinite(b[o + 4]);
|
|
322
545
|
}
|
|
323
|
-
var placeable = (s) => Number.isFinite(s.min[0]) && Number.isFinite(s.min[1]) && Number.isFinite(s.max[0]) && Number.isFinite(s.max[1]);
|
|
324
546
|
function cellOf(v, origin, size, n) {
|
|
325
547
|
const c = Math.floor((v - origin) / size);
|
|
326
548
|
if (c < 0) return 0;
|
|
327
549
|
return c > n - 1 ? n - 1 : c;
|
|
328
550
|
}
|
|
329
|
-
function gridOf(
|
|
330
|
-
const
|
|
331
|
-
|
|
551
|
+
function gridOf(world) {
|
|
552
|
+
const { count, bounds } = world;
|
|
553
|
+
let placed2 = 0;
|
|
332
554
|
let x0 = Number.POSITIVE_INFINITY;
|
|
333
555
|
let y0 = Number.POSITIVE_INFINITY;
|
|
334
556
|
let x1 = Number.NEGATIVE_INFINITY;
|
|
335
557
|
let y1 = Number.NEGATIVE_INFINITY;
|
|
336
|
-
for (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
558
|
+
for (let k = 0; k < count; k++) {
|
|
559
|
+
if (!placeable(bounds, k)) continue;
|
|
560
|
+
placed2 += 1;
|
|
561
|
+
const o = k * BOUNDS_STRIDE;
|
|
562
|
+
x0 = Math.min(x0, bounds[o] - CAST_MARGIN);
|
|
563
|
+
y0 = Math.min(y0, bounds[o + 1] - CAST_MARGIN);
|
|
564
|
+
x1 = Math.max(x1, bounds[o + 3] + CAST_MARGIN);
|
|
565
|
+
y1 = Math.max(y1, bounds[o + 4] + CAST_MARGIN);
|
|
566
|
+
}
|
|
567
|
+
if (placed2 < GRID_MIN_SOLIDS) return null;
|
|
342
568
|
const size = Math.max(GRID_CELL_MIN, (x1 - x0) / GRID_SIDE, (y1 - y0) / GRID_SIDE);
|
|
569
|
+
if (!Number.isFinite(size)) return null;
|
|
343
570
|
const nx = Math.min(GRID_SIDE, Math.floor((x1 - x0) / size) + 1);
|
|
344
571
|
const ny = Math.min(GRID_SIDE, Math.floor((y1 - y0) / size) + 1);
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
572
|
+
const visit = (onWide, onCell) => {
|
|
573
|
+
for (let k = 0; k < count; k++) {
|
|
574
|
+
if (!placeable(bounds, k)) {
|
|
575
|
+
onWide(k);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const o = k * BOUNDS_STRIDE;
|
|
579
|
+
const i0 = cellOf(bounds[o] - CAST_MARGIN, x0, size, nx);
|
|
580
|
+
const i1 = cellOf(bounds[o + 3] + CAST_MARGIN, x0, size, nx);
|
|
581
|
+
const j0 = cellOf(bounds[o + 1] - CAST_MARGIN, y0, size, ny);
|
|
582
|
+
const j1 = cellOf(bounds[o + 4] + CAST_MARGIN, y0, size, ny);
|
|
583
|
+
if ((i1 - i0 + 1) * (j1 - j0 + 1) > GRID_WIDE) {
|
|
584
|
+
onWide(k);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
for (let j = j0; j <= j1; j++) for (let i = i0; i <= i1; i++) onCell(i + j * nx, k);
|
|
351
588
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
589
|
+
};
|
|
590
|
+
const cellStart = new Int32Array(nx * ny + 1);
|
|
591
|
+
let wideCount = 0;
|
|
592
|
+
visit(
|
|
593
|
+
() => {
|
|
594
|
+
wideCount += 1;
|
|
595
|
+
},
|
|
596
|
+
(c) => {
|
|
597
|
+
cellStart[c + 1] += 1;
|
|
359
598
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
599
|
+
);
|
|
600
|
+
for (let c = 0; c < nx * ny; c++) cellStart[c + 1] = cellStart[c + 1] + cellStart[c];
|
|
601
|
+
const cellSolids = new Int32Array(cellStart[nx * ny]);
|
|
602
|
+
const wide = new Int32Array(wideCount);
|
|
603
|
+
const written = cellStart.slice();
|
|
604
|
+
let wideAt = 0;
|
|
605
|
+
visit(
|
|
606
|
+
(k) => {
|
|
607
|
+
wide[wideAt] = k;
|
|
608
|
+
wideAt += 1;
|
|
609
|
+
},
|
|
610
|
+
(c, k) => {
|
|
611
|
+
cellSolids[written[c]] = k;
|
|
612
|
+
written[c] = written[c] + 1;
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
return { x0, y0, size, nx, ny, cellStart, cellSolids, wide, merged: new Array(wideAt <= GRID_MERGED_WIDE ? nx * ny : 0) };
|
|
363
616
|
}
|
|
364
|
-
function mergeAscending(a, b) {
|
|
365
|
-
if (
|
|
617
|
+
function mergeAscending(a, b, from, to) {
|
|
618
|
+
if (to === from) return a;
|
|
366
619
|
const out = [];
|
|
367
620
|
let i = 0;
|
|
368
|
-
let k =
|
|
369
|
-
while (i < a.length || k <
|
|
370
|
-
const fromA = k >=
|
|
621
|
+
let k = from;
|
|
622
|
+
while (i < a.length || k < to) {
|
|
623
|
+
const fromA = k >= to || i < a.length && a[i] <= b[k];
|
|
371
624
|
const next = fromA ? a[i] : b[k];
|
|
372
625
|
if (fromA) i += 1;
|
|
373
626
|
else k += 1;
|
|
@@ -379,50 +632,100 @@ function solidsNear(world, lx, hx, ly, hy) {
|
|
|
379
632
|
const g = world.grid;
|
|
380
633
|
if (g === void 0 || !(Number.isFinite(lx) && Number.isFinite(hx) && Number.isFinite(ly) && Number.isFinite(hy))) return null;
|
|
381
634
|
const i0 = cellOf(lx, g.x0, g.size, g.nx);
|
|
382
|
-
const i1 = cellOf(hx, g.x0, g.size, g.nx);
|
|
635
|
+
const i1 = hx === lx ? i0 : cellOf(hx, g.x0, g.size, g.nx);
|
|
383
636
|
const j0 = cellOf(ly, g.y0, g.size, g.ny);
|
|
384
|
-
const j1 = cellOf(hy, g.y0, g.size, g.ny);
|
|
637
|
+
const j1 = hy === ly ? j0 : cellOf(hy, g.y0, g.size, g.ny);
|
|
385
638
|
if ((i1 - i0 + 1) * (j1 - j0 + 1) > GRID_QUERY_CELLS) return null;
|
|
639
|
+
if (i0 === i1 && j0 === j1 && g.merged.length > 0) {
|
|
640
|
+
const c = i0 + j0 * g.nx;
|
|
641
|
+
const kept = g.merged[c];
|
|
642
|
+
if (kept !== void 0) return kept;
|
|
643
|
+
const made = mergeAscending(g.wide, g.cellSolids, g.cellStart[c], g.cellStart[c + 1]);
|
|
644
|
+
g.merged[c] = made;
|
|
645
|
+
return made;
|
|
646
|
+
}
|
|
386
647
|
let near2 = g.wide;
|
|
387
|
-
for (let j = j0; j <= j1; j++)
|
|
648
|
+
for (let j = j0; j <= j1; j++) {
|
|
649
|
+
for (let i = i0; i <= i1; i++) {
|
|
650
|
+
const c = i + j * g.nx;
|
|
651
|
+
near2 = mergeAscending(near2, g.cellSolids, g.cellStart[c], g.cellStart[c + 1]);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
388
654
|
return near2;
|
|
389
655
|
}
|
|
390
|
-
|
|
391
|
-
|
|
656
|
+
var P = { nx: 0, ny: 0, nz: 0, e: 0 };
|
|
657
|
+
function readPlane(world, i, k) {
|
|
658
|
+
if (i < world.boxCount) {
|
|
659
|
+
const n = BOX_NORMALS[k];
|
|
660
|
+
const at2 = BOX_OFFSETS[k];
|
|
661
|
+
const v = world.bounds[i * BOUNDS_STRIDE + at2];
|
|
662
|
+
P.nx = n[0];
|
|
663
|
+
P.ny = n[1];
|
|
664
|
+
P.nz = n[2];
|
|
665
|
+
P.e = at2 < 3 ? -v : v;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const o = (i - world.boxCount) * RAMP_STRIDE + k * PLANE_STRIDE;
|
|
669
|
+
const r = world.rampPlanes;
|
|
670
|
+
P.nx = r[o];
|
|
671
|
+
P.ny = r[o + 1];
|
|
672
|
+
P.nz = r[o + 2];
|
|
673
|
+
P.e = r[o + 3];
|
|
674
|
+
}
|
|
675
|
+
var planesOf = (world, i) => i < world.boxCount ? BOX_PLANES : RAMP_PLANES;
|
|
676
|
+
function insideSolid(world, i, skin, x, y, z) {
|
|
677
|
+
const n = planesOf(world, i);
|
|
678
|
+
for (let k = 0; k < n; k++) {
|
|
679
|
+
readPlane(world, i, k);
|
|
680
|
+
if (!(P.e - (P.nx * x + P.ny * y + P.nz * z) > skin)) return false;
|
|
681
|
+
}
|
|
392
682
|
return true;
|
|
393
683
|
}
|
|
684
|
+
var solidsAt = (world, x, y) => solidsNear(world, x, x, y, y);
|
|
394
685
|
function insideWorld(world, x, y, z) {
|
|
395
|
-
|
|
686
|
+
const near2 = solidsAt(world, x, y);
|
|
687
|
+
const n = near2 === null ? world.count : near2.length;
|
|
688
|
+
for (let m = 0; m < n; m++) {
|
|
689
|
+
if (insideSolid(world, near2 === null ? m : near2[m], world.skin, x, y, z)) return true;
|
|
690
|
+
}
|
|
396
691
|
return false;
|
|
397
692
|
}
|
|
398
|
-
function segmentNear(s, p, d, len) {
|
|
693
|
+
function segmentNear(world, s, p, d, len) {
|
|
694
|
+
const o = s * BOUNDS_STRIDE;
|
|
399
695
|
for (let i = 0; i < 3; i++) {
|
|
400
696
|
const a = p[i];
|
|
401
697
|
const b = a + d[i] * len;
|
|
402
698
|
const lo = a < b ? a : b;
|
|
403
699
|
const hi = a < b ? b : a;
|
|
404
|
-
if (hi <
|
|
700
|
+
if (hi < world.bounds[o + i] - CAST_MARGIN || lo > world.bounds[o + 3 + i] + CAST_MARGIN) return false;
|
|
405
701
|
}
|
|
406
702
|
return true;
|
|
407
703
|
}
|
|
408
|
-
function bandHit(s, skin, p, d, rides) {
|
|
409
|
-
|
|
410
|
-
|
|
704
|
+
function bandHit(world, s, skin, p, d, rides) {
|
|
705
|
+
const n = planesOf(world, s);
|
|
706
|
+
for (let k = 0; k < n; k++) {
|
|
707
|
+
readPlane(world, s, k);
|
|
708
|
+
const { nx, ny, nz, e } = P;
|
|
411
709
|
const margin = e - (nx * p[0] + ny * p[1] + nz * p[2]);
|
|
412
|
-
if (margin <= skin && nx * d[0] + ny * d[1] + nz * d[2] < 0
|
|
710
|
+
if (margin <= skin && nx * d[0] + ny * d[1] + nz * d[2] < 0) {
|
|
711
|
+
const plane = [nx, ny, nz, e];
|
|
712
|
+
if (!rides?.(plane)) return { t: 0, face: plane };
|
|
713
|
+
}
|
|
413
714
|
}
|
|
414
715
|
return null;
|
|
415
716
|
}
|
|
416
717
|
var ridesOn = (up, cos) => (face) => face[2] >= cos && face[0] * up[0] + face[1] * up[1] + face[2] * up[2] >= cos;
|
|
417
|
-
function enterSolid(s, skin, p, d, len, rides) {
|
|
418
|
-
if (
|
|
419
|
-
if (
|
|
718
|
+
function enterSolid(world, s, skin, p, d, len, rides) {
|
|
719
|
+
if (insideSolid(world, s, skin, p[0], p[1], p[2])) return null;
|
|
720
|
+
if (insideSolid(world, s, -skin, p[0], p[1], p[2])) return bandHit(world, s, skin, p, d, rides);
|
|
420
721
|
let lo = 0;
|
|
421
722
|
let hi = len;
|
|
422
723
|
let last = Number.NEGATIVE_INFINITY;
|
|
423
|
-
let face =
|
|
424
|
-
|
|
425
|
-
|
|
724
|
+
let face = -1;
|
|
725
|
+
const n = planesOf(world, s);
|
|
726
|
+
for (let k = 0; k < n; k++) {
|
|
727
|
+
readPlane(world, s, k);
|
|
728
|
+
const { nx, ny, nz, e } = P;
|
|
426
729
|
const s0 = e + skin - (nx * p[0] + ny * p[1] + nz * p[2]);
|
|
427
730
|
const sd = nx * d[0] + ny * d[1] + nz * d[2];
|
|
428
731
|
if (sd === 0) {
|
|
@@ -433,26 +736,29 @@ function enterSolid(s, skin, p, d, len, rides) {
|
|
|
433
736
|
if (sd > 0 && t2 < hi) hi = t2;
|
|
434
737
|
if (sd < 0 && t2 > last) {
|
|
435
738
|
last = t2;
|
|
436
|
-
face =
|
|
739
|
+
face = k;
|
|
437
740
|
}
|
|
438
741
|
}
|
|
439
742
|
if (last > lo) lo = last;
|
|
440
|
-
if (face
|
|
441
|
-
|
|
743
|
+
if (face < 0 || !(lo < hi)) return null;
|
|
744
|
+
readPlane(world, s, face);
|
|
745
|
+
const plane = [P.nx, P.ny, P.nz, P.e];
|
|
746
|
+
if (rides?.(plane)) return null;
|
|
747
|
+
return { t: lo, face: plane };
|
|
442
748
|
}
|
|
443
749
|
function castWorld(world, p, d, len, rides) {
|
|
444
750
|
let best = { t: len, face: null };
|
|
445
751
|
if (!(len > 0)) return { t: 0, face: null };
|
|
446
752
|
const visit = (s) => {
|
|
447
|
-
if (!segmentNear(s, p, d, best.t)) return;
|
|
448
|
-
const hit = enterSolid(s, world.skin, p, d, best.t, rides);
|
|
753
|
+
if (!segmentNear(world, s, p, d, best.t)) return;
|
|
754
|
+
const hit = enterSolid(world, s, world.skin, p, d, best.t, rides);
|
|
449
755
|
if (hit !== null && hit.t < best.t) best = hit;
|
|
450
756
|
};
|
|
451
757
|
const [px, py] = [p[0], p[1]];
|
|
452
758
|
const [ex, ey] = [px + d[0] * len, py + d[1] * len];
|
|
453
759
|
const near2 = solidsNear(world, Math.min(px, ex), Math.max(px, ex), Math.min(py, ey), Math.max(py, ey));
|
|
454
|
-
if (near2 === null) for (
|
|
455
|
-
else for (
|
|
760
|
+
if (near2 === null) for (let s = 0; s < world.count; s++) visit(s);
|
|
761
|
+
else for (let k = 0; k < near2.length; k++) visit(near2[k]);
|
|
456
762
|
return best;
|
|
457
763
|
}
|
|
458
764
|
function roomAlong(world, p, d, len, rides) {
|
|
@@ -469,18 +775,22 @@ function surfaceUp(world, x, y, z, trace) {
|
|
|
469
775
|
return [hit.face[0], hit.face[1], hit.face[2]];
|
|
470
776
|
}
|
|
471
777
|
function covered(world, self, x, y, z, q) {
|
|
472
|
-
const {
|
|
473
|
-
|
|
778
|
+
const { count, skin } = world;
|
|
779
|
+
const near2 = solidsAt(world, q[0], q[1]);
|
|
780
|
+
const n = near2 === null ? count : near2.length;
|
|
781
|
+
for (let m = 0; m < n; m++) {
|
|
782
|
+
const j = near2 === null ? m : near2[m];
|
|
474
783
|
if (j === self) continue;
|
|
475
|
-
|
|
476
|
-
if (insidePlanes(planes, skin, q[0], q[1], q[2]) && !insidePlanes(planes, skin, x, y, z)) return true;
|
|
784
|
+
if (insideSolid(world, j, skin, q[0], q[1], q[2]) && !insideSolid(world, j, skin, x, y, z)) return true;
|
|
477
785
|
}
|
|
478
786
|
return false;
|
|
479
787
|
}
|
|
480
|
-
function leastFace(world, i,
|
|
788
|
+
function leastFace(world, i, x, y, z) {
|
|
481
789
|
let least = null;
|
|
482
|
-
|
|
483
|
-
|
|
790
|
+
const n = planesOf(world, i);
|
|
791
|
+
for (let k = 0; k < n - 1; k++) {
|
|
792
|
+
readPlane(world, i, k);
|
|
793
|
+
const { nx, ny, nz, e } = P;
|
|
484
794
|
const depth = e - (nx * x + ny * y + nz * z);
|
|
485
795
|
if (least !== null && !(depth < least[3])) continue;
|
|
486
796
|
if (nz > 0 && covered(world, i, x, y, z, [x + nx * depth, y + ny * depth, z + nz * depth])) continue;
|
|
@@ -489,10 +799,12 @@ function leastFace(world, i, planes, x, y, z) {
|
|
|
489
799
|
return least;
|
|
490
800
|
}
|
|
491
801
|
function insideFace(world, x, y, z) {
|
|
492
|
-
const {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
802
|
+
const { count, skin } = world;
|
|
803
|
+
const near2 = solidsAt(world, x, y);
|
|
804
|
+
const n = near2 === null ? count : near2.length;
|
|
805
|
+
for (let m = 0; m < n; m++) {
|
|
806
|
+
const i = near2 === null ? m : near2[m];
|
|
807
|
+
if (insideSolid(world, i, skin, x, y, z)) return leastFace(world, i, x, y, z);
|
|
496
808
|
}
|
|
497
809
|
return null;
|
|
498
810
|
}
|
|
@@ -528,7 +840,13 @@ var cross = (p, q) => [p[1] * q[2] - p[2] * q[1], p[2] * q[0] - p[0] * q[2], p[0
|
|
|
528
840
|
var neg = (p) => [-p[0], -p[1], -p[2]];
|
|
529
841
|
function supportOf(b, policy, env) {
|
|
530
842
|
if (env.supportUp !== void 0) return env.supportUp(b.x, b.y, b.z);
|
|
531
|
-
|
|
843
|
+
const memo = env.supports;
|
|
844
|
+
if (memo === void 0) return surfaceUp(env.world, b.x, b.y, b.z, policy.groundTrace);
|
|
845
|
+
const known = memo.get(b);
|
|
846
|
+
if (known !== void 0 && known.x === b.x && known.y === b.y && known.z === b.z) return known.up;
|
|
847
|
+
const up = surfaceUp(env.world, b.x, b.y, b.z, policy.groundTrace);
|
|
848
|
+
memo.set(b, { x: b.x, y: b.y, z: b.z, up });
|
|
849
|
+
return up;
|
|
532
850
|
}
|
|
533
851
|
function frameOf(b, policy, env) {
|
|
534
852
|
const shape = policy.shape;
|
|
@@ -981,7 +1299,9 @@ function contactPair(a, b, policy, env, cooling = false, others = []) {
|
|
|
981
1299
|
const fa = frameOf(a, policy, env);
|
|
982
1300
|
const fb = frameOf(b, policy, env);
|
|
983
1301
|
const found = contactOfFrames(fa, fb, policy);
|
|
984
|
-
|
|
1302
|
+
return found === null ? null : stepFound(fa, fb, found, rule, policy, env, cooling, others);
|
|
1303
|
+
}
|
|
1304
|
+
function stepFound(fa, fb, found, rule, policy, env, cooling, others) {
|
|
985
1305
|
const { contact, g } = found;
|
|
986
1306
|
if (contact.kind === ContactKind.Side) return { ...contact, ...sideContact(fa, fb, contact, g, policy, env, cooling), way: null, w: null };
|
|
987
1307
|
return { ...contact, ...topContact(fa, fb, contact, g, rule, policy, env, others) };
|
|
@@ -992,15 +1312,38 @@ function pairOfPass(a, b, pass, policy, env, options, seats) {
|
|
|
992
1312
|
return own === void 0 ? contactPair(a, b, policy, env, cooling, seats) : own;
|
|
993
1313
|
}
|
|
994
1314
|
var stillTouching = (c, policy) => c !== null && (c.depth > policy.slop || c.kind === ContactKind.Top && c.depth > 0);
|
|
995
|
-
|
|
1315
|
+
var memoryOf = (count) => ({ moves: new Int32Array(count), still: new Int32Array(count * count).fill(-1) });
|
|
1316
|
+
function stepIfTouching(a, b, policy, env, seats) {
|
|
1317
|
+
if (farApart(a, b, policy)) return null;
|
|
1318
|
+
const rule = topRuleOf(policy, a, b);
|
|
1319
|
+
if (rule.policy === TopPolicy.Ghost) return null;
|
|
1320
|
+
const fa = frameOf(a, policy, env);
|
|
1321
|
+
const fb = frameOf(b, policy, env);
|
|
1322
|
+
const found = contactOfFrames(fa, fb, policy);
|
|
1323
|
+
if (found === null || !stillTouching(found.contact, policy)) return null;
|
|
1324
|
+
return stepFound(fa, fb, found, rule, policy, env, false, seats);
|
|
1325
|
+
}
|
|
1326
|
+
function passOnce(seats, pass, policy, env, options, report, memory) {
|
|
996
1327
|
let touched = false;
|
|
997
|
-
|
|
998
|
-
|
|
1328
|
+
const count = seats.length;
|
|
1329
|
+
for (let i = 0; i < count; i++) {
|
|
1330
|
+
for (let k = i + 1; k < count; k++) {
|
|
999
1331
|
const a = seats[i];
|
|
1000
1332
|
const b = seats[k];
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1333
|
+
const at2 = i * count + k;
|
|
1334
|
+
if (memory !== null && pass > 0 && memory.still[at2] === memory.moves[i] + memory.moves[k]) continue;
|
|
1335
|
+
let out;
|
|
1336
|
+
if (memory !== null && pass > 0) out = stepIfTouching(a, b, policy, env, seats);
|
|
1337
|
+
else if (pass > 0 && !stillTouching(contactOf(a, b, policy, env), policy)) continue;
|
|
1338
|
+
else out = pairOfPass(a, b, pass, policy, env, options, seats);
|
|
1339
|
+
if (out === null) {
|
|
1340
|
+
if (memory !== null) memory.still[at2] = memory.moves[i] + memory.moves[k];
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (memory !== null) {
|
|
1344
|
+
memory.moves[i] = memory.moves[i] + 1;
|
|
1345
|
+
memory.moves[k] = memory.moves[k] + 1;
|
|
1346
|
+
}
|
|
1004
1347
|
touched = true;
|
|
1005
1348
|
if (out.way === SlideWay.Pinned) report.pinned += 1;
|
|
1006
1349
|
options.onPair?.(a, b, out);
|
|
@@ -1010,17 +1353,20 @@ function passOnce(seats, pass, policy, env, options, report) {
|
|
|
1010
1353
|
}
|
|
1011
1354
|
function contactPass(seats, policy, env, options = {}) {
|
|
1012
1355
|
const report = { passes: 0, pushed: /* @__PURE__ */ new Map(), walls: /* @__PURE__ */ new Map(), pinned: 0 };
|
|
1356
|
+
env = { ...env, supports: /* @__PURE__ */ new Map() };
|
|
1357
|
+
const memory = options.first === void 0 && options.onPair === void 0 && options.cooling === void 0 ? memoryOf(seats.length) : null;
|
|
1013
1358
|
for (let pass = 0; pass < policy.passes; pass++) {
|
|
1014
|
-
if (!passOnce(seats, pass, policy, env, options, report)) break;
|
|
1359
|
+
if (!passOnce(seats, pass, policy, env, options, report, memory)) break;
|
|
1015
1360
|
report.passes += 1;
|
|
1016
1361
|
if (env.world === null) continue;
|
|
1017
|
-
|
|
1362
|
+
seats.forEach((seat, s) => {
|
|
1018
1363
|
const wall = (face) => {
|
|
1019
1364
|
if (face[2] < policy.supportCos && face[3] > (report.walls.get(seat.id) ?? 0)) report.walls.set(seat.id, face[3]);
|
|
1020
1365
|
};
|
|
1021
1366
|
const depth = unstickBody(env.world, seat, wall);
|
|
1022
1367
|
if (depth > (report.pushed.get(seat.id) ?? 0)) report.pushed.set(seat.id, depth);
|
|
1023
|
-
|
|
1368
|
+
if (memory !== null && depth > 0) memory.moves[s] = memory.moves[s] + 1;
|
|
1369
|
+
});
|
|
1024
1370
|
}
|
|
1025
1371
|
return report;
|
|
1026
1372
|
}
|
|
@@ -4110,7 +4456,10 @@ var roomStateFields = {
|
|
|
4110
4456
|
joints: t.map(AuthorityJoint).optional(),
|
|
4111
4457
|
// Lift rooms (PROTOCOL.md §17.11 *Lifts*): every called lift's start tick, JSON text `{"id":tick,…}`
|
|
4112
4458
|
// (ids ascending), written when a module's ctx.lifts.call is taken. Absent = none called.
|
|
4113
|
-
lifts: t.string().optional()
|
|
4459
|
+
lifts: t.string().optional(),
|
|
4460
|
+
// The room's match key (PROTOCOL.md §17.2 *Choosing a room*): how a friend who joined by code learns
|
|
4461
|
+
// which world to hand `setWorld`. Absent = no key, and never written.
|
|
4462
|
+
matchKey: t.string().optional()
|
|
4114
4463
|
};
|
|
4115
4464
|
var AuthorityRoomState = schema(roomStateFields, "AuthorityRoomState");
|
|
4116
4465
|
var AuthorityPhysicsRoomState = schema({ ...roomStateFields, entities: t.map(AuthorityPhysicsEntity).view() }, "AuthorityRoomState");
|
|
@@ -4250,380 +4599,72 @@ function throttleSteps(t2, dtMs, stepMs) {
|
|
|
4250
4599
|
return steps;
|
|
4251
4600
|
}
|
|
4252
4601
|
|
|
4253
|
-
// src/authority/fast-
|
|
4254
|
-
var
|
|
4255
|
-
var
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
Velocity: "q-velocity",
|
|
4263
|
-
/** A boolean: the mask bit alone. */
|
|
4264
|
-
Flag: "flag"
|
|
4602
|
+
// src/authority/fast-line.ts
|
|
4603
|
+
var FastTransport = { Udp: "udp", WebSocket: "websocket" };
|
|
4604
|
+
var FastNotAsked = {
|
|
4605
|
+
/** The game turned it off (`fast: false`, `bFastChannel = false`, ...). */
|
|
4606
|
+
Off: "off",
|
|
4607
|
+
/** This runtime has no UDP socket (WebGL, a browser with no opener). */
|
|
4608
|
+
NoUdp: "no_udp",
|
|
4609
|
+
/** The seat asked and the room sent no binding. */
|
|
4610
|
+
NotOffered: "not_offered"
|
|
4265
4611
|
};
|
|
4266
|
-
var
|
|
4267
|
-
|
|
4268
|
-
["
|
|
4269
|
-
["
|
|
4270
|
-
["
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
["fuel", FieldWire.F32],
|
|
4278
|
-
["epoch", FieldWire.U32],
|
|
4279
|
-
["mstate", FieldWire.U32],
|
|
4280
|
-
["qx", FieldWire.Orientation],
|
|
4281
|
-
["qy", FieldWire.Orientation],
|
|
4282
|
-
["qz", FieldWire.Orientation],
|
|
4283
|
-
["qw", FieldWire.Orientation],
|
|
4284
|
-
["lvx", FieldWire.Velocity],
|
|
4285
|
-
["lvy", FieldWire.Velocity],
|
|
4286
|
-
["lvz", FieldWire.Velocity],
|
|
4287
|
-
["avx", FieldWire.Velocity],
|
|
4288
|
-
["avy", FieldWire.Velocity],
|
|
4289
|
-
["avz", FieldWire.Velocity],
|
|
4290
|
-
["sleep", FieldWire.Flag],
|
|
4291
|
-
["rest", FieldWire.U16]
|
|
4292
|
-
];
|
|
4293
|
-
var SEAT_FIELDS = [
|
|
4294
|
-
["inMoveX", FieldWire.F32],
|
|
4295
|
-
["inMoveY", FieldWire.F32],
|
|
4296
|
-
["inMoveZ", FieldWire.F32],
|
|
4297
|
-
["inYaw", FieldWire.F32],
|
|
4298
|
-
["inPitch", FieldWire.F32],
|
|
4299
|
-
["inButtons", FieldWire.U32],
|
|
4300
|
-
["inRep", FieldWire.U32],
|
|
4301
|
-
["inStarve", FieldWire.U32],
|
|
4302
|
-
["inBuf", FieldWire.U32],
|
|
4303
|
-
["inAck", FieldWire.U32]
|
|
4304
|
-
];
|
|
4305
|
-
var RECORD_ID_MAX_CHARS = 64;
|
|
4306
|
-
var HEAD = 2;
|
|
4307
|
-
var MASK_BYTES = 4;
|
|
4308
|
-
var UINT32_MAX = 4294967295;
|
|
4309
|
-
var UINT16_MAX = 65535;
|
|
4310
|
-
var Q_SPAN = 65534;
|
|
4311
|
-
var Q_ZERO = Q_SPAN / 2;
|
|
4312
|
-
var Q_RANGE = { [FieldWire.Orientation]: 1, [FieldWire.Velocity]: 64 };
|
|
4313
|
-
var blankEntityMotion = () => ({
|
|
4314
|
-
x: 0,
|
|
4315
|
-
y: 0,
|
|
4316
|
-
z: 0,
|
|
4317
|
-
vx: 0,
|
|
4318
|
-
vy: 0,
|
|
4319
|
-
vz: 0,
|
|
4320
|
-
yaw: 0,
|
|
4321
|
-
pitch: 0,
|
|
4322
|
-
duck: 0,
|
|
4323
|
-
spin: 0,
|
|
4324
|
-
fuel: 0,
|
|
4325
|
-
epoch: 0,
|
|
4326
|
-
mstate: 0,
|
|
4327
|
-
qx: 0,
|
|
4328
|
-
qy: 0,
|
|
4329
|
-
qz: 0,
|
|
4330
|
-
qw: 0,
|
|
4331
|
-
lvx: 0,
|
|
4332
|
-
lvy: 0,
|
|
4333
|
-
lvz: 0,
|
|
4334
|
-
avx: 0,
|
|
4335
|
-
avy: 0,
|
|
4336
|
-
avz: 0,
|
|
4337
|
-
sleep: false,
|
|
4338
|
-
rest: 0
|
|
4339
|
-
});
|
|
4340
|
-
var blankSeatEcho = () => ({ inMoveX: 0, inMoveY: 0, inMoveZ: 0, inYaw: 0, inPitch: 0, inButtons: 0, inRep: 0, inStarve: 0, inBuf: 0, inAck: 0 });
|
|
4341
|
-
function quantizeCode(half, value) {
|
|
4342
|
-
const v = Math.min(half, Math.max(-half, value));
|
|
4343
|
-
return Math.floor((v + half) / (2 * half) * Q_SPAN + 0.5);
|
|
4344
|
-
}
|
|
4345
|
-
var dequantizeCode = (half, code) => -half + code / Q_SPAN * (2 * half);
|
|
4346
|
-
var fieldsOf = (kind) => {
|
|
4347
|
-
if (kind === MotionRecordKind.Entity) return ENTITY_FIELDS;
|
|
4348
|
-
if (kind === MotionRecordKind.Seat) return SEAT_FIELDS;
|
|
4349
|
-
return null;
|
|
4612
|
+
var FAST_OFFER_WAIT_MS = FAST_HANDSHAKE_MS;
|
|
4613
|
+
var WHY = {
|
|
4614
|
+
[FastNotAsked.Off]: "the game turned the fast channel off",
|
|
4615
|
+
[FastNotAsked.NoUdp]: "this platform has no UDP",
|
|
4616
|
+
[FastNotAsked.NotOffered]: "the room does not offer the fast channel",
|
|
4617
|
+
unreachable: "fell back, the binding could not be used or its socket failed",
|
|
4618
|
+
handshake: "fell back, no UDP snapshot within 1 s of the first datagram (UDP blocked?)",
|
|
4619
|
+
stall: "fell back, no UDP snapshot for 2 s",
|
|
4620
|
+
server: "fell back, the room closed the channel",
|
|
4621
|
+
"no-webtransport": "this browser has no WebTransport",
|
|
4622
|
+
"no-relay": "the room has no WebTransport relay"
|
|
4350
4623
|
};
|
|
4351
|
-
var
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
return v === 0 ? void 0 : v;
|
|
4361
|
-
case FieldWire.U16:
|
|
4362
|
-
if (!isUint(v, UINT16_MAX)) return null;
|
|
4363
|
-
return v === 0 ? void 0 : v;
|
|
4364
|
-
case FieldWire.Flag:
|
|
4365
|
-
if (typeof v !== "boolean") return null;
|
|
4366
|
-
return v ? 1 : void 0;
|
|
4367
|
-
default: {
|
|
4368
|
-
if (typeof v !== "number" || Number.isNaN(v)) return null;
|
|
4369
|
-
const code = quantizeCode(Q_RANGE[wire], v);
|
|
4370
|
-
return code === Q_ZERO ? void 0 : code;
|
|
4371
|
-
}
|
|
4372
|
-
}
|
|
4373
|
-
}
|
|
4374
|
-
var WIDTH = { f32: 4, u32: 4, u16: 2, "q-orientation": 2, "q-velocity": 2, flag: 0 };
|
|
4375
|
-
function encodeRecord(r) {
|
|
4376
|
-
const fields = fieldsOf(r.kind);
|
|
4377
|
-
const id = typeof r.id === "string" ? new TextEncoder().encode(r.id) : null;
|
|
4378
|
-
const idOk = id !== null && r.id.length >= 1 && r.id.length <= RECORD_ID_MAX_CHARS && id.length <= 255;
|
|
4379
|
-
if (fields === null || !idOk || r.motion === null || typeof r.motion !== "object") return { ok: false, fault: FastFault.Field };
|
|
4380
|
-
const motion = r.motion;
|
|
4381
|
-
const present = [];
|
|
4382
|
-
let mask = 0;
|
|
4383
|
-
for (const [i, [name, wire]] of fields.entries()) {
|
|
4384
|
-
const value = wireValue(wire, motion[name]);
|
|
4385
|
-
if (value === null) return { ok: false, fault: FastFault.Field };
|
|
4386
|
-
if (value === void 0) continue;
|
|
4387
|
-
mask = (mask | 1 << i) >>> 0;
|
|
4388
|
-
present.push([wire, value]);
|
|
4389
|
-
}
|
|
4390
|
-
const out = new Uint8Array(HEAD + id.length + MASK_BYTES + present.reduce((n, [w]) => n + WIDTH[w], 0));
|
|
4391
|
-
const view = new DataView(out.buffer);
|
|
4392
|
-
out[0] = r.kind;
|
|
4393
|
-
out[1] = id.length;
|
|
4394
|
-
out.set(id, HEAD);
|
|
4395
|
-
let at2 = HEAD + id.length;
|
|
4396
|
-
view.setUint32(at2, mask, true);
|
|
4397
|
-
at2 += MASK_BYTES;
|
|
4398
|
-
for (const [wire, value] of present) at2 = writeField(view, at2, wire, value);
|
|
4399
|
-
return { ok: true, value: out };
|
|
4400
|
-
}
|
|
4401
|
-
function writeField(view, at2, wire, value) {
|
|
4402
|
-
if (wire === FieldWire.F32) view.setFloat32(at2, value, true);
|
|
4403
|
-
else if (wire === FieldWire.U32) view.setUint32(at2, value, true);
|
|
4404
|
-
else if (wire !== FieldWire.Flag) view.setUint16(at2, value, true);
|
|
4405
|
-
return at2 + WIDTH[wire];
|
|
4406
|
-
}
|
|
4407
|
-
function readField(view, at2, wire) {
|
|
4408
|
-
if (at2 + WIDTH[wire] > view.byteLength) return { ok: false, fault: FastFault.Truncated };
|
|
4409
|
-
switch (wire) {
|
|
4410
|
-
case FieldWire.Flag:
|
|
4411
|
-
return { ok: true, value: true };
|
|
4412
|
-
case FieldWire.F32: {
|
|
4413
|
-
const v = view.getFloat32(at2, true);
|
|
4414
|
-
return Number.isFinite(v) && v !== 0 ? { ok: true, value: v } : { ok: false, fault: FastFault.Field };
|
|
4415
|
-
}
|
|
4416
|
-
case FieldWire.U32: {
|
|
4417
|
-
const v = view.getUint32(at2, true);
|
|
4418
|
-
return v !== 0 ? { ok: true, value: v } : { ok: false, fault: FastFault.Field };
|
|
4419
|
-
}
|
|
4420
|
-
case FieldWire.U16: {
|
|
4421
|
-
const v = view.getUint16(at2, true);
|
|
4422
|
-
return v !== 0 ? { ok: true, value: v } : { ok: false, fault: FastFault.Field };
|
|
4423
|
-
}
|
|
4424
|
-
default: {
|
|
4425
|
-
const code = view.getUint16(at2, true);
|
|
4426
|
-
const canonical = code !== Q_ZERO && code <= Q_SPAN;
|
|
4427
|
-
return canonical ? { ok: true, value: dequantizeCode(Q_RANGE[wire], code) } : { ok: false, fault: FastFault.Field };
|
|
4428
|
-
}
|
|
4429
|
-
}
|
|
4430
|
-
}
|
|
4431
|
-
var UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
4432
|
-
function readId(bytes) {
|
|
4433
|
-
const len = bytes[1];
|
|
4434
|
-
if (HEAD + len > bytes.length) return { ok: false, fault: FastFault.Truncated };
|
|
4435
|
-
if (len === 0) return { ok: false, fault: FastFault.Field };
|
|
4436
|
-
try {
|
|
4437
|
-
const id = UTF8.decode(bytes.subarray(HEAD, HEAD + len));
|
|
4438
|
-
return id.length <= RECORD_ID_MAX_CHARS ? { ok: true, value: id } : { ok: false, fault: FastFault.Field };
|
|
4439
|
-
} catch {
|
|
4440
|
-
return { ok: false, fault: FastFault.Field };
|
|
4441
|
-
}
|
|
4442
|
-
}
|
|
4443
|
-
function decodeRecord(bytes) {
|
|
4444
|
-
if (bytes.length < HEAD) return { ok: false, fault: FastFault.Truncated };
|
|
4445
|
-
const fields = fieldsOf(bytes[0]);
|
|
4446
|
-
if (fields === null) return { ok: false, fault: FastFault.Kind };
|
|
4447
|
-
const id = readId(bytes);
|
|
4448
|
-
if (!id.ok) return id;
|
|
4449
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
4450
|
-
let at2 = HEAD + bytes[1];
|
|
4451
|
-
if (at2 + MASK_BYTES > bytes.length) return { ok: false, fault: FastFault.Truncated };
|
|
4452
|
-
const mask = view.getUint32(at2, true);
|
|
4453
|
-
at2 += MASK_BYTES;
|
|
4454
|
-
if (fields.length < 32 && mask >>> fields.length !== 0) return { ok: false, fault: FastFault.Flags };
|
|
4455
|
-
if (bytes[0] === MotionRecordKind.Entity) {
|
|
4456
|
-
const motion2 = readMotion(view, at2, mask, blankEntityMotion(), fields);
|
|
4457
|
-
return motion2.ok ? { ok: true, value: { kind: MotionRecordKind.Entity, id: id.value, motion: motion2.value } } : motion2;
|
|
4458
|
-
}
|
|
4459
|
-
const motion = readMotion(view, at2, mask, blankSeatEcho(), fields);
|
|
4460
|
-
return motion.ok ? { ok: true, value: { kind: MotionRecordKind.Seat, id: id.value, motion: motion.value } } : motion;
|
|
4461
|
-
}
|
|
4462
|
-
function readMotion(view, start, mask, blank, fields) {
|
|
4463
|
-
const values = {};
|
|
4464
|
-
let at2 = start;
|
|
4465
|
-
for (const [i, [name, wire]] of fields.entries()) {
|
|
4466
|
-
if ((mask & 1 << i) === 0) continue;
|
|
4467
|
-
const v = readField(view, at2, wire);
|
|
4468
|
-
if (!v.ok) return v;
|
|
4469
|
-
values[name] = v.value;
|
|
4470
|
-
at2 += WIDTH[wire];
|
|
4471
|
-
}
|
|
4472
|
-
if (at2 !== view.byteLength) return { ok: false, fault: FastFault.Trailing };
|
|
4473
|
-
return { ok: true, value: Object.assign(blank, values) };
|
|
4474
|
-
}
|
|
4475
|
-
|
|
4476
|
-
// src/authority/fast-client.ts
|
|
4477
|
-
var UINT32_MAX2 = 4294967295;
|
|
4478
|
-
var FastFallbackReason = {
|
|
4479
|
-
/** The `__fast` binding was malformed, this client cannot open its url, or its socket failed. */
|
|
4480
|
-
Unreachable: "unreachable",
|
|
4481
|
-
/** No snapshot within {@link FAST_HANDSHAKE_MS} of the first datagram. */
|
|
4482
|
-
Handshake: "handshake",
|
|
4483
|
-
/** No snapshot for {@link FAST_STALL_FALLBACK_MS} after the last one. */
|
|
4484
|
-
Stall: "stall",
|
|
4485
|
-
/** The server asked for it (`__fast:off` from the server). */
|
|
4486
|
-
Server: "server"
|
|
4487
|
-
};
|
|
4488
|
-
var isUint2 = (v, max) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= max;
|
|
4489
|
-
function isFastBind(m) {
|
|
4490
|
-
if (typeof m !== "object" || m === null) return false;
|
|
4491
|
-
const { sid, key, url } = m;
|
|
4492
|
-
const keyOk = typeof key === "string" && key.length === 2 * FAST_KEY_BYTES && /^[0-9a-f]+$/.test(key);
|
|
4493
|
-
return isUint2(sid, UINT32_MAX2) && keyOk && typeof url === "string" && url.length > 0;
|
|
4494
|
-
}
|
|
4495
|
-
function heldInputs(sent, inputAck, tickHz) {
|
|
4496
|
-
if (sent.length === 0) return [];
|
|
4497
|
-
const newest = sent[sent.length - 1].seq;
|
|
4498
|
-
const keepFrom = Math.min(inputAck, newest - inputRedundancyTicks(tickHz) + 1);
|
|
4499
|
-
const capped = Math.max(keepFrom, newest - seqAheadMax(tickHz) + 1);
|
|
4500
|
-
return sent.filter((s) => s.seq >= capped);
|
|
4501
|
-
}
|
|
4502
|
-
var recordName = (kind, id) => `${kind}:${id}`;
|
|
4503
|
-
function openFastClient(o) {
|
|
4504
|
-
let rx = emptyClientRx();
|
|
4505
|
-
let sent = [];
|
|
4506
|
-
const sentTimes = /* @__PURE__ */ new Map();
|
|
4507
|
-
let fresh = 0;
|
|
4508
|
-
let closed = false;
|
|
4509
|
-
let firstSentAt = null;
|
|
4510
|
-
let lastAppliedAt = null;
|
|
4511
|
-
let decodedByKey = /* @__PURE__ */ new Map();
|
|
4512
|
-
let latest = /* @__PURE__ */ new Map();
|
|
4513
|
-
const inputAck = () => rx.history.at(-1)?.inputAck ?? 0;
|
|
4514
|
-
const decodeChanged = (records) => {
|
|
4515
|
-
const nextByKey = /* @__PURE__ */ new Map();
|
|
4516
|
-
const nextLatest = /* @__PURE__ */ new Map();
|
|
4517
|
-
const changed = [];
|
|
4518
|
-
for (const r of records) {
|
|
4519
|
-
const before = decodedByKey.get(r.key);
|
|
4520
|
-
const same = before !== void 0 && (before.bytes === r.bytes || sameBytes(before.bytes, r.bytes));
|
|
4521
|
-
const record = same ? before.record : decodeOrNull(r.bytes);
|
|
4522
|
-
nextByKey.set(r.key, { bytes: r.bytes, record });
|
|
4523
|
-
if (record === null) continue;
|
|
4524
|
-
nextLatest.set(recordName(record.kind, record.id), record);
|
|
4525
|
-
if (!same) changed.push(record);
|
|
4526
|
-
}
|
|
4527
|
-
decodedByKey = nextByKey;
|
|
4528
|
-
latest = nextLatest;
|
|
4529
|
-
return changed;
|
|
4530
|
-
};
|
|
4531
|
-
function receive(bytes) {
|
|
4532
|
-
if (closed) return { applied: null };
|
|
4533
|
-
const r = receiveFragment(rx, bytes, o.bind.sid);
|
|
4534
|
-
rx = r.rx;
|
|
4535
|
-
if (r.fault !== void 0 || r.applied === null) return { applied: null, fault: r.fault };
|
|
4536
|
-
const held = rx.history.at(-1);
|
|
4537
|
-
if (held === void 0) return { applied: null };
|
|
4538
|
-
lastAppliedAt = o.now();
|
|
4539
|
-
sent = heldInputs(sent, held.inputAck, o.tickHz);
|
|
4540
|
-
const applied = { snap: held.snap, tick: held.tick, inputAck: held.inputAck, sent: held.sent, changed: decodeChanged(held.records) };
|
|
4541
|
-
o.onSnapshot?.(applied);
|
|
4542
|
-
return { applied };
|
|
4543
|
-
}
|
|
4544
|
-
const socket = o.open(
|
|
4545
|
-
o.bind.url,
|
|
4546
|
-
(bytes) => {
|
|
4547
|
-
receive(bytes);
|
|
4548
|
-
},
|
|
4549
|
-
o.bind
|
|
4550
|
-
);
|
|
4551
|
-
if (socket === null) return null;
|
|
4552
|
-
const largestUplink = uplinkMaxBytes(o.tickHz);
|
|
4624
|
+
var udpLine = (url) => `genex transport: udp fast@1 ${url}`;
|
|
4625
|
+
var webSocketLine = (reason) => `genex transport: websocket, ${reason}: ${WHY[reason] ?? "unknown"}`;
|
|
4626
|
+
function fastLineAnnouncer() {
|
|
4627
|
+
const said = /* @__PURE__ */ new Set();
|
|
4628
|
+
const once = (line) => {
|
|
4629
|
+
if (said.has(line)) return null;
|
|
4630
|
+
said.add(line);
|
|
4631
|
+
return line;
|
|
4632
|
+
};
|
|
4553
4633
|
return {
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
return
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
return
|
|
4560
|
-
|
|
4561
|
-
get closed() {
|
|
4562
|
-
return closed;
|
|
4634
|
+
observe(o) {
|
|
4635
|
+
if (o.joinedMs === null) return null;
|
|
4636
|
+
if (!o.asked) return once(webSocketLine(FastNotAsked.Off));
|
|
4637
|
+
if (!o.udp) return once(webSocketLine(FastNotAsked.NoUdp));
|
|
4638
|
+
if (o.fallback !== null) return once(webSocketLine(o.fallback));
|
|
4639
|
+
if (o.url !== null) return o.snapshots > 0 ? once(udpLine(o.url)) : null;
|
|
4640
|
+
return o.joinedMs >= FAST_OFFER_WAIT_MS ? once(webSocketLine(FastNotAsked.NotOffered)) : null;
|
|
4563
4641
|
},
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
sent.push({ seq: input.seq, tick: input.tick, render: input.render, input: { ...input.input } });
|
|
4567
|
-
fresh += 1;
|
|
4568
|
-
},
|
|
4569
|
-
flush() {
|
|
4570
|
-
if (closed || fresh === 0) return;
|
|
4571
|
-
const now = o.now();
|
|
4572
|
-
for (const s of sent.slice(-fresh)) sentTimes.set(s.seq, now);
|
|
4573
|
-
forgetSendTimes(sentTimes, (sent.at(-1)?.seq ?? 0) - seqAheadMax(o.tickHz) + 1);
|
|
4574
|
-
const window = inputRedundancyTicks(o.tickHz);
|
|
4575
|
-
let from = sent.length - fresh;
|
|
4576
|
-
fresh = 0;
|
|
4577
|
-
while (from < sent.length) {
|
|
4578
|
-
const to = Math.min(from + window, sent.length);
|
|
4579
|
-
const u = uplinkFor(sent.slice(0, to), { sid: o.bind.sid, key: o.bind.key, ackSnap: rx.newest, inputAck: inputAck(), fresh: to - from, tickHz: o.tickHz });
|
|
4580
|
-
from = to;
|
|
4581
|
-
const bytes = encodeUplink(u);
|
|
4582
|
-
if (!bytes.ok) continue;
|
|
4583
|
-
firstSentAt ??= now;
|
|
4584
|
-
socket.send(bytes.value);
|
|
4585
|
-
}
|
|
4586
|
-
},
|
|
4587
|
-
inputAt: (seq) => sent.find((s) => s.seq === seq),
|
|
4588
|
-
sentAt: (seq) => sentTimes.get(seq),
|
|
4589
|
-
motionOf: (kind, id) => latest.get(recordName(kind, id)),
|
|
4590
|
-
due() {
|
|
4591
|
-
if (closed) return null;
|
|
4592
|
-
if (socket.failed?.() === true) return FastFallbackReason.Unreachable;
|
|
4593
|
-
const room = socket.maxDatagramBytes?.();
|
|
4594
|
-
if (room !== void 0 && room < largestUplink) return FastFallbackReason.Unreachable;
|
|
4595
|
-
const now = o.now();
|
|
4596
|
-
if (lastAppliedAt !== null) return now - lastAppliedAt > FAST_STALL_FALLBACK_MS ? FastFallbackReason.Stall : null;
|
|
4597
|
-
return firstSentAt !== null && now - firstSentAt > FAST_HANDSHAKE_MS ? FastFallbackReason.Handshake : null;
|
|
4598
|
-
},
|
|
4599
|
-
receive,
|
|
4600
|
-
close() {
|
|
4601
|
-
if (closed) return;
|
|
4602
|
-
closed = true;
|
|
4603
|
-
socket.close();
|
|
4642
|
+
reset() {
|
|
4643
|
+
said.clear();
|
|
4604
4644
|
}
|
|
4605
4645
|
};
|
|
4606
4646
|
}
|
|
4607
|
-
function forgetSendTimes(times, from) {
|
|
4608
|
-
for (const seq of times.keys()) {
|
|
4609
|
-
if (seq >= from) return;
|
|
4610
|
-
times.delete(seq);
|
|
4611
|
-
}
|
|
4612
|
-
}
|
|
4613
|
-
function decodeOrNull(bytes) {
|
|
4614
|
-
const d = decodeRecord(bytes);
|
|
4615
|
-
return d.ok ? d.value : null;
|
|
4616
|
-
}
|
|
4617
|
-
var sameBytes = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
4618
4647
|
|
|
4619
4648
|
// src/authority/fast-session.ts
|
|
4620
4649
|
import { $refId, OPERATION } from "@colyseus/schema";
|
|
4621
4650
|
var ROOM_INPUT_RELIABLE = 19;
|
|
4651
|
+
var RETRY_AFTER_SERVER_OFF = 3;
|
|
4622
4652
|
var PROTOCOL_CODE_MASK = 31;
|
|
4623
4653
|
var ENTITY_MOTION_FIELDS = Object.keys(blankEntityMotion());
|
|
4624
4654
|
var SEAT_MOTION_FIELDS = Object.keys(blankSeatEcho()).filter((f) => f !== "inAck");
|
|
4625
4655
|
var motionFieldsOf = (kind) => kind === MotionRecordKind.Entity ? ENTITY_MOTION_FIELDS : SEAT_MOTION_FIELDS;
|
|
4626
4656
|
var ROOT_TICK_FIELDS = ["tick", "keyframe"];
|
|
4657
|
+
function defaultFastLog(line) {
|
|
4658
|
+
const stderr = globalThis.process?.stderr;
|
|
4659
|
+
if (stderr?.write !== void 0) stderr.write(`${line}
|
|
4660
|
+
`);
|
|
4661
|
+
else console.info(line);
|
|
4662
|
+
}
|
|
4663
|
+
var defaultEvery = (ms, fn) => {
|
|
4664
|
+
const timer = setInterval(fn, ms);
|
|
4665
|
+
timer.unref?.();
|
|
4666
|
+
return () => clearInterval(timer);
|
|
4667
|
+
};
|
|
4627
4668
|
function baselinesOf(handle) {
|
|
4628
4669
|
if (typeof handle !== "object" || handle === null) return null;
|
|
4629
4670
|
const h = handle;
|
|
@@ -4642,13 +4683,30 @@ function renderStampOf(handle) {
|
|
|
4642
4683
|
}
|
|
4643
4684
|
function fastSeat(o) {
|
|
4644
4685
|
const { room, handle } = o;
|
|
4645
|
-
const status = { live: false, snapshots: 0, fallback: null };
|
|
4686
|
+
const status = { live: false, snapshots: 0, fallback: o.neverAsked, fallbacks: 0, returns: 0, transport: FastTransport.WebSocket, line: null };
|
|
4687
|
+
let returnsSaid = 0;
|
|
4688
|
+
const announcer = fastLineAnnouncer();
|
|
4689
|
+
let joinedAt = null;
|
|
4690
|
+
let boundUrl = null;
|
|
4691
|
+
let snapshotsBefore = 0;
|
|
4646
4692
|
let link = null;
|
|
4647
4693
|
let fallback = null;
|
|
4648
4694
|
let fastAck = 0;
|
|
4649
4695
|
let early = [];
|
|
4650
4696
|
let pendingBind = null;
|
|
4697
|
+
let offerAwaitedFrom = null;
|
|
4698
|
+
let roundTripMs = 0;
|
|
4651
4699
|
const guard = o.open === void 0 ? null : motionGuard(room, () => link?.client ?? null);
|
|
4700
|
+
const stopTimer = o.open === void 0 ? null : (o.every ?? defaultEvery)(FAST_KEEPALIVE_BIND_MS, () => {
|
|
4701
|
+
link?.client.keepalive();
|
|
4702
|
+
probe?.client.keepalive();
|
|
4703
|
+
});
|
|
4704
|
+
const random = o.random ?? Math.random;
|
|
4705
|
+
let probe = null;
|
|
4706
|
+
let hostReturns = false;
|
|
4707
|
+
let attempt = 0;
|
|
4708
|
+
let retryAt = null;
|
|
4709
|
+
let liveSince = null;
|
|
4652
4710
|
const baselines = baselinesOf(handle);
|
|
4653
4711
|
const renderStamp = renderStampOf(handle);
|
|
4654
4712
|
const ack = () => {
|
|
@@ -4658,8 +4716,17 @@ function fastSeat(o) {
|
|
|
4658
4716
|
return ws > 0 ? Math.max(fallback.ack, fallback.at + ws) : fallback.ack;
|
|
4659
4717
|
};
|
|
4660
4718
|
const seqBase = () => room.state.players?.get(room.sessionId)?.seqBase ?? 0;
|
|
4719
|
+
const scheduleRetry = () => {
|
|
4720
|
+
if (!hostReturns || o.open === void 0) return;
|
|
4721
|
+
retryAt = o.now() + fastRetryDelayMs(attempt, random());
|
|
4722
|
+
attempt += 1;
|
|
4723
|
+
};
|
|
4661
4724
|
const fallBack = (reason) => {
|
|
4662
4725
|
if (fallback !== null) return;
|
|
4726
|
+
if (liveSince !== null && o.now() - liveSince >= FAST_RETURN_STABLE_MS) attempt = 0;
|
|
4727
|
+
if (reason === FastFallbackReason.Server) attempt = Math.max(attempt, RETRY_AFTER_SERVER_OFF);
|
|
4728
|
+
liveSince = null;
|
|
4729
|
+
status.fallbacks += 1;
|
|
4663
4730
|
const live = link;
|
|
4664
4731
|
const base = link?.seqBase ?? seqBase();
|
|
4665
4732
|
fallback = { at: handle.sentCount, wsBefore: link?.wsBefore ?? handle.sentCount, ack: ack(), reason };
|
|
@@ -4675,6 +4742,81 @@ function fastSeat(o) {
|
|
|
4675
4742
|
const off = { last: base + handle.sentCount };
|
|
4676
4743
|
room.send(FastMessage.Off, off);
|
|
4677
4744
|
guard?.release();
|
|
4745
|
+
scheduleRetry();
|
|
4746
|
+
};
|
|
4747
|
+
const offChannel = (reason) => {
|
|
4748
|
+
if (status.fallback === null) status.fallbacks += 1;
|
|
4749
|
+
status.fallback = reason;
|
|
4750
|
+
};
|
|
4751
|
+
const abandonProbe = (reason) => {
|
|
4752
|
+
if (probe === null) return;
|
|
4753
|
+
probe.client.close();
|
|
4754
|
+
probe = null;
|
|
4755
|
+
offChannel(reason);
|
|
4756
|
+
const off = { last: seqBase() + handle.sentCount };
|
|
4757
|
+
room.send(FastMessage.Off, off);
|
|
4758
|
+
scheduleRetry();
|
|
4759
|
+
};
|
|
4760
|
+
const swapToProbe = (s) => {
|
|
4761
|
+
const p = probe;
|
|
4762
|
+
if (p === null || fallback === null) return;
|
|
4763
|
+
probe = null;
|
|
4764
|
+
const base = seqBase();
|
|
4765
|
+
const wsBefore = fallback.wsBefore + (handle.sentCount - fallback.at);
|
|
4766
|
+
fastAck = ack();
|
|
4767
|
+
link = { client: p.client, seqBase: base, wsBefore, stampAtBind: baselines?._lastStamp ?? 0 };
|
|
4768
|
+
fallback = null;
|
|
4769
|
+
retryAt = null;
|
|
4770
|
+
liveSince = o.now();
|
|
4771
|
+
status.live = true;
|
|
4772
|
+
status.fallback = null;
|
|
4773
|
+
if (status.fallbacks > 0) status.returns += 1;
|
|
4774
|
+
const on = { last: base + handle.sentCount };
|
|
4775
|
+
room.send(FastMessage.On, on);
|
|
4776
|
+
applied({ ...s, changed: p.client.records() });
|
|
4777
|
+
};
|
|
4778
|
+
const askRoundTrip = (client) => {
|
|
4779
|
+
if (!client.opening()) return;
|
|
4780
|
+
room.ping((ms) => {
|
|
4781
|
+
roundTripMs = ms;
|
|
4782
|
+
});
|
|
4783
|
+
};
|
|
4784
|
+
const startProbe = (message) => {
|
|
4785
|
+
if (o.open === void 0) return;
|
|
4786
|
+
attempt = Math.max(0, attempt - 1);
|
|
4787
|
+
hostReturns = true;
|
|
4788
|
+
probe?.client.close();
|
|
4789
|
+
probe = null;
|
|
4790
|
+
if (fallback === null) status.fallback = null;
|
|
4791
|
+
fallback ??= { at: 0, wsBefore: 0, ack: 0, reason: FastFallbackReason.Unreachable };
|
|
4792
|
+
early = [];
|
|
4793
|
+
if (!isFastBind(message)) {
|
|
4794
|
+
offChannel(FastFallbackReason.Unreachable);
|
|
4795
|
+
return scheduleRetry();
|
|
4796
|
+
}
|
|
4797
|
+
boundUrl = message.url;
|
|
4798
|
+
const proof = { client: null, applied: 0 };
|
|
4799
|
+
const client = openFastClient({
|
|
4800
|
+
bind: message,
|
|
4801
|
+
tickHz: o.tickHz(),
|
|
4802
|
+
open: o.open,
|
|
4803
|
+
now: o.now,
|
|
4804
|
+
roundTripMs: () => roundTripMs,
|
|
4805
|
+
onSnapshot: (s) => {
|
|
4806
|
+
if (link !== null && link.client === proof.client) return applied(s);
|
|
4807
|
+
proof.applied += 1;
|
|
4808
|
+
if (proof.applied >= FAST_RETURN_PROOF) swapToProbe(s);
|
|
4809
|
+
}
|
|
4810
|
+
});
|
|
4811
|
+
if (client === null) {
|
|
4812
|
+
offChannel(o.open.whyNull?.(message) ?? FastFallbackReason.Unreachable);
|
|
4813
|
+
return scheduleRetry();
|
|
4814
|
+
}
|
|
4815
|
+
askRoundTrip(client);
|
|
4816
|
+
proof.client = client;
|
|
4817
|
+
probe = proof;
|
|
4818
|
+
retryAt = null;
|
|
4819
|
+
client.keepalive();
|
|
4678
4820
|
};
|
|
4679
4821
|
const applied = (s) => {
|
|
4680
4822
|
const l = link;
|
|
@@ -4693,21 +4835,49 @@ function fastSeat(o) {
|
|
|
4693
4835
|
guard?.write(s);
|
|
4694
4836
|
};
|
|
4695
4837
|
const bind = (message) => {
|
|
4696
|
-
|
|
4838
|
+
const asProbe = message?.probe === true;
|
|
4839
|
+
if (o.open === void 0 || link !== null || fallback !== null && !asProbe) return;
|
|
4697
4840
|
if (!o.stateCurrent()) {
|
|
4698
4841
|
pendingBind = message;
|
|
4699
4842
|
return;
|
|
4700
4843
|
}
|
|
4701
4844
|
pendingBind = null;
|
|
4845
|
+
if (asProbe) return startProbe(message);
|
|
4846
|
+
status.fallback = null;
|
|
4702
4847
|
if (!isFastBind(message)) return fallBack(FastFallbackReason.Unreachable);
|
|
4703
|
-
|
|
4704
|
-
|
|
4848
|
+
boundUrl = message.url;
|
|
4849
|
+
hostReturns = typeof message.rev === "number" && message.rev >= FAST_BIND_REV;
|
|
4850
|
+
const client = openFastClient({ bind: message, tickHz: o.tickHz(), open: o.open, now: o.now, roundTripMs: () => roundTripMs, onSnapshot: applied });
|
|
4851
|
+
if (client === null) return fallBack(o.open.whyNull?.(message) ?? FastFallbackReason.Unreachable);
|
|
4852
|
+
askRoundTrip(client);
|
|
4705
4853
|
link = { client, seqBase: seqBase(), wsBefore: handle.sentCount, stampAtBind: baselines?._lastStamp ?? 0 };
|
|
4706
4854
|
fastAck = 0;
|
|
4707
4855
|
status.live = true;
|
|
4856
|
+
liveSince = o.now();
|
|
4708
4857
|
for (const s of early) client.queue({ ...s, seq: link.seqBase + s.seq });
|
|
4709
4858
|
early = [];
|
|
4710
4859
|
client.flush();
|
|
4860
|
+
client.keepalive();
|
|
4861
|
+
};
|
|
4862
|
+
const noteOffer = () => {
|
|
4863
|
+
const waiting = o.open !== void 0 && link === null && fallback === null && pendingBind === null && o.stateCurrent();
|
|
4864
|
+
if (!waiting) return;
|
|
4865
|
+
offerAwaitedFrom ??= o.now();
|
|
4866
|
+
if (o.now() - offerAwaitedFrom > FAST_HANDSHAKE_MS) status.fallback = FastFallbackReason.NotOffered;
|
|
4867
|
+
};
|
|
4868
|
+
const announce = () => {
|
|
4869
|
+
if (joinedAt === null && o.stateCurrent() && room.state.players?.get(room.sessionId) !== void 0) joinedAt = o.now();
|
|
4870
|
+
const snapshots = status.snapshots - snapshotsBefore;
|
|
4871
|
+
status.transport = status.live && snapshots > 0 ? FastTransport.Udp : FastTransport.WebSocket;
|
|
4872
|
+
if (status.returns !== returnsSaid) {
|
|
4873
|
+
returnsSaid = status.returns;
|
|
4874
|
+
announcer.reset();
|
|
4875
|
+
}
|
|
4876
|
+
const fallback2 = status.fallback === FastFallbackReason.NotOffered ? null : status.fallback;
|
|
4877
|
+
const line = announcer.observe({ asked: !o.off, udp: o.open !== void 0, joinedMs: joinedAt === null ? null : o.now() - joinedAt, url: boundUrl, snapshots, fallback: fallback2 });
|
|
4878
|
+
if (line === null) return;
|
|
4879
|
+
status.line = line;
|
|
4880
|
+
o.log(line);
|
|
4711
4881
|
};
|
|
4712
4882
|
if (o.open !== void 0) {
|
|
4713
4883
|
room.onMessage(FastMessage.Bind, (m) => bind(m));
|
|
@@ -4735,8 +4905,20 @@ function fastSeat(o) {
|
|
|
4735
4905
|
link?.client.flush();
|
|
4736
4906
|
},
|
|
4737
4907
|
poll() {
|
|
4908
|
+
link?.client.framed();
|
|
4738
4909
|
const reason = link?.client.due() ?? null;
|
|
4739
4910
|
if (reason !== null) fallBack(reason);
|
|
4911
|
+
link?.client.keepalive();
|
|
4912
|
+
probe?.client.framed();
|
|
4913
|
+
const probeReason = probe?.client.due() ?? null;
|
|
4914
|
+
if (probeReason !== null) abandonProbe(probeReason);
|
|
4915
|
+
probe?.client.keepalive();
|
|
4916
|
+
if (retryAt !== null && o.now() >= retryAt && probe === null && fallback !== null) {
|
|
4917
|
+
room.send(FastMessage.Again, {});
|
|
4918
|
+
scheduleRetry();
|
|
4919
|
+
}
|
|
4920
|
+
noteOffer();
|
|
4921
|
+
announce();
|
|
4740
4922
|
},
|
|
4741
4923
|
motionKnown(id) {
|
|
4742
4924
|
return link === null || link.client.motionOf(MotionRecordKind.Entity, id) !== void 0;
|
|
@@ -4749,15 +4931,30 @@ function fastSeat(o) {
|
|
|
4749
4931
|
dropped() {
|
|
4750
4932
|
link?.client.close();
|
|
4751
4933
|
link = null;
|
|
4934
|
+
probe?.client.close();
|
|
4935
|
+
probe = null;
|
|
4936
|
+
retryAt = null;
|
|
4937
|
+
attempt = 0;
|
|
4938
|
+
liveSince = null;
|
|
4939
|
+
hostReturns = false;
|
|
4752
4940
|
guard?.forget();
|
|
4753
4941
|
fallback = null;
|
|
4754
4942
|
fastAck = 0;
|
|
4755
4943
|
early = [];
|
|
4756
4944
|
pendingBind = null;
|
|
4945
|
+
offerAwaitedFrom = null;
|
|
4757
4946
|
status.live = false;
|
|
4758
|
-
status.fallback =
|
|
4947
|
+
status.fallback = o.neverAsked;
|
|
4948
|
+
status.transport = FastTransport.WebSocket;
|
|
4949
|
+
announcer.reset();
|
|
4950
|
+
joinedAt = null;
|
|
4951
|
+
boundUrl = null;
|
|
4952
|
+
snapshotsBefore = status.snapshots;
|
|
4759
4953
|
},
|
|
4760
4954
|
dispose() {
|
|
4955
|
+
stopTimer?.();
|
|
4956
|
+
probe?.client.close();
|
|
4957
|
+
probe = null;
|
|
4761
4958
|
link?.client.close();
|
|
4762
4959
|
link = null;
|
|
4763
4960
|
status.live = false;
|
|
@@ -4809,7 +5006,7 @@ function motionGuard(room, live) {
|
|
|
4809
5006
|
const known = fields.get(field);
|
|
4810
5007
|
if (known === void 0) fields.set(field, { fast: value, ws: ref[field] ?? BLANK_MOTION[field] });
|
|
4811
5008
|
else known.fast = value;
|
|
4812
|
-
|
|
5009
|
+
writeField(ref, field, value, changes);
|
|
4813
5010
|
};
|
|
4814
5011
|
const expose = () => {
|
|
4815
5012
|
learn();
|
|
@@ -4886,7 +5083,7 @@ function motionGuard(room, live) {
|
|
|
4886
5083
|
const resent = room.state.physics !== void 0;
|
|
4887
5084
|
for (const [ref, fields] of held) {
|
|
4888
5085
|
if (!current(ref) || resent && owners.get(ref)?.kind === MotionRecordKind.Entity) continue;
|
|
4889
|
-
for (const [field, f] of fields)
|
|
5086
|
+
for (const [field, f] of fields) writeField(ref, field, f.ws, changes);
|
|
4890
5087
|
}
|
|
4891
5088
|
held.clear();
|
|
4892
5089
|
if (changes.length === 0) return;
|
|
@@ -4913,7 +5110,7 @@ function writeRecord(ref, record, write) {
|
|
|
4913
5110
|
write(ref, field, value);
|
|
4914
5111
|
}
|
|
4915
5112
|
}
|
|
4916
|
-
function
|
|
5113
|
+
function writeField(ref, field, value, changes) {
|
|
4917
5114
|
const previousValue = ref[field];
|
|
4918
5115
|
if (previousValue === value) return;
|
|
4919
5116
|
ref[field] = value;
|
|
@@ -4924,6 +5121,29 @@ function writeField2(ref, field, value, changes) {
|
|
|
4924
5121
|
import { Client, getStateCallbacks } from "@colyseus/sdk";
|
|
4925
5122
|
import { Predict } from "@colyseus/sdk/predict";
|
|
4926
5123
|
|
|
5124
|
+
// src/authority/correction.ts
|
|
5125
|
+
var VISIBLE_CORRECTION_METRES = 0.05;
|
|
5126
|
+
var CORRECTION_BUCKET_METRES = [0.5, 1, 5, 20];
|
|
5127
|
+
var PLACE = /* @__PURE__ */ new Set(["x", "y", "z"]);
|
|
5128
|
+
var UNCOUNTED = /* @__PURE__ */ new Set(["mstate", "cap", "spin", "fuel"]);
|
|
5129
|
+
var TWO_PI4 = 2 * Math.PI;
|
|
5130
|
+
function correctionSizes(fields, before, after) {
|
|
5131
|
+
let squares = 0;
|
|
5132
|
+
let state = 0;
|
|
5133
|
+
fields.forEach((field, i) => {
|
|
5134
|
+
if (UNCOUNTED.has(field)) return;
|
|
5135
|
+
const delta = (after[i] ?? 0) - (before[i] ?? 0);
|
|
5136
|
+
if (PLACE.has(field)) squares += delta * delta;
|
|
5137
|
+
else state = Math.max(state, Math.abs(field === "yaw" ? delta - TWO_PI4 * Math.floor((delta + Math.PI) / TWO_PI4) : delta));
|
|
5138
|
+
});
|
|
5139
|
+
return { place: Math.sqrt(squares), state };
|
|
5140
|
+
}
|
|
5141
|
+
var isVisibleCorrection = (place2, unitsPerMetre) => place2 > VISIBLE_CORRECTION_METRES * unitsPerMetre;
|
|
5142
|
+
function correctionBucket(place2, unitsPerMetre) {
|
|
5143
|
+
const bucket = CORRECTION_BUCKET_METRES.findIndex((edge) => place2 < edge * unitsPerMetre);
|
|
5144
|
+
return bucket < 0 ? CORRECTION_BUCKET_METRES.length : bucket;
|
|
5145
|
+
}
|
|
5146
|
+
|
|
4927
5147
|
// src/authority/tick-drawing.ts
|
|
4928
5148
|
var RING = 80;
|
|
4929
5149
|
var fold = (prev, next) => prev + Math.atan2(Math.sin(next - prev), Math.cos(next - prev));
|
|
@@ -4979,8 +5199,17 @@ var TickDrawing = class {
|
|
|
4979
5199
|
}
|
|
4980
5200
|
};
|
|
4981
5201
|
|
|
5202
|
+
// src/authority/fast-default.ts
|
|
5203
|
+
function defaultFastOpener() {
|
|
5204
|
+
const builtin = globalThis.process?.getBuiltinModule;
|
|
5205
|
+
const dgram = builtin?.("node:dgram");
|
|
5206
|
+
const dns = builtin?.("node:dns");
|
|
5207
|
+
const net = builtin?.("node:net");
|
|
5208
|
+
if (dgram === void 0 || dns === void 0 || net === void 0) return void 0;
|
|
5209
|
+
return udpFastOpener({ createSocket: dgram.createSocket, lookup: dns.lookup, isIP: net.isIP });
|
|
5210
|
+
}
|
|
5211
|
+
|
|
4982
5212
|
// src/authority/session.ts
|
|
4983
|
-
var VISIBLE_CORRECTION = 0.05;
|
|
4984
5213
|
var LIFT_BLEND_MS = 100;
|
|
4985
5214
|
var LIFT_DRAWN = ["x", "y", "z", "yaw"];
|
|
4986
5215
|
var RIDE_FRAME_DONE = 1e-3;
|
|
@@ -4990,18 +5219,16 @@ function liftLeftover(was, k, shift, decay) {
|
|
|
4990
5219
|
if (was.k === k) return kept;
|
|
4991
5220
|
return was.shift.map((s, i) => s + kept[i] - shift[i]);
|
|
4992
5221
|
}
|
|
4993
|
-
var CORRECTION_BUCKETS = [0.5, 1, 5, 20];
|
|
4994
5222
|
var CAPACITY_RETRY_MS = [1e3, 2e3, 4e3];
|
|
4995
5223
|
var MAX_CATCH_UP_STEPS = 8;
|
|
4996
5224
|
var SIDE_STALE_MS = 250;
|
|
4997
5225
|
var PROJECTILE_TTL_MIN_MS = 600;
|
|
4998
5226
|
var ALL_PREDICTED_FIELDS = ["x", "y", "z", "vx", "vy", "vz", "yaw"];
|
|
4999
|
-
var NOT_A_PLACE = /* @__PURE__ */ new Set(["mstate", "cap", "spin", "fuel"]);
|
|
5000
|
-
var ANGLES = /* @__PURE__ */ new Set(["yaw"]);
|
|
5001
5227
|
var SessionEvent = {
|
|
5002
5228
|
Reconnecting: "reconnecting",
|
|
5003
5229
|
Reconnected: "reconnected",
|
|
5004
|
-
Disconnect: "disconnect"
|
|
5230
|
+
Disconnect: "disconnect",
|
|
5231
|
+
...RejoinEvent
|
|
5005
5232
|
};
|
|
5006
5233
|
var SESSION_EVENTS = new Set(Object.values(SessionEvent));
|
|
5007
5234
|
var MESSAGE = {
|
|
@@ -5026,16 +5253,29 @@ async function joinTargets(opts) {
|
|
|
5026
5253
|
const ranked = await selectAuthorityRegions(regions, opts.probe === void 0 ? {} : { probe: opts.probe });
|
|
5027
5254
|
return ranked.map((pick) => pick.url);
|
|
5028
5255
|
}
|
|
5029
|
-
async function joinAuthority(opts) {
|
|
5256
|
+
async function joinAuthority(opts, target) {
|
|
5257
|
+
const plan = planAuthorityJoin(target);
|
|
5258
|
+
if ("refused" in plan) throw new TypeError(`genex multiplayer: this room cannot be asked for (${plan.refused})`);
|
|
5030
5259
|
const targets = await joinTargets(opts);
|
|
5031
5260
|
const delays = opts.capacityRetryMs ?? CAPACITY_RETRY_MS;
|
|
5032
5261
|
let state = { region: 0, refused: 0, step: 0 };
|
|
5033
5262
|
for (; ; ) {
|
|
5034
5263
|
const url = targets[state.region];
|
|
5035
5264
|
try {
|
|
5036
|
-
return { room: await joinOnce(new Client(url), opts), url };
|
|
5265
|
+
return { room: await joinOnce(new Client(url), opts, plan), url, targets };
|
|
5037
5266
|
} catch (err) {
|
|
5038
|
-
|
|
5267
|
+
if (plan.method === AuthorityJoinMethod.JoinById) {
|
|
5268
|
+
const region = nextCodeRegion(errorCode(err), state.region, targets.length);
|
|
5269
|
+
if (region === null) explainAuthorityRefusal(err);
|
|
5270
|
+
state = { region, refused: 0, step: 0 };
|
|
5271
|
+
continue;
|
|
5272
|
+
}
|
|
5273
|
+
const kind = refusalKind(errorCode(err));
|
|
5274
|
+
if (kind === RefusalKind.NextRegion && state.region + 1 < targets.length) {
|
|
5275
|
+
state = { region: state.region + 1, refused: 0, step: state.step };
|
|
5276
|
+
continue;
|
|
5277
|
+
}
|
|
5278
|
+
const next = kind === RefusalKind.Capacity ? nextAttempt(state, targets.length, delays) : null;
|
|
5039
5279
|
if (next === null) explainAuthorityRefusal(err);
|
|
5040
5280
|
const wait = next.attempt.waitMs;
|
|
5041
5281
|
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
@@ -5043,17 +5283,23 @@ async function joinAuthority(opts) {
|
|
|
5043
5283
|
}
|
|
5044
5284
|
}
|
|
5045
5285
|
}
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5286
|
+
var fastChoiceOf = (opts) => chooseFastSocket(opts.fast ?? defaultFastOpener());
|
|
5287
|
+
function joinOnce(client, opts, plan) {
|
|
5288
|
+
const fastCaps = fastChoiceOf(opts).open === void 0 ? [] : [AuthorityCaps.FAST, AuthorityCaps.FAST_RETURN];
|
|
5289
|
+
const options = {
|
|
5049
5290
|
...joinOptions(opts.room, opts.name, opts.auth),
|
|
5291
|
+
...plan.options,
|
|
5050
5292
|
// §17.10 *Joining*: this session decodes and draws physics bodies (physics-draw.ts, held to
|
|
5051
5293
|
// the netcode bench's physics-draw vectors), so it may join a physics room.
|
|
5052
5294
|
// §17.11: a client that can load the core predicts too.
|
|
5053
5295
|
[AuthorityJoinOption.CAPS]: predictsPhysics(opts) ? [AuthorityCaps.PHYSICS, AuthorityCaps.PHYSICS_PREDICT, AuthorityCaps.INPUT_REPEAT, AuthorityCaps.LIFTS, AuthorityCaps.INPUT_TICK, ...fastCaps] : [AuthorityCaps.PHYSICS, AuthorityCaps.INPUT_REPEAT, AuthorityCaps.LIFTS, AuthorityCaps.INPUT_TICK, ...fastCaps],
|
|
5296
|
+
[AuthorityJoinOption.SDK]: AUTHORITY_SDK_LABEL,
|
|
5054
5297
|
// Last, so nothing can misreport it (PROTOCOL.md §17.1: anything but 4 is refused with 426).
|
|
5055
5298
|
protocol: AUTHORITY_PROTOCOL_VERSION
|
|
5056
|
-
}
|
|
5299
|
+
};
|
|
5300
|
+
if (plan.method === AuthorityJoinMethod.JoinById) return client.joinById(plan.roomId, options);
|
|
5301
|
+
if (plan.method === AuthorityJoinMethod.Create) return client.create(RoomType.AUTHORITY, options);
|
|
5302
|
+
return client.joinOrCreate(RoomType.AUTHORITY, options);
|
|
5057
5303
|
}
|
|
5058
5304
|
function roomPreset(presetText) {
|
|
5059
5305
|
if (!presetText) return null;
|
|
@@ -5097,8 +5343,8 @@ function heldInput(player) {
|
|
|
5097
5343
|
}
|
|
5098
5344
|
var NOT_A_BODY = { body: false, qx: 0, qy: 0, qz: 0, qw: 1, sleep: false };
|
|
5099
5345
|
var NOTHING_YET = /* @__PURE__ */ new Map();
|
|
5100
|
-
var
|
|
5101
|
-
var wrapAngle = (v) => v -
|
|
5346
|
+
var TWO_PI5 = 2 * Math.PI;
|
|
5347
|
+
var wrapAngle = (v) => v - TWO_PI5 * Math.floor((v + Math.PI) / TWO_PI5);
|
|
5102
5348
|
var SEAT_DRAWINGS = [ContactDrawing.Predicted, ContactDrawing.Separate, ContactDrawing.Timeline];
|
|
5103
5349
|
var DEFAULT_SEAT_DRAWING = ContactDrawing.Predicted;
|
|
5104
5350
|
var renderInternals = (reconciler) => {
|
|
@@ -5107,8 +5353,35 @@ var renderInternals = (reconciler) => {
|
|
|
5107
5353
|
return ok ? r : null;
|
|
5108
5354
|
};
|
|
5109
5355
|
async function connectAuthority(opts) {
|
|
5110
|
-
const joined = await joinAuthority(opts);
|
|
5111
|
-
return attachAuthoritySession(useSdkReconnection(joined.room), opts, joined.url);
|
|
5356
|
+
const joined = await joinAuthority(opts, opts.matchKey === void 0 ? {} : { matchKey: opts.matchKey });
|
|
5357
|
+
return comingBack(attachAuthoritySession(useSdkReconnection(joined.room), opts, joined.url), opts, joined, false);
|
|
5358
|
+
}
|
|
5359
|
+
async function createPrivateAuthority(opts) {
|
|
5360
|
+
const joined = await joinAuthority(opts, { createPrivate: true, ...opts.matchKey === void 0 ? {} : { matchKey: opts.matchKey } });
|
|
5361
|
+
return comingBack(attachAuthoritySession(useSdkReconnection(joined.room), opts, joined.url), opts, joined, true);
|
|
5362
|
+
}
|
|
5363
|
+
async function joinPrivateAuthority(code, opts) {
|
|
5364
|
+
const joined = await joinAuthority(opts, { code, ...opts.matchKey === void 0 ? {} : { matchKey: opts.matchKey } });
|
|
5365
|
+
return comingBack(attachAuthoritySession(useSdkReconnection(joined.room), opts, joined.url), opts, joined, true);
|
|
5366
|
+
}
|
|
5367
|
+
function comingBack(session, opts, joined, closed) {
|
|
5368
|
+
if (opts.rejoin === false) return session;
|
|
5369
|
+
const others = joined.targets.filter((url) => url !== joined.url);
|
|
5370
|
+
return survivingSession(session, {
|
|
5371
|
+
closed,
|
|
5372
|
+
targets: [joined.url, ...others],
|
|
5373
|
+
join: async (url, way, lost) => {
|
|
5374
|
+
const room = await joinOnce(new Client(url), opts, rejoinPlan(way, lost));
|
|
5375
|
+
const next = attachAuthoritySession(useSdkReconnection(room), opts, url);
|
|
5376
|
+
await waitForFirstState(room, ClientReconnection.firstStateTimeoutMs);
|
|
5377
|
+
return next;
|
|
5378
|
+
}
|
|
5379
|
+
});
|
|
5380
|
+
}
|
|
5381
|
+
function rejoinPlan(way, lost) {
|
|
5382
|
+
if (way === RejoinWay.Room) return { method: AuthorityJoinMethod.JoinById, roomId: lost.roomId, options: {} };
|
|
5383
|
+
if (way === RejoinWay.Successor) return { method: AuthorityJoinMethod.JoinOrCreate, options: planSuccessorJoin({ lostRoomId: lost.roomId, matchKey: lost.matchKey, closed: lost.closed }) };
|
|
5384
|
+
return { method: AuthorityJoinMethod.JoinOrCreate, options: lost.matchKey === "" ? {} : { matchKey: lost.matchKey } };
|
|
5112
5385
|
}
|
|
5113
5386
|
function attachAuthoritySession(room, opts, url) {
|
|
5114
5387
|
const state = room.state;
|
|
@@ -5116,10 +5389,14 @@ function attachAuthoritySession(room, opts, url) {
|
|
|
5116
5389
|
const playerMap = () => state.players ?? NOTHING_YET;
|
|
5117
5390
|
const renderDelay = opts.renderDelayMs ?? AuthorityTiming.RENDER_DELAY_MS;
|
|
5118
5391
|
const handle = room.input({ type: AuthorityInput, mode: "reliable", renderDelay });
|
|
5392
|
+
const fastChoice = fastChoiceOf(opts);
|
|
5119
5393
|
const fast = fastSeat({
|
|
5120
5394
|
room,
|
|
5121
5395
|
handle,
|
|
5122
|
-
open:
|
|
5396
|
+
open: fastChoice.open,
|
|
5397
|
+
neverAsked: fastChoice.fallback,
|
|
5398
|
+
off: opts.fast === false,
|
|
5399
|
+
log: opts.fastLog ?? defaultFastLog,
|
|
5123
5400
|
now: opts.fastClock ?? (() => performance.now()),
|
|
5124
5401
|
tickHz: () => tickHz(),
|
|
5125
5402
|
stateCurrent: () => !resyncFirstState
|
|
@@ -5172,9 +5449,10 @@ function attachAuthoritySession(room, opts, url) {
|
|
|
5172
5449
|
inputsDropped: 0,
|
|
5173
5450
|
reconciles: 0,
|
|
5174
5451
|
visibleCorrections: 0,
|
|
5175
|
-
correctionBuckets:
|
|
5452
|
+
correctionBuckets: CORRECTION_BUCKET_METRES.map(() => 0).concat(0),
|
|
5176
5453
|
peakCorrection: 0,
|
|
5177
5454
|
lastCorrection: 0,
|
|
5455
|
+
peakStateCorrection: 0,
|
|
5178
5456
|
reseeds: 0,
|
|
5179
5457
|
bodyStates: 0,
|
|
5180
5458
|
bodySnaps: 0,
|
|
@@ -5185,8 +5463,8 @@ function attachAuthoritySession(room, opts, url) {
|
|
|
5185
5463
|
lastProjectileLeadMs: 0
|
|
5186
5464
|
};
|
|
5187
5465
|
const listeners = /* @__PURE__ */ new Map();
|
|
5188
|
-
const emit = (event, payload) => {
|
|
5189
|
-
for (const handler of listeners.get(event) ?? []) handler(payload);
|
|
5466
|
+
const emit = (event, ...payload) => {
|
|
5467
|
+
for (const handler of listeners.get(event) ?? []) handler(...payload);
|
|
5190
5468
|
};
|
|
5191
5469
|
let connected = true;
|
|
5192
5470
|
let ended = false;
|
|
@@ -5449,20 +5727,15 @@ function attachAuthoritySession(room, opts, url) {
|
|
|
5449
5727
|
};
|
|
5450
5728
|
const noteCorrection = (predicted, before, seqBefore) => {
|
|
5451
5729
|
if (predicted.reconciler.reconcileSeq === seqBefore) return;
|
|
5452
|
-
const
|
|
5453
|
-
let magnitude = 0;
|
|
5454
|
-
predicted.fields.forEach((field, i) => {
|
|
5455
|
-
if (NOT_A_PLACE.has(field)) return;
|
|
5456
|
-
const delta = (after[i] ?? 0) - (before[i] ?? 0);
|
|
5457
|
-
magnitude = Math.max(magnitude, Math.abs(ANGLES.has(field) ? wrapAngle(delta) : delta));
|
|
5458
|
-
});
|
|
5730
|
+
const { place: place2, state: stateGap } = correctionSizes(predicted.fields, before, predictedPose(predicted));
|
|
5459
5731
|
telemetry.reconciles++;
|
|
5460
|
-
telemetry.lastCorrection =
|
|
5461
|
-
telemetry.peakCorrection = Math.max(telemetry.peakCorrection,
|
|
5462
|
-
|
|
5732
|
+
telemetry.lastCorrection = place2;
|
|
5733
|
+
telemetry.peakCorrection = Math.max(telemetry.peakCorrection, place2);
|
|
5734
|
+
telemetry.peakStateCorrection = Math.max(telemetry.peakStateCorrection, stateGap);
|
|
5735
|
+
const unit2 = unitsPerMetre();
|
|
5736
|
+
if (isVisibleCorrection(place2, unit2)) {
|
|
5463
5737
|
telemetry.visibleCorrections++;
|
|
5464
|
-
|
|
5465
|
-
telemetry.correctionBuckets[bucket < 0 ? CORRECTION_BUCKETS.length : bucket] += 1;
|
|
5738
|
+
telemetry.correctionBuckets[correctionBucket(place2, unit2)] += 1;
|
|
5466
5739
|
}
|
|
5467
5740
|
};
|
|
5468
5741
|
const yawBefore = (predicted) => {
|
|
@@ -5922,10 +6195,10 @@ function attachAuthoritySession(room, opts, url) {
|
|
|
5922
6195
|
predict.dispose();
|
|
5923
6196
|
physicsPrediction.dispose();
|
|
5924
6197
|
};
|
|
5925
|
-
room.onLeave((code) => {
|
|
6198
|
+
room.onLeave((code, reason) => {
|
|
5926
6199
|
if (ended) return;
|
|
5927
6200
|
end();
|
|
5928
|
-
emit(SessionEvent.Disconnect, code);
|
|
6201
|
+
emit(SessionEvent.Disconnect, code, authorityCloseCodeOf(reason));
|
|
5929
6202
|
});
|
|
5930
6203
|
room.onMessage("*", (type, payload) => {
|
|
5931
6204
|
if (typeof type !== "string") return;
|
|
@@ -6278,6 +6551,10 @@ function attachAuthoritySession(room, opts, url) {
|
|
|
6278
6551
|
if (!ended) trackOwn();
|
|
6279
6552
|
return own ? view(own.id, own.entity) : void 0;
|
|
6280
6553
|
},
|
|
6554
|
+
code: room.roomId,
|
|
6555
|
+
get matchKey() {
|
|
6556
|
+
return state.matchKey ?? "";
|
|
6557
|
+
},
|
|
6281
6558
|
setWorld(next) {
|
|
6282
6559
|
const checked = firstpersonWorld(next);
|
|
6283
6560
|
const changed2 = checked.hash !== world.hash;
|
|
@@ -6414,7 +6691,17 @@ export {
|
|
|
6414
6691
|
regionProbePlan,
|
|
6415
6692
|
rankAuthorityRegions,
|
|
6416
6693
|
capacityWalk,
|
|
6694
|
+
RefusalKind,
|
|
6695
|
+
refusalKind,
|
|
6417
6696
|
selectAuthorityRegions,
|
|
6697
|
+
REJOIN,
|
|
6698
|
+
SUCCESSOR_OPEN_MS,
|
|
6699
|
+
RejoinWay,
|
|
6700
|
+
RejoinStop,
|
|
6701
|
+
comesBackAfter,
|
|
6702
|
+
rejoinStep,
|
|
6703
|
+
validSuccessorOf,
|
|
6704
|
+
planSuccessorJoin,
|
|
6418
6705
|
quatMul,
|
|
6419
6706
|
quatNormalize,
|
|
6420
6707
|
slerpFromIdentity,
|
|
@@ -6599,19 +6886,15 @@ export {
|
|
|
6599
6886
|
throttleState,
|
|
6600
6887
|
seatLeadClock,
|
|
6601
6888
|
throttleSteps,
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
encodeRecord,
|
|
6609
|
-
decodeRecord,
|
|
6610
|
-
FastFallbackReason,
|
|
6611
|
-
isFastBind,
|
|
6612
|
-
heldInputs,
|
|
6613
|
-
openFastClient,
|
|
6889
|
+
FastTransport,
|
|
6890
|
+
FastNotAsked,
|
|
6891
|
+
FAST_OFFER_WAIT_MS,
|
|
6892
|
+
udpLine,
|
|
6893
|
+
webSocketLine,
|
|
6894
|
+
fastLineAnnouncer,
|
|
6614
6895
|
SEAT_DRAWINGS,
|
|
6615
6896
|
DEFAULT_SEAT_DRAWING,
|
|
6616
|
-
connectAuthority
|
|
6897
|
+
connectAuthority,
|
|
6898
|
+
createPrivateAuthority,
|
|
6899
|
+
joinPrivateAuthority
|
|
6617
6900
|
};
|