@uzuhq/code-sdk 0.4.0 → 0.6.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 +3 -2
- package/dist/index.js +2 -1
- package/dist/run/local-server-action.d.ts +3 -2
- package/dist/run/local-server-action.js +83 -30
- package/dist/run/local-server-action.test.d.ts +1 -0
- package/dist/run/local-server-action.test.js +218 -0
- package/dist/run/optimistic-action-client.d.ts +17 -6
- package/dist/run/optimistic-action-client.js +108 -20
- package/dist/run/optimistic-action-client.test.d.ts +1 -0
- package/dist/run/optimistic-action-client.test.js +452 -0
- package/dist/run/server-action.js +3 -0
- package/dist/server-clock.d.ts +29 -0
- package/dist/server-clock.js +40 -0
- package/dist/server-only.d.ts +22 -15
- package/dist/server-only.js +10 -9
- package/dist/types.d.ts +162 -11
- package/dist/types.js +7 -0
- package/package.json +1 -1
package/dist/types.d.ts
CHANGED
|
@@ -56,6 +56,40 @@ export interface PlayersChangedMessage {
|
|
|
56
56
|
players: Record<string, PlayerVoiceState>;
|
|
57
57
|
}
|
|
58
58
|
export type Emit = (eventName: string, data?: Record<string, unknown>) => void;
|
|
59
|
+
/**
|
|
60
|
+
* events handler。
|
|
61
|
+
*
|
|
62
|
+
* `emit(name, data)` の `data` をそのまま受け取る。先読み中か確定後かは渡さない
|
|
63
|
+
* (どちらで呼ばれても同じ処理をする前提。 `predict` で宣言済みなので分岐の必要がない)。
|
|
64
|
+
*/
|
|
65
|
+
export type EventHandler = (data: Record<string, unknown>) => void;
|
|
66
|
+
/**
|
|
67
|
+
* events の購読宣言。
|
|
68
|
+
*
|
|
69
|
+
* `predict` は必須。 このイベントを「クライアント先読みの時点で実行してよいか」を
|
|
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
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export interface EventSubscription {
|
|
89
|
+
/** 先読み時点で実行してよいか。宣言必須。 */
|
|
90
|
+
predict: boolean;
|
|
91
|
+
handler: EventHandler;
|
|
92
|
+
}
|
|
59
93
|
/** dev harness で観測される 1 件の emit。`data` は `emit(name)` 省略時に空 object になる。 */
|
|
60
94
|
export interface ServerEvent {
|
|
61
95
|
name: string;
|
|
@@ -71,19 +105,109 @@ export interface SeededRandom {
|
|
|
71
105
|
* 素の action handler の実行文脈。空なのは意図的。
|
|
72
106
|
*
|
|
73
107
|
* 素の handler はサーバーとクライアント先読みの両方で走る。クライアントが自力で
|
|
74
|
-
* 再現できない値 (tick /
|
|
75
|
-
*
|
|
76
|
-
*
|
|
108
|
+
* 再現できない値 (tick / 乱数) をここで配ると、サーバー・ソロ・dev では本物が入るのに
|
|
109
|
+
* オンラインの先読みだけ値がズレる、という一番気付きにくい形で壊れる。
|
|
110
|
+
* そういう値が要る処理は `serverActions` 側に書く。
|
|
77
111
|
*/
|
|
78
|
-
export
|
|
79
|
-
/**
|
|
80
|
-
|
|
112
|
+
export interface ActionContext {
|
|
113
|
+
/**
|
|
114
|
+
* このアクションがサーバーで処理される時刻 (ms)。
|
|
115
|
+
*
|
|
116
|
+
* サーバーでは dispatch 時の実時刻。クライアント先読みでは、サーバーとのクロック
|
|
117
|
+
* オフセットから推定した値が入る。端末の時計がズレていても影響を受けない。
|
|
118
|
+
*
|
|
119
|
+
* IMPORTANT: 推定なのでサーバーの値とは数十 ms ずれる。時刻の **記録** に使うこと。
|
|
120
|
+
* `if (ctx.now > deadline)` のような **分岐** に使うと境界付近でクライアントと
|
|
121
|
+
* サーバーの判定が割れ、先読みが構造ごと外れる。時刻での分岐は `update()` で行う
|
|
122
|
+
* (サーバーでしか走らないので実時刻で正確)。
|
|
123
|
+
*
|
|
124
|
+
* 再適用 (reconciliation でのやり直し) では初回予測時の値を使い回す。取り直すと
|
|
125
|
+
* やり直すたびに値がズレて表示がガタつくため。
|
|
126
|
+
*/
|
|
127
|
+
now: number;
|
|
128
|
+
/**
|
|
129
|
+
* サーバー権威のタイマー予約。
|
|
130
|
+
*
|
|
131
|
+
* IMPORTANT: クライアント先読みでは**何も予約しない** (予約はサーバーだけが持つ)。
|
|
132
|
+
* 戻り値の絶対時刻は先読みでも計算されるので、表示用の値は即座に出せる。
|
|
133
|
+
*/
|
|
134
|
+
schedule(options: ScheduleOptions): number;
|
|
135
|
+
/** 予約を取り消す。クライアント先読みでは何もしない。 */
|
|
136
|
+
unschedule(key: string): void;
|
|
137
|
+
}
|
|
138
|
+
/** 予約されたタイマーの識別子付き宣言。 */
|
|
139
|
+
export interface ScheduleOptions {
|
|
140
|
+
/**
|
|
141
|
+
* 予約の識別子。同じ key で予約し直すと**置き換わる**。
|
|
142
|
+
*
|
|
143
|
+
* key を必須にしているのは、古い予約を確実に消せるようにするため。
|
|
144
|
+
* 例: GM がタイマーを 20 分 -> 30 秒に変更したとき、同じ key で予約し直せば
|
|
145
|
+
* 古い 20 分の予約は消える。key が無いと両方が生き残り、19 分後に
|
|
146
|
+
* 「もう終わったフェーズ」を進める事故になる。
|
|
147
|
+
*/
|
|
148
|
+
key: string;
|
|
149
|
+
/** 発火する絶対時刻 (ms)。`after` と排他。過去を指定すると即座に発火する。 */
|
|
150
|
+
at?: number;
|
|
151
|
+
/** 何秒後に発火するか。`at` と排他。 */
|
|
152
|
+
after?: number;
|
|
153
|
+
/** 発火時に実行する action 名。 */
|
|
154
|
+
action: string;
|
|
155
|
+
payload?: Record<string, unknown>;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* サーバー権威のタイマー。
|
|
159
|
+
*
|
|
160
|
+
* クライアントの時計やアプリのバックグラウンド化に依存せず、サーバーが指定時刻に
|
|
161
|
+
* 自分で起きて action を撃つ。`tickRate` で毎秒ポーリングする必要が無くなり、
|
|
162
|
+
* その間 Durable Object は hibernate できる。
|
|
163
|
+
*/
|
|
164
|
+
export interface Scheduler {
|
|
165
|
+
/**
|
|
166
|
+
* 予約する。戻り値は確定した絶対時刻 (ms)。
|
|
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
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* IMPORTANT: 発火は at-least-once。同じ予約が 2 回実行されうるので、handler 側で
|
|
178
|
+
* 「もう進んでいたら何もしない」ガードを書くこと。
|
|
179
|
+
*/
|
|
180
|
+
schedule(options: ScheduleOptions): number;
|
|
181
|
+
/** 予約を取り消す。存在しない key を渡しても何も起きない。 */
|
|
182
|
+
unschedule(key: string): void;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 予約から発火した action に渡る playerId。
|
|
186
|
+
*
|
|
187
|
+
* 送信者がいないので、人間の操作と区別するための定数。
|
|
188
|
+
* `if (playerId !== SCHEDULED_ACTOR) return;` で人間からの直接実行を弾ける。
|
|
189
|
+
*/
|
|
190
|
+
export declare const SCHEDULED_ACTOR = "__scheduled";
|
|
191
|
+
/** `serverActions` handler の実行文脈。サーバーでしか走らないので tick と乱数を渡せる。 */
|
|
192
|
+
export interface ServerActionContext {
|
|
81
193
|
tick: number;
|
|
194
|
+
random: SeededRandom;
|
|
195
|
+
/** サーバーの実時刻 (ms)。同じ dispatch の `actions` に渡る `ctx.now` と同一値。 */
|
|
196
|
+
now: number;
|
|
197
|
+
schedule(options: ScheduleOptions): number;
|
|
198
|
+
unschedule(key: string): void;
|
|
82
199
|
}
|
|
200
|
+
/** @deprecated `ServerActionContext` を使う。 */
|
|
201
|
+
export type ServerOnlyActionContext = ServerActionContext;
|
|
83
202
|
export type ActionHandler<S> = (state: S, payload: any, playerId: string, emit: Emit, ctx: ActionContext) => void;
|
|
84
|
-
export type
|
|
85
|
-
/**
|
|
86
|
-
export type
|
|
203
|
+
export type ServerActionHandler<S> = (state: S, payload: any, playerId: string, emit: Emit, ctx: ServerActionContext) => Promise<void> | void;
|
|
204
|
+
/** @deprecated `ServerActionHandler` を使う。 */
|
|
205
|
+
export type ServerOnlyActionHandlerFn<S> = ServerActionHandler<S>;
|
|
206
|
+
/**
|
|
207
|
+
* @deprecated `serverActions` フィールドに直接書く。
|
|
208
|
+
* `serverOnly()` で wrap された handler。`__serverOnly` brand で識別する。
|
|
209
|
+
*/
|
|
210
|
+
export type ServerOnlyAction<S> = ServerActionHandler<S> & {
|
|
87
211
|
readonly __serverOnly: true;
|
|
88
212
|
};
|
|
89
213
|
export interface GameLogic<S> {
|
|
@@ -92,10 +216,29 @@ export interface GameLogic<S> {
|
|
|
92
216
|
* ゲームの配役は kind === 'player' (または kind 省略) だけを対象にすること。
|
|
93
217
|
*/
|
|
94
218
|
setup(seats: Seat[], random: SeededRandom): S;
|
|
95
|
-
|
|
219
|
+
/**
|
|
220
|
+
* クライアント先読みとサーバーの両方で走る handler。決定的でなければならない。
|
|
221
|
+
*
|
|
222
|
+
* `serverActions` に同名のキーを置くと、同じ action の「サーバーだけで走る続き」に
|
|
223
|
+
* なる。1 つの action を「即座に反映していい部分」と「サーバーが決める部分」へ
|
|
224
|
+
* 分けられる (例: 駒の移動は先読み、持ち時間の減算はサーバー)。
|
|
225
|
+
*/
|
|
226
|
+
actions: Record<string, ActionHandler<S>>;
|
|
227
|
+
/**
|
|
228
|
+
* サーバーでのみ走る handler。実時刻 / 乱数 / fetch など、クライアント先読みで
|
|
229
|
+
* 再現できない処理をここに書く。async 可。
|
|
230
|
+
*
|
|
231
|
+
* `actions` と同名でも別名でもよい。別名だけに置けば「先読みしない action」
|
|
232
|
+
* (旧 `serverOnly()` 相当) になる。
|
|
233
|
+
*/
|
|
234
|
+
serverActions?: Record<string, ServerActionHandler<S>>;
|
|
96
235
|
update(state: S, ctx: {
|
|
97
236
|
random: SeededRandom;
|
|
98
237
|
tick: number;
|
|
238
|
+
/** サーバーの実時刻 (ms)。update はサーバーでしか走らないので常に正確。 */
|
|
239
|
+
now: number;
|
|
240
|
+
schedule(options: ScheduleOptions): number;
|
|
241
|
+
unschedule(key: string): void;
|
|
99
242
|
emit: Emit;
|
|
100
243
|
playerInputs: Record<string, Record<string, any>>;
|
|
101
244
|
}): void;
|
|
@@ -105,7 +248,15 @@ export interface GameConfig<S> extends ConnectionCallbacks {
|
|
|
105
248
|
logic: GameLogic<S>;
|
|
106
249
|
onState: (state: S, myPlayerId: string) => void;
|
|
107
250
|
inputs: (sendAction: (type: string, payload?: any) => void) => void;
|
|
108
|
-
|
|
251
|
+
/**
|
|
252
|
+
* `emit(name, data)` の購読。 キーごとに `predict` の宣言が必須。
|
|
253
|
+
*
|
|
254
|
+
* 同じ出来事が二重に実行されないよう、SDK は「先読みで実行した記録」を持ち、
|
|
255
|
+
* サーバーから同一の event (name と data が完全一致) が届いたら実行を抑止する。
|
|
256
|
+
* したがって `data` には**クライアントとサーバーで必ず同じ値になるもの**だけを
|
|
257
|
+
* 入れること。`ctx.now` や乱数 ID を混ぜると一致せず二重実行になる。
|
|
258
|
+
*/
|
|
259
|
+
events?: Record<string, EventSubscription>;
|
|
109
260
|
playerCount: number;
|
|
110
261
|
/** Dev harness のデフォルト向き。manifest.json の `orientation` を渡す。 */
|
|
111
262
|
orientation?: 'portrait' | 'landscape';
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 予約から発火した action に渡る playerId。
|
|
3
|
+
*
|
|
4
|
+
* 送信者がいないので、人間の操作と区別するための定数。
|
|
5
|
+
* `if (playerId !== SCHEDULED_ACTOR) return;` で人間からの直接実行を弾ける。
|
|
6
|
+
*/
|
|
7
|
+
export const SCHEDULED_ACTOR = '__scheduled';
|
|
1
8
|
/** Sentinel value — patch の value にセットすると、サーバーが Date.now() に置換する */
|
|
2
9
|
export const SERVER_TIME = '__SERVER_TIME__';
|
|
3
10
|
/** デフォルトのプレイヤーアイコン URL 一覧(dev / local モード用) */
|