@uzuhq/code-cli 0.3.19 → 0.3.20
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/dist/dev-server/game-room.js +34 -10
- package/package.json +1 -1
|
@@ -19,7 +19,10 @@ import { SeededRandomImpl } from './random.js';
|
|
|
19
19
|
* dev-server は SDK を import できないため実装を持つ。
|
|
20
20
|
*/
|
|
21
21
|
function isServerOnlyAction(handler) {
|
|
22
|
-
|
|
22
|
+
// handler は logic.actions[name] の結果なので undefined になりうる。型上は
|
|
23
|
+
// undefined を含まない (noUncheckedIndexedAccess 無効) ため tsc では気付けない。
|
|
24
|
+
// typeof ガードが無いと `'__serverOnly' in undefined` で TypeError になる。
|
|
25
|
+
return (typeof handler === 'function' && '__serverOnly' in handler && handler.__serverOnly === true);
|
|
23
26
|
}
|
|
24
27
|
export class GameRoom {
|
|
25
28
|
logic;
|
|
@@ -331,24 +334,45 @@ export class GameRoom {
|
|
|
331
334
|
}
|
|
332
335
|
}
|
|
333
336
|
async dispatchAction(actionName, payload, senderId, ackSeq) {
|
|
334
|
-
const
|
|
335
|
-
|
|
337
|
+
const plain = this.logic.actions[actionName];
|
|
338
|
+
// 移行期の互換: 旧 serverOnly() を actions に入れたままの logic も動かす。
|
|
339
|
+
const legacyServerOnly = isServerOnlyAction(plain) ? plain : null;
|
|
340
|
+
const serverHandler = this.logic.serverActions?.[actionName] ?? legacyServerOnly;
|
|
341
|
+
if (!plain && !serverHandler) {
|
|
336
342
|
throw new Error(`Unknown action: ${actionName}`);
|
|
337
343
|
}
|
|
338
344
|
if (!this.gameState) {
|
|
339
345
|
throw new Error('Game not started');
|
|
340
346
|
}
|
|
347
|
+
// actions 由来は送信者側で先読み時に発火済みなので ack で skip されるが、
|
|
348
|
+
// serverActions 由来は先読みで走っていないため別枠で常に配信する。
|
|
341
349
|
const events = [];
|
|
350
|
+
const serverEvents = [];
|
|
342
351
|
const emit = (name, data) => events.push({ name, data: data ?? {} });
|
|
343
|
-
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
352
|
+
const serverEmit = (name, data) => serverEvents.push({ name, data: data ?? {} });
|
|
353
|
+
// action は「全部成功か全部失敗か」にする。plain が state を変更したあと
|
|
354
|
+
// serverActions が throw すると、caller の catch が __action_error を返して
|
|
355
|
+
// broadcast されないまま変更が gameState に残り、次の無関係な broadcast の diff に
|
|
356
|
+
// 紛れて漏れる。クライアントは __action_error で予測を rollback するので、
|
|
357
|
+
// 巻き戻さないとサーバー真実と表示が食い違ったままになる。
|
|
358
|
+
const snapshot = structuredClone(this.gameState);
|
|
359
|
+
try {
|
|
360
|
+
// 決定的な部分を先に走らせ、serverActions がその結果を見られるようにする。
|
|
361
|
+
if (plain && !legacyServerOnly) {
|
|
362
|
+
plain(this.gameState, payload ?? {}, senderId, emit, {});
|
|
363
|
+
}
|
|
364
|
+
if (serverHandler) {
|
|
365
|
+
await serverHandler(this.gameState, payload ?? {}, senderId, serverEmit, {
|
|
366
|
+
tick: this.tickCount,
|
|
367
|
+
random: this.random ?? new SeededRandomImpl(this.seed),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
347
370
|
}
|
|
348
|
-
|
|
349
|
-
|
|
371
|
+
catch (err) {
|
|
372
|
+
this.gameState = snapshot;
|
|
373
|
+
throw err;
|
|
350
374
|
}
|
|
351
|
-
this.broadcastStateDelta(events, { ack: ackSeq, from: senderId });
|
|
375
|
+
this.broadcastStateDelta(events, { ack: ackSeq, from: senderId, serverEvents });
|
|
352
376
|
}
|
|
353
377
|
handleClose(ws) {
|
|
354
378
|
const attachment = this.attachments.get(ws);
|