vimp-engine 0.38.2 → 0.39.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/core/Cargo.toml +1 -1
- package/package.json +3 -1
- package/src/client/InputListener.js +3 -0
- package/src/client/SoundManager.js +13 -0
- package/src/client/boot.js +30 -7
- package/src/client/components/controller/ClientReports.js +1 -1
- package/src/client/components/controller/Controls.js +10 -0
- package/src/client/components/controller/Games.js +1 -1
- package/src/client/components/controller/Voice.js +31 -0
- package/src/client/components/model/ClientReports.js +1 -1
- package/src/client/components/model/Controls.js +64 -0
- package/src/client/components/model/Games.js +24 -0
- package/src/client/components/model/Voice.js +96 -0
- package/src/client/components/model/Vote.js +2 -1
- package/src/client/components/view/ClientReports.js +1 -1
- package/src/client/components/view/Games.js +19 -1
- package/src/client/components/view/Lobby.js +1 -7
- package/src/client/components/view/Voice.js +172 -0
- package/src/client/debug.js +1 -1
- package/src/client/lib/authApi.js +1 -1
- package/src/client/lib/diagnostics.js +19 -4
- package/src/client/lib/engineVersion.js +0 -6
- package/src/client/lib/footerLink.js +15 -14
- package/src/client/lib/retryOnNetworkError.js +60 -0
- package/src/client/lib/voiceState.js +78 -0
- package/src/client/main.js +152 -18
- package/src/client/network/HiddenHostHealthLog.js +40 -3
- package/src/client/network/HostConnectionManager.js +46 -5
- package/src/client/network/HostController.js +47 -4
- package/src/client/network/HostHealthPolicy.js +41 -0
- package/src/client/network/InlineHostBridge.js +8 -1
- package/src/client/network/LoopbackTransport.js +14 -0
- package/src/client/network/SessionSupervisor.js +24 -0
- package/src/client/network/WebRtcManager.js +45 -3
- package/src/client/session/HandoffFlow.js +5 -0
- package/src/client/session/HostRole.js +5 -2
- package/src/client/style.css +151 -9
- package/src/client/views/gameShell.js +3 -3
- package/src/client/views/includes/auth.pug +4 -4
- package/src/client/views/includes/lobby.pug +4 -4
- package/src/client/views/includes/reports.pug +1 -1
- 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/authClient.js +3 -3
- package/src/config/clientServices.js +2 -0
- package/src/config/lobby.js +4 -5
- package/src/config/opcodes.js +2 -1
- 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 +12 -0
- package/src/devtools/contract/loadContext.js +2 -2
- package/src/devtools/contract/rules/a7-package-repository.js +7 -8
- 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/index.js +5 -1
- package/src/devtools/invariants.js +1 -1
- package/src/host/DebugRecorder.js +1 -1
- package/src/host/HostGame.js +93 -15
- package/src/host/PortMachine.js +19 -3
- package/src/host/commandFailure.js +140 -0
- package/src/host/host.worker.js +15 -2
- package/src/host/meta/SocketManager.js +8 -2
- package/src/host/meta/core/CommandProcessor.js +37 -7
- 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/applyRoomOverrides.js +1 -1
- package/src/lib/buildClientConfig.js +7 -0
- package/src/lib/capabilities.js +5 -1
- package/src/lib/checkpointCodec.js +1 -2
- package/src/lib/coreConfig.js +1 -1
- package/src/lib/createHostRuntime.js +9 -1
- package/src/lib/gameConfigView.js +11 -1
- package/src/lib/gameId.js +1 -1
- package/src/lib/gamePlugin.js +6 -2
- package/src/lib/jwt.js +1 -1
- package/src/lib/packageLink.js +18 -61
- package/src/lib/validators.js +3 -3
- 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 +1 -1
- package/tests/fixtures/generations/gen-api3/README.md +2 -2
- package/tests/fixtures/generations/gen-api4/README.md +2 -2
- package/src/devtools/surface/abiParse.js +0 -175
- package/src/devtools/surface/collect.js +0 -413
package/core/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "vimp-engine-core"
|
|
3
|
-
version = "0.23.
|
|
3
|
+
version = "0.23.4"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "VIMP engine simulation core: physics, map, snapshot framing, interpolation/prediction/raycast primitives, navigation utilities. No wasm-bindgen — the game crate builds the WASM ABI wrappers."
|
|
6
6
|
license = "MIT"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vimp-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "VIMP engine SDK: browser host and client runtime, standalone mode and CLI tools (vimp-sim, vimp-contract) for VIMP game plugins",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"!src/client/_*",
|
|
36
36
|
"src/standalone",
|
|
37
37
|
"src/devtools",
|
|
38
|
+
"!src/devtools/surface/abiParse.js",
|
|
39
|
+
"!src/devtools/surface/collect.js",
|
|
38
40
|
"tests/fixtures",
|
|
39
41
|
"core/Cargo.toml",
|
|
40
42
|
"bin",
|
|
@@ -20,6 +20,9 @@ export default class InputListener {
|
|
|
20
20
|
this.publisher.emit('keyUp', event),
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
+
// keyup за пределами окна не приходит — эфир залип бы до таймаута
|
|
24
|
+
window.addEventListener('blur', () => this.publisher.emit('blur'));
|
|
25
|
+
|
|
23
26
|
const emitMouse = () => this.publisher.emit('mouseAction');
|
|
24
27
|
|
|
25
28
|
window.addEventListener('mouseup', emitMouse);
|
|
@@ -892,6 +892,19 @@ export default class SoundManager {
|
|
|
892
892
|
}
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
+
/**
|
|
896
|
+
* Выход Web Audio движка для голосовой связи: контекст Howler и его master
|
|
897
|
+
* gain. Голос подчиняется общей громкости и mute (скрытая вкладка, пауза
|
|
898
|
+
* миграции).
|
|
899
|
+
* @returns {{ ctx: AudioContext, destination: AudioNode } | null} null —
|
|
900
|
+
* Web Audio недоступен.
|
|
901
|
+
*/
|
|
902
|
+
getAudioOutput() {
|
|
903
|
+
return Howler.usingWebAudio && Howler.ctx && Howler.masterGain
|
|
904
|
+
? { ctx: Howler.ctx, destination: Howler.masterGain }
|
|
905
|
+
: null;
|
|
906
|
+
}
|
|
907
|
+
|
|
895
908
|
/**
|
|
896
909
|
* Выключает все звуки.
|
|
897
910
|
*/
|
package/src/client/boot.js
CHANGED
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
// window не вводим: они переживают перезагрузку бандла и ломают вторую
|
|
12
12
|
// вкладку SDK в том же документе.
|
|
13
13
|
|
|
14
|
+
import {
|
|
15
|
+
bodyReadFailure,
|
|
16
|
+
fetchRetrying,
|
|
17
|
+
isNetworkFailure,
|
|
18
|
+
} from './lib/retryOnNetworkError.js';
|
|
19
|
+
|
|
14
20
|
// эндпоинт режима: его отдаёт dedicated-сервер, у мастера его нет —
|
|
15
|
-
// 404
|
|
21
|
+
// 404 и мусор означают прод-лобби; сбой сети — отказ загрузки
|
|
16
22
|
const CONFIG_URL = '/config';
|
|
17
23
|
|
|
18
24
|
// умолчание игрового WS dedicated-сервера: тот же хост, путь /game
|
|
@@ -65,23 +71,40 @@ export function resetBootConfig() {
|
|
|
65
71
|
/**
|
|
66
72
|
* Разрешает режим загрузки: инъекция SDK → GET /config → лобби.
|
|
67
73
|
* @param {Function} [fetchImpl] - Реализация fetch (тесты подставляют свою).
|
|
68
|
-
* @
|
|
74
|
+
* @param {Object} [retry] - Параметры fetchRetrying (тесты отключают паузы).
|
|
75
|
+
* @returns {Promise<Object>} Конфиг загрузки; отклоняется NetworkFailure, если
|
|
76
|
+
* сеть не ответила после повторов.
|
|
69
77
|
*/
|
|
70
|
-
export async function resolveBootConfig(fetchImpl) {
|
|
78
|
+
export async function resolveBootConfig(fetchImpl, retry) {
|
|
71
79
|
if (bootConfig) {
|
|
72
80
|
return bootConfig;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
const doFetch = fetchImpl ?? ((...args) => fetch(...args));
|
|
84
|
+
let res = null;
|
|
76
85
|
|
|
77
86
|
try {
|
|
78
|
-
|
|
87
|
+
res = await fetchRetrying(doFetch, CONFIG_URL, retry);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// сети нет: режим неизвестен, и лобби здесь — догадка, а не факт
|
|
90
|
+
if (isNetworkFailure(e)) {
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
79
94
|
|
|
80
|
-
|
|
95
|
+
if (res?.ok) {
|
|
96
|
+
try {
|
|
81
97
|
bootConfig = normalize(await res.json());
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// мусор в JSON — лобби; обрыв при чтении тела — сбой сети
|
|
100
|
+
const failure = bodyReadFailure(e);
|
|
101
|
+
|
|
102
|
+
if (isNetworkFailure(failure)) {
|
|
103
|
+
throw failure;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
bootConfig = null;
|
|
82
107
|
}
|
|
83
|
-
} catch (e) {
|
|
84
|
-
// сети/эндпоинта нет — это и есть прод-лобби, шуметь не о чем
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
bootConfig = bootConfig ?? { mode: 'lobby' };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
let clientReportsCtrl;
|
|
4
4
|
|
|
5
|
-
// Контроллер журнала клиентских
|
|
5
|
+
// Контроллер журнала клиентских ошибок:
|
|
6
6
|
// связывает view-события с моделью, собственного сетевого I/O не делает
|
|
7
7
|
export default class ClientReportsCtrl {
|
|
8
8
|
constructor(model, view) {
|
|
@@ -54,6 +54,16 @@ export default class ControlsCtrl {
|
|
|
54
54
|
this._model.resendHeld();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// назначает клавишу связи (push-to-talk)
|
|
58
|
+
setPushToTalk(keyCode) {
|
|
59
|
+
this._model.setPushToTalk(keyCode);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// отпускает связь (потеря фокуса, скрытая вкладка)
|
|
63
|
+
releasePtt() {
|
|
64
|
+
this._model.releasePtt();
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
// скрывает курсор
|
|
58
68
|
resetCursorHideTimer() {
|
|
59
69
|
this._view.resetCursorHideTimer();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Singleton VoiceCtrl
|
|
2
|
+
|
|
3
|
+
let voiceCtrl;
|
|
4
|
+
|
|
5
|
+
export default class VoiceCtrl {
|
|
6
|
+
constructor(model, view) {
|
|
7
|
+
if (voiceCtrl) {
|
|
8
|
+
return voiceCtrl;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
voiceCtrl = this;
|
|
12
|
+
|
|
13
|
+
this._model = model;
|
|
14
|
+
this._view = view;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// состояние своего эфира
|
|
18
|
+
showOwn(kind, data) {
|
|
19
|
+
this._model.showOwn(kind, data);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// чужой эфир (null — скрыть)
|
|
23
|
+
setIncoming(speaker) {
|
|
24
|
+
this._model.setIncoming(speaker);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
destroy() {
|
|
28
|
+
this._model.destroy();
|
|
29
|
+
this._view.destroy();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -4,7 +4,7 @@ import Publisher from '../../../lib/Publisher.js';
|
|
|
4
4
|
|
|
5
5
|
let clientReportsModel;
|
|
6
6
|
|
|
7
|
-
// Модель журнала клиентских ошибок в
|
|
7
|
+
// Модель журнала клиентских ошибок в лобби:
|
|
8
8
|
// страницы журнала, фильтры и решение админа по строке. Как GamesModel,
|
|
9
9
|
// сетевые запросы делает сама (простой REST к мастеру) и DOM не трогает.
|
|
10
10
|
// Наружу — одно событие 'changed' с полным состоянием: список, счётчик,
|
|
@@ -2,6 +2,17 @@ import Publisher from '../../../lib/Publisher.js';
|
|
|
2
2
|
|
|
3
3
|
// Singleton ControlsModel
|
|
4
4
|
|
|
5
|
+
// keyCode самих модификаторов: Ctrl, Alt, Meta (два кода и Firefox)
|
|
6
|
+
const MODIFIER_CODES = new Set([17, 18, 91, 93, 224]);
|
|
7
|
+
|
|
8
|
+
// фокус в поле ввода: клавиша печатает букву
|
|
9
|
+
function isEditableTarget(target) {
|
|
10
|
+
return (
|
|
11
|
+
target?.isContentEditable === true ||
|
|
12
|
+
['INPUT', 'TEXTAREA', 'SELECT'].includes(target?.tagName)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
let controlsModel;
|
|
6
17
|
|
|
7
18
|
export default class ControlsModel {
|
|
@@ -25,6 +36,8 @@ export default class ControlsModel {
|
|
|
25
36
|
// возобновления сессии должен знать, что игрок держит на самом деле
|
|
26
37
|
this._heldCodes = new Set();
|
|
27
38
|
this._areKeysEnabled = false; // статус возможности нажатия клавиш
|
|
39
|
+
this._pttKey = null; // keyCode клавиши связи, null — связи нет
|
|
40
|
+
this._pttHeld = false;
|
|
28
41
|
|
|
29
42
|
// канал указателя: игра, не объявившая modules.controls.pointer, его не
|
|
30
43
|
// получает вовсе — ни одного лишнего сообщения по проводу
|
|
@@ -79,6 +92,34 @@ export default class ControlsModel {
|
|
|
79
92
|
this.publisher.emit('mode', mode);
|
|
80
93
|
}
|
|
81
94
|
} else {
|
|
95
|
+
// Cmd, нажатый при уже зажатой клавише связи: её keyup на macOS не
|
|
96
|
+
// придёт — эфир отпускается сразу
|
|
97
|
+
if (event.metaKey && this._pttHeld && keyCode !== this._pttKey) {
|
|
98
|
+
this.releasePtt();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// клавиша связи никогда не уходит в игру командой. Автоповтор keydown
|
|
102
|
+
// игнорируется — удержание и так длится до keyup. Фокус в поле ввода
|
|
103
|
+
// (любом, не только в строке чата) — клавиша печатает букву. С
|
|
104
|
+
// Ctrl/Cmd/Alt — системная комбинация (вставка), не связь: на macOS при
|
|
105
|
+
// Cmd keyup обычной клавиши не приходит и связь залипла бы
|
|
106
|
+
// Сам модификатор может быть клавишей связи: его keydown несёт свой флаг
|
|
107
|
+
if (
|
|
108
|
+
keyCode === this._pttKey &&
|
|
109
|
+
(MODIFIER_CODES.has(keyCode) ||
|
|
110
|
+
!(event.ctrlKey || event.metaKey || event.altKey)) &&
|
|
111
|
+
!isEditableTarget(event.target)
|
|
112
|
+
) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
|
|
115
|
+
if (!event.repeat && !this._pttHeld) {
|
|
116
|
+
this._pttHeld = true;
|
|
117
|
+
this.publisher.emit('ptt', true);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
82
123
|
// если клавиша ещё не зажата
|
|
83
124
|
if (!this._pressedKeys[keyCode]) {
|
|
84
125
|
const name = this._currentKeySet[keyCode];
|
|
@@ -111,6 +152,11 @@ export default class ControlsModel {
|
|
|
111
152
|
|
|
112
153
|
this._heldCodes.delete(keyCode);
|
|
113
154
|
|
|
155
|
+
// отпускание засчитывается всегда, без проверки цели
|
|
156
|
+
if (keyCode === this._pttKey && this._pttHeld) {
|
|
157
|
+
this.releasePtt();
|
|
158
|
+
}
|
|
159
|
+
|
|
114
160
|
// если клавиша была зажата
|
|
115
161
|
if (this._pressedKeys[keyCode]) {
|
|
116
162
|
const name = this._currentKeySet[keyCode];
|
|
@@ -131,6 +177,10 @@ export default class ControlsModel {
|
|
|
131
177
|
if (status === 'opened') {
|
|
132
178
|
this._currentModes[mode] = true;
|
|
133
179
|
|
|
180
|
+
if (mode === 'chat') {
|
|
181
|
+
this.releasePtt();
|
|
182
|
+
}
|
|
183
|
+
|
|
134
184
|
// иначе змейка так и ехала бы к последней точке, пока игрок набирает
|
|
135
185
|
// сообщение в чат
|
|
136
186
|
this._releasePointer(this._lastTapX, this._lastTapY);
|
|
@@ -260,6 +310,20 @@ export default class ControlsModel {
|
|
|
260
310
|
|
|
261
311
|
if (isEnabled === false) {
|
|
262
312
|
this._releasePointer(this._lastTapX, this._lastTapY);
|
|
313
|
+
this.releasePtt();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// клавиша связи (push-to-talk); null выключает
|
|
318
|
+
setPushToTalk(keyCode) {
|
|
319
|
+
this._pttKey = keyCode ?? null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// отпускает связь (клавиша вверх, потеря фокуса, чат, запрет ввода)
|
|
323
|
+
releasePtt() {
|
|
324
|
+
if (this._pttHeld) {
|
|
325
|
+
this._pttHeld = false;
|
|
326
|
+
this.publisher.emit('ptt', false);
|
|
263
327
|
}
|
|
264
328
|
}
|
|
265
329
|
}
|
|
@@ -222,6 +222,24 @@ export default class GamesModel {
|
|
|
222
222
|
await this.loadAdmin();
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
// одобрение называет заявку, которую модератор видит в карточке
|
|
226
|
+
// (this._all — то, что нарисовано): переподанная после загрузки очереди
|
|
227
|
+
// заявка вернёт 409 moderationStale
|
|
228
|
+
async approve(id) {
|
|
229
|
+
const game = this._all.find(item => item.id === id);
|
|
230
|
+
|
|
231
|
+
if (!game) {
|
|
232
|
+
this._fail('admin', { error: 'unknownGame' });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await this.moderate(id, {
|
|
237
|
+
status: 'approved',
|
|
238
|
+
expectedPendingVersion: game.pendingVersion ?? null,
|
|
239
|
+
expectedPendingRepoUrl: game.pendingRepoUrl ?? null,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
225
243
|
async moderate(id, patch) {
|
|
226
244
|
const { ok, json } = await this._request(this._config.urls.moderate(id), {
|
|
227
245
|
method: 'PATCH',
|
|
@@ -229,6 +247,12 @@ export default class GamesModel {
|
|
|
229
247
|
});
|
|
230
248
|
|
|
231
249
|
if (!ok) {
|
|
250
|
+
// карточка устарела: показать свежую и только потом отказ — иначе
|
|
251
|
+
// перерисовка сотрёт его тем же тиком
|
|
252
|
+
if (json?.error === 'moderationStale') {
|
|
253
|
+
await this.loadAdmin();
|
|
254
|
+
}
|
|
255
|
+
|
|
232
256
|
this._fail('admin', json);
|
|
233
257
|
return;
|
|
234
258
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
+
|
|
3
|
+
// вспышка ошибки гаснет сама
|
|
4
|
+
const ERROR_FLASH_MS = 1500;
|
|
5
|
+
|
|
6
|
+
// Singleton VoiceModel
|
|
7
|
+
|
|
8
|
+
let voiceModel;
|
|
9
|
+
|
|
10
|
+
export default class VoiceModel {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} data
|
|
13
|
+
* @param {Object} data.texts - CONFIG_DATA.voice.texts.
|
|
14
|
+
*/
|
|
15
|
+
constructor({ texts }) {
|
|
16
|
+
if (voiceModel) {
|
|
17
|
+
return voiceModel;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
voiceModel = this;
|
|
21
|
+
|
|
22
|
+
this._texts = texts ?? {};
|
|
23
|
+
// один таймер авто-скрытия: новый показ снимает прежний, иначе таймер
|
|
24
|
+
// старой ошибки погасил бы эфир, начатый после неё
|
|
25
|
+
this._timer = null;
|
|
26
|
+
|
|
27
|
+
// 'own' ({ kind, text, endsAt, totalMs }) и 'incoming' (speaker | null)
|
|
28
|
+
this.publisher = new Publisher();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Состояние своего эфира.
|
|
33
|
+
* @param {'hidden'|'talking'|'busy'|'cooldown'|'error'} kind
|
|
34
|
+
* @param {Object} [data] - talking: { endsAt, totalMs }, cooldown:
|
|
35
|
+
* { endsAt }, error: { textKey }.
|
|
36
|
+
*/
|
|
37
|
+
showOwn(kind, data = {}) {
|
|
38
|
+
this._clearTimer();
|
|
39
|
+
|
|
40
|
+
let text = '';
|
|
41
|
+
let endsAt = 0;
|
|
42
|
+
let totalMs = 0;
|
|
43
|
+
|
|
44
|
+
if (kind === 'busy') {
|
|
45
|
+
text = this._texts.busy ?? '';
|
|
46
|
+
} else if (kind === 'cooldown') {
|
|
47
|
+
text = this._texts.cooldown ?? '';
|
|
48
|
+
endsAt = data.endsAt;
|
|
49
|
+
} else if (kind === 'error') {
|
|
50
|
+
text = this._texts[data.textKey] ?? '';
|
|
51
|
+
} else if (kind === 'talking') {
|
|
52
|
+
endsAt = data.endsAt;
|
|
53
|
+
totalMs = data.totalMs;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.publisher.emit('own', { kind, text, endsAt, totalMs });
|
|
57
|
+
|
|
58
|
+
if (kind === 'error') {
|
|
59
|
+
this._arm(ERROR_FLASH_MS);
|
|
60
|
+
} else if (kind === 'cooldown') {
|
|
61
|
+
this._arm(Math.max(0, endsAt - performance.now()));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Чужой эфир.
|
|
67
|
+
* @param {{ name: string, teamId: number|null, color: string|null }|null}
|
|
68
|
+
* speaker - null скрывает.
|
|
69
|
+
*/
|
|
70
|
+
setIncoming(speaker) {
|
|
71
|
+
this.publisher.emit('incoming', speaker);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
destroy() {
|
|
75
|
+
this._clearTimer();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_arm(ms) {
|
|
79
|
+
this._timer = setTimeout(() => {
|
|
80
|
+
this._timer = null;
|
|
81
|
+
this.publisher.emit('own', {
|
|
82
|
+
kind: 'hidden',
|
|
83
|
+
text: '',
|
|
84
|
+
endsAt: 0,
|
|
85
|
+
totalMs: 0,
|
|
86
|
+
});
|
|
87
|
+
}, ms);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_clearTimer() {
|
|
91
|
+
if (this._timer !== null) {
|
|
92
|
+
clearTimeout(this._timer);
|
|
93
|
+
this._timer = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
+
import { VOTE_ANSWER_KEYS } from '../../../config/voteKeys.js';
|
|
2
3
|
|
|
3
4
|
// Singleton VoteModel
|
|
4
5
|
|
|
@@ -215,7 +216,7 @@ export default class VoteModel {
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
// если keyCode это число от 0 до 9
|
|
218
|
-
if (
|
|
219
|
+
if (VOTE_ANSWER_KEYS[0] <= keyCode && keyCode <= VOTE_ANSWER_KEYS[1]) {
|
|
219
220
|
number = String.fromCharCode(keyCode);
|
|
220
221
|
number = parseInt(number, 10);
|
|
221
222
|
|
|
@@ -57,7 +57,7 @@ function truncate(text, max) {
|
|
|
57
57
|
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Представление журнала клиентских
|
|
60
|
+
// Представление журнала клиентских ошибок.
|
|
61
61
|
//
|
|
62
62
|
// ***** БЕЗОПАСНОСТЬ *****
|
|
63
63
|
//
|
|
@@ -19,6 +19,9 @@ const ERROR_MESSAGES = {
|
|
|
19
19
|
gameExists: 'A game with this id already exists',
|
|
20
20
|
tooManyGames: 'Too many submissions from one author',
|
|
21
21
|
unknownGame: 'Game is not in the registry',
|
|
22
|
+
// автор переподал заявку, пока модератор смотрел карточку
|
|
23
|
+
moderationStale:
|
|
24
|
+
'The request changed while you were reviewing it — check the refreshed card',
|
|
22
25
|
gamePublished: 'Published game — ask an admin to disable it first',
|
|
23
26
|
unknownUser: 'No player with this nick',
|
|
24
27
|
invalidGameId: 'Invalid game id',
|
|
@@ -270,6 +273,7 @@ export default class GamesView {
|
|
|
270
273
|
);
|
|
271
274
|
item.appendChild(this._line(this._statusLine(game)));
|
|
272
275
|
this._appendRepo(item, game.repoUrl);
|
|
276
|
+
this._appendPendingRepo(item, game);
|
|
273
277
|
|
|
274
278
|
if (game.moderatorNote) {
|
|
275
279
|
item.appendChild(this._line(`Note: ${game.moderatorNote}`));
|
|
@@ -390,6 +394,7 @@ export default class GamesView {
|
|
|
390
394
|
),
|
|
391
395
|
);
|
|
392
396
|
this._appendRepo(item, game.repoUrl);
|
|
397
|
+
this._appendPendingRepo(item, game);
|
|
393
398
|
item.appendChild(
|
|
394
399
|
this._line(
|
|
395
400
|
`Served: ${game.version ?? '—'}; requested: ${game.pendingVersion ?? '—'}` +
|
|
@@ -460,7 +465,7 @@ export default class GamesView {
|
|
|
460
465
|
// принимает только http(s): href — единственное место представления, где
|
|
461
466
|
// содержимое поля становится исполняемым (javascript:), и полагаться на
|
|
462
467
|
// одну проверку на другой стороне сети тут не стоит
|
|
463
|
-
_appendRepo(item, url) {
|
|
468
|
+
_appendRepo(item, url, prefix = '') {
|
|
464
469
|
if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) {
|
|
465
470
|
return;
|
|
466
471
|
}
|
|
@@ -472,10 +477,23 @@ export default class GamesView {
|
|
|
472
477
|
link.textContent = url;
|
|
473
478
|
link.target = '_blank';
|
|
474
479
|
link.rel = 'noopener noreferrer';
|
|
480
|
+
|
|
481
|
+
if (prefix) {
|
|
482
|
+
line.append(prefix);
|
|
483
|
+
}
|
|
484
|
+
|
|
475
485
|
line.appendChild(link);
|
|
476
486
|
item.appendChild(line);
|
|
477
487
|
}
|
|
478
488
|
|
|
489
|
+
// адрес, который версия на модерации принесёт при одобрении: строка
|
|
490
|
+
// появляется, только если он отличается от текущего
|
|
491
|
+
_appendPendingRepo(item, game) {
|
|
492
|
+
if (game.pendingRepoUrl && game.pendingRepoUrl !== game.repoUrl) {
|
|
493
|
+
this._appendRepo(item, game.pendingRepoUrl, 'After approval: ');
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
479
497
|
_statusLine(game) {
|
|
480
498
|
const status = STATUS_TITLES[game.status] ?? game.status;
|
|
481
499
|
const date = game.createdAt
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
-
import {
|
|
3
|
-
import { renderProjectLink } from '../../lib/footerLink.js';
|
|
2
|
+
import { ENGINE_VERSION } from '../../lib/engineVersion.js';
|
|
4
3
|
import { absoluteLink, formatRoomLink } from '../../lib/roomLink.js';
|
|
5
4
|
|
|
6
5
|
// Singleton LobbyView
|
|
@@ -65,11 +64,6 @@ export default class LobbyView {
|
|
|
65
64
|
version.textContent = ENGINE_VERSION;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
renderProjectLink(
|
|
69
|
-
document.getElementById(elems.linkId),
|
|
70
|
-
ENGINE_PROJECT_URL,
|
|
71
|
-
);
|
|
72
|
-
|
|
73
67
|
// заголовок игры для "<TITLE> TOP-N" (SVG-ориентир) — задаётся controller'ом
|
|
74
68
|
// при выборе игры (сама модель не хранит title манифеста)
|
|
75
69
|
this._gameTitle = '';
|