vimp-engine 0.34.0 → 0.34.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/core/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "vimp-engine-core"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.21.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "VIMP — движковый каркас симуляции (физика, карта, снапшот-фрейминг, интерполяция/предикт/raycast-примитивы, нав-утилиты). Без wasm-bindgen — обёртки для WASM-ABI собирает game-crate."
|
|
6
6
|
license = "MIT"
|
package/package.json
CHANGED
|
@@ -32,6 +32,10 @@ const RECORDED_PORTS = {
|
|
|
32
32
|
sendTechInform: 'techInform',
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// Сообщения, перед которыми отложенные кадры клиента доставляются сразу:
|
|
36
|
+
// они сбрасывают или заменяют его мир (см. routeFrame).
|
|
37
|
+
const FLUSHING_METHODS = new Set(['sendClear', 'sendMap', 'sendFirstShot']);
|
|
38
|
+
|
|
35
39
|
// В headless-контуре нет ни мастера, ни auth-сервиса — участник стартует с
|
|
36
40
|
// пустым профилем (lib/offlinePlayerData.js, общее со standalone/dedicated).
|
|
37
41
|
const emptyProfileFetch = offlinePlayerData();
|
|
@@ -133,6 +137,12 @@ export async function runScenario(rawScenario, options = {}) {
|
|
|
133
137
|
|
|
134
138
|
async function execute(scenario, plugin, virtualClock, { captureFrames }) {
|
|
135
139
|
const clients = new Map(); // socketId → VirtualClient
|
|
140
|
+
// sendShot не вталкивается в клиента сразу: кадр момента T, доставленный
|
|
141
|
+
// до render(T), сравнивался бы детектором рассинхрона с репликой на
|
|
142
|
+
// T − timeStep — ложный дрейф v · timeStep. Кадры копятся здесь и
|
|
143
|
+
// доставляются после рендер-тика (docs/*/debugging.md, «Prediction
|
|
144
|
+
// divergence detector»)
|
|
145
|
+
const pendingShots = new Map(); // socketId → [bytes]
|
|
136
146
|
const byParticipant = new Map(); // id сценария → { socketId, gameId }
|
|
137
147
|
const participantLog = []; // [{ who, socketId, gameId, joinTick, leaveTick }]
|
|
138
148
|
const mapChanges = [];
|
|
@@ -165,7 +175,8 @@ async function execute(scenario, plugin, virtualClock, { captureFrames }) {
|
|
|
165
175
|
// получить те же порты и ту же игровую параметризацию
|
|
166
176
|
createSocketManager: (ports, gameOpts) => {
|
|
167
177
|
socketManager = new RecordingSocketManager(ports, gameOpts, {
|
|
168
|
-
onFrame: frame =>
|
|
178
|
+
onFrame: frame =>
|
|
179
|
+
routeFrame(frame, clients, pendingShots, virtualClock, host),
|
|
169
180
|
});
|
|
170
181
|
|
|
171
182
|
return socketManager;
|
|
@@ -211,6 +222,7 @@ async function execute(scenario, plugin, virtualClock, { captureFrames }) {
|
|
|
211
222
|
clientCfg,
|
|
212
223
|
plugin,
|
|
213
224
|
clients,
|
|
225
|
+
pendingShots,
|
|
214
226
|
byParticipant,
|
|
215
227
|
participantLog,
|
|
216
228
|
inputSeq,
|
|
@@ -224,8 +236,9 @@ async function execute(scenario, plugin, virtualClock, { captureFrames }) {
|
|
|
224
236
|
await virtualClock.advance(stepMs);
|
|
225
237
|
}
|
|
226
238
|
|
|
227
|
-
for (const client of clients
|
|
239
|
+
for (const [socketId, client] of clients) {
|
|
228
240
|
client.render(virtualClock.monotonic());
|
|
241
|
+
deliverShots(socketId, clients, pendingShots, virtualClock);
|
|
229
242
|
}
|
|
230
243
|
|
|
231
244
|
if (dumpTicks.has(tick)) {
|
|
@@ -240,6 +253,16 @@ async function execute(scenario, plugin, virtualClock, { captureFrames }) {
|
|
|
240
253
|
}
|
|
241
254
|
}
|
|
242
255
|
|
|
256
|
+
// кадры последнего advance доставляются уже после финального дампа (срез
|
|
257
|
+
// сцены не меняется), но ещё раз проходят рендер: без него проверки
|
|
258
|
+
// finiteValues/hotLayout/renderCoverage не видели бы последнего тика, а
|
|
259
|
+
// агрегаты детектора (render вычерпывает их первым делом) — его кадров.
|
|
260
|
+
// Время то же — реплика не шагает повторно
|
|
261
|
+
for (const [socketId, client] of clients) {
|
|
262
|
+
deliverShots(socketId, clients, pendingShots, virtualClock);
|
|
263
|
+
client.render(virtualClock.monotonic());
|
|
264
|
+
}
|
|
265
|
+
|
|
243
266
|
return buildReport({
|
|
244
267
|
scenario,
|
|
245
268
|
plugin,
|
|
@@ -261,13 +284,31 @@ async function execute(scenario, plugin, virtualClock, { captureFrames }) {
|
|
|
261
284
|
|
|
262
285
|
// исходящий кадр хоста → клиент-получатель. sendShot несёт байты, которые в
|
|
263
286
|
// проде ушли бы в data channel — их и скармливаем клиентскому ядру
|
|
264
|
-
function routeFrame(frame, clients, virtualClock, host) {
|
|
287
|
+
function routeFrame(frame, clients, pendingShots, virtualClock, host) {
|
|
265
288
|
const client = clients.get(frame.socketId);
|
|
266
289
|
|
|
267
290
|
if (!client) {
|
|
268
291
|
return;
|
|
269
292
|
}
|
|
270
293
|
|
|
294
|
+
// кадр ждёт рендер-тика своего момента (см. pendingShots в runScenario)
|
|
295
|
+
if (frame.method === 'sendShot') {
|
|
296
|
+
if (!pendingShots.has(frame.socketId)) {
|
|
297
|
+
pendingShots.set(frame.socketId, []);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
pendingShots.get(frame.socketId).push(frame.args[0]);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// отложенные кадры выпускают только сообщения, меняющие мир клиента: кадр
|
|
305
|
+
// старой карты не должен доехать после CLEAR/MAP/первого снапшота. Пинг
|
|
306
|
+
// (каждые rttPingInterval), панель и прочие очередь не трогают — иначе
|
|
307
|
+
// кадр их тика доставлялся бы до рендера, на шаг раньше реплики
|
|
308
|
+
if (FLUSHING_METHODS.has(frame.method)) {
|
|
309
|
+
deliverShots(frame.socketId, clients, pendingShots, virtualClock);
|
|
310
|
+
}
|
|
311
|
+
|
|
271
312
|
// pong отвечается мгновенно: латентность headless-прогона равна нулю, и
|
|
272
313
|
// это единственная честная её модель
|
|
273
314
|
if (frame.method === 'sendPing') {
|
|
@@ -275,11 +316,6 @@ function routeFrame(frame, clients, virtualClock, host) {
|
|
|
275
316
|
return;
|
|
276
317
|
}
|
|
277
318
|
|
|
278
|
-
if (frame.method === 'sendShot') {
|
|
279
|
-
client.pushFrame(frame.args[0], virtualClock.monotonic());
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
319
|
// первый снапшот мира: в браузере он применяется сразу (applyShot), а не
|
|
284
320
|
// едет через буфер интерполяции — сущность, доехавшая только им, иначе не
|
|
285
321
|
// попадёт ни в сцену, ни в проверки
|
|
@@ -307,10 +343,27 @@ function routeFrame(frame, clients, virtualClock, host) {
|
|
|
307
343
|
}
|
|
308
344
|
}
|
|
309
345
|
|
|
346
|
+
function deliverShots(socketId, clients, pendingShots, virtualClock) {
|
|
347
|
+
const queue = pendingShots.get(socketId);
|
|
348
|
+
|
|
349
|
+
if (!queue?.length) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
pendingShots.delete(socketId);
|
|
354
|
+
|
|
355
|
+
const client = clients.get(socketId);
|
|
356
|
+
|
|
357
|
+
for (const bytes of queue) {
|
|
358
|
+
client?.pushFrame(bytes, virtualClock.monotonic());
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
310
362
|
async function applyOp(op, ctx) {
|
|
311
363
|
const {
|
|
312
364
|
host,
|
|
313
365
|
clients,
|
|
366
|
+
pendingShots,
|
|
314
367
|
byParticipant,
|
|
315
368
|
participantLog,
|
|
316
369
|
inputSeq,
|
|
@@ -333,6 +386,7 @@ async function applyOp(op, ctx) {
|
|
|
333
386
|
// текучкой участников растит процесс
|
|
334
387
|
clients.get(entry.socketId)?.destroy();
|
|
335
388
|
clients.delete(entry.socketId);
|
|
389
|
+
pendingShots.delete(entry.socketId);
|
|
336
390
|
byParticipant.delete(op.who);
|
|
337
391
|
|
|
338
392
|
const logged = participantLog.find(
|
|
@@ -222,7 +222,7 @@ class VirtualClient {
|
|
|
222
222
|
render(localNow) {
|
|
223
223
|
// записи копятся в ядре на push_frame, поэтому вычерпываются до sample —
|
|
224
224
|
// иначе тик с пустым hot-буфером (ранний выход ниже) их бы потерял
|
|
225
|
-
this.
|
|
225
|
+
this.drainDivergence();
|
|
226
226
|
|
|
227
227
|
const len = this.core.sample(localNow);
|
|
228
228
|
|
|
@@ -299,8 +299,9 @@ class VirtualClient {
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
// Записи детектора рассинхрона предикта из ядра. Агрегаты накопительные,
|
|
302
|
-
// поэтому просто перезаписываются; записи — вычерпываются.
|
|
303
|
-
|
|
302
|
+
// поэтому просто перезаписываются; записи — вычерпываются. Публичный:
|
|
303
|
+
// раннер вычерпывает кадры, доставленные после последнего рендер-тика.
|
|
304
|
+
drainDivergence() {
|
|
304
305
|
if (typeof this.core.take_divergence !== 'function') {
|
|
305
306
|
return;
|
|
306
307
|
}
|
|
@@ -40,6 +40,10 @@ export default class FakeClientCore {
|
|
|
40
40
|
this._inputs = []; // [{ time, vy }] в локальном времени
|
|
41
41
|
this._base = null; // { x, y, time, vy } — последнее авторитетное, в серверном
|
|
42
42
|
this._offset = 0;
|
|
43
|
+
// локальное время последнего sample(): реплика Rust-ядра шагает только
|
|
44
|
+
// в нём, и predicted_state() отдаёт состояние на этот момент, а не на
|
|
45
|
+
// момент кадра — фикстура сравнивает так же, чтобы сдвиг был виден
|
|
46
|
+
this._sampledAt = null;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
push_frame(data, localNow = 0) {
|
|
@@ -66,7 +70,9 @@ export default class FakeClientCore {
|
|
|
66
70
|
return 0;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
sample() {
|
|
73
|
+
sample(localNow = 0) {
|
|
74
|
+
this._sampledAt = localNow;
|
|
75
|
+
|
|
70
76
|
if (!this._frame || !this._hotKey) {
|
|
71
77
|
this._hot = new Float32Array(0);
|
|
72
78
|
|
|
@@ -157,7 +163,11 @@ export default class FakeClientCore {
|
|
|
157
163
|
const time = frame.serverTime;
|
|
158
164
|
|
|
159
165
|
if (this._base) {
|
|
160
|
-
|
|
166
|
+
// до первого sample() момента реплики ещё нет — сравнение на момент
|
|
167
|
+
// кадра, как было до учёта sample (сдвига в этот момент нет)
|
|
168
|
+
const predicted = this._predictAt(
|
|
169
|
+
this._sampledAt === null ? time : this._sampledAt + this._offset,
|
|
170
|
+
);
|
|
161
171
|
|
|
162
172
|
this._compare(predicted, authoritative, frame);
|
|
163
173
|
}
|
|
@@ -273,6 +283,7 @@ export default class FakeClientCore {
|
|
|
273
283
|
this._hot = new Float32Array(0);
|
|
274
284
|
this._base = null;
|
|
275
285
|
this._inputs = [];
|
|
286
|
+
this._sampledAt = null;
|
|
276
287
|
}
|
|
277
288
|
|
|
278
289
|
// зеркало ClientState::debug_json; у фикстуры буфер — один последний
|