@uzuhq/code-sdk 0.6.0 → 0.7.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/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* scenario ディレクトリで `uzu dev` を実行するだけで良い。 子 iframe は既存 online mode
|
|
16
16
|
* (`?server=ws://localhost:<port>` 経路) で dev-server に接続する。
|
|
17
17
|
*/
|
|
18
|
-
export type { PlayScreenMessage, BridgeChannel, BridgeMessage, Seat, SeatKind, Emit, ServerEvent, SeededRandom, GameLogic, GameConfig, SyncConfig, PatchFn, SetFn, Operation, ConnectionState, ConnectionCallbacks, PlayerVoiceState, PlayersChangedMessage, ActionContext, ActionHandler, EventHandler, EventSubscription, ScheduleOptions, Scheduler, ServerActionContext, ServerActionHandler, ServerOnlyAction, ServerOnlyActionContext, ServerOnlyActionHandlerFn, } from './types.js';
|
|
18
|
+
export type { PlayScreenMessage, BridgeChannel, BridgeMessage, Seat, SeatKind, Emit, ServerEvent, SeededRandom, GameLogic, GameConfig, SyncConfig, PatchFn, SetFn, Operation, ConnectionState, ConnectionCallbacks, PlayerVoiceState, PlayersChangedMessage, ActionArgs, ActionContext, ActionHandler, ServerActionArgs, UpdateArgs, UpdateContext, EventHandler, EventSubscription, ScheduleOptions, Scheduler, ServerActionContext, ServerActionHandler, ServerOnlyAction, ServerOnlyActionContext, ServerOnlyActionHandlerFn, } from './types.js';
|
|
19
19
|
export { SERVER_TIME, DEFAULT_ICON_URLS, SCHEDULED_ACTOR } from './types.js';
|
|
20
20
|
export { serverOnly, isServerOnlyAction } from './server-only.js';
|
|
21
21
|
export { serverNow } from './server-clock.js';
|
|
@@ -76,7 +76,12 @@ export function runLocalServerAction(config) {
|
|
|
76
76
|
const now = Date.now();
|
|
77
77
|
if (plain && !legacyServerOnly) {
|
|
78
78
|
try {
|
|
79
|
-
plain(
|
|
79
|
+
plain({
|
|
80
|
+
state,
|
|
81
|
+
payload: payload ?? {},
|
|
82
|
+
playerId: myId,
|
|
83
|
+
ctx: { now, emit: plainEmit, ...makeScheduleCtx() },
|
|
84
|
+
});
|
|
80
85
|
}
|
|
81
86
|
catch (err) {
|
|
82
87
|
console.warn('[SDK LocalServerAction] Action error:', err);
|
|
@@ -89,11 +94,11 @@ export function runLocalServerAction(config) {
|
|
|
89
94
|
return;
|
|
90
95
|
void (async () => {
|
|
91
96
|
try {
|
|
92
|
-
await server(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
...makeScheduleCtx(),
|
|
97
|
+
await server({
|
|
98
|
+
state,
|
|
99
|
+
payload: payload ?? {},
|
|
100
|
+
playerId: myId,
|
|
101
|
+
ctx: { tick, random, now, emit: serverEmit, ...makeScheduleCtx() },
|
|
97
102
|
});
|
|
98
103
|
}
|
|
99
104
|
catch (err) {
|
|
@@ -112,13 +117,16 @@ export function runLocalServerAction(config) {
|
|
|
112
117
|
const tickEvents = [];
|
|
113
118
|
const tickEmit = (name, data) => tickEvents.push({ name, data: data ?? {} });
|
|
114
119
|
try {
|
|
115
|
-
logic.update(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
logic.update({
|
|
121
|
+
state,
|
|
122
|
+
ctx: {
|
|
123
|
+
random,
|
|
124
|
+
tick,
|
|
125
|
+
now: Date.now(),
|
|
126
|
+
...makeScheduleCtx(),
|
|
127
|
+
emit: tickEmit,
|
|
128
|
+
playerInputs,
|
|
129
|
+
},
|
|
122
130
|
});
|
|
123
131
|
}
|
|
124
132
|
catch (err) {
|
|
@@ -55,9 +55,9 @@ describe('runLocalServerAction', () => {
|
|
|
55
55
|
it('actions だけなら同期で state と events が確定する', () => {
|
|
56
56
|
const h = run(baseLogic({
|
|
57
57
|
actions: {
|
|
58
|
-
move: (state,
|
|
58
|
+
move: ({ state, ctx }) => {
|
|
59
59
|
state.moves += 1;
|
|
60
|
-
emit('moved');
|
|
60
|
+
ctx.emit('moved');
|
|
61
61
|
},
|
|
62
62
|
},
|
|
63
63
|
}));
|
|
@@ -69,10 +69,10 @@ describe('runLocalServerAction', () => {
|
|
|
69
69
|
it('serverActions だけなら await 後に反映される', async () => {
|
|
70
70
|
const h = run(baseLogic({
|
|
71
71
|
serverActions: {
|
|
72
|
-
notifyExternal: async (state,
|
|
72
|
+
notifyExternal: async ({ state, ctx }) => {
|
|
73
73
|
await tick();
|
|
74
74
|
state.charged += 1;
|
|
75
|
-
emit('charged');
|
|
75
|
+
ctx.emit('charged');
|
|
76
76
|
},
|
|
77
77
|
},
|
|
78
78
|
}));
|
|
@@ -91,13 +91,13 @@ describe('runLocalServerAction', () => {
|
|
|
91
91
|
const order = [];
|
|
92
92
|
const h = run(baseLogic({
|
|
93
93
|
actions: {
|
|
94
|
-
move: (state) => {
|
|
94
|
+
move: ({ state }) => {
|
|
95
95
|
order.push('actions');
|
|
96
96
|
state.moves += 1;
|
|
97
97
|
},
|
|
98
98
|
},
|
|
99
99
|
serverActions: {
|
|
100
|
-
move: (state,
|
|
100
|
+
move: ({ state, ctx }) => {
|
|
101
101
|
order.push('serverActions');
|
|
102
102
|
// actions の結果が見えている
|
|
103
103
|
expect(state.moves).toBe(1);
|
|
@@ -124,16 +124,16 @@ describe('runLocalServerAction', () => {
|
|
|
124
124
|
it('actions の events は serverActions の await を待たない', async () => {
|
|
125
125
|
const h = run(baseLogic({
|
|
126
126
|
actions: {
|
|
127
|
-
move: (state,
|
|
127
|
+
move: ({ state, ctx }) => {
|
|
128
128
|
state.moves += 1;
|
|
129
|
-
emit('moved');
|
|
129
|
+
ctx.emit('moved');
|
|
130
130
|
},
|
|
131
131
|
},
|
|
132
132
|
serverActions: {
|
|
133
|
-
move: async (state,
|
|
133
|
+
move: async ({ state, ctx }) => {
|
|
134
134
|
await tick();
|
|
135
135
|
state.charged += 1;
|
|
136
|
-
emit('charged');
|
|
136
|
+
ctx.emit('charged');
|
|
137
137
|
},
|
|
138
138
|
},
|
|
139
139
|
}));
|
|
@@ -171,9 +171,9 @@ describe('runLocalServerAction', () => {
|
|
|
171
171
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
172
172
|
const h = run(baseLogic({
|
|
173
173
|
actions: {
|
|
174
|
-
move: (state,
|
|
174
|
+
move: ({ state, ctx }) => {
|
|
175
175
|
state.moves += 1;
|
|
176
|
-
emit('moved');
|
|
176
|
+
ctx.emit('moved');
|
|
177
177
|
},
|
|
178
178
|
},
|
|
179
179
|
serverActions: {
|
|
@@ -196,13 +196,14 @@ describe('runLocalServerAction', () => {
|
|
|
196
196
|
it('actions に入った serverOnly() は server 側として実行される', async () => {
|
|
197
197
|
const logic = baseLogic({
|
|
198
198
|
actions: {
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
199
|
+
// 検証対象は `__serverOnly` brand の判定であって、旧シグネチャの互換ではない。
|
|
200
|
+
// handler 自体は現行の呼び出し形で書いている (旧形式の logic.js は現行テンプレでは
|
|
201
|
+
// 動かない。公開済みゲームが無い前提で互換を切っている)。
|
|
202
|
+
// @ts-expect-error actions に serverOnly() を入れるのは型違反だが、brand 判定を試す
|
|
203
|
+
legacy: serverOnly(async ({ state, ctx }) => {
|
|
203
204
|
await tick();
|
|
204
205
|
state.charged += ctx.tick + 1;
|
|
205
|
-
emit('charged');
|
|
206
|
+
ctx.emit('charged');
|
|
206
207
|
}),
|
|
207
208
|
},
|
|
208
209
|
});
|
|
@@ -98,9 +98,11 @@ export function createOptimisticActionClient(config) {
|
|
|
98
98
|
// publish されてしまう。1 件ごとに直前の確定形から作り直し、成功したものだけ採用する。
|
|
99
99
|
const base = structuredClone(displayState);
|
|
100
100
|
try {
|
|
101
|
-
runPredicted(action, () => handler(
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
runPredicted(action, () => handler({
|
|
102
|
+
state: displayState,
|
|
103
|
+
payload,
|
|
104
|
+
playerId,
|
|
105
|
+
ctx: { now, emit: noopEmit, ...predictedScheduleCtx(now) },
|
|
104
106
|
}));
|
|
105
107
|
i++;
|
|
106
108
|
}
|
|
@@ -153,9 +155,11 @@ export function createOptimisticActionClient(config) {
|
|
|
153
155
|
// サーバー専用のはずの副作用がクライアントでも走る。brand を見て弾く。
|
|
154
156
|
if (handler && !isServerOnlyAction(handler) && target !== null) {
|
|
155
157
|
try {
|
|
156
|
-
runPredicted(type, () => handler(
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
runPredicted(type, () => handler({
|
|
159
|
+
state: target,
|
|
160
|
+
payload: payload ?? {},
|
|
161
|
+
playerId,
|
|
162
|
+
ctx: { now, emit: predictEmit, ...predictedScheduleCtx(now) },
|
|
159
163
|
}));
|
|
160
164
|
pendingActions.push({ seq, action: type, payload: payload ?? {}, now });
|
|
161
165
|
onState(target, playerId);
|
|
@@ -13,20 +13,23 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
13
13
|
import { createOptimisticActionClient } from './optimistic-action-client.js';
|
|
14
14
|
import { serverOnly } from '../server-only.js';
|
|
15
15
|
import { resetServerTimeOffset } from '../server-clock.js';
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* `serverOnly()` の brand が付いた handler。`actions` に入っていても先読みされないこと
|
|
18
|
+
* (brand 判定が効くこと) を確かめる。旧シグネチャの互換は検証対象ではない。
|
|
19
|
+
*/
|
|
20
|
+
const legacyServerOnly = serverOnly(({ state }) => {
|
|
18
21
|
state.charged += 1000;
|
|
19
22
|
});
|
|
20
23
|
const makeLogic = () => ({
|
|
21
24
|
setup: () => ({ moves: 0, charged: 0, stampedAt: 0 }),
|
|
22
25
|
actions: {
|
|
23
|
-
move: (state,
|
|
26
|
+
move: ({ state, ctx }) => {
|
|
24
27
|
state.moves += 1;
|
|
25
28
|
state.stampedAt = ctx.now;
|
|
26
|
-
emit('moved', {});
|
|
29
|
+
ctx.emit('moved', {});
|
|
27
30
|
},
|
|
28
31
|
// 先読みでも ctx.schedule を呼ぶ action。型にはあるので呼べてしまう。
|
|
29
|
-
startTimer: (state,
|
|
32
|
+
startTimer: ({ state, ctx }) => {
|
|
30
33
|
state.stampedAt = ctx.schedule({
|
|
31
34
|
key: 'timer',
|
|
32
35
|
after: 60,
|
|
@@ -34,15 +37,15 @@ const makeLogic = () => ({
|
|
|
34
37
|
});
|
|
35
38
|
},
|
|
36
39
|
// 予測の結果によって別の event を出す (予測ミスの再現用)
|
|
37
|
-
guess: (state, payload,
|
|
40
|
+
guess: ({ state, payload, ctx }) => {
|
|
38
41
|
state.moves += 1;
|
|
39
|
-
emit(payload?.willEmit ?? 'accepted', payload?.data ?? {});
|
|
42
|
+
ctx.emit(payload?.willEmit ?? 'accepted', payload?.data ?? {});
|
|
40
43
|
},
|
|
41
44
|
boom: () => {
|
|
42
45
|
throw new Error('always fails');
|
|
43
46
|
},
|
|
44
47
|
// 途中まで state を書き換えてから throw する (部分ミューテーションの検証用)
|
|
45
|
-
partial: (state) => {
|
|
48
|
+
partial: ({ state }) => {
|
|
46
49
|
state.moves += 1;
|
|
47
50
|
throw new Error('fails after mutating');
|
|
48
51
|
},
|
|
@@ -52,12 +55,12 @@ const makeLogic = () => ({
|
|
|
52
55
|
},
|
|
53
56
|
serverActions: {
|
|
54
57
|
// move と同名 = 同じ action の「サーバーだけで走る続き」
|
|
55
|
-
move: (state,
|
|
58
|
+
move: ({ state, ctx }) => {
|
|
56
59
|
state.charged += ctx.tick;
|
|
57
|
-
emit('charged', {});
|
|
60
|
+
ctx.emit('charged', {});
|
|
58
61
|
},
|
|
59
62
|
// actions に無い名前 = 先読みされない action (旧 serverOnly 相当)
|
|
60
|
-
notifyExternal: (state) => {
|
|
63
|
+
notifyExternal: ({ state }) => {
|
|
61
64
|
state.charged += 100;
|
|
62
65
|
},
|
|
63
66
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -66,24 +66,8 @@ export type EventHandler = (data: Record<string, unknown>) => void;
|
|
|
66
66
|
/**
|
|
67
67
|
* events の購読宣言。
|
|
68
68
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* | `predict` | 実行タイミング | 向いているもの |
|
|
73
|
-
* |---|---|---|
|
|
74
|
-
* | `true` | 先読み時に即実行。確定は待たない | 短く上書きできる SE、画面フラッシュ |
|
|
75
|
-
* | `false` | サーバー確定後に 1 回だけ | 長い演出、ハプティクス、計測、外部通知 |
|
|
76
|
-
*
|
|
77
|
-
* IMPORTANT: 先読みは外れることがあり、実行してしまったものは取り消せない。
|
|
78
|
-
* 「起きなかった出来事」で実行されて困るもの (analytics / 実績解除 / 長いカットイン)
|
|
79
|
-
* は必ず `predict: false` にすること。誤送信は静かに、そして永久に残る。
|
|
80
|
-
*
|
|
81
|
-
* ```ts
|
|
82
|
-
* events: {
|
|
83
|
-
* 'dialogue.line': { predict: true, handler: (d) => playSound('page') },
|
|
84
|
-
* 'game.finished': { predict: false, handler: () => playFanfare() },
|
|
85
|
-
* }
|
|
86
|
-
* ```
|
|
69
|
+
* 先読みは外れることがあり、実行してしまったものは取り消せない。取り消せない副作用
|
|
70
|
+
* (analytics / 実績解除 / 長い演出) は必ず `predict: false` にすること。
|
|
87
71
|
*/
|
|
88
72
|
export interface EventSubscription {
|
|
89
73
|
/** 先読み時点で実行してよいか。宣言必須。 */
|
|
@@ -111,46 +95,27 @@ export interface SeededRandom {
|
|
|
111
95
|
*/
|
|
112
96
|
export interface ActionContext {
|
|
113
97
|
/**
|
|
114
|
-
*
|
|
98
|
+
* サーバーで処理される時刻 (ms)。先読みではクロックオフセットからの推定値。
|
|
115
99
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
100
|
+
* 推定なのでサーバーとは数十 ms ずれる。時刻での分岐に使うと境界で判定が割れるので、
|
|
101
|
+
* 分岐は `update()` (サーバー専用) で行う。
|
|
118
102
|
*
|
|
119
|
-
*
|
|
120
|
-
* `if (ctx.now > deadline)` のような **分岐** に使うと境界付近でクライアントと
|
|
121
|
-
* サーバーの判定が割れ、先読みが構造ごと外れる。時刻での分岐は `update()` で行う
|
|
122
|
-
* (サーバーでしか走らないので実時刻で正確)。
|
|
123
|
-
*
|
|
124
|
-
* 再適用 (reconciliation でのやり直し) では初回予測時の値を使い回す。取り直すと
|
|
125
|
-
* やり直すたびに値がズレて表示がガタつくため。
|
|
103
|
+
* 同じ action を再適用しても値は変わらない (初回予測時の値を使い回す)。
|
|
126
104
|
*/
|
|
127
105
|
now: number;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
*
|
|
131
|
-
* IMPORTANT: クライアント先読みでは**何も予約しない** (予約はサーバーだけが持つ)。
|
|
132
|
-
* 戻り値の絶対時刻は先読みでも計算されるので、表示用の値は即座に出せる。
|
|
133
|
-
*/
|
|
106
|
+
emit: Emit;
|
|
107
|
+
/** 予約はサーバーだけが持つ。先読みでは戻り値を計算するだけで、予約も取り消しもしない。 */
|
|
134
108
|
schedule(options: ScheduleOptions): number;
|
|
135
|
-
/** 予約を取り消す。クライアント先読みでは何もしない。 */
|
|
136
109
|
unschedule(key: string): void;
|
|
137
110
|
}
|
|
138
111
|
/** 予約されたタイマーの識別子付き宣言。 */
|
|
139
112
|
export interface ScheduleOptions {
|
|
140
|
-
/**
|
|
141
|
-
* 予約の識別子。同じ key で予約し直すと**置き換わる**。
|
|
142
|
-
*
|
|
143
|
-
* key を必須にしているのは、古い予約を確実に消せるようにするため。
|
|
144
|
-
* 例: GM がタイマーを 20 分 -> 30 秒に変更したとき、同じ key で予約し直せば
|
|
145
|
-
* 古い 20 分の予約は消える。key が無いと両方が生き残り、19 分後に
|
|
146
|
-
* 「もう終わったフェーズ」を進める事故になる。
|
|
147
|
-
*/
|
|
113
|
+
/** 予約の識別子。同じ key で予約し直すと置き換わる (古い予約が残ると二重に発火する)。 */
|
|
148
114
|
key: string;
|
|
149
|
-
/**
|
|
115
|
+
/** 絶対時刻 (ms)。`after` と排他。過去なら即座に発火する。 */
|
|
150
116
|
at?: number;
|
|
151
|
-
/**
|
|
117
|
+
/** 何秒後か。`at` と排他。 */
|
|
152
118
|
after?: number;
|
|
153
|
-
/** 発火時に実行する action 名。 */
|
|
154
119
|
action: string;
|
|
155
120
|
payload?: Record<string, unknown>;
|
|
156
121
|
}
|
|
@@ -163,22 +128,12 @@ export interface ScheduleOptions {
|
|
|
163
128
|
*/
|
|
164
129
|
export interface Scheduler {
|
|
165
130
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* 表示用の `endsAt` と発火の予約が二重管理で drift しないよう、戻り値をそのまま
|
|
169
|
-
* state に入れられるようにしている。
|
|
170
|
-
*
|
|
171
|
-
* ```ts
|
|
172
|
-
* state.game.timerEndsAt = ctx.schedule({
|
|
173
|
-
* key: 'phaseTimer', after: 1200, action: 'phase.timeout', payload: { phaseId },
|
|
174
|
-
* });
|
|
175
|
-
* ```
|
|
131
|
+
* 予約して確定した絶対時刻 (ms) を返す。表示用の endsAt にそのまま使える。
|
|
176
132
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
133
|
+
* 発火は at-least-once。同じ予約が 2 回実行されうるので、handler 側で「自分が予約した
|
|
134
|
+
* ものか」を state と突き合わせて弾くこと (action 名や key では区別できない)。
|
|
179
135
|
*/
|
|
180
136
|
schedule(options: ScheduleOptions): number;
|
|
181
|
-
/** 予約を取り消す。存在しない key を渡しても何も起きない。 */
|
|
182
137
|
unschedule(key: string): void;
|
|
183
138
|
}
|
|
184
139
|
/**
|
|
@@ -194,13 +149,33 @@ export interface ServerActionContext {
|
|
|
194
149
|
random: SeededRandom;
|
|
195
150
|
/** サーバーの実時刻 (ms)。同じ dispatch の `actions` に渡る `ctx.now` と同一値。 */
|
|
196
151
|
now: number;
|
|
152
|
+
emit: Emit;
|
|
197
153
|
schedule(options: ScheduleOptions): number;
|
|
198
154
|
unschedule(key: string): void;
|
|
199
155
|
}
|
|
200
156
|
/** @deprecated `ServerActionContext` を使う。 */
|
|
201
157
|
export type ServerOnlyActionContext = ServerActionContext;
|
|
202
|
-
|
|
203
|
-
|
|
158
|
+
/**
|
|
159
|
+
* action handler の引数。
|
|
160
|
+
*
|
|
161
|
+
* オブジェクトなのは使うものだけ書けるようにするため。`ctx` を入れ子で残しているのは、
|
|
162
|
+
* 実行環境が与えるものをひとまとまりで helper へ渡せるようにするため。
|
|
163
|
+
*/
|
|
164
|
+
export interface ActionArgs<S> {
|
|
165
|
+
state: S;
|
|
166
|
+
payload: any;
|
|
167
|
+
playerId: string;
|
|
168
|
+
ctx: ActionContext;
|
|
169
|
+
}
|
|
170
|
+
export type ActionHandler<S> = (args: ActionArgs<S>) => void;
|
|
171
|
+
/** `serverActions` handler の引数。`ctx` にサーバー限定の tick / random が入る。 */
|
|
172
|
+
export interface ServerActionArgs<S> {
|
|
173
|
+
state: S;
|
|
174
|
+
payload: any;
|
|
175
|
+
playerId: string;
|
|
176
|
+
ctx: ServerActionContext;
|
|
177
|
+
}
|
|
178
|
+
export type ServerActionHandler<S> = (args: ServerActionArgs<S>) => Promise<void> | void;
|
|
204
179
|
/** @deprecated `ServerActionHandler` を使う。 */
|
|
205
180
|
export type ServerOnlyActionHandlerFn<S> = ServerActionHandler<S>;
|
|
206
181
|
/**
|
|
@@ -210,6 +185,21 @@ export type ServerOnlyActionHandlerFn<S> = ServerActionHandler<S>;
|
|
|
210
185
|
export type ServerOnlyAction<S> = ServerActionHandler<S> & {
|
|
211
186
|
readonly __serverOnly: true;
|
|
212
187
|
};
|
|
188
|
+
/** `update()` の ctx。サーバーでしか走らないので tick / random / playerInputs を持つ。 */
|
|
189
|
+
export interface UpdateContext {
|
|
190
|
+
random: SeededRandom;
|
|
191
|
+
tick: number;
|
|
192
|
+
/** サーバーの実時刻 (ms)。update はサーバーでしか走らないので常に正確。 */
|
|
193
|
+
now: number;
|
|
194
|
+
schedule(options: ScheduleOptions): number;
|
|
195
|
+
unschedule(key: string): void;
|
|
196
|
+
emit: Emit;
|
|
197
|
+
playerInputs: Record<string, Record<string, any>>;
|
|
198
|
+
}
|
|
199
|
+
export interface UpdateArgs<S> {
|
|
200
|
+
state: S;
|
|
201
|
+
ctx: UpdateContext;
|
|
202
|
+
}
|
|
213
203
|
export interface GameLogic<S> {
|
|
214
204
|
/**
|
|
215
205
|
* seats には kind !== 'player' の席 (spectator / admin) も含まれる。
|
|
@@ -232,16 +222,7 @@ export interface GameLogic<S> {
|
|
|
232
222
|
* (旧 `serverOnly()` 相当) になる。
|
|
233
223
|
*/
|
|
234
224
|
serverActions?: Record<string, ServerActionHandler<S>>;
|
|
235
|
-
update(
|
|
236
|
-
random: SeededRandom;
|
|
237
|
-
tick: number;
|
|
238
|
-
/** サーバーの実時刻 (ms)。update はサーバーでしか走らないので常に正確。 */
|
|
239
|
-
now: number;
|
|
240
|
-
schedule(options: ScheduleOptions): number;
|
|
241
|
-
unschedule(key: string): void;
|
|
242
|
-
emit: Emit;
|
|
243
|
-
playerInputs: Record<string, Record<string, any>>;
|
|
244
|
-
}): void;
|
|
225
|
+
update(args: UpdateArgs<S>): void;
|
|
245
226
|
tickRate?: number;
|
|
246
227
|
}
|
|
247
228
|
export interface GameConfig<S> extends ConnectionCallbacks {
|