vimp-engine 0.35.6 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/vimp-sim.js +19 -1
- package/core/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/client/components/controller/Controls.js +5 -0
- package/src/client/components/controller/Lobby.js +4 -4
- package/src/client/components/controller/RoomMenu.js +43 -0
- package/src/client/components/controller/Vote.js +18 -2
- package/src/client/components/model/Chat.js +19 -2
- package/src/client/components/model/ClientReports.js +6 -2
- package/src/client/components/model/Controls.js +27 -0
- package/src/client/components/model/Games.js +5 -1
- package/src/client/components/model/Lobby.js +32 -24
- package/src/client/components/model/LobbyAuth.js +20 -4
- package/src/client/components/model/RoomMenu.js +121 -0
- package/src/client/components/model/Stat.js +2 -1
- package/src/client/components/model/Vote.js +122 -4
- package/src/client/components/view/Auth.js +20 -10
- package/src/client/components/view/ClientReports.js +16 -5
- package/src/client/components/view/Games.js +39 -14
- package/src/client/components/view/Lobby.js +90 -43
- package/src/client/components/view/LobbyAuth.js +13 -7
- package/src/client/components/view/Panel.js +1 -2
- package/src/client/components/view/RoomMenu.js +69 -0
- package/src/client/debug.js +24 -1
- package/src/client/lib/FpsMeter.js +46 -0
- package/src/client/lib/JoinRetry.js +73 -0
- package/src/client/lib/diagnostics.js +28 -5
- package/src/client/lib/hostCaps.js +82 -0
- package/src/client/lib/hostUnloadGuard.js +108 -0
- package/src/client/lib/hostVoteCommand.js +92 -0
- package/src/client/lib/pageReload.js +29 -0
- package/src/client/lib/roomLink.js +257 -0
- package/src/client/lib/signalingErrors.js +74 -0
- package/src/client/main.js +676 -451
- package/src/client/network/HiddenHostHealthLog.js +76 -0
- package/src/client/network/HostConnectionManager.js +62 -8
- package/src/client/network/HostController.js +376 -23
- package/src/client/network/HostHealthPolicy.js +307 -0
- package/src/client/network/HostHealthReporter.js +54 -0
- package/src/client/network/HostPrewarm.js +235 -0
- package/src/client/network/LoopbackTransport.js +16 -1
- package/src/client/network/PlannedHandoff.js +366 -0
- package/src/client/network/Promotion.js +420 -0
- package/src/client/network/RoomPeersReporter.js +81 -0
- package/src/client/network/SessionSupervisor.js +730 -0
- package/src/client/network/SignalingClient.js +183 -30
- package/src/client/network/StandbyReceiver.js +287 -0
- package/src/client/network/StandbySender.js +319 -0
- package/src/client/network/TokenHandoffTimer.js +81 -0
- package/src/client/network/WebRtcManager.js +338 -11
- package/src/client/network/policyClose.js +32 -2
- package/src/client/network/standbyChunks.js +185 -0
- package/src/client/session/GuestSession.js +428 -0
- package/src/client/session/HandoffFlow.js +399 -0
- package/src/client/session/HostRole.js +708 -0
- package/src/client/session/Membership.js +179 -0
- package/src/client/session/PromotionFlow.js +325 -0
- package/src/client/session/RouteBoot.js +432 -0
- package/src/client/session/StandbyRole.js +195 -0
- package/src/client/session/hostRoomPrep.js +152 -0
- package/src/client/session/roomContext.js +32 -0
- package/src/client/style.css +83 -22
- package/src/client/views/gameShell.js +7 -0
- package/src/client/views/includes/informer.pug +4 -0
- package/src/client/views/includes/lobby.pug +0 -3
- package/src/client/views/includes/roomMenu.pug +9 -0
- package/src/client/views/index.pug +1 -0
- package/src/config/chatMessages.js +45 -0
- package/src/config/closeCodes.js +1 -1
- package/src/config/env.js +37 -0
- package/src/config/hostDefaults.js +24 -0
- package/src/config/lobby.js +154 -3
- package/src/config/master.js +104 -21
- package/src/config/wsports.js +8 -0
- package/src/devtools/RecordingSocketManager.js +3 -2
- package/src/devtools/ScenarioRunner.js +123 -8
- package/src/devtools/VirtualClient.js +20 -0
- package/src/devtools/contract/format.js +4 -1
- package/src/devtools/contract/index.js +1 -2
- package/src/devtools/contract/rules/a2-package-scripts.js +1 -3
- package/src/devtools/contract/rules/b8-system-messages.js +2 -2
- package/src/devtools/contract/rules/b9-vote-names.js +25 -1
- package/src/devtools/contract/rules/c11-stat-mode.js +4 -1
- package/src/devtools/contract/rules/c6-stat-columns.js +1 -1
- package/src/devtools/contract/rules/c7-key-sets.js +8 -4
- package/src/devtools/contract/rules/d3-snapshot-interp.js +2 -1
- package/src/devtools/contract/rules/e1-sound-pairs.js +3 -1
- package/src/devtools/contract/rules/e4-map-layers.js +13 -2
- package/src/devtools/contract/rules/e5-map-radar-walls.js +7 -1
- package/src/devtools/contract/rules/index.js +38 -5
- package/src/devtools/invariants.js +10 -4
- package/src/devtools/pluginLoader.js +1 -4
- package/src/devtools/report.js +17 -1
- package/src/host/DebugRecorder.js +6 -1
- package/src/host/GameCoreAdapter.js +45 -0
- package/src/host/HostGame.js +928 -59
- package/src/host/PortMachine.js +268 -20
- package/src/host/host.worker.js +222 -16
- package/src/host/meta/SocketManager.js +20 -0
- package/src/host/meta/core/RoundManager.js +89 -14
- package/src/host/meta/modules/Accolades.js +4 -1
- package/src/host/meta/modules/Panel.js +30 -0
- package/src/host/meta/modules/PlayerDataSync.js +255 -46
- package/src/host/meta/modules/RTTManager.js +34 -0
- package/src/host/meta/modules/TimerManager.js +215 -18
- package/src/host/meta/modules/chat/systemMessages.js +29 -3
- package/src/host/meta/player/HumanParticipant.js +10 -0
- package/src/host/meta/player/ParticipantManager.js +98 -5
- package/src/lib/AbstractTimer.js +63 -1
- package/src/lib/Publisher.js +26 -0
- package/src/lib/capabilities.js +5 -0
- package/src/lib/checkpointCodec.js +125 -0
- package/src/lib/clientIp.js +9 -2
- package/src/lib/createHostRuntime.js +95 -6
- package/src/lib/formOptions.js +3 -1
- package/src/lib/gameConfigView.js +10 -0
- package/src/lib/gameId.js +13 -0
- package/src/lib/jwt.js +3 -1
- package/src/lib/loadGamePackage.js +2 -2
- package/src/lib/resumeKey.js +38 -0
- package/src/lib/roomId.js +39 -0
- package/src/lib/roomSettings.js +59 -0
- package/src/lib/validators.js +7 -0
- package/tests/fixtures/generations/gen-api3/host/ScriptedManager.js +2 -3
- package/tests/fixtures/generations/gen-api3/host/fakeCore.js +31 -5
- package/tests/fixtures/generations/gen-api3/manifest.json +1 -3
- package/tests/fixtures/generations/gen-api4/host/ScriptedManager.js +2 -3
- package/tests/fixtures/generations/gen-api4/host/fakeCore.js +31 -5
- package/tests/fixtures/generations/gen-api4/manifest.json +1 -3
- package/tests/fixtures/miniGame/config/client.js +0 -20
- package/tests/fixtures/miniGame/host/ScriptedManager.js +2 -3
- package/tests/fixtures/miniGame/host/fakeCore.js +31 -5
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import Publisher from '../../lib/Publisher.js';
|
|
2
2
|
|
|
3
3
|
// Клиент сигнального WebSocket мастер-сервера (src/master/SignalingServer.js).
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Координация установки P2P и членство в комнате: приём welcome (id
|
|
5
|
+
// соединения + iceServers), регистрация/возврат комнаты хостом, вход/выход
|
|
6
|
+
// участника, обмен SDP-офферами/ответами и ICE-кандидатами, сигнальный
|
|
7
|
+
// ping/pong.
|
|
6
8
|
// Игровой трафик идёт по WebRTC-каналам (WebRtcManager), не через мастер.
|
|
7
9
|
//
|
|
8
10
|
// Входящие сообщения ретранслируются подписчикам через Publisher по полю type;
|
|
@@ -53,30 +55,192 @@ export default class SignalingClient {
|
|
|
53
55
|
ws.onmessage = event => this._onMessage(event.data);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
// клиент → SDP-оффер
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
// клиент → SDP-оффер текущему хосту комнаты (мастер резолвит roomId).
|
|
59
|
+
// resume — переподключение к матчу (host-migration этап 4): хост ждёт
|
|
60
|
+
// RESUME_REQUEST вместо хендшейка
|
|
61
|
+
sendOffer(roomId, sdp, memberId, { resume = false } = {}) {
|
|
62
|
+
this._send({
|
|
63
|
+
type: 'webrtc_offer',
|
|
64
|
+
roomId,
|
|
65
|
+
sdp,
|
|
66
|
+
memberId,
|
|
67
|
+
...(resume ? { resume: true } : {}),
|
|
68
|
+
});
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
// хост → регистрация комнаты у мастера (ответ — событие 'host_registered'
|
|
62
|
-
// token — Bearer identity-токен
|
|
63
|
-
// мастер отклоняет
|
|
64
|
-
//
|
|
65
|
-
|
|
71
|
+
// хост → регистрация комнаты у мастера (ответ — событие 'host_registered'
|
|
72
|
+
// с roomId/epoch/roomSecret). token — Bearer identity-токен хоста: без
|
|
73
|
+
// него мастер отклоняет регистрацию (комната привязана к проверенному
|
|
74
|
+
// пользователю)
|
|
75
|
+
// info — строка карточки лобби (gameConfig.lobbyInfo игры), null — нет;
|
|
76
|
+
// caps — возможности вкладки (host-migration этап 6, lib/hostCaps.js);
|
|
77
|
+
// settings — настройки комнаты для холодного перезапуска преемником
|
|
78
|
+
// (этап 7.6, lib/roomSettings.js); promotion — { roomId, epoch,
|
|
79
|
+
// promotionToken } из promote мастера (этап 7.4): преемник занимает
|
|
80
|
+
// существующую комнату, а не создаёт новую
|
|
81
|
+
registerHost({
|
|
82
|
+
gameId,
|
|
83
|
+
gameVersion,
|
|
84
|
+
maxPlayers,
|
|
85
|
+
info,
|
|
86
|
+
token,
|
|
87
|
+
memberId,
|
|
88
|
+
caps,
|
|
89
|
+
settings,
|
|
90
|
+
promotion = null,
|
|
91
|
+
}) {
|
|
66
92
|
this._send({
|
|
67
93
|
type: 'register_host',
|
|
68
|
-
name,
|
|
69
|
-
maxPlayers,
|
|
70
|
-
mapName,
|
|
71
94
|
gameId,
|
|
72
95
|
gameVersion,
|
|
96
|
+
maxPlayers,
|
|
97
|
+
info,
|
|
98
|
+
token,
|
|
99
|
+
memberId,
|
|
100
|
+
caps,
|
|
101
|
+
settings,
|
|
102
|
+
...(promotion
|
|
103
|
+
? {
|
|
104
|
+
roomId: promotion.roomId,
|
|
105
|
+
epoch: promotion.epoch,
|
|
106
|
+
promotionToken: promotion.promotionToken,
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// хост → та же комната после реконнекта сигналинга или рестарта мастера.
|
|
113
|
+
// Поля комнаты (игра, вместимость, карточка) нужны мастеру, чтобы создать её
|
|
114
|
+
// заново, если реестр пуст
|
|
115
|
+
reclaimHost({
|
|
116
|
+
roomId,
|
|
117
|
+
epoch,
|
|
118
|
+
roomSecret,
|
|
119
|
+
memberId,
|
|
120
|
+
token,
|
|
121
|
+
gameId,
|
|
122
|
+
gameVersion,
|
|
123
|
+
maxPlayers,
|
|
124
|
+
info,
|
|
125
|
+
caps,
|
|
126
|
+
settings,
|
|
127
|
+
}) {
|
|
128
|
+
this._send({
|
|
129
|
+
type: 'reclaim_host',
|
|
130
|
+
roomId,
|
|
131
|
+
epoch,
|
|
132
|
+
roomSecret,
|
|
133
|
+
memberId,
|
|
73
134
|
token,
|
|
135
|
+
gameId,
|
|
136
|
+
gameVersion,
|
|
137
|
+
maxPlayers,
|
|
138
|
+
info,
|
|
139
|
+
caps,
|
|
140
|
+
settings,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// хост → актуализация карточки комнаты (заодно heartbeat); info: null
|
|
145
|
+
// очищает строку у мастера
|
|
146
|
+
updateHost({ info } = {}) {
|
|
147
|
+
this._send({ type: 'update_host', info });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// гость → вошёл в матч комнаты (ответ — 'room_joined'); повторяется с тем
|
|
151
|
+
// же memberId после реконнекта сигналинга
|
|
152
|
+
// caps — возможности вкладки: по ним мастер выбирает преемника хоста
|
|
153
|
+
joinRoom({ roomId, memberId, token, caps }) {
|
|
154
|
+
this._send({ type: 'join_room', roomId, memberId, token, caps });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// участник → возможности изменились (вкладка спрятана/показана, сменился
|
|
158
|
+
// тип ICE-кандидата) — host-migration этап 6
|
|
159
|
+
memberUpdate(roomId, caps) {
|
|
160
|
+
this._send({ type: 'member_update', roomId, caps });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// бета → у неё есть полная контрольная точка (id и свежесть: ageMs —
|
|
164
|
+
// сколько назад бета её получила)
|
|
165
|
+
standbyStatus({ roomId, epoch, checkpointId, createdAt, ageMs }) {
|
|
166
|
+
this._send({
|
|
167
|
+
type: 'standby_status',
|
|
168
|
+
roomId,
|
|
169
|
+
epoch,
|
|
170
|
+
checkpointId,
|
|
171
|
+
createdAt,
|
|
172
|
+
ageMs,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// хост → участники с открытыми каналами к нему: мастер считает игроков
|
|
177
|
+
// и кандидатов в беты только по ним
|
|
178
|
+
roomPeers({ roomId, epoch, memberIds }) {
|
|
179
|
+
this._send({ type: 'room_peers', roomId, epoch, memberIds });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// хост → ответ на пробу мастера (host-migration этап 7.3): главный поток
|
|
183
|
+
// жив и держит сигналинг
|
|
184
|
+
probeAck(nonce) {
|
|
185
|
+
this._send({ type: 'probe_ack', nonce });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// гость → WebRTC к хосту комнаты оборвался (host-migration этап 7.2):
|
|
189
|
+
// свидетельство мастеру, он решает — проба хоста или смена
|
|
190
|
+
hostUnreachable(roomId, epoch) {
|
|
191
|
+
this._send({ type: 'host_unreachable', roomId, epoch });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// преемник → поднять матч не удалось (плагин, wasm, init): мастер берёт
|
|
195
|
+
// следующего кандидата (host-migration этап 7.4)
|
|
196
|
+
promoteFailed({ roomId, epoch, promotionToken }) {
|
|
197
|
+
this._send({ type: 'promote_failed', roomId, epoch, promotionToken });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// хост → плановая передача роли бете (host-migration этап 8): ответ —
|
|
201
|
+
// handoff_go {epoch} или handoff_unavailable {reason}. stay — останется ли
|
|
202
|
+
// вкладка в комнате гостем (мастеру не нужно, для журнала)
|
|
203
|
+
handoffBegin({ roomId, epoch, reason, stay }) {
|
|
204
|
+
this._send({ type: 'handoff_begin', roomId, epoch, reason, stay });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// хост → здоровье матча (host-migration этап 9c, раз в ~2 с): мастер
|
|
208
|
+
// сравнивает сеть хоста и беты и при стойком лаге шлёт request_handoff
|
|
209
|
+
hostHealth({ roomId, epoch, tickRate, peerRttMedian, peerCount }) {
|
|
210
|
+
this._send({
|
|
211
|
+
type: 'host_health',
|
|
212
|
+
roomId,
|
|
213
|
+
epoch,
|
|
214
|
+
tickRate,
|
|
215
|
+
peerRttMedian,
|
|
216
|
+
peerCount,
|
|
74
217
|
});
|
|
75
218
|
}
|
|
76
219
|
|
|
77
|
-
// хост →
|
|
78
|
-
|
|
79
|
-
|
|
220
|
+
// хост → вкладка уходит (pagehide, «Leave server» без передачи): мастер
|
|
221
|
+
// начинает аварийную миграцию сразу, не дожидаясь обрыва WS
|
|
222
|
+
hostLeaving(roomId, epoch) {
|
|
223
|
+
this._send({ type: 'host_leaving', roomId, epoch });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// хост → в комнате нет людей, он пишет очки перед закрытием: мастер
|
|
227
|
+
// скрывает комнату и закрывает её для входа до host_leaving
|
|
228
|
+
hostClosing(roomId, epoch) {
|
|
229
|
+
this._send({ type: 'host_closing', roomId, epoch });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// гость → покинул комнату
|
|
233
|
+
leaveRoom(roomId) {
|
|
234
|
+
this._send({ type: 'leave_room', roomId });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// голосование «Change host» (host-migration этап 10): голоса считает мастер
|
|
238
|
+
hostVoteStart(roomId) {
|
|
239
|
+
this._send({ type: 'host_vote_start', roomId });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
hostVoteAnswer({ roomId, voteId, value }) {
|
|
243
|
+
this._send({ type: 'host_vote_answer', roomId, voteId, value });
|
|
80
244
|
}
|
|
81
245
|
|
|
82
246
|
// хост → SDP-ответ конкретному клиенту
|
|
@@ -89,25 +253,14 @@ export default class SignalingClient {
|
|
|
89
253
|
this._send({ type: 'pong_host', clientId, pingId });
|
|
90
254
|
}
|
|
91
255
|
|
|
92
|
-
// обмен ICE-кандидатами (targetId —
|
|
256
|
+
// обмен ICE-кандидатами (targetId — roomId со стороны клиента)
|
|
93
257
|
sendIceCandidate(targetId, candidate) {
|
|
94
258
|
this._send({ type: 'ice_candidate', targetId, candidate });
|
|
95
259
|
}
|
|
96
260
|
|
|
97
261
|
// сигнальный ping хосту (замер приблизительный: клиент→мастер→хост)
|
|
98
|
-
pingHost(
|
|
99
|
-
this._send({ type: 'ping_host',
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// /like·/unlike напрямую мастеру (минуя хоста-читера), server-rating
|
|
103
|
-
// этап 2: reason — причина (обязательна), token — Bearer identity-токен
|
|
104
|
-
// голосующего
|
|
105
|
-
likeHost(hostId, reason, token) {
|
|
106
|
-
this._send({ type: 'like_host', hostId, reason, token });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
unlikeHost(hostId, reason, token) {
|
|
110
|
-
this._send({ type: 'unlike_host', hostId, reason, token });
|
|
262
|
+
pingHost(roomId, pingId) {
|
|
263
|
+
this._send({ type: 'ping_host', roomId, pingId });
|
|
111
264
|
}
|
|
112
265
|
|
|
113
266
|
close() {
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import hostDefaults from '../../config/hostDefaults.js';
|
|
2
|
+
import wsports from '../../config/wsports.js';
|
|
3
|
+
import Publisher from '../../lib/Publisher.js';
|
|
4
|
+
import { decodeStandbyChunk, parseStandbyStream } from './standbyChunks.js';
|
|
5
|
+
|
|
6
|
+
const PS_SHOT_DATA = wsports.server.SHOT_DATA;
|
|
7
|
+
|
|
8
|
+
// заголовок бинарного кадра: u8 port, u8 version, u32 seq (big-endian)
|
|
9
|
+
const FRAME_SEQ_OFFSET = 2;
|
|
10
|
+
const FRAME_MIN_BYTES = FRAME_SEQ_OFFSET + 4;
|
|
11
|
+
|
|
12
|
+
// Преемник (бета, host-migration этап 6): собирает куски контрольных точек
|
|
13
|
+
// из канала standby и держит последнюю полную. Незавершённая точка
|
|
14
|
+
// выбрасывается, как только пошла более новая (в канале ordered куски одной
|
|
15
|
+
// точки идут подряд). Заодно помнит seq последнего кадра хоста, который
|
|
16
|
+
// видел этот клиент, — seqFloor восстановленного матча (этап 5).
|
|
17
|
+
//
|
|
18
|
+
// События publisher: 'checkpoint' (последняя полная точка).
|
|
19
|
+
export default class StandbyReceiver {
|
|
20
|
+
/**
|
|
21
|
+
* @param {Object} [options]
|
|
22
|
+
* @param {number} [options.maxBytes] - предел логического потока точки.
|
|
23
|
+
* @param {Function} [options.now] - часы (мс).
|
|
24
|
+
* @param {Object} [options.timers] - { setTimeout, clearTimeout } (тесты).
|
|
25
|
+
*/
|
|
26
|
+
constructor({
|
|
27
|
+
maxBytes = hostDefaults.maxCheckpointBytes,
|
|
28
|
+
now = () => Date.now(),
|
|
29
|
+
timers = globalThis,
|
|
30
|
+
} = {}) {
|
|
31
|
+
this._maxBytes = maxBytes;
|
|
32
|
+
this._now = now;
|
|
33
|
+
this._timers = timers;
|
|
34
|
+
|
|
35
|
+
this._channel = null;
|
|
36
|
+
this._partial = null;
|
|
37
|
+
this._latest = null;
|
|
38
|
+
this._lastSeenSeq = null;
|
|
39
|
+
this._waiters = new Set();
|
|
40
|
+
// передача сорвалась раньше, чем дошла её финальная точка: та ещё может
|
|
41
|
+
// доехать (ждала разгрузки канала у хоста) и должна прийти периодической
|
|
42
|
+
this._expectStaleFinal = false;
|
|
43
|
+
|
|
44
|
+
this.publisher = new Publisher();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// seq последнего кадра хоста, который видел этот клиент (null — ни одного)
|
|
48
|
+
get lastSeenSeq() {
|
|
49
|
+
return this._lastSeenSeq;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Последняя полная точка.
|
|
54
|
+
* @returns {Object|null} { bytes, checkpointId, wireId, seq, createdAt,
|
|
55
|
+
* final, mode, game, receivedAt }; game — { id, version } из
|
|
56
|
+
* дескриптора или null (хост его не прислал).
|
|
57
|
+
*/
|
|
58
|
+
latest() {
|
|
59
|
+
return this._latest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Подключает канал standby (новый канал заменяет прежний).
|
|
64
|
+
* @param {RTCDataChannel} channel
|
|
65
|
+
*/
|
|
66
|
+
attach(channel) {
|
|
67
|
+
this._detachChannel();
|
|
68
|
+
this._channel = channel;
|
|
69
|
+
channel.binaryType = 'arraybuffer';
|
|
70
|
+
channel.onmessage = event => this.receive(event.data);
|
|
71
|
+
channel.onclose = () => {
|
|
72
|
+
if (this._channel === channel) {
|
|
73
|
+
this._detachChannel();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Кадр хоста из игрового транспорта: запоминает его seq.
|
|
80
|
+
* @param {ArrayBuffer} data
|
|
81
|
+
*/
|
|
82
|
+
noteFrame(data) {
|
|
83
|
+
if (!(data instanceof ArrayBuffer) || data.byteLength < FRAME_MIN_BYTES) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const view = new DataView(data);
|
|
88
|
+
|
|
89
|
+
if (view.getUint8(0) !== PS_SHOT_DATA) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this._lastSeenSeq = view.getUint32(FRAME_SEQ_OFFSET, false);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Один кусок из канала.
|
|
98
|
+
* @param {ArrayBuffer|Uint8Array} raw
|
|
99
|
+
*/
|
|
100
|
+
receive(raw) {
|
|
101
|
+
const chunk = decodeStandbyChunk(raw);
|
|
102
|
+
|
|
103
|
+
if (!chunk || chunk.totalBytes > this._maxBytes) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// устаревшее: точка не новее уже собранной
|
|
108
|
+
if (this._latest && chunk.wireId <= this._latest.wireId) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// периодическая, снятая не позже финальной, её не вытесняет: бета
|
|
113
|
+
// поднимает матч из финальной (тот же тик, без отката)
|
|
114
|
+
if (
|
|
115
|
+
this._latest?.final &&
|
|
116
|
+
!chunk.final &&
|
|
117
|
+
Number.isInteger(chunk.seq) &&
|
|
118
|
+
chunk.seq <= this._latest.seq
|
|
119
|
+
) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let partial = this._partial;
|
|
124
|
+
|
|
125
|
+
if (partial && chunk.wireId < partial.wireId) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!partial || chunk.wireId > partial.wireId) {
|
|
130
|
+
// пошла более новая — незавершённая выбрасывается
|
|
131
|
+
if (chunk.index !== 0) {
|
|
132
|
+
this._partial = null;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
partial = {
|
|
137
|
+
wireId: chunk.wireId,
|
|
138
|
+
count: chunk.count,
|
|
139
|
+
totalBytes: chunk.totalBytes,
|
|
140
|
+
seq: chunk.seq,
|
|
141
|
+
final: chunk.final,
|
|
142
|
+
buffer: new Uint8Array(chunk.totalBytes),
|
|
143
|
+
offset: 0,
|
|
144
|
+
next: 0,
|
|
145
|
+
};
|
|
146
|
+
this._partial = partial;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
chunk.index !== partial.next ||
|
|
151
|
+
chunk.count !== partial.count ||
|
|
152
|
+
chunk.totalBytes !== partial.totalBytes ||
|
|
153
|
+
partial.offset + chunk.data.byteLength > partial.totalBytes
|
|
154
|
+
) {
|
|
155
|
+
this._partial = null; // дыра или несогласованный кусок — точка битая
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
partial.buffer.set(chunk.data, partial.offset);
|
|
160
|
+
partial.offset += chunk.data.byteLength;
|
|
161
|
+
partial.next += 1;
|
|
162
|
+
|
|
163
|
+
if (partial.next < partial.count) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this._partial = null;
|
|
168
|
+
|
|
169
|
+
if (partial.offset !== partial.totalBytes) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const parsed = parseStandbyStream(partial.buffer);
|
|
174
|
+
|
|
175
|
+
if (!parsed) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// канал ordered, а хост после финальной не шлёт периодических, пока она
|
|
180
|
+
// не уйдёт: опоздавшая финальная сорвавшейся передачи — первая точка
|
|
181
|
+
// после discardFinal, если она вообще будет
|
|
182
|
+
const final = partial.final && !this._expectStaleFinal;
|
|
183
|
+
|
|
184
|
+
this._expectStaleFinal = false;
|
|
185
|
+
this._latest = {
|
|
186
|
+
...parsed,
|
|
187
|
+
wireId: partial.wireId,
|
|
188
|
+
seq: partial.seq,
|
|
189
|
+
final,
|
|
190
|
+
receivedAt: this._now(),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
this.publisher.emit('checkpoint', this._latest);
|
|
194
|
+
|
|
195
|
+
if (final) {
|
|
196
|
+
for (const waiter of [...this._waiters]) {
|
|
197
|
+
if (partial.wireId >= waiter.minWireId) {
|
|
198
|
+
waiter.resolve(this._latest);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Ждёт финальную точку (плановая передача, этап 8).
|
|
206
|
+
* @param {number} [minWireId] - не старше этого номера точки; уже
|
|
207
|
+
* полученная финальная подходит сразу.
|
|
208
|
+
* @param {number} [timeoutMs]
|
|
209
|
+
* @returns {Promise<Object>} Точка, как latest().
|
|
210
|
+
*/
|
|
211
|
+
waitForFinal(minWireId = 0, timeoutMs = 5000) {
|
|
212
|
+
const latest = this._latest;
|
|
213
|
+
|
|
214
|
+
if (latest?.final && latest.wireId >= minWireId) {
|
|
215
|
+
return Promise.resolve(latest);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
const waiter = {
|
|
220
|
+
minWireId,
|
|
221
|
+
resolve: checkpoint => {
|
|
222
|
+
this._timers.clearTimeout(waiter.timer);
|
|
223
|
+
this._waiters.delete(waiter);
|
|
224
|
+
resolve(checkpoint);
|
|
225
|
+
},
|
|
226
|
+
reject: error => {
|
|
227
|
+
this._timers.clearTimeout(waiter.timer);
|
|
228
|
+
this._waiters.delete(waiter);
|
|
229
|
+
reject(error);
|
|
230
|
+
},
|
|
231
|
+
timer: null,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
waiter.timer = this._timers.setTimeout(
|
|
235
|
+
() => waiter.reject(new Error('final checkpoint timed out')),
|
|
236
|
+
timeoutMs,
|
|
237
|
+
);
|
|
238
|
+
this._waiters.add(waiter);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Плановая передача сорвалась (promote_cancelled): её финальная точка
|
|
244
|
+
* остаётся последней, но следующую передачу не завершит — хост разморожен
|
|
245
|
+
* и пришлёт новую финальную. Ещё не дошедшая финальная примется
|
|
246
|
+
* периодической.
|
|
247
|
+
*/
|
|
248
|
+
discardFinal() {
|
|
249
|
+
if (this._latest?.final) {
|
|
250
|
+
this._latest = { ...this._latest, final: false };
|
|
251
|
+
} else {
|
|
252
|
+
this._expectStaleFinal = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// standby_released, уход из комнаты, смена эпохи: точки выбросить
|
|
257
|
+
destroy() {
|
|
258
|
+
this._detachChannel();
|
|
259
|
+
this._partial = null;
|
|
260
|
+
this._latest = null;
|
|
261
|
+
this._expectStaleFinal = false;
|
|
262
|
+
|
|
263
|
+
for (const waiter of [...this._waiters]) {
|
|
264
|
+
waiter.reject(new Error('standby released'));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_detachChannel() {
|
|
269
|
+
const channel = this._channel;
|
|
270
|
+
|
|
271
|
+
this._channel = null;
|
|
272
|
+
this._partial = null;
|
|
273
|
+
|
|
274
|
+
if (!channel) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
channel.onmessage = null;
|
|
279
|
+
channel.onclose = null;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
channel.close();
|
|
283
|
+
} catch {
|
|
284
|
+
// уже закрыт
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|