vimp-engine 0.19.0 → 0.21.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/package.json +1 -1
- package/src/client/components/controller/Lobby.js +42 -1
- package/src/client/components/model/Stat.js +123 -2
- package/src/client/components/view/Lobby.js +32 -1
- package/src/client/components/view/Stat.js +43 -0
- package/src/client/lib/accolades.js +56 -0
- package/src/client/main.js +52 -11
- package/src/client/style.css +45 -0
- package/src/client/views/includes/lobby.pug +6 -0
- package/src/config/lobby.js +81 -0
- package/src/config/master.js +30 -0
- package/src/config/opcodes.js +3 -1
- package/src/config/wsports.js +3 -0
- package/src/devtools/contract/rules/c11-stat-mode.js +60 -0
- package/src/devtools/contract/rules/c4-component-dependencies.js +9 -2
- package/src/devtools/contract/rules/c6-stat-columns.js +29 -1
- package/src/devtools/contract/rules/index.js +2 -1
- package/src/host/HostGame.js +114 -5
- package/src/host/meta/SocketManager.js +11 -0
- package/src/host/meta/core/RoundManager.js +9 -4
- package/src/host/meta/modules/Accolades.js +283 -0
- package/src/host/meta/modules/PlayerDataSync.js +484 -85
- package/src/lib/validators.js +47 -0
package/src/config/lobby.js
CHANGED
|
@@ -44,12 +44,87 @@ export default {
|
|
|
44
44
|
stateUrl: '/auth/state',
|
|
45
45
|
},
|
|
46
46
|
|
|
47
|
+
// синхронизация профилей участников с мастером (snakes-v3 этап 3):
|
|
48
|
+
// пределами записи в БД владеет движок, а не игра — «игр сотни, серверов
|
|
49
|
+
// сотни», и одна игра, зовущая flushPlayerData() каждую секунду, не должна
|
|
50
|
+
// мочь положить auth-сервис. Игра может попросить синхронизацию, но не
|
|
51
|
+
// может участить её сверх minFlushInterval (кроме срочных границ:
|
|
52
|
+
// уход участника и destroy() комнаты)
|
|
53
|
+
playerData: {
|
|
54
|
+
// результат игры (PUT { points, best }); GET рангов больше не ходит
|
|
55
|
+
// сюда — три среза приезжают одним запросом на placementsUrl
|
|
56
|
+
rankUrl: '/auth/rank',
|
|
57
|
+
stateUrl: '/auth/state',
|
|
58
|
+
// агрегирующий роут мастера: { day, month, all } за один поход хоста
|
|
59
|
+
placementsUrl: '/auth/placements',
|
|
60
|
+
// точечный перезапрос одного среза (refreshPlacement)
|
|
61
|
+
placementUrl: '/auth/placement',
|
|
62
|
+
// ***** ОТКУДА ЭТО ЧИСЛО *****
|
|
63
|
+
//
|
|
64
|
+
// Целевой масштаб — 100 игр × 100 серверов × 8 игроков = 80 000 игроков
|
|
65
|
+
// одновременно. Участник со свежим результатом стоит двух запросов
|
|
66
|
+
// (PUT rank + PUT state) за интервал, поэтому весь мир пишет
|
|
67
|
+
//
|
|
68
|
+
// 80 000 × 2 / интервал.
|
|
69
|
+
//
|
|
70
|
+
// 60 с → 2700 запросов/с — на каждый пишущая транзакция, и это без
|
|
71
|
+
// единого всплеска: столько auth-сервис держать не обязан;
|
|
72
|
+
// 300 с → 530 запросов/с — с запасом, и запас этот нужен на всплески
|
|
73
|
+
// (конец раунда синхронизирует комнату целиком).
|
|
74
|
+
//
|
|
75
|
+
// Платится за это СВЕЖЕСТЬЮ ГЛОБАЛЬНЫХ рейтингов, и только их: очки
|
|
76
|
+
// склеиваются в памяти комнаты (сумма складывается, максимум берётся
|
|
77
|
+
// максимумом — finishGame), поэтому не теряется ничего, а свои значения
|
|
78
|
+
// игрок видит сразу, локально. Срочные границы (уход участника, destroy
|
|
79
|
+
// комнаты) интервал обходят, так что «ушёл и потерял» тоже не про это.
|
|
80
|
+
minFlushInterval: 300000, // мс на участника
|
|
81
|
+
flushJitter: 0.2, // ±20 % на комнату: сотни серверов по круглому таймеру
|
|
82
|
+
// потолок очереди запросов комнаты. Держится СТРОГО НИЖЕ потолка
|
|
83
|
+
// мастера (master:playerData:writesPerMinute / 60 = 2/с): очередь должна
|
|
84
|
+
// тормозить сама, а не через 429 — отказ стоит round-trip'а и уводит в
|
|
85
|
+
// бэкофф всю комнату, то есть задерживает и тех, кто ни при чём.
|
|
86
|
+
// Комната на 32 при интервале в 5 минут выпускает 64 запроса — при 1/с
|
|
87
|
+
// это чуть больше минуты, вчетверо быстрее следующего интервала
|
|
88
|
+
maxRequestsPerSecond: 1,
|
|
89
|
+
// Пауза комнаты после 5xx/429/сетевого сбоя, экспоненциальная. Обе
|
|
90
|
+
// границы соразмерны minFlushInterval, и это не украшение: потолок НИЖЕ
|
|
91
|
+
// интервала не значил бы ничего — обычный flush и так ждёт интервал,
|
|
92
|
+
// пауза короче него не отложила бы ни одного запроса, и бэкофф оказался
|
|
93
|
+
// бы мёртвым кодом. Поэтому 30 с (заметно, но не наказание за одну
|
|
94
|
+
// осечку) → 15 минут (втрое дольше интервала: лежащий сервис комната
|
|
95
|
+
// перестаёт трогать почти совсем)
|
|
96
|
+
backoff: { baseMs: 30000, maxMs: 900000 },
|
|
97
|
+
placementTtl: 30000, // троттлинг refreshPlacement, мс
|
|
98
|
+
},
|
|
99
|
+
|
|
47
100
|
// рейтинг игры (lobby-page-plan): публичный топ-N и позиция вызывающего,
|
|
48
101
|
// проксируемые мастером под тем же origin — правки CSP не нужны
|
|
49
102
|
leaderboardUrl: '/auth/leaderboard',
|
|
50
103
|
placementUrl: '/auth/placement',
|
|
51
104
|
leaderboardLimit: 10,
|
|
52
105
|
|
|
106
|
+
// награды за место в глобальном топе (snakes-v3 этап 4): хост комнаты
|
|
107
|
+
// периодически спрашивает тот же публичный топ, что рисует лобби, и
|
|
108
|
+
// рассылает участникам их места. Награда про игрока, а не про комнату,
|
|
109
|
+
// поэтому источник глобальный, а сопоставление — по нику. Запрос идёт с
|
|
110
|
+
// If-None-Match: неизменившийся топ стоит 304 и ни одного обращения к БД
|
|
111
|
+
accolades: {
|
|
112
|
+
refreshInterval: 45000, // мс между опросами топа
|
|
113
|
+
// срезы, за которыми ходит хост: ключ ответа -> ?period=
|
|
114
|
+
periods: { daily: 'day', monthly: 'month' },
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// rank-periods: срезы рейтинга и тот, что открыт по умолчанию. Порядок
|
|
118
|
+
// здесь — порядок кнопок; `id` едет в auth как ?period=, `title` идёт в
|
|
119
|
+
// заголовок списка. Значения должны совпадать с RANK_PERIODS auth-сервиса:
|
|
120
|
+
// на всё прочее он отвечает 400
|
|
121
|
+
leaderboardPeriods: [
|
|
122
|
+
{ id: 'day', title: 'TODAY' },
|
|
123
|
+
{ id: 'month', title: 'THIS MONTH' },
|
|
124
|
+
{ id: 'all', title: 'ALL-TIME' },
|
|
125
|
+
],
|
|
126
|
+
defaultLeaderboardPeriod: 'all',
|
|
127
|
+
|
|
53
128
|
// переподключение сигнального WS хоста (комната без него выпадает из
|
|
54
129
|
// выдачи мастера): экспоненциальный бэкофф от baseDelay до maxDelay (мс)
|
|
55
130
|
reconnect: {
|
|
@@ -93,6 +168,12 @@ export default {
|
|
|
93
168
|
serversContentId: 'lobby-servers-content',
|
|
94
169
|
leaderboardContentId: 'lobby-leaderboard-content',
|
|
95
170
|
leaderboardListId: 'lobby-leaderboard-list',
|
|
171
|
+
// кнопки срезов (rank-periods): id периода -> id элемента
|
|
172
|
+
periodBtnIds: {
|
|
173
|
+
day: 'btn-period-day',
|
|
174
|
+
month: 'btn-period-month',
|
|
175
|
+
all: 'btn-period-all',
|
|
176
|
+
},
|
|
96
177
|
leaderboardTitleId: 'leaderboard-title',
|
|
97
178
|
leaderboardTotalId: 'leaderboard-total',
|
|
98
179
|
myPlacementId: 'lobby-my-placement',
|
package/src/config/master.js
CHANGED
|
@@ -29,6 +29,10 @@ export default {
|
|
|
29
29
|
// пакетами @vimp-games/* из node_modules (master/localGames.js), чтобы
|
|
30
30
|
// прилинкованная игра попадала в лобби без правки этого массива: он
|
|
31
31
|
// публикуется вместе с пакетом vimp-engine
|
|
32
|
+
// maxGameScore (snakes-v3 этап 3.3, необязательное поле рядом с
|
|
33
|
+
// id/package) — потолок результата ОДНОЙ игры для этой игры; мастер
|
|
34
|
+
// клампит им `best`/`points` PUT /auth/rank. Не задан — дефолт
|
|
35
|
+
// master:playerData:maxGameScore
|
|
32
36
|
games: [{ id: 'tanks', package: '@vimp-games/tanks' }],
|
|
33
37
|
|
|
34
38
|
// список серверов (GET /servers)
|
|
@@ -60,6 +64,32 @@ export default {
|
|
|
60
64
|
maxLimit: 100,
|
|
61
65
|
},
|
|
62
66
|
|
|
67
|
+
// GET /auth/placement + агрегирующий GET /auth/placements (snakes-v3
|
|
68
|
+
// этап 3.3): место меняется медленно, а каждый вход участника стоит трёх
|
|
69
|
+
// срезов. Кэш здесь про round-trip до auth; стоимость самого запроса
|
|
70
|
+
// снята на стороне auth (RankDistribution)
|
|
71
|
+
placement: {
|
|
72
|
+
cacheTtl: 30000,
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// пределы записи профилей в БД (snakes-v3 этап 3, решение пользователя 9):
|
|
76
|
+
// «игр сотни, серверов сотни» — минимальный интервал держит движок на
|
|
77
|
+
// стороне хоста, а мастер держит потолок для сломанного или злонамеренного
|
|
78
|
+
// сервера, который этот интервал обошёл
|
|
79
|
+
playerData: {
|
|
80
|
+
// PUT /auth/rank + /auth/state на комнату (проверенный hostId) в минуту.
|
|
81
|
+
// Честная комната на 32 при lobbyConfig.playerData.minFlushInterval в
|
|
82
|
+
// 5 минут пишет 64 запроса за эти 5 минут, то есть ~13/мин; остальное —
|
|
83
|
+
// запас на срочные границы (уход участника обходит интервал), и его
|
|
84
|
+
// хватает даже комнате, полностью сменившей состав дважды за минуту.
|
|
85
|
+
// Потолок держит не честную комнату, а сломанную или злонамеренную,
|
|
86
|
+
// поэтому запас считается от честной, а не «пусть будет побольше»
|
|
87
|
+
writesPerMinute: 120,
|
|
88
|
+
// потолок результата ОДНОЙ игры, если игра не объявила свой
|
|
89
|
+
// (master:games[].maxGameScore): обоснование — plan/snakes-v3/stage_2.md
|
|
90
|
+
maxGameScore: 10000,
|
|
91
|
+
},
|
|
92
|
+
|
|
63
93
|
// рейтинг хостера комнаты (server-rating этап 2, plan/server-rating/
|
|
64
94
|
// stage_2.md): /like·/unlike гостей комнаты вместо соц-модерации /ban.
|
|
65
95
|
// Дефолт движка для всех игр; auth-сервис хранит и клампит рейтинг
|
package/src/config/opcodes.js
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
// в plugin-api.md), движок больше не выводит контролы из типа значения
|
|
10
10
|
// v3: набор control сокращён до нативных элементов — 'select'|'text'|
|
|
11
11
|
// 'checkbox'|'radio' (были ещё 'range'/'number'/'toggle'/'segmented')
|
|
12
|
-
|
|
12
|
+
// v4: порт ACCOLADES_DATA (места участников в глобальном топе) и пятый
|
|
13
|
+
// сервис пула зависимостей клиента (`accolades`) — snakes-v3 этап 4
|
|
14
|
+
export const ENGINE_API_VERSION = 4;
|
|
13
15
|
|
|
14
16
|
// версия формата кадра: первый байт после порта;
|
|
15
17
|
// увеличивать при любом изменении байтовой раскладки в ядре
|
package/src/config/wsports.js
CHANGED
|
@@ -19,6 +19,9 @@ export default {
|
|
|
19
19
|
CHAT_DATA: 15,
|
|
20
20
|
VOTE_DATA: 16,
|
|
21
21
|
KEYSET_DATA: 17,
|
|
22
|
+
// места участников в глобальном топе (snakes-v3 этап 4): косметика
|
|
23
|
+
// для parts игры, в снапшот не идёт
|
|
24
|
+
ACCOLADES_DATA: 18,
|
|
22
25
|
},
|
|
23
26
|
// порты получения данных от клиента
|
|
24
27
|
client: {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ERROR, skip, verdict } from '../result.js';
|
|
2
|
+
|
|
3
|
+
// Режим статистики по Tab объявляется ДВАЖДЫ, и обе половины обязаны
|
|
4
|
+
// совпадать (snakes-v3 этап 4):
|
|
5
|
+
//
|
|
6
|
+
// gameConfig.statMode — читает хост (host/HostGame.js);
|
|
7
|
+
// modules.stat.params.mode — читает клиент (client/…/model/Stat.js).
|
|
8
|
+
//
|
|
9
|
+
// Рассинхронизация не падает и ничем себя не выдаёт, поэтому её ловит
|
|
10
|
+
// правило, а не рантайм:
|
|
11
|
+
//
|
|
12
|
+
// объявлен только клиент — хост каждый тик собирает и рассылает таблицу
|
|
13
|
+
// комнаты, которую клиент выбрасывает первой же строкой update();
|
|
14
|
+
// объявлен только хост — хост не шлёт таблицу вовсе, а клиент рисует ту
|
|
15
|
+
// самую таблицу комнаты и она навсегда остаётся пустой.
|
|
16
|
+
//
|
|
17
|
+
// Первое стоит трафика, второе — пустого экрана; оба варианта — дефект
|
|
18
|
+
// конфигурации, а не выбор.
|
|
19
|
+
export default {
|
|
20
|
+
id: 'C11',
|
|
21
|
+
name: 'statMode',
|
|
22
|
+
level: ERROR,
|
|
23
|
+
title: 'gameConfig.statMode and modules.stat.params.mode agree',
|
|
24
|
+
|
|
25
|
+
check(ctx) {
|
|
26
|
+
const params = ctx.clientConfig?.modules?.stat?.params;
|
|
27
|
+
|
|
28
|
+
if (!params || !ctx.gameConfig) {
|
|
29
|
+
return skip('no client stat params');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const client = params.mode ?? null;
|
|
33
|
+
const host = ctx.gameConfig.statMode ?? null;
|
|
34
|
+
|
|
35
|
+
if (client === host) {
|
|
36
|
+
return verdict([], client ? `both declare '${client}'` : 'the room table');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (client && !host) {
|
|
40
|
+
return verdict([
|
|
41
|
+
`modules.stat.params.mode is '${client}', but gameConfig.statMode is ` +
|
|
42
|
+
'not set: the host keeps broadcasting the room table every tick and ' +
|
|
43
|
+
'the client throws every one of those messages away',
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (host && !client) {
|
|
48
|
+
return verdict([
|
|
49
|
+
`gameConfig.statMode is '${host}', but modules.stat.params.mode is ` +
|
|
50
|
+
'not set: the host sends no stat at all and the client draws the ' +
|
|
51
|
+
'room table, which stays empty forever',
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return verdict([
|
|
56
|
+
`gameConfig.statMode is '${host}' and modules.stat.params.mode is ` +
|
|
57
|
+
`'${client}': the two halves of the same decision disagree`,
|
|
58
|
+
]);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { ERROR, WARN, skip, verdict } from '../result.js';
|
|
2
2
|
|
|
3
|
-
// Движковых сервисов ровно
|
|
3
|
+
// Движковых сервисов ровно пять (client/main.js), но пул ими не
|
|
4
4
|
// исчерпывается: игра доливает туда свои через ClientPlugin.hooks.services(core)
|
|
5
5
|
// — например геометрию предсказанной динамики карты
|
|
6
6
|
// (docs/en/plugin-api.md «hooks.services»). Незнакомое имя не ошибка для
|
|
7
7
|
// движка: part получает undefined и рисует пустоту — карта без assetsBase
|
|
8
8
|
// выглядит как чистый холст без единой строки в консоли, ради этого правило
|
|
9
9
|
// и существует.
|
|
10
|
-
const SERVICES = [
|
|
10
|
+
const SERVICES = [
|
|
11
|
+
'renderer',
|
|
12
|
+
'soundManager',
|
|
13
|
+
'assetsBase',
|
|
14
|
+
'localPlayer',
|
|
15
|
+
// места в глобальном топе (snakes-v3 этап 4)
|
|
16
|
+
'accolades',
|
|
17
|
+
];
|
|
11
18
|
|
|
12
19
|
export default {
|
|
13
20
|
id: 'C4',
|
|
@@ -67,6 +67,19 @@ function styledColumns(styles, total) {
|
|
|
67
67
|
return covered;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Режим 'leaderboard' (snakes-v3 этап 4): движок даёт списку только
|
|
71
|
+
// каркас, внешний вид — дело игры. Достаточно любого правила плагина про
|
|
72
|
+
// `.stat-leaderboard` — раскладка колонок там на контейнере, а не на ячейках
|
|
73
|
+
function styledLeaderboard(styles) {
|
|
74
|
+
for (const [, selector] of String(styles || '').matchAll(RULE)) {
|
|
75
|
+
if (selector.includes('.stat-leaderboard')) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
export default {
|
|
71
84
|
id: 'C6',
|
|
72
85
|
name: 'statColumns',
|
|
@@ -74,12 +87,27 @@ export default {
|
|
|
74
87
|
title: 'stat columns past the engine layout are styled by the plugin',
|
|
75
88
|
|
|
76
89
|
check(ctx) {
|
|
77
|
-
const
|
|
90
|
+
const params = ctx.clientConfig?.modules?.stat?.params;
|
|
91
|
+
const columns = params?.columns;
|
|
78
92
|
|
|
79
93
|
if (!columns) {
|
|
80
94
|
return skip('no client stat columns');
|
|
81
95
|
}
|
|
82
96
|
|
|
97
|
+
// snakes-v3 этап 4: в режиме 'leaderboard' движковых таблиц нет вовсе —
|
|
98
|
+
// рисуется один список `.stat-leaderboard`, и число колонок движковой
|
|
99
|
+
// раскладке ничего не говорит. Проверять здесь надо другое: привезла
|
|
100
|
+
// ли игра свои стили под этот список
|
|
101
|
+
if (params.mode === 'leaderboard') {
|
|
102
|
+
return styledLeaderboard(ctx.clientPlugin?.styles)
|
|
103
|
+
? verdict([], 'leaderboard list, laid out by the plugin\'s own styles')
|
|
104
|
+
: verdict([
|
|
105
|
+
"stat declares mode 'leaderboard', but ClientPlugin.styles says " +
|
|
106
|
+
'nothing about #stat .stat-leaderboard: the engine CSS gives ' +
|
|
107
|
+
'the list only a bare three-column skeleton',
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
|
|
83
111
|
// меньше пяти — движковых ширин просто хватает с запасом
|
|
84
112
|
if (columns.length <= ENGINE_COLUMNS) {
|
|
85
113
|
return verdict([]);
|
|
@@ -25,6 +25,7 @@ import c7 from './c7-key-sets.js';
|
|
|
25
25
|
import c8 from './c8-baked-assets.js';
|
|
26
26
|
import c9 from './c9-chat-messages.js';
|
|
27
27
|
import c10 from './c10-auth-schema.js';
|
|
28
|
+
import c11 from './c11-stat-mode.js';
|
|
28
29
|
import d1 from './d1-snapshot-ids.js';
|
|
29
30
|
import d2 from './d2-snapshot-classes.js';
|
|
30
31
|
import d3 from './d3-snapshot-interp.js';
|
|
@@ -37,7 +38,7 @@ import e3 from './e3-sound-registry.js';
|
|
|
37
38
|
export const rules = [
|
|
38
39
|
a1, a2, a3, a4, a5, a6, a7,
|
|
39
40
|
b1, b2, b3, b4, b5, b6, b7, b8, b9, b10,
|
|
40
|
-
c1, c2, c3, c4, c5, c6, c7, c8, c9, c10,
|
|
41
|
+
c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11,
|
|
41
42
|
d1, d2, d3,
|
|
42
43
|
e1, e2, e3,
|
|
43
44
|
];
|
package/src/host/HostGame.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Panel from './meta/modules/Panel.js';
|
|
2
|
+
import Accolades from './meta/modules/Accolades.js';
|
|
2
3
|
import PlayerDataSync from './meta/modules/PlayerDataSync.js';
|
|
3
4
|
import Stat from './meta/modules/Stat.js';
|
|
4
5
|
import Chat from './meta/modules/chat/index.js';
|
|
@@ -117,6 +118,10 @@ export default class HostGame {
|
|
|
117
118
|
// endlessRound — раунд не перезапускается сам. Флаги независимы
|
|
118
119
|
this._noSpectators = data.noSpectators === true;
|
|
119
120
|
this._endlessRound = data.endlessRound === true;
|
|
121
|
+
// statMode — чем игра занимает экран по Tab. 'leaderboard' значит, что
|
|
122
|
+
// клиент рисует глобальный топ, который тянет сам: движковый stat в
|
|
123
|
+
// этом режиме никто не рисует, и платить за его рассылку незачем
|
|
124
|
+
this._statLeaderboard = data.statMode === 'leaderboard';
|
|
120
125
|
this._spectatorTeam = this._noSpectators ? null : data.spectatorTeam;
|
|
121
126
|
this._spectatorId = this._noSpectators
|
|
122
127
|
? null
|
|
@@ -146,6 +151,18 @@ export default class HostGame {
|
|
|
146
151
|
// мастера) — подменяется только там, в проде остаётся глобальный fetch
|
|
147
152
|
...(playerDataFetch ? { fetchImpl: playerDataFetch } : {}),
|
|
148
153
|
});
|
|
154
|
+
// места участников в глобальном топе (snakes-v3 этап 4): косметика для
|
|
155
|
+
// parts игры, движок не знает, какой знак игра нарисует за место
|
|
156
|
+
this._accolades = new Accolades({
|
|
157
|
+
participants: this._participants,
|
|
158
|
+
gameId: this._gameId,
|
|
159
|
+
// место и очки самого участника: их привозит PlayerDataSync на входе,
|
|
160
|
+
// и без них игрок вне топа-10 не увидел бы по Tab собственной строки
|
|
161
|
+
getRating: (id, period) => this._playerDataSync.getRating(id, period),
|
|
162
|
+
// headless-прогону некуда ходить за топом — тот же подмен, что и у
|
|
163
|
+
// PlayerDataSync
|
|
164
|
+
...(playerDataFetch ? { fetchImpl: playerDataFetch } : {}),
|
|
165
|
+
});
|
|
149
166
|
this._chat = new Chat();
|
|
150
167
|
this._vote = new Vote();
|
|
151
168
|
|
|
@@ -344,9 +361,20 @@ export default class HostGame {
|
|
|
344
361
|
removedPlayersList.pop();
|
|
345
362
|
}
|
|
346
363
|
|
|
364
|
+
// фоновый опрос глобального топа: сам решает, пора ли (refreshInterval)
|
|
365
|
+
this._accolades.tick();
|
|
366
|
+
|
|
347
367
|
const userList = this._participants.getNetworkedReady();
|
|
348
368
|
const panelUpdates = this._panel.processUpdates();
|
|
349
|
-
|
|
369
|
+
// getLast() — ЕДИНСТВЕННЫЙ дренаж Stat: reset() в _lastBody только
|
|
370
|
+
// дописывает. Поэтому буфер осушается всегда, даже когда рассылки не
|
|
371
|
+
// будет: пропуск вызова в режиме leaderboard растил бы эти массивы всё
|
|
372
|
+
// время жизни комнаты (endlessRound — она живёт часами), а StatBridge
|
|
373
|
+
// пишет в них на каждый подобранный кристалл. Гейтится ОТПРАВКА
|
|
374
|
+
const lastStat = this._stat.getLast();
|
|
375
|
+
// в режиме leaderboard движковый stat не рисуется вовсе — не шлём
|
|
376
|
+
const stat = this._statLeaderboard ? null : lastStat;
|
|
377
|
+
const accolades = this._accolades.shift();
|
|
350
378
|
const chat = this._chat.shift();
|
|
351
379
|
const vote = this._vote.shift();
|
|
352
380
|
|
|
@@ -421,6 +449,10 @@ export default class HostGame {
|
|
|
421
449
|
this._socketManager.sendStat(socketId, stat);
|
|
422
450
|
}
|
|
423
451
|
|
|
452
|
+
if (accolades) {
|
|
453
|
+
this._socketManager.sendAccolades(socketId, accolades);
|
|
454
|
+
}
|
|
455
|
+
|
|
424
456
|
const chatUser = chat || this._chat.shiftByUser(gameId);
|
|
425
457
|
if (chatUser) {
|
|
426
458
|
this._socketManager.sendChat(socketId, chatUser);
|
|
@@ -510,6 +542,20 @@ export default class HostGame {
|
|
|
510
542
|
user.isReady = true;
|
|
511
543
|
this._socketManager.sendTechInform(socketId);
|
|
512
544
|
this._socketManager.sendFirstVote(socketId);
|
|
545
|
+
|
|
546
|
+
// топ и места в нём — целиком и лично, как и первый кадр stat.
|
|
547
|
+
// Периодическая рассылка (shift() в игровом цикле) уходит только тем,
|
|
548
|
+
// кто УЖЕ готов, а места новичка посчитаны на его входе, за всю загрузку
|
|
549
|
+
// карты до этой строки: та рассылка ушла бы без него и не повторилась
|
|
550
|
+
// бы — места с тех пор не менялись, — и ни знак, ни таблица по Tab не
|
|
551
|
+
// появились бы до первого чужого входа. В комнате, куда больше никто не
|
|
552
|
+
// заходит, никогда
|
|
553
|
+
const accoladesNow = this._accolades.current();
|
|
554
|
+
|
|
555
|
+
if (Object.keys(accoladesNow.places).length) {
|
|
556
|
+
this._socketManager.sendAccolades(socketId, accoladesNow);
|
|
557
|
+
}
|
|
558
|
+
|
|
513
559
|
this._chat.pushSystem('USER_JOINED', [user.name]);
|
|
514
560
|
|
|
515
561
|
// noSpectators: голосования за вход нет — участник уже в играющей
|
|
@@ -643,7 +689,11 @@ export default class HostGame {
|
|
|
643
689
|
// иначе removeUser стартует второй flush с той же накопленной дельтой
|
|
644
690
|
// (двойной зачёт рейтинга), а destroy разрешился бы раньше, чем эти
|
|
645
691
|
// запросы уйдут
|
|
646
|
-
|
|
692
|
+
// незакрытые игры участников закрываются здесь: их очки иначе не
|
|
693
|
+
// попали бы ни в сумму, ни в максимум и пропали бы вместе с комнатой
|
|
694
|
+
// (правило 7 snakes-v3 этап 3 — запись гарантирована в destroy())
|
|
695
|
+
this._playerDataSync.finishAllGames();
|
|
696
|
+
await this._playerDataSync.flushAll({ urgent: true });
|
|
647
697
|
|
|
648
698
|
// getAll() отдаёт копию — снятие внутри цикла реестр не ломает
|
|
649
699
|
for (const user of this._participants.getAll()) {
|
|
@@ -863,6 +913,11 @@ export default class HostGame {
|
|
|
863
913
|
// вход; сбой auth-сервиса оставляет участника с дефолтами
|
|
864
914
|
this._playerDataSync.load(gameId, params.token);
|
|
865
915
|
|
|
916
|
+
// место новичка в глобальном топе: знак должен появиться сразу, а не
|
|
917
|
+
// на следующем периодическом опросе. Это пересчёт по уже известным
|
|
918
|
+
// срезам, а не поход за ними — за топом ходит только tick()
|
|
919
|
+
this._accolades.noteRoster();
|
|
920
|
+
|
|
866
921
|
queueMicrotask(() => {
|
|
867
922
|
cb(gameId);
|
|
868
923
|
});
|
|
@@ -886,9 +941,12 @@ export default class HostGame {
|
|
|
886
941
|
this._vote.removeUser(gameId);
|
|
887
942
|
this._panel.removeUser(gameId);
|
|
888
943
|
|
|
889
|
-
// финальная синхронизация
|
|
944
|
+
// финальная синхронизация профиля перед уходом участника (Этап B4):
|
|
945
|
+
// незавершённая игра закрывается — второго шанса записать её очки не
|
|
946
|
+
// будет, — и интервал синхронизации эта граница обходит (urgent)
|
|
947
|
+
this._playerDataSync.finishGame(gameId);
|
|
890
948
|
this._playerDataSync
|
|
891
|
-
.flush(gameId)
|
|
949
|
+
.flush(gameId, { urgent: true })
|
|
892
950
|
.catch(err =>
|
|
893
951
|
console.warn('[playerData] final flush failed:', err.message),
|
|
894
952
|
)
|
|
@@ -1058,11 +1116,62 @@ export default class HostGame {
|
|
|
1058
1116
|
|
|
1059
1117
|
// прибавка к рангу для игр, которые не эмитят CoreEvent::Death и потому
|
|
1060
1118
|
// никогда не проходят через RoundManager.reportKill: прямая прокладка к
|
|
1061
|
-
// PlayerDataSync, без раунд-логики и без проверки team-wipe
|
|
1119
|
+
// PlayerDataSync, без раунд-логики и без проверки team-wipe.
|
|
1120
|
+
// @deprecated snakes-v3: алиас addPlayerPoints
|
|
1062
1121
|
addPlayerRank(gameId, delta) {
|
|
1063
1122
|
this._playerDataSync.addRank(gameId, delta);
|
|
1064
1123
|
}
|
|
1065
1124
|
|
|
1125
|
+
// ***** результат игры (snakes-v3 этап 3) ***** //
|
|
1126
|
+
|
|
1127
|
+
// очки ТЕКУЩЕЙ игры участника: жизнь, раунд, матч — что игра называет
|
|
1128
|
+
// игрой. В рейтинги они попадают только на finishPlayerGame
|
|
1129
|
+
addPlayerPoints(gameId, delta) {
|
|
1130
|
+
this._playerDataSync.addPoints(gameId, delta);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// игра участника закончилась: накопленные очки уходят в сумму (месячный
|
|
1134
|
+
// рейтинг) и в максимум (дневной). Игра без раундов зовёт это сама — у
|
|
1135
|
+
// игры с раундами обе границы закрывает RoundManager
|
|
1136
|
+
finishPlayerGame(gameId) {
|
|
1137
|
+
this._playerDataSync.finishGame(gameId);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// значения среза для показа: { value, placement, total } или null
|
|
1141
|
+
getPlayerRating(gameId, period) {
|
|
1142
|
+
return this._playerDataSync.getRating(gameId, period);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// приехал ли СРЕЗ с мастера: ноль незагруженного рейтинга и настоящий
|
|
1146
|
+
// ноль — разные вещи, и игре, которая на них смотрит, надо их различать
|
|
1147
|
+
isPlayerRatingLoaded(gameId, period) {
|
|
1148
|
+
return this._playerDataSync.isRatingLoaded(gameId, period);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// точечный перезапрос места в срезе (чат-команда /rank): место меняют
|
|
1152
|
+
// чужие игры, локально его не пересчитать. Троттлинг — в PlayerDataSync
|
|
1153
|
+
refreshPlayerPlacement(gameId, period) {
|
|
1154
|
+
return this._playerDataSync.refreshPlacement(gameId, period);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// синхронизация профилей (rank/state) всех участников на мастер прямо
|
|
1158
|
+
// сейчас — для игр, у которых нет ни конца раунда, ни смены карты
|
|
1159
|
+
// (`endlessRound` + карта, пересобираемая через overrideMapData): обе
|
|
1160
|
+
// штатные границы flushAll живут в RoundManager, и такая игра не проходит
|
|
1161
|
+
// ни через одну из них. Без этого накопленный за матч ранг уезжает в auth
|
|
1162
|
+
// только на выходе участника, а закрытая вкладка хоста теряет его вовсе.
|
|
1163
|
+
//
|
|
1164
|
+
// Best-effort, как и весь PlayerDataSync: промис не отвергается, сбой
|
|
1165
|
+
// логируется в `[playerData]` и повторится следующим flush'ем.
|
|
1166
|
+
//
|
|
1167
|
+
// snakes-v3 этап 3: это просьба, а не команда — участник, у которого с
|
|
1168
|
+
// прошлой синхронизации прошло меньше lobbyConfig.playerData.
|
|
1169
|
+
// minFlushInterval, пропускается. Срочные границы (уход участника,
|
|
1170
|
+
// destroy комнаты) интервал обходят, игре они недоступны
|
|
1171
|
+
flushPlayerData({ urgent = false } = {}) {
|
|
1172
|
+
return this._playerDataSync.flushAll({ urgent });
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1066
1175
|
// hostId + per-room секрет комнаты, подтверждённые мастером при
|
|
1067
1176
|
// register_host (кодревью №1) — не известны при создании HostGame (Worker
|
|
1068
1177
|
// стартует раньше ответа мастера); нужны PlayerDataSync для атрибуции
|
|
@@ -39,6 +39,7 @@ export default class SocketManager {
|
|
|
39
39
|
this._PORT_CHAT_DATA = ports.CHAT_DATA;
|
|
40
40
|
this._PORT_VOTE_DATA = ports.VOTE_DATA;
|
|
41
41
|
this._PORT_KEYSET_DATA = ports.KEYSET_DATA;
|
|
42
|
+
this._PORT_ACCOLADES_DATA = ports.ACCOLADES_DATA;
|
|
42
43
|
|
|
43
44
|
this._soundCues = soundCues;
|
|
44
45
|
this._initialVote = initialVote;
|
|
@@ -337,6 +338,16 @@ export default class SocketManager {
|
|
|
337
338
|
this._send(socketId, this._PORT_STAT_DATA, data);
|
|
338
339
|
}
|
|
339
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Отправка мест участников в глобальном топе (snakes-v3 этап 4). Движок
|
|
343
|
+
* раздаёт числа, знак за место рисует part игры.
|
|
344
|
+
* @param {string} socketId
|
|
345
|
+
* @param {Object} data - { [gameId]: { daily, monthly } }
|
|
346
|
+
*/
|
|
347
|
+
sendAccolades(socketId, data) {
|
|
348
|
+
this._send(socketId, this._PORT_ACCOLADES_DATA, data);
|
|
349
|
+
}
|
|
350
|
+
|
|
340
351
|
/**
|
|
341
352
|
* Отправка сообщения чата.
|
|
342
353
|
* @param {string} socketId
|
|
@@ -146,8 +146,11 @@ class RoundManager {
|
|
|
146
146
|
this._stat.reset();
|
|
147
147
|
this._voteCoordinator.reset();
|
|
148
148
|
|
|
149
|
-
// синхронизация накопленных
|
|
150
|
-
// (Этап B4) — естественная граница жизненного
|
|
149
|
+
// синхронизация накопленных профилей на мастер перед сменой карты
|
|
150
|
+
// (Этап B4) — естественная граница жизненного цикла. Смена карты
|
|
151
|
+
// заканчивает игру каждого участника (snakes-v3 этап 3): без этого у
|
|
152
|
+
// игры с раундами дневной максимум навсегда остался бы нулём
|
|
153
|
+
this._playerDataSync?.finishAllGames?.();
|
|
151
154
|
this._playerDataSync?.flushAll();
|
|
152
155
|
|
|
153
156
|
this._snapshotManager.reset();
|
|
@@ -696,8 +699,10 @@ class RoundManager {
|
|
|
696
699
|
);
|
|
697
700
|
}
|
|
698
701
|
|
|
699
|
-
// синхронизация накопленных
|
|
700
|
-
// (Этап B4) — естественная граница жизненного
|
|
702
|
+
// синхронизация накопленных профилей на мастер по итогам раунда
|
|
703
|
+
// (Этап B4) — естественная граница жизненного цикла. Конец раунда и
|
|
704
|
+
// есть конец игры участника (snakes-v3 этап 3)
|
|
705
|
+
this._playerDataSync?.finishAllGames?.();
|
|
701
706
|
this._playerDataSync?.flushAll();
|
|
702
707
|
|
|
703
708
|
if (winnerTeam) {
|