vimp-engine 0.38.3 → 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/InputListener.js +3 -0
- package/src/client/SoundManager.js +13 -0
- package/src/client/boot.js +11 -87
- package/src/client/components/controller/Controls.js +10 -0
- package/src/client/components/controller/Voice.js +31 -0
- package/src/client/components/model/Controls.js +64 -0
- package/src/client/components/model/RoomMenu.js +1 -1
- package/src/client/components/model/Voice.js +96 -0
- package/src/client/components/model/Vote.js +2 -1
- package/src/client/components/view/Auth.js +3 -3
- package/src/client/components/view/Voice.js +172 -0
- 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/lib/voiceState.js +78 -0
- package/src/client/main.js +160 -90
- package/src/client/network/HostConnectionManager.js +46 -5
- package/src/client/network/HostController.js +43 -0
- package/src/client/network/LoopbackTransport.js +14 -0
- package/src/client/network/SessionSupervisor.js +38 -14
- package/src/client/network/WebRtcManager.js +46 -5
- package/src/client/network/policyClose.js +10 -77
- package/src/client/style.css +150 -9
- 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/client/voice/JitterBuffer.js +137 -0
- package/src/client/voice/MicCapture.js +307 -0
- package/src/client/voice/PttMachine.js +369 -0
- package/src/client/voice/VoiceClient.js +336 -0
- package/src/client/voice/VoiceCues.js +295 -0
- package/src/client/voice/VoiceEncoder.js +174 -0
- package/src/client/voice/VoicePlayback.js +558 -0
- package/src/client/voice/audioData.js +35 -0
- package/src/client/voice/captureProcessor.worklet.js +136 -0
- package/src/client/voice/opusConfig.js +49 -0
- package/src/client/voice/voiceOutput.js +72 -0
- package/src/client/voice/voiceSupport.js +71 -0
- package/src/client/voice/voiceTones.js +90 -0
- package/src/config/clientServices.js +2 -0
- 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/voiceDefaults.js +44 -0
- package/src/config/voiceProtocol.js +80 -0
- package/src/config/voteKeys.js +4 -0
- package/src/config/wsports.js +13 -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/b11-voice-config.js +34 -0
- package/src/devtools/contract/rules/c12-voice-hotkey.js +70 -0
- package/src/devtools/contract/rules/c4-component-dependencies.js +1 -1
- package/src/devtools/contract/rules/index.js +4 -0
- package/src/devtools/invariants.js +1 -1
- package/src/devtools/pluginLoader.js +2 -2
- package/src/host/HostGame.js +62 -4
- package/src/host/PortMachine.js +23 -24
- package/src/host/commandFailure.js +2 -2
- package/src/host/host.worker.js +7 -0
- package/src/host/identity.js +3 -3
- package/src/host/meta/SocketManager.js +6 -0
- package/src/host/meta/core/CommandProcessor.js +2 -2
- package/src/host/meta/modules/Accolades.js +1 -1
- package/src/host/meta/modules/voice/Voice.js +430 -0
- package/src/host/meta/modules/voice/channels.js +58 -0
- package/src/host/meta/modules/voice/index.js +1 -0
- package/src/lib/VoiceRelay.js +200 -0
- package/src/lib/buildClientConfig.js +7 -0
- package/src/lib/capabilities.js +4 -0
- package/src/lib/createHostRuntime.js +8 -0
- package/src/lib/gameConfigView.js +11 -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/voiceConfig.js +136 -0
- package/src/lib/voiceMessages.js +172 -0
- package/src/lib/voicePacket.js +142 -0
- package/src/standalone/index.js +3 -2
- package/src/client/lib/footerLink.js +0 -30
- package/src/client/network/WebSocketTransport.js +0 -78
|
@@ -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 = [
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { inspectVoiceConfig } from '../../../lib/voiceConfig.js';
|
|
2
|
+
import { WARN, skip, verdict } from '../result.js';
|
|
3
|
+
|
|
4
|
+
// Блок voice необязателен: без него связь выключена. Но объявленный блок с
|
|
5
|
+
// опечаткой — тихий откат на дефолт (или зажим числа). Разбор берётся из
|
|
6
|
+
// lib/voiceConfig.js — того же модуля, что читает рантайм, иначе правило и
|
|
7
|
+
// движок разошлись бы. Хуки voiceSounds/voiceFilter статически не
|
|
8
|
+
// проверяются: их ответ виден только в браузере, и его проверяет рантайм.
|
|
9
|
+
export default {
|
|
10
|
+
id: 'B11',
|
|
11
|
+
name: 'voiceConfig',
|
|
12
|
+
level: WARN,
|
|
13
|
+
title: 'the voice block is well-formed',
|
|
14
|
+
|
|
15
|
+
check(ctx) {
|
|
16
|
+
if (!ctx.gameConfig) {
|
|
17
|
+
return skip('no gameConfig');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (ctx.gameConfig.voice === undefined || ctx.gameConfig.voice === null) {
|
|
21
|
+
return skip('no voice block — voice is off');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { problems } = inspectVoiceConfig(ctx.gameConfig.voice);
|
|
25
|
+
|
|
26
|
+
// забытый enabled: блок есть, связи нет, и ни одно правило не скажет.
|
|
27
|
+
// enabled: false — осознанное выключение, не нарушение
|
|
28
|
+
if (ctx.gameConfig.voice.enabled === undefined) {
|
|
29
|
+
problems.push('voice block is declared but voice.enabled is not set');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return verdict(problems);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import clientDefaults from '../../../config/clientDefaults.js';
|
|
2
|
+
import { VOTE_ANSWER_KEYS } from '../../../config/voteKeys.js';
|
|
3
|
+
import { inspectVoiceConfig } from '../../../lib/voiceConfig.js';
|
|
4
|
+
import { ERROR, skip, verdict } from '../result.js';
|
|
5
|
+
|
|
6
|
+
// Клавиша связи не должна совпасть ни с режимом/командой движка или игры,
|
|
7
|
+
// ни с клавишей игрового набора: ControlsModel.addKey забирает её раньше
|
|
8
|
+
// всех — совпавший режим (чат, стата, escape), команда или действие игры
|
|
9
|
+
// перестали бы работать. Коды движка берутся из clientDefaults, а не
|
|
10
|
+
// списком: правило не отстаёт от движка.
|
|
11
|
+
export default {
|
|
12
|
+
id: 'C12',
|
|
13
|
+
name: 'voiceHotkey',
|
|
14
|
+
level: ERROR,
|
|
15
|
+
title: 'the push-to-talk key collides with no other key',
|
|
16
|
+
|
|
17
|
+
check(ctx) {
|
|
18
|
+
const { config } = inspectVoiceConfig(ctx.gameConfig?.voice);
|
|
19
|
+
|
|
20
|
+
if (!config.enabled) {
|
|
21
|
+
return skip('voice is off');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const code = String(config.hotkey);
|
|
25
|
+
const violations = [];
|
|
26
|
+
const engine = clientDefaults.modules.controls;
|
|
27
|
+
const game = ctx.clientConfig?.modules?.controls;
|
|
28
|
+
|
|
29
|
+
for (const [owner, controls] of [
|
|
30
|
+
['the engine', engine],
|
|
31
|
+
['the game', game],
|
|
32
|
+
]) {
|
|
33
|
+
for (const group of ['modes', 'cmds']) {
|
|
34
|
+
const name = controls?.[group]?.[code];
|
|
35
|
+
|
|
36
|
+
if (name) {
|
|
37
|
+
violations.push(
|
|
38
|
+
`voice.hotkey ${code} is bound by ${owner} (${group}: ${name})`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// клиент отдаёт клавишу связи раньше, чем публикует код в голосование,
|
|
45
|
+
// а Vote принимает цифры 0..9: такая клавиша не выбрала бы вариант
|
|
46
|
+
if (
|
|
47
|
+
config.hotkey >= VOTE_ANSWER_KEYS[0] &&
|
|
48
|
+
config.hotkey <= VOTE_ANSWER_KEYS[1]
|
|
49
|
+
) {
|
|
50
|
+
violations.push(`voice.hotkey ${code} is a vote answer key (digits 0-9)`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const keySetList = game?.keySetList;
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(keySetList)) {
|
|
56
|
+
keySetList.forEach((set, index) => {
|
|
57
|
+
const action = set?.[code];
|
|
58
|
+
|
|
59
|
+
if (action) {
|
|
60
|
+
violations.push(
|
|
61
|
+
`voice.hotkey ${code} is bound in keySetList[${index}] ` +
|
|
62
|
+
`("${action}")`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return verdict(violations);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -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: оно работает на
|
|
@@ -15,6 +15,7 @@ import b7 from './b7-chat-commands.js';
|
|
|
15
15
|
import b8 from './b8-system-messages.js';
|
|
16
16
|
import b9 from './b9-vote-names.js';
|
|
17
17
|
import b10 from './b10-respawns.js';
|
|
18
|
+
import b11 from './b11-voice-config.js';
|
|
18
19
|
import c1 from './c1-client-shape.js';
|
|
19
20
|
import c2 from './c2-parts-registered.js';
|
|
20
21
|
import c3 from './c3-game-sets-coverage.js';
|
|
@@ -26,6 +27,7 @@ import c8 from './c8-baked-assets.js';
|
|
|
26
27
|
import c9 from './c9-chat-messages.js';
|
|
27
28
|
import c10 from './c10-auth-schema.js';
|
|
28
29
|
import c11 from './c11-stat-mode.js';
|
|
30
|
+
import c12 from './c12-voice-hotkey.js';
|
|
29
31
|
import d1 from './d1-snapshot-ids.js';
|
|
30
32
|
import d2 from './d2-snapshot-classes.js';
|
|
31
33
|
import d3 from './d3-snapshot-interp.js';
|
|
@@ -57,6 +59,7 @@ export const rules = [
|
|
|
57
59
|
b8,
|
|
58
60
|
b9,
|
|
59
61
|
b10,
|
|
62
|
+
b11,
|
|
60
63
|
c1,
|
|
61
64
|
c2,
|
|
62
65
|
c3,
|
|
@@ -68,6 +71,7 @@ export const rules = [
|
|
|
68
71
|
c9,
|
|
69
72
|
c10,
|
|
70
73
|
c11,
|
|
74
|
+
c12,
|
|
71
75
|
d1,
|
|
72
76
|
d2,
|
|
73
77
|
d3,
|
|
@@ -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
|
@@ -5,6 +5,7 @@ import Stat from './meta/modules/Stat.js';
|
|
|
5
5
|
import Chat from './meta/modules/chat/index.js';
|
|
6
6
|
import { registerCodes } from './meta/modules/chat/systemMessages.js';
|
|
7
7
|
import Vote from './meta/modules/Vote.js';
|
|
8
|
+
import Voice from './meta/modules/voice/index.js';
|
|
8
9
|
import RTTManager from './meta/modules/RTTManager.js';
|
|
9
10
|
import TimerManager from './meta/modules/TimerManager.js';
|
|
10
11
|
import ParticipantManager from './meta/player/ParticipantManager.js';
|
|
@@ -12,6 +13,7 @@ import VoteCoordinator from './meta/core/VoteCoordinator.js';
|
|
|
12
13
|
import RoundManager from './meta/core/RoundManager.js';
|
|
13
14
|
import CommandProcessor from './meta/core/CommandProcessor.js';
|
|
14
15
|
import { sanitizeMessage } from '../lib/sanitizers.js';
|
|
16
|
+
import { parseFloorMessage } from '../lib/voiceMessages.js';
|
|
15
17
|
import closeCodes from '../config/closeCodes.js';
|
|
16
18
|
import clock from '../lib/clock.js';
|
|
17
19
|
import { createResumeKey } from '../lib/resumeKey.js';
|
|
@@ -250,6 +252,20 @@ export default class HostGame {
|
|
|
250
252
|
|
|
251
253
|
this._socketManager = socketManager;
|
|
252
254
|
|
|
255
|
+
// голосовая связь: эфир решается здесь, аудио сюда не
|
|
256
|
+
// попадает — его раздаёт ретранслятор транспортного уровня по маршрутам
|
|
257
|
+
// модуля. Эфир не выдаётся, пока матч стоит: заморозка плановой передачи,
|
|
258
|
+
// пауза эстафеты Worker'ов, ожидание старта из контрольной точки, закрытие
|
|
259
|
+
this._voice = new Voice(data.voice, {
|
|
260
|
+
participants: this._participants,
|
|
261
|
+
socketManager: this._socketManager,
|
|
262
|
+
isAvailable: () =>
|
|
263
|
+
!this._frozen &&
|
|
264
|
+
!this._handoffPaused &&
|
|
265
|
+
!this._restorePending &&
|
|
266
|
+
!this._isDestroying,
|
|
267
|
+
});
|
|
268
|
+
|
|
253
269
|
this._networkSendRate = data.timers.networkSendRate;
|
|
254
270
|
this._snapshotManager = new SnapshotThrottle(this._networkSendRate);
|
|
255
271
|
|
|
@@ -363,6 +379,9 @@ export default class HostGame {
|
|
|
363
379
|
this._restoreMode = null; // 'midRound' | 'soft'
|
|
364
380
|
this._restoredTimers = null;
|
|
365
381
|
this._frozen = false;
|
|
382
|
+
// эстафета Worker'ов прошла границу раунда: игра стоит до terminate()
|
|
383
|
+
// этого Worker'а или resumeAfterHandoff()
|
|
384
|
+
this._handoffPaused = false;
|
|
366
385
|
// точка снята на замороженном хосте: pack_body осушил тело кадра вне
|
|
367
386
|
// цикла, и его события (удаления, взрывы) никому не ушли
|
|
368
387
|
this._drainedWhileFrozen = false;
|
|
@@ -608,6 +627,11 @@ export default class HostGame {
|
|
|
608
627
|
}
|
|
609
628
|
});
|
|
610
629
|
|
|
630
|
+
// эфиры: таймауты, отзыв, слушатели — с частотой снапшотов; после цикла
|
|
631
|
+
// рассылки, потому что getCamera в нём исправляет watchedGameId
|
|
632
|
+
// наблюдателей
|
|
633
|
+
this._voice.tick();
|
|
634
|
+
|
|
611
635
|
// граница кадра: pack_body опустошил накопители снапшота — единственный
|
|
612
636
|
// момент, когда дамп ядра корректен
|
|
613
637
|
this._maybeCheckpoint();
|
|
@@ -815,6 +839,7 @@ export default class HostGame {
|
|
|
815
839
|
})),
|
|
816
840
|
activeList: this._participants.getActiveList(),
|
|
817
841
|
},
|
|
842
|
+
voice: this._voice.debugState(),
|
|
818
843
|
core: this._game.debugJson(),
|
|
819
844
|
};
|
|
820
845
|
}
|
|
@@ -833,13 +858,15 @@ export default class HostGame {
|
|
|
833
858
|
|
|
834
859
|
/**
|
|
835
860
|
* Публичный teardown матча: останов таймеров, финальная синхронизация
|
|
836
|
-
* профилей и снятие всех участников.
|
|
837
|
-
*
|
|
838
|
-
*
|
|
861
|
+
* профилей и снятие всех участников. Его зовут Worker при закрытии
|
|
862
|
+
* комнаты и inline-хост solo при останове матча: без него таймеры
|
|
863
|
+
* переживают матч, а rank/state теряются.
|
|
839
864
|
* @returns {Promise} Завершение финальной синхронизации профилей.
|
|
840
865
|
*/
|
|
841
866
|
async destroy() {
|
|
842
867
|
this._isDestroying = true;
|
|
868
|
+
// комната закрывается, сообщать некому
|
|
869
|
+
this._voice.reset({ notify: false });
|
|
843
870
|
|
|
844
871
|
if (this._resumeWait) {
|
|
845
872
|
clock.clearTimeout(this._resumeWait.timer);
|
|
@@ -882,6 +909,11 @@ export default class HostGame {
|
|
|
882
909
|
this._roundManager.requestHandoff(() => {
|
|
883
910
|
this._timerManager.stopGameTimers();
|
|
884
911
|
this._timerManager.stopIdleCheckTimer();
|
|
912
|
+
// без тиков эфир не закончился бы сам, а новый Worker начинает без
|
|
913
|
+
// эфиров: говорящие получают END, слушатели — STOP; до terminate()
|
|
914
|
+
// или resumeAfterHandoff() запрос получает UNAVAILABLE
|
|
915
|
+
this._handoffPaused = true;
|
|
916
|
+
this._voice.reset();
|
|
885
917
|
this._flushBeforeHandoff().then(() => cb(this._collectHandoff()));
|
|
886
918
|
});
|
|
887
919
|
}
|
|
@@ -928,6 +960,7 @@ export default class HostGame {
|
|
|
928
960
|
// отказ от эстафеты (новый Worker не поднялся): вернуть таймеры и
|
|
929
961
|
// продолжить жить на старой версии кода
|
|
930
962
|
resumeAfterHandoff() {
|
|
963
|
+
this._handoffPaused = false;
|
|
931
964
|
this._roundManager.cancelHandoff();
|
|
932
965
|
this._timerManager.startIdleCheckTimer();
|
|
933
966
|
this._timerManager.resumeGameTimers(this._timerManager.getMapTimeLeft());
|
|
@@ -1072,6 +1105,15 @@ export default class HostGame {
|
|
|
1072
1105
|
this._healthSink = typeof sink === 'function' ? sink : null;
|
|
1073
1106
|
}
|
|
1074
1107
|
|
|
1108
|
+
/**
|
|
1109
|
+
* Приёмник маршрутов голосовых эфиров для ретранслятора:
|
|
1110
|
+
* ({ speakerSocketId, route }) => void, route === null — маршрут снят.
|
|
1111
|
+
* @param {Function|null} sink
|
|
1112
|
+
*/
|
|
1113
|
+
setVoiceRouteSink(sink) {
|
|
1114
|
+
this._voice.setRouteSink(sink);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1075
1117
|
// RTT — только удалённые люди: хост-игрок на loopback связь хоста не мерит
|
|
1076
1118
|
_onLoopStats(stats) {
|
|
1077
1119
|
if (!this._healthSink) {
|
|
@@ -1543,6 +1585,9 @@ export default class HostGame {
|
|
|
1543
1585
|
|
|
1544
1586
|
this._frozen = true;
|
|
1545
1587
|
this._timerManager.pause();
|
|
1588
|
+
// без тиков эфир не закончился бы сам: говорящие получают END,
|
|
1589
|
+
// слушатели — STOP
|
|
1590
|
+
this._voice.reset();
|
|
1546
1591
|
}
|
|
1547
1592
|
|
|
1548
1593
|
// разморозка (передача сорвалась): отсчёты продолжаются с остатков.
|
|
@@ -1672,6 +1717,7 @@ export default class HostGame {
|
|
|
1672
1717
|
this._stat.removeUser(gameId, teamId);
|
|
1673
1718
|
this._chat.removeUser(gameId);
|
|
1674
1719
|
this._vote.removeUser(gameId);
|
|
1720
|
+
this._voice.removeUser(gameId);
|
|
1675
1721
|
this._panel.removeUser(gameId);
|
|
1676
1722
|
|
|
1677
1723
|
// финальная синхронизация профиля перед уходом участника (Этап B4):
|
|
@@ -1702,6 +1748,18 @@ export default class HostGame {
|
|
|
1702
1748
|
}
|
|
1703
1749
|
}
|
|
1704
1750
|
|
|
1751
|
+
// управление эфиром: [1] — запрос, [0, talkId] — отпускание. В рекордер не
|
|
1752
|
+
// пишется: на симуляцию не влияет
|
|
1753
|
+
voiceFloor(gameId, data) {
|
|
1754
|
+
const message = parseFloorMessage(data);
|
|
1755
|
+
|
|
1756
|
+
if (message?.op === 'request') {
|
|
1757
|
+
this._voice.request(gameId);
|
|
1758
|
+
} else if (message?.op === 'release') {
|
|
1759
|
+
this._voice.release(gameId, message.talkId);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1705
1763
|
// обновляет команды (формат wire: 'seq:action:name', указатель —
|
|
1706
1764
|
// 'seq:aim:x:y:flags')
|
|
1707
1765
|
updateKeys(gameId, keyStr) {
|
|
@@ -1994,7 +2052,7 @@ export default class HostGame {
|
|
|
1994
2052
|
};
|
|
1995
2053
|
}
|
|
1996
2054
|
|
|
1997
|
-
// есть ли у участника identity-токен: в standalone
|
|
2055
|
+
// есть ли у участника identity-токен: в standalone личность
|
|
1998
2056
|
// гостевая, и обновлять там нечего
|
|
1999
2057
|
hasToken(gameId) {
|
|
2000
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';
|
|
@@ -25,8 +24,10 @@ const PC_PONG = wsports.client.PONG;
|
|
|
25
24
|
const PC_RESUME_REQUEST = wsports.client.RESUME_REQUEST;
|
|
26
25
|
const PC_LEAVE = wsports.client.LEAVE;
|
|
27
26
|
const PC_TOKEN_REFRESH = wsports.client.TOKEN_REFRESH;
|
|
27
|
+
const PC_VOICE_FLOOR = wsports.client.VOICE_FLOOR;
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// VOICE_AUDIO (13) бинарный и порт-машиной не разбирается
|
|
30
|
+
const PORT_COUNT = 13;
|
|
30
31
|
|
|
31
32
|
// версии формата RESUME_REQUEST, которые понимает хост
|
|
32
33
|
const RESUME_VERSION = 1;
|
|
@@ -41,6 +42,7 @@ const IN_GAME_PORTS = [
|
|
|
41
42
|
PC_PONG,
|
|
42
43
|
PC_LEAVE,
|
|
43
44
|
PC_TOKEN_REFRESH,
|
|
45
|
+
PC_VOICE_FLOOR,
|
|
44
46
|
];
|
|
45
47
|
|
|
46
48
|
export default class PortMachine {
|
|
@@ -54,7 +56,7 @@ export default class PortMachine {
|
|
|
54
56
|
* @param {Object} deps.identity - стратегия идентичности (./identity.js).
|
|
55
57
|
* @param {number} [deps.resumeGraceMs] - сколько держать место участника,
|
|
56
58
|
* чей транспорт оборвался (host-migration этап 4); 0 — снимать сразу
|
|
57
|
-
* (
|
|
59
|
+
* (standalone).
|
|
58
60
|
* @param {number} [deps.resumeRequestTimeoutMs] - сколько ждать
|
|
59
61
|
* RESUME_REQUEST от возобновляющего соединения.
|
|
60
62
|
*/
|
|
@@ -244,8 +246,18 @@ export default class PortMachine {
|
|
|
244
246
|
return;
|
|
245
247
|
}
|
|
246
248
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
const port = Array.isArray(msg) ? msg[0] : null;
|
|
250
|
+
|
|
251
|
+
// номер порта — только целое из диапазона: строковый ключ вроде
|
|
252
|
+
// '__proto__' или 'constructor' прошёл бы проверку `enabled[...]` по
|
|
253
|
+
// прототипу массива и вызвал бы не обработчик
|
|
254
|
+
if (
|
|
255
|
+
Number.isInteger(port) &&
|
|
256
|
+
port >= 0 &&
|
|
257
|
+
port < PORT_COUNT &&
|
|
258
|
+
state.enabled[port]
|
|
259
|
+
) {
|
|
260
|
+
state.methods[port](msg[1]);
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
263
|
|
|
@@ -453,22 +465,6 @@ export default class PortMachine {
|
|
|
453
465
|
}
|
|
454
466
|
}
|
|
455
467
|
|
|
456
|
-
/**
|
|
457
|
-
* @param {string} socketId
|
|
458
|
-
* @returns {boolean} Есть ли порт-машина у этого соединения.
|
|
459
|
-
*/
|
|
460
|
-
has(socketId) {
|
|
461
|
-
return this._clients.has(socketId);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* @param {string} socketId
|
|
466
|
-
* @returns {boolean} Дошёл ли клиент до созданного участника матча.
|
|
467
|
-
*/
|
|
468
|
-
hasParticipant(socketId) {
|
|
469
|
-
return this._clients.get(socketId)?.gameId !== undefined;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
468
|
/**
|
|
473
469
|
* @returns {Iterator<string>} socketId живых соединений (для
|
|
474
470
|
* host.completeHandoff(new Set(...))).
|
|
@@ -603,6 +599,9 @@ export default class PortMachine {
|
|
|
603
599
|
|
|
604
600
|
// 11: token refresh
|
|
605
601
|
data => this._refreshToken(socketId, state, data),
|
|
602
|
+
|
|
603
|
+
// 12: управление эфиром ([1] / [0, talkId]); разбор — HostGame.voiceFloor
|
|
604
|
+
data => host.voiceFloor(state.gameId, data),
|
|
606
605
|
];
|
|
607
606
|
}
|
|
608
607
|
}
|
|
@@ -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/host.worker.js
CHANGED
|
@@ -135,6 +135,8 @@ async function onInit(room, handoff = null, checkpointBytes = null, seqFloor) {
|
|
|
135
135
|
// общая с headless-runner'ом сборка (lib/createHostRuntime.js) — чтобы
|
|
136
136
|
// отладочный прогон крутил ровно тот код, что и прод
|
|
137
137
|
const runtime = await createHostRuntime(room, {
|
|
138
|
+
// ретранслятор голоса — в главном потоке (HostController)
|
|
139
|
+
voiceRelay: true,
|
|
138
140
|
hostOptions: {
|
|
139
141
|
// map_changed остаётся для главного потока до host-migration этапа 2
|
|
140
142
|
onMapChange: mapName =>
|
|
@@ -151,6 +153,11 @@ async function onInit(room, handoff = null, checkpointBytes = null, seqFloor) {
|
|
|
151
153
|
// здоровье хоста (host-migration этап 9a): главный поток решает, не пора
|
|
152
154
|
// ли отдать роль
|
|
153
155
|
host.setHealthSink(health => self.postMessage({ type: 'health', health }));
|
|
156
|
+
// голосовая связь: маршруты эфиров — ретранслятору главного потока
|
|
157
|
+
// (VoiceRelay). Сам голос Worker не видит никогда
|
|
158
|
+
host.setVoiceRouteSink(({ speakerSocketId, route }) =>
|
|
159
|
+
self.postMessage({ type: 'voice_route', speakerSocketId, route }),
|
|
160
|
+
);
|
|
154
161
|
|
|
155
162
|
// сбой кода игры, пойманный хостом (чат-команда): в журнал клиентских
|
|
156
163
|
// ошибок тем же сообщением, что и необработанный reject
|
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
|
* Ограничение (принимается осознанно): гостевые ники не уникальны и не
|
|
@@ -41,6 +41,7 @@ export default class SocketManager {
|
|
|
41
41
|
this._PORT_ACCOLADES_DATA = ports.ACCOLADES_DATA;
|
|
42
42
|
this._PORT_SESSION_DATA = ports.SESSION_DATA;
|
|
43
43
|
this._PORT_RESUME_RESULT = ports.RESUME_RESULT;
|
|
44
|
+
this._PORT_VOICE_DATA = ports.VOICE_DATA;
|
|
44
45
|
|
|
45
46
|
this._soundCues = soundCues;
|
|
46
47
|
this._initialVote = initialVote;
|
|
@@ -349,6 +350,11 @@ export default class SocketManager {
|
|
|
349
350
|
this._send(socketId, this._PORT_ACCOLADES_DATA, data);
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
// голосовая связь: управление эфиром (lib/voiceMessages.js), персонально
|
|
354
|
+
sendVoice(socketId, data) {
|
|
355
|
+
this._send(socketId, this._PORT_VOICE_DATA, data);
|
|
356
|
+
}
|
|
357
|
+
|
|
352
358
|
/**
|
|
353
359
|
* Отправка сообщения чата.
|
|
354
360
|
* @param {string} socketId
|
|
@@ -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
|
// выдало бы ему чужую корону — стоит она ровно того, что значит.
|