@uzuhq/code-cli 0.3.17 → 0.3.19
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 +15 -1
- package/dist/dev.js +5 -2
- package/dist/harness/dev-button.js +1 -1
- package/dist/harness/mount.js +59 -1
- package/package.json +1 -1
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
import { randomUUID } from 'crypto';
|
|
15
15
|
import { compare, applyJsonMergePatch, applyJsonPatch } from './json-patch.js';
|
|
16
16
|
import { SeededRandomImpl } from './random.js';
|
|
17
|
+
/**
|
|
18
|
+
* serverOnly() で wrap された handler かを判定する (SDK の isServerOnlyAction と同じ brand)。
|
|
19
|
+
* dev-server は SDK を import できないため実装を持つ。
|
|
20
|
+
*/
|
|
21
|
+
function isServerOnlyAction(handler) {
|
|
22
|
+
return '__serverOnly' in handler && handler.__serverOnly === true;
|
|
23
|
+
}
|
|
17
24
|
export class GameRoom {
|
|
18
25
|
logic;
|
|
19
26
|
tickRate;
|
|
@@ -333,7 +340,14 @@ export class GameRoom {
|
|
|
333
340
|
}
|
|
334
341
|
const events = [];
|
|
335
342
|
const emit = (name, data) => events.push({ name, data: data ?? {} });
|
|
336
|
-
|
|
343
|
+
// serverOnly handler はサーバーでしか走らないので tick を渡せる。素の handler は
|
|
344
|
+
// クライアント先読みでも同じコードが走るため、そこで再現できない値は渡さない。
|
|
345
|
+
if (isServerOnlyAction(handler)) {
|
|
346
|
+
await handler(this.gameState, payload ?? {}, senderId, emit, { tick: this.tickCount });
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
handler(this.gameState, payload ?? {}, senderId, emit, {});
|
|
350
|
+
}
|
|
337
351
|
this.broadcastStateDelta(events, { ack: ackSeq, from: senderId });
|
|
338
352
|
}
|
|
339
353
|
handleClose(ws) {
|
package/dist/dev.js
CHANGED
|
@@ -107,7 +107,7 @@ export async function runDevCommand() {
|
|
|
107
107
|
port: harnessPort,
|
|
108
108
|
// 同一 LAN の実機 (スマホ等) から harness / 単一プレイヤー画面を開けるよう
|
|
109
109
|
// 全 interface で listen する。 GameRoom は in-memory の dev 専用 state のみ。
|
|
110
|
-
host:
|
|
110
|
+
host: HARNESS_HOST,
|
|
111
111
|
logic: loaded?.logic ?? null,
|
|
112
112
|
harness: { html: harnessHtml(), js: harnessJs },
|
|
113
113
|
meta,
|
|
@@ -207,13 +207,16 @@ function watchForReady(child, pattern) {
|
|
|
207
207
|
}
|
|
208
208
|
const DEFAULT_HARNESS_PORT = 10001;
|
|
209
209
|
const MAX_HARNESS_PORT_ATTEMPTS = 100;
|
|
210
|
+
const HARNESS_HOST = '0.0.0.0';
|
|
210
211
|
async function findFreePort() {
|
|
211
212
|
const net = await import('net');
|
|
212
213
|
const tryPort = (port) => new Promise((resolve) => {
|
|
213
214
|
const server = net.createServer();
|
|
214
215
|
server.unref();
|
|
215
216
|
server.once('error', () => resolve(null));
|
|
216
|
-
|
|
217
|
+
// host は実 listen (dev-server/server.ts) と揃える。省略すると IPv6 wildcard に
|
|
218
|
+
// bind してしまい、別プロセスが 0.0.0.0 で掴んでいるポートを空きと誤判定する。
|
|
219
|
+
server.listen(port, HARNESS_HOST, () => {
|
|
217
220
|
// listen(0) は OS が空きポートを動的割当するため、引数ではなく実際に割り当てられた
|
|
218
221
|
// 番号を読む必要がある。close 後は address() が null を返すので閉じる前に取る。
|
|
219
222
|
const address = server.address();
|
|
@@ -91,7 +91,7 @@ export function attachUzuMenu(button, opts = {}) {
|
|
|
91
91
|
addAction('Emulator', '#e17055', '#fab1a0', () => {
|
|
92
92
|
const currentUrl = new URL(window.location.href);
|
|
93
93
|
const endpoint = currentUrl.origin + currentUrl.pathname;
|
|
94
|
-
const emulatorUrl = new URL('https://
|
|
94
|
+
const emulatorUrl = new URL('https://emulator.code.uzu-app.com/');
|
|
95
95
|
emulatorUrl.searchParams.set('endpoint', endpoint);
|
|
96
96
|
if (playerCount) {
|
|
97
97
|
emulatorUrl.searchParams.set('player_count', String(playerCount));
|
package/dist/harness/mount.js
CHANGED
|
@@ -254,7 +254,16 @@ function createLifecycleStatusRow() {
|
|
|
254
254
|
item.appendChild(text);
|
|
255
255
|
row.appendChild(item);
|
|
256
256
|
}
|
|
257
|
-
|
|
257
|
+
// native title の tooltip は的が小さく遅延も長いので、クリックで開くパネル (render 側で
|
|
258
|
+
// frameWrap に置く) を主にする。title は補助。
|
|
259
|
+
const predictionWarn = document.createElement('span');
|
|
260
|
+
predictionWarn.style.cssText = [
|
|
261
|
+
'display:none;align-items:center;gap:3px',
|
|
262
|
+
'color:#1a1a2e;background:#ffb86c;border-radius:6px',
|
|
263
|
+
'padding:0 5px;cursor:pointer;user-select:none;font-weight:bold',
|
|
264
|
+
].join(';');
|
|
265
|
+
row.appendChild(predictionWarn);
|
|
266
|
+
return { el: row, dots, predictionWarn };
|
|
258
267
|
}
|
|
259
268
|
function seatAt(opts, index) {
|
|
260
269
|
const seat = opts.seats[index];
|
|
@@ -505,6 +514,7 @@ export function mountIframeGrid(opts) {
|
|
|
505
514
|
const cells = [];
|
|
506
515
|
const iframeStatusDots = new Map();
|
|
507
516
|
const iframeSplashes = new Map();
|
|
517
|
+
const iframePredictionWarns = new Map();
|
|
508
518
|
// 擬似ノッチ (各 cell 共通トグル)。 iframe を再生成せず inset だけ切り替える。
|
|
509
519
|
let notchSimOn = true;
|
|
510
520
|
const notchAppliers = [];
|
|
@@ -552,6 +562,7 @@ export function mountIframeGrid(opts) {
|
|
|
552
562
|
cells.length = 0;
|
|
553
563
|
iframeStatusDots.clear();
|
|
554
564
|
iframeSplashes.clear();
|
|
565
|
+
iframePredictionWarns.clear();
|
|
555
566
|
for (let i = 0; i < opts.seats.length; i++) {
|
|
556
567
|
const seat = seatAt(opts, i);
|
|
557
568
|
const cell = document.createElement('div');
|
|
@@ -595,9 +606,25 @@ export function mountIframeGrid(opts) {
|
|
|
595
606
|
if (seat.kind !== 'admin') {
|
|
596
607
|
screen.appendChild(createCellChrome(opts, i, false, toggleNotchSim));
|
|
597
608
|
}
|
|
609
|
+
// 先読み警告の内訳パネル。 バッジクリックで開閉する。 画面の上に重ねるので
|
|
610
|
+
// ゲームの見た目は普段どおりのまま、 必要なときだけ前に出る。
|
|
611
|
+
const predictionPanel = document.createElement('div');
|
|
612
|
+
predictionPanel.style.cssText = [
|
|
613
|
+
'display:none;position:absolute;z-index:20;left:6px;right:6px;top:6px',
|
|
614
|
+
'background:#16213e;border:1px solid #ffb86c;border-radius:6px',
|
|
615
|
+
'padding:8px 10px;font-family:monospace;font-size:10px;line-height:1.6',
|
|
616
|
+
'color:#ffb86c;text-align:left;white-space:pre-wrap;cursor:pointer',
|
|
617
|
+
].join(';');
|
|
618
|
+
predictionPanel.addEventListener('click', () => {
|
|
619
|
+
predictionPanel.style.display = 'none';
|
|
620
|
+
});
|
|
598
621
|
const notchDecor = createNotchDecor();
|
|
599
622
|
frameWrap.appendChild(screen);
|
|
600
623
|
frameWrap.appendChild(notchDecor);
|
|
624
|
+
frameWrap.appendChild(predictionPanel);
|
|
625
|
+
statusRow.predictionWarn.addEventListener('click', () => {
|
|
626
|
+
predictionPanel.style.display = predictionPanel.style.display === 'none' ? 'block' : 'none';
|
|
627
|
+
});
|
|
601
628
|
// 擬似ノッチ ON/OFF を screen の inset だけで切り替える (iframe は再生成しない)。
|
|
602
629
|
const applyNotch = (on) => {
|
|
603
630
|
if (seat.kind === 'admin')
|
|
@@ -628,6 +655,11 @@ export function mountIframeGrid(opts) {
|
|
|
628
655
|
if (iframe.contentWindow) {
|
|
629
656
|
iframeStatusDots.set(iframe.contentWindow, statusRow.dots);
|
|
630
657
|
iframeSplashes.set(iframe.contentWindow, splash);
|
|
658
|
+
iframePredictionWarns.set(iframe.contentWindow, {
|
|
659
|
+
badge: statusRow.predictionWarn,
|
|
660
|
+
panel: predictionPanel,
|
|
661
|
+
entries: [],
|
|
662
|
+
});
|
|
631
663
|
}
|
|
632
664
|
}
|
|
633
665
|
layout();
|
|
@@ -658,6 +690,32 @@ export function mountIframeGrid(opts) {
|
|
|
658
690
|
iframeSplashes.delete(event.source);
|
|
659
691
|
}
|
|
660
692
|
}
|
|
693
|
+
if (type === 'predictionWarning') {
|
|
694
|
+
const slot = iframePredictionWarns.get(event.source);
|
|
695
|
+
const payload = data.payload;
|
|
696
|
+
if (!slot || typeof payload !== 'object' || payload === null)
|
|
697
|
+
return;
|
|
698
|
+
if (!('action' in payload) || !('api' in payload))
|
|
699
|
+
return;
|
|
700
|
+
const { action, api } = payload;
|
|
701
|
+
if (typeof action !== 'string' || typeof api !== 'string')
|
|
702
|
+
return;
|
|
703
|
+
slot.entries.push(`${action} → ${api}`);
|
|
704
|
+
const body = [
|
|
705
|
+
'⚠ 先読み中に、サーバーと結果が一致しない API が呼ばれました',
|
|
706
|
+
'',
|
|
707
|
+
...slot.entries.map((e) => ` ${e}`),
|
|
708
|
+
'',
|
|
709
|
+
'この action を serverOnly() で wrap するか、値を state に書かないでください。',
|
|
710
|
+
'直し方の詳細は console を参照。',
|
|
711
|
+
'',
|
|
712
|
+
'(クリックで閉じる)',
|
|
713
|
+
].join('\n');
|
|
714
|
+
slot.badge.textContent = `⚠ ${slot.entries.length}`;
|
|
715
|
+
slot.badge.title = body;
|
|
716
|
+
slot.badge.style.display = 'inline-flex';
|
|
717
|
+
slot.panel.textContent = body;
|
|
718
|
+
}
|
|
661
719
|
};
|
|
662
720
|
window.addEventListener('message', onParentMessage);
|
|
663
721
|
window.addEventListener('resize', layout);
|