@colyseus/core 0.17.44 → 0.18.1
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/build/MatchMaker.cjs +22 -13
- package/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.d.ts +10 -0
- package/build/MatchMaker.mjs +22 -14
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Protocol.cjs +102 -37
- package/build/Protocol.cjs.map +2 -2
- package/build/Protocol.d.ts +33 -2
- package/build/Protocol.mjs +102 -37
- package/build/Protocol.mjs.map +2 -2
- package/build/Rewind.cjs +385 -0
- package/build/Rewind.cjs.map +7 -0
- package/build/Rewind.d.ts +291 -0
- package/build/Rewind.mjs +359 -0
- package/build/Rewind.mjs.map +7 -0
- package/build/Room.cjs +425 -149
- package/build/Room.cjs.map +3 -3
- package/build/Room.d.ts +314 -27
- package/build/Room.mjs +434 -152
- package/build/Room.mjs.map +3 -3
- package/build/RoomMessages.cjs +265 -0
- package/build/RoomMessages.cjs.map +7 -0
- package/build/RoomMessages.d.ts +49 -0
- package/build/RoomMessages.mjs +240 -0
- package/build/RoomMessages.mjs.map +7 -0
- package/build/RoomPlugin.cjs +252 -0
- package/build/RoomPlugin.cjs.map +7 -0
- package/build/RoomPlugin.d.ts +271 -0
- package/build/RoomPlugin.mjs +220 -0
- package/build/RoomPlugin.mjs.map +7 -0
- package/build/Server.cjs +49 -11
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +26 -0
- package/build/Server.mjs +50 -12
- package/build/Server.mjs.map +2 -2
- package/build/Transport.cjs +64 -2
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +108 -5
- package/build/Transport.mjs +63 -2
- package/build/Transport.mjs.map +2 -2
- package/build/errors/RoomExceptions.cjs +7 -4
- package/build/errors/RoomExceptions.cjs.map +2 -2
- package/build/errors/RoomExceptions.d.ts +15 -3
- package/build/errors/RoomExceptions.mjs +5 -3
- package/build/errors/RoomExceptions.mjs.map +2 -2
- package/build/index.cjs +30 -4
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +9 -5
- package/build/index.mjs +29 -6
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +536 -0
- package/build/input/InputBuffer.cjs.map +7 -0
- package/build/input/InputBuffer.d.ts +226 -0
- package/build/input/InputBuffer.mjs +507 -0
- package/build/input/InputBuffer.mjs.map +7 -0
- package/build/input/RoomInput.cjs +306 -0
- package/build/input/RoomInput.cjs.map +7 -0
- package/build/input/RoomInput.d.ts +91 -0
- package/build/input/RoomInput.mjs +287 -0
- package/build/input/RoomInput.mjs.map +7 -0
- package/build/input/types.cjs +18 -0
- package/build/input/types.cjs.map +7 -0
- package/build/input/types.d.ts +476 -0
- package/build/input/types.mjs +0 -0
- package/build/input/types.mjs.map +7 -0
- package/build/internal.cjs +61 -0
- package/build/internal.cjs.map +7 -0
- package/build/internal.d.ts +9 -0
- package/build/internal.mjs +29 -0
- package/build/internal.mjs.map +7 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
- package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
- package/build/matchmaker/driver.cjs.map +1 -1
- package/build/matchmaker/driver.d.ts +12 -0
- package/build/matchmaker/driver.mjs.map +1 -1
- package/build/presence/LocalPresence.d.ts +1 -1
- package/build/rooms/LobbyRoom.cjs +8 -10
- package/build/rooms/LobbyRoom.cjs.map +2 -2
- package/build/rooms/LobbyRoom.d.ts +4 -3
- package/build/rooms/LobbyRoom.mjs +8 -10
- package/build/rooms/LobbyRoom.mjs.map +2 -2
- package/build/rooms/QueueRoom.cjs +1 -1
- package/build/rooms/QueueRoom.cjs.map +2 -2
- package/build/rooms/QueueRoom.mjs +1 -1
- package/build/rooms/QueueRoom.mjs.map +2 -2
- package/build/rooms/RelayRoom.cjs +12 -16
- package/build/rooms/RelayRoom.cjs.map +2 -2
- package/build/rooms/RelayRoom.d.ts +32 -11
- package/build/rooms/RelayRoom.mjs +10 -16
- package/build/rooms/RelayRoom.mjs.map +2 -2
- package/build/router/default_routes.cjs +4 -1
- package/build/router/default_routes.cjs.map +2 -2
- package/build/router/default_routes.mjs +4 -1
- package/build/router/default_routes.mjs.map +2 -2
- package/build/router/index.cjs +71 -5
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.d.ts +30 -6
- package/build/router/index.mjs +72 -7
- package/build/router/index.mjs.map +3 -3
- package/build/serializer/NoneSerializer.cjs +2 -2
- package/build/serializer/NoneSerializer.cjs.map +2 -2
- package/build/serializer/NoneSerializer.d.ts +3 -3
- package/build/serializer/NoneSerializer.mjs +2 -2
- package/build/serializer/NoneSerializer.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs +44 -14
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +3 -3
- package/build/serializer/SchemaSerializer.mjs +46 -16
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/build/serializer/Serializer.cjs.map +1 -1
- package/build/serializer/Serializer.d.ts +12 -2
- package/build/utils/DevMode.cjs +14 -14
- package/build/utils/DevMode.cjs.map +2 -2
- package/build/utils/DevMode.d.ts +0 -2
- package/build/utils/DevMode.mjs +14 -12
- package/build/utils/DevMode.mjs.map +2 -2
- package/build/utils/UserSessionIndex.cjs +162 -0
- package/build/utils/UserSessionIndex.cjs.map +7 -0
- package/build/utils/UserSessionIndex.d.ts +166 -0
- package/build/utils/UserSessionIndex.mjs +130 -0
- package/build/utils/UserSessionIndex.mjs.map +7 -0
- package/build/utils/Utils.cjs +3 -0
- package/build/utils/Utils.cjs.map +2 -2
- package/build/utils/Utils.d.ts +8 -0
- package/build/utils/Utils.mjs +2 -0
- package/build/utils/Utils.mjs.map +2 -2
- package/package.json +19 -14
- package/src/MatchMaker.ts +47 -15
- package/src/Protocol.ts +130 -59
- package/src/Rewind.ts +572 -0
- package/src/Room.ts +649 -235
- package/src/RoomMessages.ts +342 -0
- package/src/RoomPlugin.ts +563 -0
- package/src/Server.ts +82 -15
- package/src/Transport.ts +172 -9
- package/src/errors/RoomExceptions.ts +18 -4
- package/src/index.ts +23 -3
- package/src/input/InputBuffer.ts +549 -0
- package/src/input/RoomInput.ts +337 -0
- package/src/input/types.ts +503 -0
- package/src/internal.ts +46 -0
- package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
- package/src/matchmaker/driver.ts +13 -0
- package/src/rooms/LobbyRoom.ts +12 -8
- package/src/rooms/QueueRoom.ts +1 -1
- package/src/rooms/RelayRoom.ts +9 -15
- package/src/router/default_routes.ts +6 -1
- package/src/router/index.ts +117 -11
- package/src/serializer/NoneSerializer.ts +3 -3
- package/src/serializer/SchemaSerializer.ts +110 -19
- package/src/serializer/Serializer.ts +13 -2
- package/src/utils/DevMode.ts +18 -13
- package/src/utils/UserSessionIndex.ts +311 -0
- package/src/utils/Utils.ts +9 -0
package/build/Protocol.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// packages/core/src/Protocol.ts
|
|
2
|
-
import { Packr } from "
|
|
2
|
+
import { Packr, RESERVE_START_SPACE } from "msgpackr";
|
|
3
3
|
import { encode } from "@colyseus/schema";
|
|
4
4
|
import { Protocol } from "@colyseus/shared-types";
|
|
5
5
|
var IpcProtocol = {
|
|
@@ -9,63 +9,128 @@ var IpcProtocol = {
|
|
|
9
9
|
};
|
|
10
10
|
var packr = new Packr({
|
|
11
11
|
useRecords: false
|
|
12
|
-
//
|
|
12
|
+
// interop with non-msgpackr decoders
|
|
13
13
|
});
|
|
14
|
-
|
|
14
|
+
var frameBuffer = Buffer.allocUnsafe(8192);
|
|
15
|
+
function ensureFrameCapacity(capacity) {
|
|
16
|
+
if (capacity > frameBuffer.byteLength) {
|
|
17
|
+
const next = Buffer.allocUnsafe(capacity);
|
|
18
|
+
frameBuffer.copy(next);
|
|
19
|
+
frameBuffer = next;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
var it = { offset: 0 };
|
|
15
23
|
var getMessageBytes = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Build the JOIN_ROOM payload.
|
|
26
|
+
*
|
|
27
|
+
* Wire layout:
|
|
28
|
+
* [JOIN_ROOM byte][rt-len][rt][sid-len][sid][stateReflectionLen varint][stateReflection][...sections]
|
|
29
|
+
*
|
|
30
|
+
* The varint length prefix on `stateReflection` is required because the
|
|
31
|
+
* schema decoder on the SDK side runs `while (offset < bytes.byteLength)`
|
|
32
|
+
* — without a boundary it consumes past the state reflection into the
|
|
33
|
+
* trailing tagged-section bytes, producing "definition mismatch" warnings.
|
|
34
|
+
* Length is `0` when no state reflection is present.
|
|
35
|
+
*
|
|
36
|
+
* Each trailing section is `[tag (uint8)][length (varint)][payload]`. The
|
|
37
|
+
* SDK skips unknown tags via `length`, so new sections can be added without
|
|
38
|
+
* breaking older clients. See {@link HandshakeSection} in shared-types.
|
|
39
|
+
*/
|
|
40
|
+
[Protocol.JOIN_ROOM]: (reconnectionToken, serializerId, handshake, extraSections) => {
|
|
41
|
+
const reconnectionTokenLength = Buffer.byteLength(reconnectionToken, "utf8");
|
|
42
|
+
const serializerIdLength = Buffer.byteLength(serializerId, "utf8");
|
|
23
43
|
let handshakeLength = handshake?.byteLength || 0;
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
let extraLength = 0;
|
|
45
|
+
if (extraSections !== void 0) {
|
|
46
|
+
for (let i = 0; i < extraSections.length; i++) {
|
|
47
|
+
extraLength += 1 + 9 + extraSections[i].bytes.byteLength;
|
|
48
|
+
}
|
|
26
49
|
}
|
|
50
|
+
ensureFrameCapacity(1 + 1 + reconnectionTokenLength + 1 + serializerIdLength + 9 + handshakeLength + extraLength);
|
|
51
|
+
it.offset = 1;
|
|
52
|
+
frameBuffer[0] = Protocol.JOIN_ROOM;
|
|
53
|
+
frameBuffer[it.offset++] = reconnectionTokenLength;
|
|
54
|
+
encode.utf8Write(frameBuffer, reconnectionToken, it);
|
|
55
|
+
frameBuffer[it.offset++] = serializerIdLength;
|
|
56
|
+
encode.utf8Write(frameBuffer, serializerId, it);
|
|
57
|
+
encode.number(frameBuffer, handshakeLength, it);
|
|
27
58
|
if (handshakeLength > 0) {
|
|
28
|
-
|
|
59
|
+
frameBuffer.set(handshake, it.offset);
|
|
60
|
+
it.offset += handshakeLength;
|
|
29
61
|
}
|
|
30
|
-
|
|
62
|
+
if (extraSections !== void 0) {
|
|
63
|
+
for (let i = 0; i < extraSections.length; i++) {
|
|
64
|
+
const section = extraSections[i];
|
|
65
|
+
frameBuffer[it.offset++] = section.tag;
|
|
66
|
+
encode.number(frameBuffer, section.bytes.byteLength, it);
|
|
67
|
+
frameBuffer.set(section.bytes, it.offset);
|
|
68
|
+
it.offset += section.bytes.byteLength;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return Buffer.from(frameBuffer.subarray(0, it.offset));
|
|
31
72
|
},
|
|
32
73
|
[Protocol.ERROR]: (code, message = "") => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
encode.
|
|
37
|
-
|
|
74
|
+
ensureFrameCapacity(1 + 9 + 9 + Buffer.byteLength(message, "utf8"));
|
|
75
|
+
it.offset = 1;
|
|
76
|
+
frameBuffer[0] = Protocol.ERROR;
|
|
77
|
+
encode.number(frameBuffer, code, it);
|
|
78
|
+
encode.string(frameBuffer, message, it);
|
|
79
|
+
return Buffer.from(frameBuffer.subarray(0, it.offset));
|
|
38
80
|
},
|
|
39
81
|
[Protocol.ROOM_STATE]: (bytes) => {
|
|
40
82
|
return [Protocol.ROOM_STATE, ...bytes];
|
|
41
83
|
},
|
|
84
|
+
/**
|
|
85
|
+
* Reply to a client {@link Protocol.ROOM_REQUEST}.
|
|
86
|
+
*
|
|
87
|
+
* Wire layout: `[ROOM_RESPONSE byte][requestId varint][status uint8][msgpack payload?]`
|
|
88
|
+
*
|
|
89
|
+
* `requestId` is opaque — echoed back exactly as the SDK sent it so the
|
|
90
|
+
* pending callback/promise can be correlated. `status` is a
|
|
91
|
+
* {@link ResponseStatus} (0 = OK, 1 = REJECTED, 2 = ERROR). The payload is
|
|
92
|
+
* omitted when a handler resolves with `undefined`. Returns a fresh Buffer copy
|
|
93
|
+
* for the same back-pressure reason documented on `raw` below.
|
|
94
|
+
*/
|
|
95
|
+
[Protocol.ROOM_RESPONSE]: (requestId, status, message) => {
|
|
96
|
+
it.offset = 1;
|
|
97
|
+
frameBuffer[0] = Protocol.ROOM_RESPONSE;
|
|
98
|
+
encode.number(frameBuffer, requestId, it);
|
|
99
|
+
frameBuffer[it.offset++] = status;
|
|
100
|
+
const headerLength = it.offset;
|
|
101
|
+
if (message !== void 0) {
|
|
102
|
+
const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);
|
|
103
|
+
packed.set(frameBuffer.subarray(0, headerLength), 0);
|
|
104
|
+
return Buffer.from(packed);
|
|
105
|
+
}
|
|
106
|
+
return Buffer.from(frameBuffer.subarray(0, headerLength));
|
|
107
|
+
},
|
|
42
108
|
[Protocol.PING]: () => {
|
|
43
|
-
|
|
44
|
-
return Buffer.from(
|
|
109
|
+
frameBuffer[0] = Protocol.PING;
|
|
110
|
+
return Buffer.from(frameBuffer.subarray(0, 1));
|
|
45
111
|
},
|
|
112
|
+
// Returns a fresh copy: frameBuffer/packr are reused next call, but callers pass
|
|
113
|
+
// the result to async consumers (Node retains it for libuv writev across ticks).
|
|
114
|
+
// See benchmark/test-buffer-corruption.ts for the back-pressure repro.
|
|
46
115
|
raw: (code, type, message, rawMessage) => {
|
|
47
|
-
|
|
48
|
-
|
|
116
|
+
it.offset = 1;
|
|
117
|
+
frameBuffer[0] = code;
|
|
49
118
|
if (typeof type === "string") {
|
|
50
|
-
encode.string(
|
|
119
|
+
encode.string(frameBuffer, type, it);
|
|
51
120
|
} else {
|
|
52
|
-
encode.number(
|
|
121
|
+
encode.number(frameBuffer, type, it);
|
|
53
122
|
}
|
|
123
|
+
const headerLength = it.offset;
|
|
54
124
|
if (message !== void 0) {
|
|
55
|
-
packr.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
const endOfBufferOffset = packr.pack(message, 2048 + it.offset).byteLength;
|
|
60
|
-
return Buffer.from(packr.buffer.subarray(0, endOfBufferOffset));
|
|
125
|
+
const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);
|
|
126
|
+
packed.set(frameBuffer.subarray(0, headerLength), 0);
|
|
127
|
+
return Buffer.from(packed);
|
|
61
128
|
} else if (rawMessage !== void 0) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
packr.buffer.set(rawMessage, it.offset);
|
|
66
|
-
return Buffer.from(packr.buffer.subarray(0, it.offset + rawMessage.byteLength));
|
|
129
|
+
ensureFrameCapacity(headerLength + rawMessage.byteLength);
|
|
130
|
+
frameBuffer.set(rawMessage, headerLength);
|
|
131
|
+
return Buffer.from(frameBuffer.subarray(0, headerLength + rawMessage.byteLength));
|
|
67
132
|
} else {
|
|
68
|
-
return Buffer.from(
|
|
133
|
+
return Buffer.from(frameBuffer.subarray(0, headerLength));
|
|
69
134
|
}
|
|
70
135
|
}
|
|
71
136
|
};
|
package/build/Protocol.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/Protocol.ts"],
|
|
4
|
-
"sourcesContent": ["import { Packr } from '
|
|
5
|
-
"mappings": ";AAAA,SAAS,
|
|
4
|
+
"sourcesContent": ["import { Packr, RESERVE_START_SPACE } from 'msgpackr';\nimport { encode, type Iterator } from '@colyseus/schema';\nimport { Protocol } from '@colyseus/shared-types';\n\n// Inter-process communication protocol\nexport const IpcProtocol = {\n SUCCESS: 0,\n ERROR: 1,\n TIMEOUT: 2,\n} as const;\nexport type IpcProtocol = typeof IpcProtocol[keyof typeof IpcProtocol];\n\nconst packr = new Packr({\n useRecords: false, // interop with non-msgpackr decoders\n});\n\n// Buffer for assembling outgoing frames; keeps us off msgpackr's internal\n// buffer so `core` can use the upstream package directly.\nlet frameBuffer = Buffer.allocUnsafe(8192);\n\n// Grow to `capacity`, preserving written bytes (`rawMessage` writes the header\n// before the payload size is known).\nfunction ensureFrameCapacity(capacity: number) {\n if (capacity > frameBuffer.byteLength) {\n const next = Buffer.allocUnsafe(capacity);\n frameBuffer.copy(next);\n frameBuffer = next;\n }\n}\n\n// Shared across calls to avoid a per-call allocation on the hot send path.\n// Safe: encode.* mutate `.offset` in place and never re-enter this module.\nconst it: Iterator = { offset: 0 };\n\nexport const getMessageBytes = {\n /**\n * Build the JOIN_ROOM payload.\n *\n * Wire layout:\n * [JOIN_ROOM byte][rt-len][rt][sid-len][sid][stateReflectionLen varint][stateReflection][...sections]\n *\n * The varint length prefix on `stateReflection` is required because the\n * schema decoder on the SDK side runs `while (offset < bytes.byteLength)`\n * \u2014 without a boundary it consumes past the state reflection into the\n * trailing tagged-section bytes, producing \"definition mismatch\" warnings.\n * Length is `0` when no state reflection is present.\n *\n * Each trailing section is `[tag (uint8)][length (varint)][payload]`. The\n * SDK skips unknown tags via `length`, so new sections can be added without\n * breaking older clients. See {@link HandshakeSection} in shared-types.\n */\n [Protocol.JOIN_ROOM]: (\n reconnectionToken: string,\n serializerId: string,\n handshake?: Uint8Array,\n extraSections?: Array<{ tag: number; bytes: Uint8Array }>,\n ) => {\n const reconnectionTokenLength = Buffer.byteLength(reconnectionToken, \"utf8\");\n const serializerIdLength = Buffer.byteLength(serializerId, \"utf8\");\n let handshakeLength = handshake?.byteLength || 0;\n\n // per section: 1 tag byte + 9 (max varint) + payload\n let extraLength = 0;\n if (extraSections !== undefined) {\n for (let i = 0; i < extraSections.length; i++) {\n extraLength += 1 + 9 + extraSections[i].bytes.byteLength;\n }\n }\n\n // capacity: header + 9 (handshake-len varint) + handshake + sections\n ensureFrameCapacity(1 + 1 + reconnectionTokenLength + 1 + serializerIdLength + 9 + handshakeLength + extraLength);\n\n it.offset = 1;\n frameBuffer[0] = Protocol.JOIN_ROOM;\n\n frameBuffer[it.offset++] = reconnectionTokenLength;\n encode.utf8Write(frameBuffer, reconnectionToken, it);\n\n frameBuffer[it.offset++] = serializerIdLength;\n encode.utf8Write(frameBuffer, serializerId, it);\n\n encode.number(frameBuffer, handshakeLength, it);\n if (handshakeLength > 0) {\n frameBuffer.set(handshake, it.offset);\n it.offset += handshakeLength;\n }\n\n if (extraSections !== undefined) {\n for (let i = 0; i < extraSections.length; i++) {\n const section = extraSections[i];\n frameBuffer[it.offset++] = section.tag;\n encode.number(frameBuffer, section.bytes.byteLength, it);\n frameBuffer.set(section.bytes, it.offset);\n it.offset += section.bytes.byteLength;\n }\n }\n\n return Buffer.from(frameBuffer.subarray(0, it.offset));\n },\n\n [Protocol.ERROR]: (code: number, message: string = '') => {\n // capacity: 1 + code varint + length-prefixed message\n ensureFrameCapacity(1 + 9 + 9 + Buffer.byteLength(message, \"utf8\"));\n\n it.offset = 1;\n frameBuffer[0] = Protocol.ERROR;\n\n encode.number(frameBuffer, code, it);\n encode.string(frameBuffer, message, it);\n\n return Buffer.from(frameBuffer.subarray(0, it.offset));\n },\n\n [Protocol.ROOM_STATE]: (bytes: number[]) => {\n return [Protocol.ROOM_STATE, ...bytes];\n },\n\n /**\n * Reply to a client {@link Protocol.ROOM_REQUEST}.\n *\n * Wire layout: `[ROOM_RESPONSE byte][requestId varint][status uint8][msgpack payload?]`\n *\n * `requestId` is opaque \u2014 echoed back exactly as the SDK sent it so the\n * pending callback/promise can be correlated. `status` is a\n * {@link ResponseStatus} (0 = OK, 1 = REJECTED, 2 = ERROR). The payload is\n * omitted when a handler resolves with `undefined`. Returns a fresh Buffer copy\n * for the same back-pressure reason documented on `raw` below.\n */\n [Protocol.ROOM_RESPONSE]: (requestId: number, status: number, message?: any): Buffer => {\n it.offset = 1;\n frameBuffer[0] = Protocol.ROOM_RESPONSE;\n\n encode.number(frameBuffer, requestId, it);\n frameBuffer[it.offset++] = status;\n const headerLength = it.offset;\n\n if (message !== undefined) {\n // reserve `headerLength` bytes up front in the pack output for the header\n const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);\n packed.set(frameBuffer.subarray(0, headerLength), 0);\n return Buffer.from(packed);\n }\n\n return Buffer.from(frameBuffer.subarray(0, headerLength));\n },\n\n [Protocol.PING]: () => {\n frameBuffer[0] = Protocol.PING;\n return Buffer.from(frameBuffer.subarray(0, 1));\n },\n\n // Returns a fresh copy: frameBuffer/packr are reused next call, but callers pass\n // the result to async consumers (Node retains it for libuv writev across ticks).\n // See benchmark/test-buffer-corruption.ts for the back-pressure repro.\n raw: (code: Protocol, type: string | number, message?: any, rawMessage?: Uint8Array | Buffer): Buffer => {\n it.offset = 1;\n frameBuffer[0] = code;\n\n if (typeof (type) === 'string') {\n encode.string(frameBuffer, type, it);\n\n } else {\n encode.number(frameBuffer, type, it);\n }\n const headerLength = it.offset;\n\n if (message !== undefined) {\n // reserve `headerLength` bytes up front in the pack output for the header\n const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);\n packed.set(frameBuffer.subarray(0, headerLength), 0);\n return Buffer.from(packed);\n\n } else if (rawMessage !== undefined) {\n ensureFrameCapacity(headerLength + rawMessage.byteLength);\n frameBuffer.set(rawMessage, headerLength);\n return Buffer.from(frameBuffer.subarray(0, headerLength + rawMessage.byteLength));\n\n } else {\n return Buffer.from(frameBuffer.subarray(0, headerLength));\n }\n },\n\n};\n\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/Rewind.cjs
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// packages/core/src/Rewind.ts
|
|
21
|
+
var Rewind_exports = {};
|
|
22
|
+
__export(Rewind_exports, {
|
|
23
|
+
Rewind: () => Rewind,
|
|
24
|
+
RewindView: () => RewindView
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(Rewind_exports);
|
|
27
|
+
var import_schema = require("@colyseus/schema");
|
|
28
|
+
var import_Utils = require("./utils/Utils.cjs");
|
|
29
|
+
var DEFAULT_MAX_REWIND_MS = 500;
|
|
30
|
+
var DEFAULT_SAMPLE_INTERVAL_MS = 1e3 / 60;
|
|
31
|
+
var $HISTORY = /* @__PURE__ */ Symbol("rewind.history");
|
|
32
|
+
function fieldIndexOf(instance, field) {
|
|
33
|
+
const md = instance.constructor[import_Utils.$METADATA];
|
|
34
|
+
const idx = md?.[field];
|
|
35
|
+
return typeof idx === "number" ? idx : -1;
|
|
36
|
+
}
|
|
37
|
+
var EntityHistory = class {
|
|
38
|
+
constructor(fields, fieldIdx, maxRewindMs, sampleIntervalMs, step, reckoned, groupId) {
|
|
39
|
+
// hold (vs lerp) between samples
|
|
40
|
+
this.head = 0;
|
|
41
|
+
// next write slot
|
|
42
|
+
this.count = 0;
|
|
43
|
+
this.fields = fields;
|
|
44
|
+
this.fieldIdx = fieldIdx;
|
|
45
|
+
this.step = step;
|
|
46
|
+
this.reckoned = reckoned;
|
|
47
|
+
this.groupId = groupId;
|
|
48
|
+
this.cap = Math.max(2, Math.ceil(maxRewindMs / sampleIntervalMs) + 4);
|
|
49
|
+
this.t = new Float64Array(this.cap);
|
|
50
|
+
this.cols = fields.map(() => new Float64Array(this.cap));
|
|
51
|
+
}
|
|
52
|
+
/** `values` = the entity's dense `$values` array. Direct array reads — no
|
|
53
|
+
* per-field megamorphic accessor. */
|
|
54
|
+
record(time, values) {
|
|
55
|
+
const fieldIdx = this.fieldIdx;
|
|
56
|
+
this.t[this.head] = time;
|
|
57
|
+
for (let f = 0; f < fieldIdx.length; f++) this.cols[f][this.head] = values[fieldIdx[f]];
|
|
58
|
+
this.head = (this.head + 1) % this.cap;
|
|
59
|
+
if (this.count < this.cap) this.count++;
|
|
60
|
+
}
|
|
61
|
+
/** Interpolated value of column `col` at `time`, clamped to the retained range. */
|
|
62
|
+
valueAt(time, col) {
|
|
63
|
+
const c = this.cols[col];
|
|
64
|
+
const oldest = (this.head - this.count + this.cap) % this.cap;
|
|
65
|
+
if (time <= this.t[oldest]) return c[oldest];
|
|
66
|
+
const newest = (this.head - 1 + this.cap) % this.cap;
|
|
67
|
+
if (time >= this.t[newest]) return c[newest];
|
|
68
|
+
let prev = oldest;
|
|
69
|
+
for (let i = 1; i < this.count; i++) {
|
|
70
|
+
const idx = (oldest + i) % this.cap;
|
|
71
|
+
if (this.t[idx] >= time) {
|
|
72
|
+
if (this.step) return c[prev];
|
|
73
|
+
const t0 = this.t[prev], t1 = this.t[idx];
|
|
74
|
+
const a = t1 > t0 ? (time - t0) / (t1 - t0) : 0;
|
|
75
|
+
return c[prev] + (c[idx] - c[prev]) * a;
|
|
76
|
+
}
|
|
77
|
+
prev = idx;
|
|
78
|
+
}
|
|
79
|
+
return c[newest];
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
function historyForField(instance, field) {
|
|
83
|
+
const arr = instance[$HISTORY];
|
|
84
|
+
if (arr === void 0) return void 0;
|
|
85
|
+
for (let i = 0; i < arr.length; i++) {
|
|
86
|
+
if (arr[i].fields.indexOf(field) >= 0) return arr[i];
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
var RewindView = class {
|
|
91
|
+
constructor() {
|
|
92
|
+
this._time = 0;
|
|
93
|
+
this._reckonTime = 0;
|
|
94
|
+
}
|
|
95
|
+
/** The clamped server-time (ms) this view reads at — after the maxRewindMs
|
|
96
|
+
* clamp and the live (newest-sample) fallback. Mostly for logging/debug. */
|
|
97
|
+
get time() {
|
|
98
|
+
return this._time;
|
|
99
|
+
}
|
|
100
|
+
/** The clamped server-time (ms) `mode: "reckon"` groups read at — the
|
|
101
|
+
* client's reconstructed simulation instant, not the raw stamp (see
|
|
102
|
+
* {@link Rewind.at}). Always a resolved instant: a clamped direct stamp,
|
|
103
|
+
* or the midpoint/live reconstruction when unstamped. `0` only before the
|
|
104
|
+
* first `record()`. */
|
|
105
|
+
get reckonTime() {
|
|
106
|
+
return this._reckonTime;
|
|
107
|
+
}
|
|
108
|
+
/** @internal Aim at a rewind + clamped times ({@link Rewind.at} owns the clamps). */
|
|
109
|
+
_retarget(rewind, at, reckonAt) {
|
|
110
|
+
this.rewind = rewind;
|
|
111
|
+
this._time = at;
|
|
112
|
+
this._reckonTime = reckonAt;
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
/** Rewound value of a numeric `field` on `entity` (live if it isn't tracked). */
|
|
116
|
+
value(entity, field) {
|
|
117
|
+
if (this.rewind === void 0) {
|
|
118
|
+
throw new Error("RewindView is not aimed \u2014 obtain it from rewind.at()/lastSeenBy(), or pass it to them as `out`.");
|
|
119
|
+
}
|
|
120
|
+
const h = historyForField(entity, field);
|
|
121
|
+
if (h === void 0) return entity[field];
|
|
122
|
+
return h.valueAt(h.reckoned ? this._reckonTime : this._time, h.fields.indexOf(field));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Batch {@link value} reads: the `fields` YOU list define the result's shape
|
|
126
|
+
* (`Record<field, number>`). Pass `out` to fill (and return) a reused scratch
|
|
127
|
+
* instead of allocating — its properties beyond `fields` are left untouched,
|
|
128
|
+
* so a scratch can carry extra context (an `alive` flag, say).
|
|
129
|
+
*/
|
|
130
|
+
read(entity, fields, out) {
|
|
131
|
+
const o = out ?? {};
|
|
132
|
+
for (let i = 0; i < fields.length; i++) o[fields[i]] = this.value(entity, fields[i]);
|
|
133
|
+
return o;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var Rewind = class _Rewind {
|
|
137
|
+
constructor(defaultMaxRewindMs) {
|
|
138
|
+
this.groups = [];
|
|
139
|
+
this._sampleIntervalMs = DEFAULT_SAMPLE_INTERVAL_MS;
|
|
140
|
+
this._lastRecordedAt = -1;
|
|
141
|
+
/** Default view returned by at/lastSeenBy when no `out` is given — one per
|
|
142
|
+
* Rewind, re-aimed per call (zero alloc, zero userland setup). */
|
|
143
|
+
this._view = new RewindView();
|
|
144
|
+
this.defaultMaxRewindMs = defaultMaxRewindMs;
|
|
145
|
+
}
|
|
146
|
+
static {
|
|
147
|
+
this.byRoom = /* @__PURE__ */ new WeakMap();
|
|
148
|
+
}
|
|
149
|
+
/** One `Rewind` per room (idempotent). `room` is only the cache key. */
|
|
150
|
+
static get(room, opts) {
|
|
151
|
+
let r = _Rewind.byRoom.get(room);
|
|
152
|
+
if (r === void 0) {
|
|
153
|
+
r = new _Rewind(opts?.maxRewindMs ?? DEFAULT_MAX_REWIND_MS);
|
|
154
|
+
_Rewind.byRoom.set(room, r);
|
|
155
|
+
}
|
|
156
|
+
return r;
|
|
157
|
+
}
|
|
158
|
+
/** The default rewind window (ms). Use it to bound a hit test:
|
|
159
|
+
* `Math.max(renderTime, now - rewind.maxRewindMs)`. */
|
|
160
|
+
get maxRewindMs() {
|
|
161
|
+
return this.defaultMaxRewindMs;
|
|
162
|
+
}
|
|
163
|
+
/** Server time of the last {@link record} (or -1). The auto-record skips a tick
|
|
164
|
+
* whose time was already recorded manually — your `record()` wins. */
|
|
165
|
+
get lastRecordedAt() {
|
|
166
|
+
return this._lastRecordedAt;
|
|
167
|
+
}
|
|
168
|
+
/** Track every entity in a collection (Map/Array/Set schema). `fields` narrows
|
|
169
|
+
* to its element's numeric fields. `interpolate` (a mode or a per-entity fn,
|
|
170
|
+
* default `linear`) picks how `valueAt` reconstructs between samples — use
|
|
171
|
+
* `step` for discrete-motion entities (e.g. teleporters). `mode` picks the
|
|
172
|
+
* lag-comp timeline (default `"snapshot"`; see {@link AttachOptions}). */
|
|
173
|
+
attachAll(collection, opts) {
|
|
174
|
+
const c = collection;
|
|
175
|
+
this.groups.push({ entities: () => c.values(), fields: opts.fields, resolvedByCtor: /* @__PURE__ */ new Map(), maxRewindMs: opts.maxRewindMs ?? this.defaultMaxRewindMs, interpolate: opts.interpolate ?? "linear", reckoned: opts.mode === "reckon", groupId: this.groups.length });
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
/** Track a single entity (e.g. a boss). `fields` narrows to its numeric fields. */
|
|
179
|
+
attach(instance, opts) {
|
|
180
|
+
const one = [instance];
|
|
181
|
+
this.groups.push({ entities: () => one, fields: opts.fields, resolvedByCtor: /* @__PURE__ */ new Map(), maxRewindMs: opts.maxRewindMs ?? this.defaultMaxRewindMs, interpolate: opts.interpolate ?? "linear", reckoned: opts.mode === "reckon", groupId: this.groups.length });
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Snapshot every tracked entity at `now`. Call once per tick, AFTER they move —
|
|
186
|
+
* {@link Room.allowRewindState} does this for you. `sampleIntervalMs` (the gap
|
|
187
|
+
* between records) sizes the history rings; the framework passes the sim
|
|
188
|
+
* interval. Calling this yourself during a tick suppresses that tick's
|
|
189
|
+
* auto-record (see {@link lastRecordedAt}).
|
|
190
|
+
*/
|
|
191
|
+
record(now, sampleIntervalMs) {
|
|
192
|
+
if (sampleIntervalMs !== void 0 && sampleIntervalMs > 0) this._sampleIntervalMs = sampleIntervalMs;
|
|
193
|
+
this._lastRecordedAt = now;
|
|
194
|
+
for (const g of this.groups) {
|
|
195
|
+
for (const e of g.entities()) {
|
|
196
|
+
const t = e;
|
|
197
|
+
let arr = t[$HISTORY];
|
|
198
|
+
if (arr === void 0) {
|
|
199
|
+
arr = [];
|
|
200
|
+
t[$HISTORY] = arr;
|
|
201
|
+
}
|
|
202
|
+
let h;
|
|
203
|
+
for (let i = 0; i < arr.length; i++) {
|
|
204
|
+
if (arr[i].groupId === g.groupId) {
|
|
205
|
+
h = arr[i];
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (h === void 0) {
|
|
210
|
+
h = this.createHistory(g, e);
|
|
211
|
+
arr.push(h);
|
|
212
|
+
}
|
|
213
|
+
h.record(now, t[import_schema.$values]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Cold path — first time an entity is seen by `record()` for a given group.
|
|
218
|
+
* Resolves the field→`$values` indices (cached per constructor for array-form
|
|
219
|
+
* `fields`; fn-form resolves per entity) and bakes in the group's `mode`. */
|
|
220
|
+
createHistory(g, e) {
|
|
221
|
+
let rf;
|
|
222
|
+
if (typeof g.fields === "function") {
|
|
223
|
+
rf = this.resolveFields(e, g.fields(e));
|
|
224
|
+
} else {
|
|
225
|
+
let cached = g.resolvedByCtor.get(e.constructor);
|
|
226
|
+
if (cached === void 0) {
|
|
227
|
+
cached = this.resolveFields(e, g.fields);
|
|
228
|
+
g.resolvedByCtor.set(e.constructor, cached);
|
|
229
|
+
}
|
|
230
|
+
rf = cached;
|
|
231
|
+
}
|
|
232
|
+
const interp = typeof g.interpolate === "function" ? g.interpolate(e) : g.interpolate;
|
|
233
|
+
return new EntityHistory(rf.fields, rf.idx, g.maxRewindMs, this._sampleIntervalMs, interp === "step", g.reckoned, g.groupId);
|
|
234
|
+
}
|
|
235
|
+
/** Keep only the fields this entity's type declares (unknown → read live). */
|
|
236
|
+
resolveFields(e, names) {
|
|
237
|
+
const fields = [];
|
|
238
|
+
const idx = [];
|
|
239
|
+
for (const name of names) {
|
|
240
|
+
const i = fieldIndexOf(e, name);
|
|
241
|
+
if (i >= 0) {
|
|
242
|
+
fields.push(name);
|
|
243
|
+
idx.push(i);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { fields, idx };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* `instance`'s `field` value at past `time` (interpolated from history). Falls
|
|
250
|
+
* back to the live value when the entity has no history yet, or the field
|
|
251
|
+
* isn't tracked.
|
|
252
|
+
*/
|
|
253
|
+
valueAt(instance, time, field) {
|
|
254
|
+
const h = historyForField(instance, field);
|
|
255
|
+
if (h === void 0) return instance[field];
|
|
256
|
+
return h.valueAt(time, h.fields.indexOf(field));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Which timeline(s) the attached groups rewind to — `reckon` true iff any
|
|
260
|
+
* group is `mode:"reckon"`, `snapshot` true iff any is `mode:"snapshot"`. The
|
|
261
|
+
* room reads this once (groups are declared in `onCreate`, before any join) to
|
|
262
|
+
* pick which stamp(s) the client must ship: reckon-only / render-only / both /
|
|
263
|
+
* none. @internal
|
|
264
|
+
*/
|
|
265
|
+
timelineMode() {
|
|
266
|
+
let reckon = false, snapshot = false;
|
|
267
|
+
for (const g of this.groups) {
|
|
268
|
+
if (g.reckoned) reckon = true;
|
|
269
|
+
else snapshot = true;
|
|
270
|
+
}
|
|
271
|
+
return { reckon, snapshot };
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* @internal Wire a resolver from sessionId → that client's auto-stamped
|
|
275
|
+
* `renderTime`. Called by {@link Room.allowRewindState} so {@link lastSeenBy}
|
|
276
|
+
* works for `mode:"snapshot"` groups; auto-enabled by attaching one.
|
|
277
|
+
*/
|
|
278
|
+
bindRenderTime(resolver) {
|
|
279
|
+
this._renderTimeOf = resolver;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* @internal Wire a resolver for the CURRENT server time (ms, same epoch as
|
|
283
|
+
* `record` times). Called by {@link Room.allowRewindState}; standalone users
|
|
284
|
+
* (tests/harnesses) should bind their own. Anchors the reckon midpoint in
|
|
285
|
+
* {@link at} at the true processing instant — the `lastRecordedAt` fallback
|
|
286
|
+
* is one tick stale by hit-test time, biasing the aim ~half a tick early:
|
|
287
|
+
* imperceptible on slow horizontal motion, but enough to flip knife-edge
|
|
288
|
+
* stomp/hit verdicts on fast vertical movers (a bobbing jumper).
|
|
289
|
+
*/
|
|
290
|
+
bindNow(resolver) {
|
|
291
|
+
this._nowOf = resolver;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* @internal Wire a resolver from sessionId → that client's auto-stamped
|
|
295
|
+
* reckon-display instant (`input(sid).reckonTime`). Called by
|
|
296
|
+
* {@link Room.allowRewindState}. With it, {@link lastSeenBy} aims
|
|
297
|
+
* `mode:"reckon"` groups at the EXACT instant the client displayed them
|
|
298
|
+
* (immune to its RTT-estimation error); without it (or while the stamp is
|
|
299
|
+
* still 0), the midpoint reconstruction in {@link at} is the fallback.
|
|
300
|
+
*/
|
|
301
|
+
bindReckonTime(resolver) {
|
|
302
|
+
this._reckonTimeOf = resolver;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* A {@link RewindView} of the tracked world at `time` (an acting client's render
|
|
306
|
+
* time), with the two things every hit test re-implements baked in:
|
|
307
|
+
* - **clamp** to `[lastRecordedAt − maxRewindMs, lastRecordedAt]` — an
|
|
308
|
+
* anti-spoof / clock-skew bound (a client can't rewind arbitrarily far), and
|
|
309
|
+
* - **live fallback**: `time <= 0` (the client's clock hasn't synced) → the
|
|
310
|
+
* newest recorded sample (≈ current position).
|
|
311
|
+
*
|
|
312
|
+
* Sugar over {@link valueAt}; pass the render time yourself (e.g. from
|
|
313
|
+
* `input(sid).renderTime`, or a value stored on the entity). For the common
|
|
314
|
+
* "rewind to a specific client's view" case use {@link lastSeenBy}.
|
|
315
|
+
*
|
|
316
|
+
* Re-aims and returns this Rewind's internal view by default — zero alloc,
|
|
317
|
+
* nothing to declare. Pass `out` to aim a view of your own instead; needed
|
|
318
|
+
* only to hold several views at once — see the {@link RewindView} doc.
|
|
319
|
+
*/
|
|
320
|
+
at(time, out) {
|
|
321
|
+
return this._aim(time, 0, out);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Shared aiming: clamp the snapshot-timeline `time`, and resolve the reckon
|
|
325
|
+
* timeline either from the DIRECT `reckonStamp` (the instant the client's
|
|
326
|
+
* forward-reckoned entities were displayed at — exact, immune to the
|
|
327
|
+
* client's RTT-estimation error) or, when absent (0), by reconstruction.
|
|
328
|
+
*/
|
|
329
|
+
_aim(time, reckonStamp, out) {
|
|
330
|
+
const newest = this._lastRecordedAt;
|
|
331
|
+
const oldest = newest - this.defaultMaxRewindMs;
|
|
332
|
+
const at = time <= 0 ? newest : time < oldest ? oldest : time > newest ? newest : time;
|
|
333
|
+
let reckonAt;
|
|
334
|
+
if (reckonStamp > 0) {
|
|
335
|
+
reckonAt = reckonStamp < oldest ? oldest : reckonStamp > newest ? newest : reckonStamp;
|
|
336
|
+
} else if (time <= 0) {
|
|
337
|
+
reckonAt = newest;
|
|
338
|
+
} else {
|
|
339
|
+
const anchor = this._nowOf !== void 0 ? this._nowOf() : newest;
|
|
340
|
+
const mid = (time + anchor) / 2;
|
|
341
|
+
reckonAt = mid < oldest ? oldest : mid > newest ? newest : mid;
|
|
342
|
+
}
|
|
343
|
+
return (out ?? this._view)._retarget(this, at, reckonAt);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* A {@link RewindView} of the world as session `sessionId` LAST saw it —
|
|
347
|
+
* resolves the render time stamped on that client's most recently CONSUMED
|
|
348
|
+
* input (hence "last": under a multi-frame `drain()` it is the newest frame's
|
|
349
|
+
* stamp) and hands off to {@link at}. This is the "what you see is what you
|
|
350
|
+
* hit" path:
|
|
351
|
+
*
|
|
352
|
+
* ```ts
|
|
353
|
+
* const seen = this.rewind.lastSeenBy(shooterSessionId);
|
|
354
|
+
* const hit = overlaps(bullet, seen.value(target, "x"), seen.value(target, "y"));
|
|
355
|
+
* ```
|
|
356
|
+
*
|
|
357
|
+
* Requires the room's framework input (`this.defineInput(Input)`) — the
|
|
358
|
+
* per-client stamp auto-enables from the `attachAll` `mode` of the groups you
|
|
359
|
+
* rewind (declaration order vs `allowRewindState` doesn't matter). A client
|
|
360
|
+
* that merely hasn't stamped yet (clock still syncing, unknown sessionId) is
|
|
361
|
+
* NOT an error — it reads 0 and the view falls back to live. Use
|
|
362
|
+
* {@link at} with an explicit time if you stamp render times yourself.
|
|
363
|
+
*
|
|
364
|
+
* Re-aims and returns this Rewind's internal view by default — zero alloc,
|
|
365
|
+
* nothing to declare. Pass `out` to aim a view of your own instead; needed
|
|
366
|
+
* only to hold several views at once — see the {@link RewindView} doc.
|
|
367
|
+
*/
|
|
368
|
+
lastSeenBy(sessionId, out) {
|
|
369
|
+
if (this._renderTimeOf === void 0) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
"Rewind.lastSeenBy(sessionId) needs the framework input API. Declare `this.defineInput(Input)` (stamps auto-enable from your attachAll `mode`), or call `at(time)` and pass the render time yourself."
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return this._aim(
|
|
375
|
+
this._renderTimeOf(sessionId),
|
|
376
|
+
this._reckonTimeOf !== void 0 ? this._reckonTimeOf(sessionId) : 0,
|
|
377
|
+
out
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
382
|
+
0 && (module.exports = {
|
|
383
|
+
Rewind,
|
|
384
|
+
RewindView
|
|
385
|
+
});
|