vimp-engine 0.35.2 → 0.35.4
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
CHANGED
|
@@ -514,8 +514,13 @@ export default class SoundManager {
|
|
|
514
514
|
// rate только на изменение: Howler на каждый вызов делает два seek(),
|
|
515
515
|
// переписывает _rateSeek/_playStart и пересоздаёт таймер конца петли
|
|
516
516
|
// — на 60 Гц это лишняя нагрузка и лишние события 'end' на каждом
|
|
517
|
-
//
|
|
518
|
-
|
|
517
|
+
// обороте.
|
|
518
|
+
// И только конечный: громкость Howler проверяет сам, а rate — нет,
|
|
519
|
+
// NaN/Infinity уходят прямо в playbackRate.setValueAtTime, тот
|
|
520
|
+
// бросает TypeError, и тикер Pixi останавливается (как с позицией в
|
|
521
|
+
// _writePos). К тому же NaN !== NaN — вызов повторялся бы каждый кадр.
|
|
522
|
+
// До первого конечного значения звучит прежний rate
|
|
523
|
+
if (Number.isFinite(rate) && rate !== activeInstance.rate) {
|
|
519
524
|
sound.rate(rate, soundId);
|
|
520
525
|
activeInstance.rate = rate;
|
|
521
526
|
}
|
|
@@ -716,8 +721,11 @@ export default class SoundManager {
|
|
|
716
721
|
|
|
717
722
|
// отсечка в мировых координатах: она обязана совпадать с maxDistance
|
|
718
723
|
// самого PannerNode, поэтому зумом НЕ масштабируется — иначе движок
|
|
719
|
-
// считал бы источник слышимым там, где узел уже отдал
|
|
720
|
-
|
|
724
|
+
// считал бы источник слышимым там, где узел уже отдал тишину.
|
|
725
|
+
// «не ближе maxDistance», а не «дальше»: NaN-дистанция (координата
|
|
726
|
+
// источника или слушателя не число) иначе считалась бы слышимой на
|
|
727
|
+
// полной громкости
|
|
728
|
+
if (!(Math.hypot(dx, dy) < this._spatial.maxDistance)) {
|
|
721
729
|
sound.volume(0, soundId);
|
|
722
730
|
|
|
723
731
|
return false;
|
|
@@ -787,7 +795,8 @@ export default class SoundManager {
|
|
|
787
795
|
* @private Пишет позицию в паннер, если она изменилась заметнее
|
|
788
796
|
* POSITION_EPSILON. Единственная точка записи: у неподвижного источника
|
|
789
797
|
* позиция уже в узле, и повторная запись — только лишняя автоматизация,
|
|
790
|
-
* а именно её поток WebKit и не переносит.
|
|
798
|
+
* а именно её поток WebKit и не переносит. Неконечная позиция
|
|
799
|
+
* пропускается: не пишется и не запоминается.
|
|
791
800
|
* @param {Howl} sound - Экземпляр Howl.
|
|
792
801
|
* @param {number} soundId - ID конкретного проигрываемого экземпляра.
|
|
793
802
|
* @param {number} px - Координата X в осях Web Audio.
|
|
@@ -795,6 +804,17 @@ export default class SoundManager {
|
|
|
795
804
|
* @param {number} pz - Координата Z в осях Web Audio.
|
|
796
805
|
*/
|
|
797
806
|
_writePos(sound, soundId, px, py, pz) {
|
|
807
|
+
// Неконечная координата в узел не пишется: Web Audio бросает на ней
|
|
808
|
+
// TypeError из setValueAtTime, исключение уходит из рендер-тика в тикер
|
|
809
|
+
// Pixi, и тот больше не запрашивает кадр — игра замирает целиком.
|
|
810
|
+
// Позиции источников задаёт и игра (registerSound/updateSoundData),
|
|
811
|
+
// поэтому одного фильтра камеры (lib/applyCamera.js) здесь мало.
|
|
812
|
+
// Запомненная позиция не трогается: следующую конечную сравним с
|
|
813
|
+
// настоящей
|
|
814
|
+
if (!Number.isFinite(px) || !Number.isFinite(py) || !Number.isFinite(pz)) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
798
818
|
const written = this._pannerPos.get(soundId);
|
|
799
819
|
|
|
800
820
|
if (written !== undefined) {
|
|
@@ -14,12 +14,31 @@
|
|
|
14
14
|
* @param {object} soundManager - Экземпляр SoundManager.
|
|
15
15
|
* @param {Array | number} camera - `[x, y, cameraReset, shakeData]` из
|
|
16
16
|
* горячего буфера ядра; `0` или пустое значение — кадра камеры нет.
|
|
17
|
+
* @param {Function} [onInvalid] - Зовётся с кадром, у которого x или y — не
|
|
18
|
+
* конечное число (NaN, Infinity, undefined, null); такой кадр не применяется.
|
|
17
19
|
*/
|
|
18
|
-
export default function applyCamera(
|
|
20
|
+
export default function applyCamera(
|
|
21
|
+
canvasManager,
|
|
22
|
+
soundManager,
|
|
23
|
+
camera,
|
|
24
|
+
onInvalid,
|
|
25
|
+
) {
|
|
19
26
|
if (!camera || camera === 0) {
|
|
20
27
|
return;
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
// Неконечная координата не применяется ни к полотну, ни к слушателю.
|
|
31
|
+
// Полотну один NaN вредит надолго: CanvasManagerModel сглаживает камеру
|
|
32
|
+
// через lerp, NaN оседает в его накопителях до cameraReset — масштаб
|
|
33
|
+
// сцены NaN, картинки нет. Слушателю — сразу: NaN уходит в Howl.pos(),
|
|
34
|
+
// тот бросает, и тикер Pixi останавливается. null отсекается тоже: serde
|
|
35
|
+
// пишет неконечное число JSON-кадра как null, а в арифметике null молча
|
|
36
|
+
// стал бы нулём — прыжок камеры в начало координат
|
|
37
|
+
if (!Number.isFinite(camera[0]) || !Number.isFinite(camera[1])) {
|
|
38
|
+
onInvalid?.(camera);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
canvasManager.updateCoords(camera);
|
|
24
43
|
soundManager.setListenerPosition(
|
|
25
44
|
camera[0],
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { HOT_FLAGS } from '../../config/opcodes.js';
|
|
2
|
+
import { reconstructHot } from '../../lib/reconstructHot.js';
|
|
3
|
+
|
|
4
|
+
// Рендер-тик клиентского ядра: сэмпл, событийные кадры, горячие данные,
|
|
5
|
+
// камера. Вынесено из main.js ради одной вещи — порядка ЧТЕНИЯ, который
|
|
6
|
+
// ничем больше не защищён: main.js целиком в тесте не поднять.
|
|
7
|
+
//
|
|
8
|
+
// Hot-буфер читается view поверх памяти WASM (zero-copy), а view живёт
|
|
9
|
+
// ровно до следующего роста этой памяти: `memory.grow` отцепляет старый
|
|
10
|
+
// ArrayBuffer, длина view становится 0, hot[i] — undefined. Растить память
|
|
11
|
+
// может любой аллоцирующий вызов ядра: take_frames() собирает строку JSON
|
|
12
|
+
// в куче WASM, парты при разборе кадров сами зовут ядро. Поэтому всё нужное
|
|
13
|
+
// из буфера снимается копией СРАЗУ, а применяется в прежнем порядке.
|
|
14
|
+
// Раньше камера читалась после take_frames(): undefined в ней становился
|
|
15
|
+
// NaN, NaN навсегда оседал в сглаживании камеры (картинка пропадала до
|
|
16
|
+
// cameraReset), а слушатель звука с NaN ронял Howl.pos() — и тикер Pixi.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Один рендер-тик клиентского ядра.
|
|
20
|
+
* @param {Object} deps
|
|
21
|
+
* @param {Object} deps.core - ClientCore: sample, hot_ptr, take_frames.
|
|
22
|
+
* @param {WebAssembly.Memory} deps.memory - Память WASM клиентского ядра.
|
|
23
|
+
* @param {Object} deps.snapshotKeysById - Результат buildSnapshotKeysById.
|
|
24
|
+
* @param {number} deps.now - Время рендера (performance.now()).
|
|
25
|
+
* @param {Function} deps.applyShot - (game, camera): событийный кадр.
|
|
26
|
+
* @param {Function} deps.applyGameData - (game): горячие данные сущностей.
|
|
27
|
+
* @param {Function} deps.applyCamera - ([x, y]): камера тика.
|
|
28
|
+
*/
|
|
29
|
+
export default function runHotTick({
|
|
30
|
+
core,
|
|
31
|
+
memory,
|
|
32
|
+
snapshotKeysById,
|
|
33
|
+
now,
|
|
34
|
+
applyShot,
|
|
35
|
+
applyGameData,
|
|
36
|
+
applyCamera,
|
|
37
|
+
}) {
|
|
38
|
+
const len = core.sample(now);
|
|
39
|
+
// указатель — раньше memory.buffer: аргументы `new Float32Array(...)`
|
|
40
|
+
// вычисляются слева направо, и buffer, взятый до вызова ядра, мог бы уже
|
|
41
|
+
// оказаться отцепленным
|
|
42
|
+
const ptr = core.hot_ptr();
|
|
43
|
+
const hot = new Float32Array(memory.buffer, ptr, len);
|
|
44
|
+
const flags = hot[0];
|
|
45
|
+
// reconstructHot копирует поля (Array.from): после разбора view не нужен
|
|
46
|
+
const game =
|
|
47
|
+
flags & (HOT_FLAGS.GAME | HOT_FLAGS.PREDICTED)
|
|
48
|
+
? reconstructHot(hot, snapshotKeysById)
|
|
49
|
+
: null;
|
|
50
|
+
const camera = flags & HOT_FLAGS.CAMERA ? [hot[1], hot[2]] : null;
|
|
51
|
+
|
|
52
|
+
if (flags & HOT_FLAGS.FRAMES) {
|
|
53
|
+
JSON.parse(core.take_frames()).forEach(frame => {
|
|
54
|
+
applyShot(frame.game, frame.camera);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (game) {
|
|
59
|
+
applyGameData(game);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// камера уже разрешена ядром: предсказанная позиция либо интерполированная
|
|
63
|
+
if (camera) {
|
|
64
|
+
applyCamera(camera);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/client/main.js
CHANGED
|
@@ -52,10 +52,7 @@ import { readCoreAbi, dispatchCoreOp, ABI_UNKNOWN } from '../lib/coreAbi.js';
|
|
|
52
52
|
import { ABI_OP_DEBUG_JSON } from '../config/abiOps.js';
|
|
53
53
|
import { createDebugApi, debugLog, DEBUG_PREFIX } from './debug.js';
|
|
54
54
|
import { buildClientCoreConfig } from '../lib/clientCoreConfig.js';
|
|
55
|
-
import {
|
|
56
|
-
buildSnapshotKeysById,
|
|
57
|
-
reconstructHot,
|
|
58
|
-
} from '../lib/reconstructHot.js';
|
|
55
|
+
import { buildSnapshotKeysById } from '../lib/reconstructHot.js';
|
|
59
56
|
import Factory from '../lib/factory.js';
|
|
60
57
|
import { formatMessage } from '../lib/formatters.js';
|
|
61
58
|
import { sanitizeMessage } from '../lib/sanitizers.js';
|
|
@@ -87,7 +84,6 @@ import ClientReportsView from './components/view/ClientReports.js';
|
|
|
87
84
|
import ClientReportsCtrl from './components/controller/ClientReports.js';
|
|
88
85
|
import BakingProvider from './providers/BakingProvider.js';
|
|
89
86
|
import DependencyProvider from './providers/DependencyProvider.js';
|
|
90
|
-
import { HOT_FLAGS } from '../config/opcodes.js';
|
|
91
87
|
import wsports from '../config/wsports.js';
|
|
92
88
|
import GAME_CODES from '../config/gameCodes.js';
|
|
93
89
|
import {
|
|
@@ -99,6 +95,7 @@ import lobbyConfig from '../config/lobby.js';
|
|
|
99
95
|
import authClientConfig from '../config/authClient.js';
|
|
100
96
|
import clientDefaults from '../config/clientDefaults.js';
|
|
101
97
|
import applyCamera from './lib/applyCamera.js';
|
|
98
|
+
import runHotTick from './lib/hotTick.js';
|
|
102
99
|
|
|
103
100
|
// Динамическая загрузка игры по каталогу мастера (Этап 6.3): ClientPlugin
|
|
104
101
|
// (parts, bakers, игровой CSS, хуки ядра) грузится по entries.client манифеста
|
|
@@ -951,15 +948,28 @@ function applyGameData(game) {
|
|
|
951
948
|
});
|
|
952
949
|
}
|
|
953
950
|
|
|
951
|
+
// Камеру с NaN/undefined applyCamera не применяет (lib/applyCamera.js), но
|
|
952
|
+
// молчать о ней нельзя: это симптом сбоя выше по течению (ядро, чтение
|
|
953
|
+
// hot-буфера), и журнал — единственный способ узнать о нём с прода.
|
|
954
|
+
// String(): JSON.stringify превратил бы NaN в null, а undefined потерял бы
|
|
955
|
+
function reportBadCamera(camera) {
|
|
956
|
+
diagnostics.warn(
|
|
957
|
+
'engine.camera.non-finite',
|
|
958
|
+
{ x: String(camera[0]), y: String(camera[1]) },
|
|
959
|
+
{ source: 'client' },
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
954
963
|
// применяет кадр целиком (первый кадр и дискретные кадры интерполяции)
|
|
955
964
|
function applyShot(game, camera) {
|
|
956
965
|
applyGameData(game);
|
|
957
|
-
applyCamera(modules.canvasManager, soundManager, camera);
|
|
966
|
+
applyCamera(modules.canvasManager, soundManager, camera, reportBadCamera);
|
|
958
967
|
}
|
|
959
968
|
|
|
960
969
|
// рендер-тик: ядро выдаёт пересечённые кадры (события, создания/удаления)
|
|
961
970
|
// JSON-очередью, а горячие позиции (танки/динамика/камера + предсказанный
|
|
962
|
-
// свой танк) — плоским Float32-буфером zero-copy из памяти WASM
|
|
971
|
+
// свой танк) — плоским Float32-буфером zero-copy из памяти WASM; порядок
|
|
972
|
+
// чтения буфера — в lib/hotTick.js
|
|
963
973
|
function renderTick() {
|
|
964
974
|
if (!clientCore) {
|
|
965
975
|
return;
|
|
@@ -974,26 +984,16 @@ function renderTick() {
|
|
|
974
984
|
task();
|
|
975
985
|
}
|
|
976
986
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
if (flags & (HOT_FLAGS.GAME | HOT_FLAGS.PREDICTED)) {
|
|
990
|
-
applyGameData(reconstructHot(hot, snapshotKeysById));
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (flags & HOT_FLAGS.CAMERA) {
|
|
994
|
-
// камера уже разрешена ядром: предсказанная позиция либо интерполированная
|
|
995
|
-
applyCamera(modules.canvasManager, soundManager, [hot[1], hot[2]]);
|
|
996
|
-
}
|
|
987
|
+
runHotTick({
|
|
988
|
+
core: clientCore,
|
|
989
|
+
memory: wasm.memory,
|
|
990
|
+
snapshotKeysById,
|
|
991
|
+
now: performance.now(),
|
|
992
|
+
applyShot,
|
|
993
|
+
applyGameData,
|
|
994
|
+
applyCamera: camera =>
|
|
995
|
+
applyCamera(modules.canvasManager, soundManager, camera, reportBadCamera),
|
|
996
|
+
});
|
|
997
997
|
|
|
998
998
|
soundManager.processAudibility();
|
|
999
999
|
soundManager.updateActiveSounds();
|
|
@@ -8,9 +8,12 @@ import { HOT_FLAGS } from '../config/opcodes.js';
|
|
|
8
8
|
// процессе — без этого отказы тира 1–2 (сущность не спавнится, поля схемы
|
|
9
9
|
// перепутаны, панель не обновляется) не видны нигде, кроме чёрного холста.
|
|
10
10
|
//
|
|
11
|
-
// Рендер-конвейер повторяет client/
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// Рендер-конвейер повторяет client/lib/hotTick.js (его зовёт renderTick в
|
|
12
|
+
// client/main.js): порядок применения тот же — кадры → горячие данные →
|
|
13
|
+
// камера. Hot читается через hot_values() (копия), а не hot_ptr()+память
|
|
14
|
+
// WASM: в Node нет смысла в zero-copy, а копия не детачится при росте
|
|
15
|
+
// памяти ядра — поэтому порядок ЧТЕНИЯ, ради которого hotTick.js вынесен
|
|
16
|
+
// из main.js, здесь не важен.
|
|
14
17
|
|
|
15
18
|
// предел числа сохраняемых однотипных нарушений (дальше только счётчик)
|
|
16
19
|
const NOTE_LIMIT = 20;
|
|
@@ -216,7 +219,7 @@ class VirtualClient {
|
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
/**
|
|
219
|
-
* Рендер-тик: зеркало client/
|
|
222
|
+
* Рендер-тик: зеркало client/lib/hotTick.js без PixiJS.
|
|
220
223
|
* @param {number} localNow - Локальное время клиента (мс).
|
|
221
224
|
*/
|
|
222
225
|
render(localNow) {
|