vimp-engine 0.37.1 → 0.38.1
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/boot.js +1 -1
- package/src/client/components/controller/ClientReports.js +2 -1
- package/src/client/components/controller/Games.js +1 -1
- package/src/client/components/controller/LobbyAuth.js +101 -15
- package/src/client/components/controller/Profile.js +95 -0
- package/src/client/components/model/Games.js +1 -1
- package/src/client/components/model/LobbyAuth.js +382 -115
- package/src/client/components/model/Profile.js +144 -0
- package/src/client/components/view/LobbyAuth.js +150 -62
- package/src/client/components/view/Profile.js +372 -0
- package/src/client/lib/authApi.js +106 -0
- package/src/client/lib/hostCaps.js +3 -2
- package/src/client/lib/pkce.js +23 -0
- package/src/client/lib/roomLink.js +1 -1
- package/src/client/lib/sessionSync.js +45 -0
- package/src/client/lib/signalingErrors.js +3 -3
- package/src/client/lib/turnstile.js +130 -0
- package/src/client/main.js +132 -26
- package/src/client/network/TokenRenewer.js +99 -0
- package/src/client/session/GuestSession.js +63 -17
- package/src/client/session/Membership.js +2 -1
- package/src/client/style.css +128 -16
- package/src/client/views/includes/lobby.pug +11 -6
- package/src/client/views/includes/lobbyAuth.pug +34 -16
- package/src/client/views/includes/profile.pug +73 -0
- package/src/client/views/index.pug +1 -0
- package/src/config/authClient.js +55 -30
- package/src/config/chatMessages.js +1 -2
- package/src/config/env.js +2 -2
- package/src/config/lobby.js +45 -1
- package/src/config/master.js +8 -5
- package/src/config/wsports.js +4 -1
- package/src/devtools/builtinScenario.js +1 -1
- 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/b8-system-messages.js +1 -1
- package/src/devtools/invariants.js +1 -1
- package/src/devtools/pluginLoader.js +1 -1
- package/src/host/GameCoreAdapter.js +0 -3
- package/src/host/HostGame.js +28 -0
- package/src/host/PortMachine.js +50 -1
- package/src/host/meta/SocketManager.js +0 -13
- package/src/host/meta/core/RoundManager.js +0 -19
- package/src/host/meta/modules/PlayerDataSync.js +29 -3
- package/src/host/meta/modules/chat/systemMessages.js +1 -4
- package/src/lib/loadGamePackage.js +1 -1
- package/src/lib/validators.js +8 -0
- package/src/standalone/index.js +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
+
import { isSessionRejected } from '../../lib/authApi.js';
|
|
3
|
+
|
|
4
|
+
// Singleton ProfileModel
|
|
5
|
+
|
|
6
|
+
let profileModel;
|
|
7
|
+
|
|
8
|
+
// Модель панели профиля: привязка и отвязка OAuth, выход, выгрузка данных,
|
|
9
|
+
// удаление. Личность и сессию держит LobbyAuthModel, сеть — authApi; сама
|
|
10
|
+
// модель только оркестрирует и сообщает исход событиями.
|
|
11
|
+
//
|
|
12
|
+
// События: profile <profile>, busy <boolean>, export { data, fileName },
|
|
13
|
+
// left (выход или удаление — вкладка меняет личность),
|
|
14
|
+
// error { area: 'link' | 'session' | 'export' | 'delete', code }.
|
|
15
|
+
export default class ProfileModel {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Object} config - Блок `profile` конфига лобби.
|
|
18
|
+
* @param {Object} deps
|
|
19
|
+
* @param {Object} deps.auth - LobbyAuthModel.
|
|
20
|
+
* @param {Object} deps.api - client/lib/authApi.js.
|
|
21
|
+
*/
|
|
22
|
+
constructor(config, { auth, api }) {
|
|
23
|
+
if (profileModel) {
|
|
24
|
+
return profileModel;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
profileModel = this;
|
|
28
|
+
|
|
29
|
+
this._config = config;
|
|
30
|
+
this._auth = auth;
|
|
31
|
+
this._api = api;
|
|
32
|
+
this._busy = false;
|
|
33
|
+
|
|
34
|
+
this.publisher = new Publisher();
|
|
35
|
+
|
|
36
|
+
auth.publisher.on('profile-changed', profile =>
|
|
37
|
+
this.publisher.emit('profile', profile),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getProfile() {
|
|
42
|
+
return this._auth.getProfile();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getClientConfig() {
|
|
46
|
+
return this._auth.getClientConfig();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// версия согласий — версия условий из /client-config: чекбоксы панели
|
|
50
|
+
// принимают именно их
|
|
51
|
+
async linkUrl(provider) {
|
|
52
|
+
return this._auth.linkUrl(provider, this.getClientConfig().legal.version);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
takeLinkResult() {
|
|
56
|
+
return this._auth.takeLinkResult();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async unlink(provider) {
|
|
60
|
+
const data = await this._run('link', () =>
|
|
61
|
+
this._api.unlinkOauth(this._auth.getSession(), provider),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (data) {
|
|
65
|
+
this._auth.setProfile(data.profile);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async signOut() {
|
|
70
|
+
const data = await this._run('session', () =>
|
|
71
|
+
this._api.logout(this._auth.getSession()),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (data !== undefined) {
|
|
75
|
+
this._leave('signedOut');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async deleteProfile(confirmNick) {
|
|
80
|
+
const data = await this._run('delete', () =>
|
|
81
|
+
this._api.deleteAccount(this._auth.getSession(), confirmNick),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (data !== undefined) {
|
|
85
|
+
this._leave('deleted');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async exportData() {
|
|
90
|
+
const data = await this._run('export', () =>
|
|
91
|
+
this._api.exportData(this._auth.getSession()),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (data) {
|
|
95
|
+
this.publisher.emit('export', {
|
|
96
|
+
data,
|
|
97
|
+
fileName: this._config.exportFileName(this.getProfile().nick),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// сессия стёрта, уведомление ждёт экран Welcome: дальше вкладка
|
|
103
|
+
// перезагружается (решение ◆13), состояние по частям не чистится
|
|
104
|
+
_leave(notice) {
|
|
105
|
+
this._auth.clearSession();
|
|
106
|
+
this._auth.setNotice(notice);
|
|
107
|
+
this.publisher.emit('left');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// один запрос за раз; успех — data тела ответа (null у 204); отказ —
|
|
111
|
+
// undefined и событие error. Если auth-сервис не нашёл сессию
|
|
112
|
+
// (401 sessionUnknown: профиль удалён или сессия закрыта в другой вкладке) —
|
|
113
|
+
// вместо error событие left с уведомлением profileGone, уже после снятия
|
|
114
|
+
// busy; запрос во время другого не стартует
|
|
115
|
+
async _run(area, request) {
|
|
116
|
+
if (this._busy) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this._busy = true;
|
|
121
|
+
this.publisher.emit('busy', true);
|
|
122
|
+
|
|
123
|
+
let res;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
res = await request();
|
|
127
|
+
} finally {
|
|
128
|
+
this._busy = false;
|
|
129
|
+
this.publisher.emit('busy', false);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (res.ok) {
|
|
133
|
+
return res.data;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isSessionRejected(res)) {
|
|
137
|
+
this._leave('profileGone');
|
|
138
|
+
} else {
|
|
139
|
+
this.publisher.emit('error', { area, code: res.error });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
+
import { ENGINE_VERSION } from '../../lib/engineVersion.js';
|
|
2
3
|
|
|
3
4
|
// Singleton LobbyAuthView
|
|
4
5
|
|
|
5
6
|
let lobbyAuthView;
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const REGISTER_ERRORS = {
|
|
9
|
+
invalidNick:
|
|
10
|
+
'Invalid nick: 2–15 characters, starting with a letter. Letters, digits, spaces, "_" and "#" are allowed; the last character is a letter, digit or "_".',
|
|
11
|
+
nickTaken: 'This nick is already taken.',
|
|
12
|
+
turnstileBlocked:
|
|
13
|
+
'Security check blocked. Please disable your ad blocker (or Brave Shields) and click Retry.',
|
|
14
|
+
turnstileRequired: 'Security check failed. Please try again.',
|
|
15
|
+
turnstileFailed: 'Security check failed. Please try again.',
|
|
16
|
+
turnstileUnavailable:
|
|
17
|
+
'Registration is temporarily unavailable. Please try again later.',
|
|
18
|
+
rateLimited: 'Too many attempts. Please try again later.',
|
|
19
|
+
termsRequired: 'The terms have been updated. Reload the page and try again.',
|
|
12
20
|
};
|
|
13
21
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
const REGISTER_ERROR_FALLBACK = 'Network error. Please try again.';
|
|
23
|
+
|
|
24
|
+
const RESTORE_ERRORS = {
|
|
25
|
+
noLinkedProfile:
|
|
26
|
+
'No profile is linked to this account. Create a profile first — you can link the account in Profile settings.',
|
|
27
|
+
denied: 'Sign-in was cancelled.',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const RESTORE_ERROR_FALLBACK = 'Sign-in failed. Please try again.';
|
|
31
|
+
|
|
32
|
+
const NOTICES = {
|
|
33
|
+
profileGone:
|
|
34
|
+
'Your profile was not found — it may have been deleted after a period of inactivity.',
|
|
35
|
+
deleted: 'Your profile has been deleted.',
|
|
36
|
+
signedOut: 'You have signed out.',
|
|
18
37
|
};
|
|
19
38
|
|
|
20
|
-
// Представление
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
39
|
+
// Представление экрана профиля: загрузка, отказ сервиса, Welcome и бейдж
|
|
40
|
+
// игрока в лобби (#lobby-user). Скрывает #lobby целиком, пока у вкладки нет
|
|
41
|
+
// личности; показывает лобби main.js, когда маршрут в него ведёт: вход по
|
|
42
|
+
// ссылке минует лобби вовсе. Текст в DOM — только через textContent.
|
|
24
43
|
export default class LobbyAuthView {
|
|
25
44
|
constructor(model, config) {
|
|
26
45
|
if (lobbyAuthView) {
|
|
@@ -29,69 +48,103 @@ export default class LobbyAuthView {
|
|
|
29
48
|
|
|
30
49
|
lobbyAuthView = this;
|
|
31
50
|
|
|
32
|
-
const { elems
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
const { elems } = config;
|
|
52
|
+
const byId = id => document.getElementById(id);
|
|
53
|
+
|
|
54
|
+
this._model = model;
|
|
55
|
+
this._container = byId(elems.containerId);
|
|
56
|
+
this._status = byId(elems.statusId);
|
|
57
|
+
this._statusText = byId(elems.statusTextId);
|
|
58
|
+
this._statusRetry = byId(elems.statusRetryId);
|
|
59
|
+
this._welcome = byId(elems.welcomeId);
|
|
60
|
+
this._footer = byId(elems.footerId);
|
|
61
|
+
this._notice = byId(elems.noticeId);
|
|
62
|
+
this._nickInput = byId(elems.nickInputId);
|
|
63
|
+
this._nickError = byId(elems.nickErrorId);
|
|
64
|
+
this._turnstileRetry = byId(elems.turnstileRetryId);
|
|
65
|
+
this._turnstile = byId(elems.turnstileId);
|
|
66
|
+
this._register = byId(elems.registerId);
|
|
67
|
+
this._terms = byId(elems.termsId);
|
|
68
|
+
this._privacy = byId(elems.privacyId);
|
|
69
|
+
byId(elems.versionId).textContent = ENGINE_VERSION;
|
|
70
|
+
this._restore = byId(elems.restoreId);
|
|
71
|
+
this._providers = byId(elems.providersId);
|
|
72
|
+
this._restoreError = byId(elems.restoreErrorId);
|
|
35
73
|
this._providerButtons = document.querySelectorAll(
|
|
36
|
-
`.${
|
|
74
|
+
`.${elems.providerClass}`,
|
|
37
75
|
);
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
41
|
-
this._loginError = document.getElementById(elems.loginErrorId);
|
|
42
|
-
this._nickSection = document.getElementById(elems.nickSectionId);
|
|
43
|
-
this._nickInput = document.getElementById(elems.nickInputId);
|
|
44
|
-
this._nickError = document.getElementById(elems.nickErrorId);
|
|
45
|
-
this._nickSubmit = document.getElementById(elems.nickSubmitId);
|
|
46
|
-
this._lobby = document.getElementById(elems.lobbyId);
|
|
47
|
-
this._user = document.getElementById(elems.userId);
|
|
48
|
-
this._userNick = document.getElementById(elems.userNickId);
|
|
49
|
-
this._userLogout = document.getElementById(elems.userLogoutId);
|
|
76
|
+
this._lobby = byId(elems.lobbyId);
|
|
77
|
+
this._user = byId(elems.userId);
|
|
78
|
+
this._userNick = byId(elems.userNickId);
|
|
50
79
|
|
|
51
80
|
this.publisher = new Publisher();
|
|
52
81
|
|
|
53
|
-
this.
|
|
54
|
-
btn.onclick = () => this.publisher.emit('login', btn.dataset.provider);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
this._nickSubmit.onclick = () => this._emitNick();
|
|
82
|
+
this._register.onclick = () => this._emitRegister();
|
|
58
83
|
this._nickInput.onkeydown = e => {
|
|
59
84
|
if (e.key === 'Enter') {
|
|
60
|
-
this.
|
|
85
|
+
this._emitRegister();
|
|
61
86
|
}
|
|
62
87
|
};
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
this._restore.onclick = () => {
|
|
89
|
+
this._providers.style.display = 'block';
|
|
90
|
+
};
|
|
91
|
+
this._providerButtons.forEach(btn => {
|
|
92
|
+
btn.onclick = () => this.publisher.emit('restore', btn.dataset.provider);
|
|
93
|
+
});
|
|
94
|
+
this._statusRetry.onclick = () => this.publisher.emit('retry');
|
|
95
|
+
this._turnstileRetry.onclick = () => this.publisher.emit('turnstile-retry');
|
|
65
96
|
|
|
66
97
|
const mp = model.publisher;
|
|
67
98
|
|
|
68
|
-
mp.on('
|
|
69
|
-
mp.on('
|
|
99
|
+
mp.on('loading', 'showLoading', this);
|
|
100
|
+
mp.on('service-error', 'showServiceError', this);
|
|
101
|
+
mp.on('welcome', 'showWelcome', this);
|
|
70
102
|
mp.on('authenticated', 'showLobby', this);
|
|
71
|
-
mp.on('
|
|
72
|
-
mp.on('
|
|
103
|
+
mp.on('register-error', 'renderRegisterError', this);
|
|
104
|
+
mp.on('restore-error', 'renderRestoreError', this);
|
|
73
105
|
}
|
|
74
106
|
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.
|
|
107
|
+
showLoading() {
|
|
108
|
+
this._showStatus('Loading…', false);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
showServiceError() {
|
|
112
|
+
this._showStatus('The profile service is unavailable.', true);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_showStatus(text, retry) {
|
|
116
|
+
this._enterAuth();
|
|
117
|
+
this._status.style.display = 'block';
|
|
118
|
+
this._welcome.style.display = 'none';
|
|
119
|
+
this._footer.style.display = 'none';
|
|
120
|
+
this._statusText.textContent = text;
|
|
121
|
+
this._statusRetry.style.display = retry ? '' : 'none';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
showWelcome({ notice = null } = {}) {
|
|
125
|
+
const clientConfig = this._model.getClientConfig();
|
|
126
|
+
const { providers } = clientConfig;
|
|
127
|
+
|
|
128
|
+
this._enterAuth();
|
|
129
|
+
this._status.style.display = 'none';
|
|
130
|
+
this._welcome.style.display = 'block';
|
|
131
|
+
this._footer.style.display = '';
|
|
132
|
+
|
|
133
|
+
this._notice.textContent = NOTICES[notice] ?? '';
|
|
134
|
+
this._terms.href = clientConfig.legal.termsUrl;
|
|
135
|
+
this._privacy.href = clientConfig.legal.privacyUrl;
|
|
81
136
|
|
|
82
137
|
this._providerButtons.forEach(btn => {
|
|
83
138
|
btn.style.display = providers.includes(btn.dataset.provider)
|
|
84
139
|
? ''
|
|
85
140
|
: 'none';
|
|
86
141
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this._container.style.display = 'block';
|
|
91
|
-
this._loginSection.style.display = 'none';
|
|
92
|
-
this._nickSection.style.display = 'block';
|
|
142
|
+
this._restore.style.display = providers.length > 0 ? '' : 'none';
|
|
143
|
+
this._providers.style.display = 'none';
|
|
144
|
+
this._restoreError.textContent = '';
|
|
93
145
|
this._nickError.textContent = '';
|
|
94
|
-
this.
|
|
146
|
+
this._turnstileRetry.style.display = 'none';
|
|
147
|
+
|
|
95
148
|
this._nickInput.focus();
|
|
96
149
|
}
|
|
97
150
|
|
|
@@ -101,17 +154,52 @@ export default class LobbyAuthView {
|
|
|
101
154
|
this._userNick.textContent = nick;
|
|
102
155
|
}
|
|
103
156
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
// ошибка регистрации по коду; null снимает текст
|
|
158
|
+
renderRegisterError(code) {
|
|
159
|
+
this._nickError.textContent =
|
|
160
|
+
code === null ? '' : (REGISTER_ERRORS[code] ?? REGISTER_ERROR_FALLBACK);
|
|
107
161
|
}
|
|
108
162
|
|
|
109
|
-
|
|
110
|
-
this.
|
|
111
|
-
|
|
163
|
+
renderRestoreError(code) {
|
|
164
|
+
this._providers.style.display = 'block';
|
|
165
|
+
this._restoreError.textContent =
|
|
166
|
+
RESTORE_ERRORS[code] ?? RESTORE_ERROR_FALLBACK;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// блокирует поле и кнопки на время запроса
|
|
170
|
+
setBusy(busy) {
|
|
171
|
+
this._nickInput.disabled = busy;
|
|
172
|
+
this._register.disabled = busy;
|
|
173
|
+
this._restore.disabled = busy;
|
|
174
|
+
this._providerButtons.forEach(btn => {
|
|
175
|
+
btn.disabled = busy;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
getTurnstileContainer() {
|
|
180
|
+
return this._turnstile;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
showTurnstileRetry(visible) {
|
|
184
|
+
this._turnstileRetry.style.display = visible ? '' : 'none';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getNickValue() {
|
|
188
|
+
return this._nickInput.value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
focusNick() {
|
|
192
|
+
this._nickInput.focus();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// экран профиля поверх скрытого лобби
|
|
196
|
+
_enterAuth() {
|
|
197
|
+
this._container.style.display = 'block';
|
|
198
|
+
this._lobby.style.display = 'none';
|
|
199
|
+
this._user.style.display = 'none';
|
|
112
200
|
}
|
|
113
201
|
|
|
114
|
-
|
|
115
|
-
this.publisher.emit('
|
|
202
|
+
_emitRegister() {
|
|
203
|
+
this.publisher.emit('register', this._nickInput.value);
|
|
116
204
|
}
|
|
117
205
|
}
|