vimp-engine 0.39.0 → 0.40.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/README.md +2 -2
- package/bin/vimp-sim.js +2 -2
- package/core/Cargo.toml +2 -2
- package/package.json +3 -4
- package/src/client/boot.js +11 -87
- package/src/client/components/model/RoomMenu.js +1 -1
- package/src/client/components/view/Auth.js +3 -3
- package/src/client/lib/formBuilder.js +7 -6
- package/src/client/lib/hostVoteCommand.js +2 -2
- package/src/client/lib/pickActiveGame.js +3 -12
- package/src/client/lib/retryOnNetworkError.js +0 -9
- package/src/client/main.js +43 -90
- package/src/client/network/SessionSupervisor.js +14 -14
- package/src/client/network/WebRtcManager.js +1 -2
- package/src/client/network/policyClose.js +10 -77
- package/src/client/style.css +4 -6
- package/src/client/views/gameShell.js +2 -4
- package/src/client/views/includes/auth.pug +1 -4
- package/src/client/views/includes/lobby.pug +3 -6
- package/src/config/closeCodes.js +7 -9
- package/src/config/gameCodes.js +1 -1
- package/src/config/hostDefaults.js +2 -2
- package/src/config/lobby.js +3 -4
- package/src/config/wsports.js +1 -1
- package/src/devtools/ScenarioRunner.js +1 -1
- package/src/devtools/builtinScenario.js +2 -2
- package/src/devtools/contract/rules/a2-package-scripts.js +1 -1
- package/src/devtools/contract/rules/a4-vite-config.js +1 -1
- package/src/devtools/contract/rules/c4-component-dependencies.js +1 -1
- package/src/devtools/invariants.js +1 -1
- package/src/devtools/pluginLoader.js +2 -2
- package/src/host/HostGame.js +4 -4
- package/src/host/PortMachine.js +4 -21
- package/src/host/commandFailure.js +2 -2
- package/src/host/identity.js +3 -3
- package/src/host/meta/core/CommandProcessor.js +2 -2
- package/src/host/meta/modules/Accolades.js +1 -1
- package/src/lib/createHostRuntime.js +1 -1
- package/src/lib/loadGamePackage.js +7 -9
- package/src/lib/offlinePlayerData.js +1 -1
- package/src/lib/validators.js +2 -2
- package/src/lib/voicePacket.js +1 -1
- package/src/standalone/index.js +3 -2
- package/src/client/lib/footerLink.js +0 -30
- package/src/client/network/WebSocketTransport.js +0 -78
|
@@ -52,8 +52,8 @@ export default class SessionSupervisor {
|
|
|
52
52
|
* @param {Object} opts
|
|
53
53
|
* @param {Function} opts.onMessage - входящее сообщение текущего
|
|
54
54
|
* транспорта (строка или ArrayBuffer).
|
|
55
|
-
* @param {Function} opts.onTerminal -
|
|
56
|
-
*
|
|
55
|
+
* @param {Function} opts.onTerminal - сессия закрыта окончательно
|
|
56
|
+
* (handleDisconnect).
|
|
57
57
|
* @param {Function} [opts.onVoice] - (data) голосовой пакет текущего
|
|
58
58
|
* транспорта в состоянии inGame (бинарный, без разбора).
|
|
59
59
|
* @param {Function} [opts.onStateChange] - (state, prev).
|
|
@@ -63,10 +63,10 @@ export default class SessionSupervisor {
|
|
|
63
63
|
* матче (до переподключения): свидетельство мастеру (host_unreachable).
|
|
64
64
|
* @param {Function} [opts.onColdRestart] - новый хост поднял матч заново
|
|
65
65
|
* (host_changed cold): возобновлять нечего, только чистый вход.
|
|
66
|
-
* @param {Function} [opts.isTerminalClose] - (
|
|
67
|
-
*
|
|
66
|
+
* @param {Function} [opts.isTerminalClose] - () закрытие после
|
|
67
|
+
* терминальной причины (кик, полная комната): возвращаться некуда.
|
|
68
68
|
* @param {Object|null} [opts.reconnect] - null — переподключения нет
|
|
69
|
-
* (solo,
|
|
69
|
+
* (solo, вкладка-хост на loopback). Иначе:
|
|
70
70
|
* { createTransport() → новый транспорт с resume (уже connect'нутый),
|
|
71
71
|
* getToken() → identity-токен для RESUME_REQUEST }.
|
|
72
72
|
* @param {Object} [opts.timing] - { reconnectWindowMs, reconnectBaseDelayMs,
|
|
@@ -493,7 +493,7 @@ export default class SessionSupervisor {
|
|
|
493
493
|
const handlers = {
|
|
494
494
|
message: data => this._handleMessage(transport, data),
|
|
495
495
|
voice: data => this._handleVoice(transport, data),
|
|
496
|
-
close:
|
|
496
|
+
close: () => this._handleClose(transport),
|
|
497
497
|
open: () => this._handleOpen(transport),
|
|
498
498
|
};
|
|
499
499
|
|
|
@@ -573,7 +573,7 @@ export default class SessionSupervisor {
|
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
-
_handleClose(transport
|
|
576
|
+
_handleClose(transport) {
|
|
577
577
|
if (transport !== this._transport) {
|
|
578
578
|
return;
|
|
579
579
|
}
|
|
@@ -590,7 +590,7 @@ export default class SessionSupervisor {
|
|
|
590
590
|
// транспорт, выданный resumeWith без фабрики попыток: повторять нечем
|
|
591
591
|
if (this._reconnect === null) {
|
|
592
592
|
if (!this._failRoleResume('closed')) {
|
|
593
|
-
this._terminate(
|
|
593
|
+
this._terminate();
|
|
594
594
|
}
|
|
595
595
|
|
|
596
596
|
return;
|
|
@@ -600,23 +600,23 @@ export default class SessionSupervisor {
|
|
|
600
600
|
return;
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
-
if (this._canResume(
|
|
603
|
+
if (this._canResume()) {
|
|
604
604
|
this._startReconnect();
|
|
605
605
|
return;
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
this._terminate(
|
|
608
|
+
this._terminate();
|
|
609
609
|
}
|
|
610
610
|
|
|
611
611
|
// нетерминальным бывает только обрыв посреди матча, у которого есть место
|
|
612
612
|
// для возврата и кому возвращаться
|
|
613
|
-
_canResume(
|
|
613
|
+
_canResume() {
|
|
614
614
|
return (
|
|
615
615
|
this._reconnect !== null &&
|
|
616
616
|
!this._userLeft &&
|
|
617
617
|
this._state === S.inGame &&
|
|
618
618
|
this._session !== null &&
|
|
619
|
-
!this._isTerminalClose(
|
|
619
|
+
!this._isTerminalClose()
|
|
620
620
|
);
|
|
621
621
|
}
|
|
622
622
|
|
|
@@ -728,7 +728,7 @@ export default class SessionSupervisor {
|
|
|
728
728
|
|
|
729
729
|
// ***** завершение ***** //
|
|
730
730
|
|
|
731
|
-
_terminate(
|
|
731
|
+
_terminate() {
|
|
732
732
|
if (this._state === S.closed) {
|
|
733
733
|
return;
|
|
734
734
|
}
|
|
@@ -738,7 +738,7 @@ export default class SessionSupervisor {
|
|
|
738
738
|
this._clearMigrationTimer();
|
|
739
739
|
this._onRoleResumeFailed = null;
|
|
740
740
|
this._setState(S.closed);
|
|
741
|
-
this._onTerminal(
|
|
741
|
+
this._onTerminal();
|
|
742
742
|
}
|
|
743
743
|
|
|
744
744
|
_setState(state) {
|
|
@@ -493,8 +493,7 @@ export default class WebRtcManager {
|
|
|
493
493
|
channel.onmessage = event => this.publisher.emit('voice', event.data);
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
// кода закрытия у data channel нет: 'close' эмитится без
|
|
497
|
-
// и раньше (код приходит только от WebSocket-транспорта dedicated)
|
|
496
|
+
// кода закрытия у data channel нет: 'close' эмитится без аргумента
|
|
498
497
|
_emitClose() {
|
|
499
498
|
if (this._closed) {
|
|
500
499
|
return;
|
|
@@ -1,84 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
// знание разведено на два экспорта (review-4.md, R4-2):
|
|
10
|
-
//
|
|
11
|
-
// - перезагружаться ли. Ни на одном из этих кодов перезагрузка не помогает:
|
|
12
|
-
// лимит частоты подключений съел бы ей же очередное соединение, таймаут
|
|
13
|
-
// хендшейка запустился бы заново, origin страницы не меняется вовсе, а
|
|
14
|
-
// слот в полной комнате перезагрузка не освобождает. Брошенная вкладка
|
|
15
|
-
// перезагружалась бы вечно, а ждущий слот игрок за 30 витков сам упёрся бы
|
|
16
|
-
// в лимит подключений и получил бы чужую причину отказа;
|
|
17
|
-
// - какой текст показать. Тексты запасные: `handleDisconnect` берёт их
|
|
18
|
-
// только тогда, когда сервер причину не прислал сам (`terminalInformShown`
|
|
19
|
-
// — `client/main.js`). Полная комната, например, приезжает с причиной от
|
|
20
|
-
// порт-машины (TECH_INFORM перед `close`), и она всегда побеждает —
|
|
21
|
-
// запись 4006 ниже отработает, только если тот кадр не доехал. Тексты
|
|
22
|
-
// здесь, а не в `techInformList`: список переопределяет игра, и движковый
|
|
23
|
-
// индекс дал бы у неё «Unknown error».
|
|
24
|
-
|
|
25
|
-
import closeCodes from '../../config/closeCodes.js';
|
|
26
|
-
|
|
27
|
-
const {
|
|
28
|
-
invalidOrigin,
|
|
29
|
-
roomFull,
|
|
30
|
-
handshakeTimeout,
|
|
31
|
-
tooManyConnections,
|
|
32
|
-
kickIdle,
|
|
33
|
-
kickForMaxLatency,
|
|
34
|
-
kickForMissedPings,
|
|
35
|
-
} = closeCodes;
|
|
36
|
-
|
|
37
|
-
const NO_RELOAD_CLOSE_CODES = new Set([
|
|
38
|
-
invalidOrigin,
|
|
39
|
-
roomFull,
|
|
40
|
-
handshakeTimeout,
|
|
41
|
-
tooManyConnections,
|
|
42
|
-
]);
|
|
43
|
-
|
|
44
|
-
export const POLICY_CLOSE_INFORMS = {
|
|
45
|
-
[invalidOrigin]:
|
|
46
|
-
'This page is not allowed by the game server. Check the address you opened.',
|
|
47
|
-
[roomFull]: 'The room is full. Try again later.',
|
|
48
|
-
[handshakeTimeout]:
|
|
49
|
-
'Idle connection closed before the match started. Reload the page when you are ready to play.',
|
|
50
|
-
[tooManyConnections]:
|
|
51
|
-
'Too many connections from your address. Wait a minute and reload the page.',
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @param {number} [closeCode] - Код закрытия транспорта. Его отдаёт только
|
|
56
|
-
* WebSocket-транспорт (dedicated); в P2P и solo кода нет вовсе.
|
|
57
|
-
* @returns {boolean} Уместна ли перезагрузка страницы после разрыва.
|
|
58
|
-
*/
|
|
59
|
-
export function shouldReloadAfterClose(closeCode) {
|
|
60
|
-
return !NO_RELOAD_CLOSE_CODES.has(closeCode);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Кики хоста (host/HostGame.js). В P2P кода закрытия нет вовсе — причина
|
|
64
|
-
// приезжает только TECH_INFORM-ключом перед закрытием (индексы
|
|
65
|
-
// TECH_CODES из host/meta/SocketManager.js = индексы techInformList);
|
|
66
|
-
// в dedicated есть и код. Лобби-режим уводит кикнутого на главную без
|
|
67
|
-
// перезагрузки (client/lib/roomLink.js decideExitRoute): быстрая игра по
|
|
68
|
-
// hash вернула бы его в ту же комнату
|
|
69
|
-
const KICK_CLOSE_CODES = new Set([
|
|
70
|
-
kickIdle,
|
|
71
|
-
kickForMaxLatency,
|
|
72
|
-
kickForMissedPings,
|
|
73
|
-
]);
|
|
74
|
-
|
|
1
|
+
// Кики хоста (host/HostGame.js). Кода закрытия у транспортов клиента нет —
|
|
2
|
+
// причина приезжает только TECH_INFORM-ключом перед закрытием (индексы
|
|
3
|
+
// TECH_CODES из host/meta/SocketManager.js = индексы techInformList).
|
|
4
|
+
// Лобби-режим уводит кикнутого на главную без перезагрузки
|
|
5
|
+
// (client/lib/roomLink.js decideExitRoute): быстрая игра по hash вернула бы
|
|
6
|
+
// его в ту же комнату.
|
|
7
|
+
|
|
8
|
+
// kickIdle, kickForMaxLatency, kickForMissedPings
|
|
75
9
|
export const KICK_TECH_KEYS = new Set([3, 4, 5]);
|
|
76
10
|
|
|
77
11
|
/**
|
|
78
|
-
* @param {number} [closeCode] - Код закрытия транспорта (только WebSocket).
|
|
79
12
|
* @param {number|null} [techKey] - Последний терминальный TECH_INFORM-ключ.
|
|
80
13
|
* @returns {boolean} Закрыто ли соединение киком хоста.
|
|
81
14
|
*/
|
|
82
|
-
export function isKickClose(
|
|
83
|
-
return
|
|
15
|
+
export function isKickClose(techKey) {
|
|
16
|
+
return KICK_TECH_KEYS.has(techKey);
|
|
84
17
|
}
|
package/src/client/style.css
CHANGED
|
@@ -726,8 +726,8 @@ body.hide-cursor {
|
|
|
726
726
|
display: block;
|
|
727
727
|
}
|
|
728
728
|
|
|
729
|
-
/* футеры лобби
|
|
730
|
-
|
|
729
|
+
/* футеры лобби (Terms/Privacy | версия движка | копирайт) и формы входа
|
|
730
|
+
(версия игры | копирайт): одна полоса, оба заполняет JS, поэтому вид
|
|
731
731
|
держится в одном месте (#lobby-footer лежит в секции lobby ниже только
|
|
732
732
|
разметкой) */
|
|
733
733
|
#auth-link,
|
|
@@ -761,9 +761,8 @@ body.hide-cursor {
|
|
|
761
761
|
padding-left: 10px;
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
-
/*
|
|
765
|
-
|
|
766
|
-
и при space-between версия съезжала бы с центра */
|
|
764
|
+
/* равные ячейки: версия игры у левого края, копирайт у правого; пустая
|
|
765
|
+
версия (у пакета игры может не быть метаданных) не сдвигает копирайт */
|
|
767
766
|
#auth-link p {
|
|
768
767
|
margin: 0;
|
|
769
768
|
flex: 1;
|
|
@@ -781,7 +780,6 @@ body.hide-cursor {
|
|
|
781
780
|
text-align: left;
|
|
782
781
|
}
|
|
783
782
|
|
|
784
|
-
#auth-version,
|
|
785
783
|
#lobby-version {
|
|
786
784
|
text-align: center;
|
|
787
785
|
}
|
|
@@ -55,9 +55,7 @@ const SHELL = [
|
|
|
55
55
|
tag: 'div',
|
|
56
56
|
id: 'auth-link',
|
|
57
57
|
children: [
|
|
58
|
-
// версия пакета ИГРЫ (из манифеста)
|
|
59
|
-
// (только в dev-сборке) — заполняет main.js по AUTH_DATA
|
|
60
|
-
{ tag: 'p', children: [{ tag: 'a', id: 'auth-docs-link' }] },
|
|
58
|
+
// версия пакета ИГРЫ (из манифеста) — заполняет main.js по AUTH_DATA
|
|
61
59
|
{ tag: 'p', id: 'auth-version' },
|
|
62
60
|
{ tag: 'p', text: `© ${new Date().getFullYear()} VIMP` },
|
|
63
61
|
],
|
|
@@ -67,7 +65,7 @@ const SHELL = [
|
|
|
67
65
|
],
|
|
68
66
|
},
|
|
69
67
|
{ tag: 'div', id: 'game-informer' },
|
|
70
|
-
// оверлей переподключения (host-migration этап 4): в solo
|
|
68
|
+
// оверлей переподключения (host-migration этап 4): в solo не
|
|
71
69
|
// показывается, но разметка общая с informer.pug
|
|
72
70
|
{
|
|
73
71
|
tag: 'div',
|
|
@@ -16,9 +16,6 @@ div#auth
|
|
|
16
16
|
|
|
17
17
|
div#auth-link
|
|
18
18
|
//- версия пакета ИГРЫ (мастер кладёт её в манифест, читая package.json
|
|
19
|
-
//- пакета)
|
|
20
|
-
//- проставляет main.js по AUTH_DATA; без данных ячейки остаются пустыми
|
|
21
|
-
p
|
|
22
|
-
a#auth-docs-link
|
|
19
|
+
//- пакета) — проставляет main.js по AUTH_DATA; без данных ячейка пустая
|
|
23
20
|
p#auth-version
|
|
24
21
|
p © #{new Date().getFullYear()} VIMP
|
|
@@ -32,8 +32,7 @@ div#lobby(style='display:none')
|
|
|
32
32
|
label.form-label(for='lobby-game')= 'Select game'
|
|
33
33
|
select#lobby-game.field-select
|
|
34
34
|
//- поля настроек комнаты генерируются по явной схеме roomForm
|
|
35
|
-
//- манифеста активной игры (docs
|
|
36
|
-
//- schema")
|
|
35
|
+
//- манифеста активной игры (VIMP docs: plugin-api.md § "Form schema")
|
|
37
36
|
div#lobby-fields
|
|
38
37
|
//- ошибки формы (formBuilder.renderFormErrors) и отказ загрузки
|
|
39
38
|
//- плагина выбранной игры — строкой в форме, а не в #tech-informer:
|
|
@@ -67,15 +66,13 @@ div#lobby(style='display:none')
|
|
|
67
66
|
span#leaderboard-total
|
|
68
67
|
ol#lobby-leaderboard-list
|
|
69
68
|
p#lobby-my-placement
|
|
70
|
-
//- футер:
|
|
71
|
-
//-
|
|
69
|
+
//- футер: Terms/Privacy, версия пакета движка (проставляет LobbyView,
|
|
70
|
+
//- engineVersion.js), копирайт
|
|
72
71
|
div#lobby-footer
|
|
73
72
|
p
|
|
74
73
|
a#lobby-terms-link(target='_blank', rel='noopener') Terms of Service
|
|
75
74
|
| ·
|
|
76
75
|
|
|
|
77
76
|
a#lobby-privacy-link(target='_blank', rel='noopener') Privacy Policy
|
|
78
|
-
p
|
|
79
|
-
a#lobby-link(href='https://docs.vimp.lgick.dev', target='_blank', rel='noopener') docs
|
|
80
77
|
p#lobby-version
|
|
81
78
|
p © #{new Date().getFullYear()} VIMP
|
package/src/config/closeCodes.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
// коды закрытия транспорта — общий контракт
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// каждому коду из этой карты
|
|
1
|
+
// коды закрытия транспорта — общий контракт сигналинга мастера и
|
|
2
|
+
// Worker-хоста, тот же приём, что в gameCodes.js: значение не должно
|
|
3
|
+
// расходиться между ними молча. Игровой транспорт клиента кода не передаёт
|
|
4
|
+
// (у data channel его нет): причину кика или отказа клиент узнаёт по
|
|
5
|
+
// TECH_INFORM перед закрытием
|
|
7
6
|
export default {
|
|
8
7
|
staleHost: 4000, // master/SignalingServer.js — сигнальный WS хоста протух
|
|
9
|
-
invalidOrigin: 4001, // master/SignalingServer.js
|
|
8
|
+
invalidOrigin: 4001, // master/SignalingServer.js
|
|
10
9
|
// 4002 retired (server rating removed) — номер не переиспользуется
|
|
11
10
|
kickForMaxLatency: 4003, // host/HostGame.js
|
|
12
11
|
kickForMissedPings: 4004, // host/HostGame.js
|
|
13
12
|
kickIdle: 4005, // host/HostGame.js
|
|
14
13
|
roomFull: 4006, // host/PortMachine.js
|
|
15
14
|
// 4007 свободен
|
|
16
|
-
handshakeTimeout: 4008, //
|
|
17
|
-
tooManyConnections: 4009, // dedicated/main.js
|
|
15
|
+
handshakeTimeout: 4008, // host/PortMachine.js — нет RESUME_REQUEST вовремя
|
|
18
16
|
};
|
package/src/config/gameCodes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// коды игровых информ-сообщений (порт GAME_INFORM_DATA, см.
|
|
2
|
-
// docs.
|
|
2
|
+
// VIMP docs: snapshot.md) — общий контракт хоста (SocketManager) и
|
|
3
3
|
// клиента (main.js): значение не должно расходиться между ними молча
|
|
4
4
|
export default {
|
|
5
5
|
winnerTeam: [0],
|
|
@@ -66,8 +66,8 @@ export default {
|
|
|
66
66
|
|
|
67
67
|
// возобновление сессии (host-migration этап 4). Сколько хост держит место
|
|
68
68
|
// участника, чей транспорт оборвался без LEAVE: слот занят, актор в мире,
|
|
69
|
-
// клавиши отпущены. Включает его только Worker лобби —
|
|
70
|
-
//
|
|
69
|
+
// клавиши отпущены. Включает его только Worker лобби — standalone
|
|
70
|
+
// поднимает PortMachine без этой опции (0 — снять сразу)
|
|
71
71
|
resumeGraceMs: 20000,
|
|
72
72
|
|
|
73
73
|
// сколько возобновляющее соединение может молчать до RESUME_REQUEST
|
package/src/config/lobby.js
CHANGED
|
@@ -50,9 +50,8 @@ export default {
|
|
|
50
50
|
// клиент берёт не из assetsBase, а по отдельному URL, и версионность
|
|
51
51
|
// каталога обязана доехать и сюда, иначе комната на застейдженной версии
|
|
52
52
|
// играла бы на картах одобренной. `mapsBase` проставляет мастер при
|
|
53
|
-
// ребейзе версионного каталога; его отсутствие (dev, standalone,
|
|
54
|
-
//
|
|
55
|
-
// путь по id
|
|
53
|
+
// ребейзе версионного каталога; его отсутствие (dev, standalone, старый
|
|
54
|
+
// мастер) — законный случай, тогда работает путь по id
|
|
56
55
|
maps: {
|
|
57
56
|
manifestUrl: manifest =>
|
|
58
57
|
`${manifest.mapsBase ?? `/games/${manifest.id}/maps`}/manifest.json`,
|
|
@@ -305,7 +304,7 @@ export default {
|
|
|
305
304
|
debugReportUrl: '/debug/report',
|
|
306
305
|
|
|
307
306
|
// журнал клиентских ошибок: приём на том же боксе,
|
|
308
|
-
// что раздал
|
|
307
|
+
// что раздал страницу
|
|
309
308
|
clientReportUrl: '/client-reports',
|
|
310
309
|
|
|
311
310
|
// размер страницы для «Загрузить ещё» (offset/limit к мастеру)
|
package/src/config/wsports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Номера портов JSON-канала (docs.
|
|
1
|
+
// Номера портов JSON-канала (VIMP docs: snapshot.md). Реестр
|
|
2
2
|
// append-only, как и остальные словари плагинной поверхности (этап 3 плана
|
|
3
3
|
// plugin-forward-compat): номер НИКОГДА не переиспользуется и не
|
|
4
4
|
// перенумеровывается — уже опубликованная сборка игры шлёт и слушает те
|
|
@@ -39,7 +39,7 @@ const RECORDED_PORTS = {
|
|
|
39
39
|
const FLUSHING_METHODS = new Set(['sendClear', 'sendMap', 'sendFirstShot']);
|
|
40
40
|
|
|
41
41
|
// В headless-контуре нет ни мастера, ни auth-сервиса — участник стартует с
|
|
42
|
-
// пустым профилем (lib/offlinePlayerData.js, общее со standalone
|
|
42
|
+
// пустым профилем (lib/offlinePlayerData.js, общее со standalone).
|
|
43
43
|
const emptyProfileFetch = offlinePlayerData();
|
|
44
44
|
|
|
45
45
|
/**
|
|
@@ -13,7 +13,7 @@ import { isFixture } from './pluginLoader.js';
|
|
|
13
13
|
// Ввод начинается на 40-м тике, а не сразу: кадр спавна с force_reset
|
|
14
14
|
// приходит примерно на interpolation.delay (движковый дефолт — 100 мс, тут
|
|
15
15
|
// ~3× запаса) позже входа и чистит удержанные клавиши предиктора
|
|
16
|
-
// (docs.
|
|
16
|
+
// (VIMP docs: debugging.md) — встроенный сценарий не должен
|
|
17
17
|
// демонстрировать ровно ту ловушку, от которой предостерегает документация
|
|
18
18
|
const INPUT_DOWN_TICK = 40;
|
|
19
19
|
const INPUT_UP_TICK = 100;
|
|
@@ -26,7 +26,7 @@ const FIXTURE_UNUSED_KEYS = ['e1'];
|
|
|
26
26
|
const firstKey = dict => Object.keys(dict ?? {})[0];
|
|
27
27
|
|
|
28
28
|
// Клавиша для смоука обязана быть удерживаемой: по конвенции playerKeys
|
|
29
|
-
// (docs
|
|
29
|
+
// (VIMP docs: plugin-api.md) `type: 1` — триггер, у которого 'up'
|
|
30
30
|
// игнорируется, и «нажал — отпустил» не даёт ни удержания, ни движения.
|
|
31
31
|
// Порядок объявления в playerKeys контрактом не задан, поэтому берём первую
|
|
32
32
|
// без type; если игра объявила одни триггеры — первую любую, смоук всё равно
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ERROR, skip, verdict } from '../result.js';
|
|
2
2
|
|
|
3
|
-
// Набор npm-скриптов пакета игры (docs.
|
|
3
|
+
// Набор npm-скриптов пакета игры (VIMP docs: packaging.md). Это не
|
|
4
4
|
// вкусовое требование: инструкции движка и CI сгенерированной игры зовут их по
|
|
5
5
|
// имени, а отсутствующий core:build:node тихо оставляет headless-прогон без
|
|
6
6
|
// ядра.
|
|
@@ -3,7 +3,7 @@ import { ERROR, skip, verdict } from '../result.js';
|
|
|
3
3
|
// vite.config.js проверяется как текст, а не импортом: конфиг —
|
|
4
4
|
// функция от mode и вне Vite не исполняется. Каждый пункт здесь ловит
|
|
5
5
|
// молчаливую поломку сборки, а не стилистику
|
|
6
|
-
// (docs.
|
|
6
|
+
// (VIMP docs: packaging.md): без preserveEntrySignatures
|
|
7
7
|
// default-экспорт плагина вытряхивает tree-shaking, а build.lib всегда инлайнит
|
|
8
8
|
// ассеты.
|
|
9
9
|
const REQUIRED = [
|
|
@@ -4,7 +4,7 @@ import { SERVICES } from '../../../config/clientServices.js';
|
|
|
4
4
|
// Движковый пул сервисов держит реестр (config/clientServices.js), но пул
|
|
5
5
|
// ими не исчерпывается: игра доливает туда свои через
|
|
6
6
|
// ClientPlugin.hooks.services(core) — например геометрию предсказанной
|
|
7
|
-
// динамики карты (docs
|
|
7
|
+
// динамики карты (VIMP docs: plugin-api.md § "Services").
|
|
8
8
|
// Незнакомое имя не ошибка для движка: part получает undefined и рисует пустоту
|
|
9
9
|
// — карта без assetsBase выглядит как чистый холст без единой строки в консоли,
|
|
10
10
|
// ради этого правило и существует. Правило остаётся ERROR: оно работает на
|
|
@@ -484,7 +484,7 @@ function predictionDrift({ clients, scenario }) {
|
|
|
484
484
|
// Компоненты player-блока — игровая раскладка, движок знает только их
|
|
485
485
|
// порядок, поэтому нарушение адресуется индексом. Исключение — уровень 0
|
|
486
486
|
// (source 'camera'): там движок сравнивает ровно мировые x/y, и называть их
|
|
487
|
-
// «#0/#1» значит прятать причину (см. docs.
|
|
487
|
+
// «#0/#1» значит прятать причину (см. VIMP docs: debugging.md).
|
|
488
488
|
const CAMERA_COMPONENTS = ['x', 'y'];
|
|
489
489
|
|
|
490
490
|
function formatDivergence(record) {
|
|
@@ -12,7 +12,7 @@ import { loadGamePackage } from '../lib/loadGamePackage.js';
|
|
|
12
12
|
// 3. ничего — фикстура miniGame из этого репозитория.
|
|
13
13
|
//
|
|
14
14
|
// entries.wasmNode — новое ОПЦИОНАЛЬНОЕ поле манифеста: конвенция
|
|
15
|
-
// core/pkg-node/ уже описана в docs.
|
|
15
|
+
// core/pkg-node/ уже описана в VIMP docs: packaging.md, не хватало
|
|
16
16
|
// только пути в манифесте. Его отсутствие не ошибка — просто игра не
|
|
17
17
|
// прогоняется headless своим настоящим ядром.
|
|
18
18
|
|
|
@@ -51,7 +51,7 @@ export async function loadGameForSim({ game = null, core = null } = {}) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// путь --game указывает либо на пакет игры, либо прямо на манифест; сама
|
|
54
|
-
// загрузка
|
|
54
|
+
// загрузка — lib/loadGamePackage.js.
|
|
55
55
|
// client: true — прогону виртуальные клиенты обязательны, и отказ на
|
|
56
56
|
// загрузке пакета лучше отказа посреди сценария
|
|
57
57
|
async function loadFromManifest(game, core) {
|
package/src/host/HostGame.js
CHANGED
|
@@ -858,9 +858,9 @@ export default class HostGame {
|
|
|
858
858
|
|
|
859
859
|
/**
|
|
860
860
|
* Публичный teardown матча: останов таймеров, финальная синхронизация
|
|
861
|
-
* профилей и снятие всех участников.
|
|
862
|
-
*
|
|
863
|
-
*
|
|
861
|
+
* профилей и снятие всех участников. Его зовут Worker при закрытии
|
|
862
|
+
* комнаты и inline-хост solo при останове матча: без него таймеры
|
|
863
|
+
* переживают матч, а rank/state теряются.
|
|
864
864
|
* @returns {Promise} Завершение финальной синхронизации профилей.
|
|
865
865
|
*/
|
|
866
866
|
async destroy() {
|
|
@@ -2052,7 +2052,7 @@ export default class HostGame {
|
|
|
2052
2052
|
};
|
|
2053
2053
|
}
|
|
2054
2054
|
|
|
2055
|
-
// есть ли у участника identity-токен: в standalone
|
|
2055
|
+
// есть ли у участника identity-токен: в standalone личность
|
|
2056
2056
|
// гостевая, и обновлять там нечего
|
|
2057
2057
|
hasToken(gameId) {
|
|
2058
2058
|
const token = this._participants.get(gameId)?.token;
|
package/src/host/PortMachine.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// Хендшейк-машина клиента, вынутая из host.worker.js: автомат по клиентским
|
|
2
|
-
// портам (таблица портов — docs.
|
|
2
|
+
// портам (таблица портов — VIMP docs: snapshot.md). Изоморфна — ни
|
|
3
3
|
// self, ни postMessage, ни DOM: всё, что знает про транспорт, приходит через
|
|
4
4
|
// makeSocket. Поэтому её крутит и Worker браузерного хоста, и inline-хост
|
|
5
|
-
// standalone SDK, и
|
|
6
|
-
//
|
|
7
|
-
// createHostRuntime.
|
|
5
|
+
// standalone SDK, и headless-прогон: копий автомата быть не должно, иначе
|
|
6
|
+
// они разъедутся ровно так, как разъезжались бы копии createHostRuntime.
|
|
8
7
|
|
|
9
8
|
import wsports from '../config/wsports.js';
|
|
10
9
|
import closeCodes from '../config/closeCodes.js';
|
|
@@ -57,7 +56,7 @@ export default class PortMachine {
|
|
|
57
56
|
* @param {Object} deps.identity - стратегия идентичности (./identity.js).
|
|
58
57
|
* @param {number} [deps.resumeGraceMs] - сколько держать место участника,
|
|
59
58
|
* чей транспорт оборвался (host-migration этап 4); 0 — снимать сразу
|
|
60
|
-
* (
|
|
59
|
+
* (standalone).
|
|
61
60
|
* @param {number} [deps.resumeRequestTimeoutMs] - сколько ждать
|
|
62
61
|
* RESUME_REQUEST от возобновляющего соединения.
|
|
63
62
|
*/
|
|
@@ -466,22 +465,6 @@ export default class PortMachine {
|
|
|
466
465
|
}
|
|
467
466
|
}
|
|
468
467
|
|
|
469
|
-
/**
|
|
470
|
-
* @param {string} socketId
|
|
471
|
-
* @returns {boolean} Есть ли порт-машина у этого соединения.
|
|
472
|
-
*/
|
|
473
|
-
has(socketId) {
|
|
474
|
-
return this._clients.has(socketId);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* @param {string} socketId
|
|
479
|
-
* @returns {boolean} Дошёл ли клиент до созданного участника матча.
|
|
480
|
-
*/
|
|
481
|
-
hasParticipant(socketId) {
|
|
482
|
-
return this._clients.get(socketId)?.gameId !== undefined;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
468
|
/**
|
|
486
469
|
* @returns {Iterator<string>} socketId живых соединений (для
|
|
487
470
|
* host.completeHandoff(new Set(...))).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Отчёт о сбое чат-команды игры для приёмника диагностики
|
|
2
2
|
// (HostGame.setDiagnosticSink): сообщение в одну строку и стек без текста
|
|
3
|
-
// ошибки. Один отчёт на все среды — Worker (журнал платформы),
|
|
4
|
-
// (
|
|
3
|
+
// ошибки. Один отчёт на все среды — Worker (журнал платформы), standalone
|
|
4
|
+
// (консоль вкладки), headless-прогон (лог процесса). Чистые функции, без DOM и
|
|
5
5
|
// Node API: модуль работает в Worker'е
|
|
6
6
|
|
|
7
7
|
// символы, подделывающие строки лога: управляющие (перевод строки, ESC),
|
package/src/host/identity.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Стратегии идентичности хоста: как порт-машина (PortMachine.js) узнаёт,
|
|
2
2
|
// под каким ником участник входит в комнату. Прод-лобби доверяет только
|
|
3
|
-
// подписанному identity-токену центрального auth-сервиса; standalone
|
|
4
|
-
//
|
|
3
|
+
// подписанному identity-токену центрального auth-сервиса; standalone-контур
|
|
4
|
+
// мастера не имеет вовсе и пускает гостей по нику из формы.
|
|
5
5
|
//
|
|
6
6
|
// Контракт стратегии:
|
|
7
7
|
// { params: Array, errorField: string, resolve(data, socketId): Promise<string> }
|
|
@@ -61,7 +61,7 @@ export function createTokenIdentity({ jwksUrl, issuer }) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Гостевой вход (standalone
|
|
64
|
+
* Гостевой вход (standalone): ник — поле формы, которое хост
|
|
65
65
|
* валидирует сам движковым isValidName, без кода игры.
|
|
66
66
|
*
|
|
67
67
|
* Ограничение (принимается осознанно): гостевые ники не уникальны и не
|
|
@@ -51,8 +51,8 @@ class CommandProcessor {
|
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// в Worker'е сбой стал бы необработанным reject'ом,
|
|
55
|
-
//
|
|
54
|
+
// в Worker'е сбой стал бы необработанным reject'ом, то есть падением
|
|
55
|
+
// матча всех игроков
|
|
56
56
|
try {
|
|
57
57
|
const result = handler(this._ctx, gameId, arr);
|
|
58
58
|
|
|
@@ -234,7 +234,7 @@ export default class Accolades {
|
|
|
234
234
|
// payload.nick, и PortMachine передаёт в createUser именно его), а не
|
|
235
235
|
// то, что игрок написал в форме.
|
|
236
236
|
//
|
|
237
|
-
// Меняет это в гостевом контуре (standalone
|
|
237
|
+
// Меняет это в гостевом контуре (standalone), где ник —
|
|
238
238
|
// поле формы и «не защищён от подмены» по прямому признанию
|
|
239
239
|
// createGuestIdentity. Токена у гостя нет, и сопоставление по нику
|
|
240
240
|
// выдало бы ему чужую корону — стоит она ровно того, что значит.
|
|
@@ -31,7 +31,7 @@ import HostGame from '../host/HostGame.js';
|
|
|
31
31
|
* прогону (сценарий подкручивает networkSendRate и таймеры).
|
|
32
32
|
* @param {boolean} [options.voiceRelay] - У контура есть ретранслятор голоса
|
|
33
33
|
* на транспортном уровне (браузерный хост лобби); без него связь
|
|
34
|
-
* выключается, даже если игра её объявила (standalone, headless
|
|
34
|
+
* выключается, даже если игра её объявила (standalone, headless).
|
|
35
35
|
* @returns {Promise<Object>} { hostPlugin, game, seed, core, clientCfg,
|
|
36
36
|
* socketManager, host }.
|
|
37
37
|
*/
|