gamekit777 0.1.1 → 0.1.2
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/README.md +1 -1
- package/create-game/template/CLAUDE.md.tmpl +1 -1
- package/create-game/template/package.json.tmpl +1 -1
- package/package.json +1 -1
- package/studio/app/App.svelte +1 -1
- package/studio/app/lib/identity.svelte.ts +4 -4
- package/studio/src/game-vite.ts +24 -2
- package/studio/src/studio-plugin.ts +11 -7
- package/create-game/template/index.html.tmpl +0 -11
- package/create-game/template/src/dev.ts +0 -8
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ mount(host: Host<Config>, el: HTMLElement): Promise<GameInstance<Outcome>>
|
|
|
157
157
|
- 长任务返回 `jobId` 就用 `job_status` 轮询。
|
|
158
158
|
- `case_run` 任何结果变化都是 `fail`。是有意改的规则且这个 mode 没发布过,`case_bless` 接受;已发布过的 mode 解码逻辑**不能改**,要改就换 slug。
|
|
159
159
|
- `preview_set` 被否决会在 `rejected` 里说明——那是平台的否决权在起作用,不是 bug。
|
|
160
|
-
- 手动:`bun run dev
|
|
160
|
+
- 手动:`bun run dev` 起 studio(页面 `http://127.0.0.1:4301/`,左边是预览机台,右边是状态 / 调参 / 建表 / Case / 发布 / 日志);`bun run verify`、`bun run test`、`bun run build`。
|
|
161
161
|
|
|
162
162
|
## 不要做的事
|
|
163
163
|
|
package/package.json
CHANGED
package/studio/app/App.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* studio 页面的身份:始终是**站点上的那个人**,不在这里另建。
|
|
2
2
|
*
|
|
3
3
|
* 页面对 /v1 的请求同源,cookie 落在 127.0.0.1 下;第一次没有 cookie 时把整个窗口
|
|
4
|
-
* 跳到站点的 #/connect 借身份,站点签一个一次性 code 带回 /
|
|
4
|
+
* 跳到站点的 #/connect 借身份,站点签一个一次性 code 带回 /connect,这里拿 code
|
|
5
5
|
* 经反代换一个同一用户的新会话。之后发布、下注都用这份 cookie。 */
|
|
6
6
|
import type { SessionView } from 'gamekit777/protocol';
|
|
7
7
|
import { client } from './upload.ts';
|
|
@@ -14,11 +14,11 @@ class Identity {
|
|
|
14
14
|
error = $state<string | null>(null);
|
|
15
15
|
|
|
16
16
|
connectUrl(site: string): string {
|
|
17
|
-
const back = `${location.origin}/
|
|
17
|
+
const back = `${location.origin}/connect`;
|
|
18
18
|
return `${site.replace(/\/+$/, '')}/#/connect?return=${encodeURIComponent(back)}`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/** 页面启动:/
|
|
21
|
+
/** 页面启动:/connect 带 code 就先换会话,然后一律 GET /v1/me */
|
|
22
22
|
async boot(site: string): Promise<void> {
|
|
23
23
|
const code = new URLSearchParams(location.search).get('code');
|
|
24
24
|
if (location.pathname.endsWith('/connect') && code) {
|
|
@@ -26,7 +26,7 @@ class Identity {
|
|
|
26
26
|
try {
|
|
27
27
|
await client.pair.claim(code);
|
|
28
28
|
// 去掉地址栏里的 code:刷新不该再换一次(code 已作废,会报错)
|
|
29
|
-
history.replaceState(null, '', '/
|
|
29
|
+
history.replaceState(null, '', '/');
|
|
30
30
|
} catch (e) {
|
|
31
31
|
this.phase = 'error';
|
|
32
32
|
this.error = `换会话失败:${e instanceof Error ? e.message : String(e)}`;
|
package/studio/src/game-vite.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 这样预览走的就是 `bun run dev` 那条路——同一份 defineGameConfig、同一套 HMR,
|
|
5
5
|
* 不存在「studio 里演得好好的,真 build 出来不一样」的分叉空间。 */
|
|
6
6
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
7
|
+
import { createServer as createNetServer } from 'node:net';
|
|
7
8
|
import { join, resolve } from 'node:path';
|
|
8
9
|
import { createLogger, createServer, searchForWorkspaceRoot, type Logger, type ProxyOptions, type ViteDevServer } from 'vite';
|
|
9
10
|
import type { Engine } from './engine.ts';
|
|
@@ -75,7 +76,28 @@ function proxyRule(backendUrl: string): ProxyOptions {
|
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* 自己挑端口,不用 vite 的 strictPort:false 重试。
|
|
81
|
+
*
|
|
82
|
+
* vite 的重试是在第一次 listen 报 EADDRINUSE 后再 listen(++port),指望第一次挂上的 'listening'
|
|
83
|
+
* 回调在第二次成功时触发。Bun 的 node:http 兼容层里这个回调在出错后就没了——服务其实起在了新端口,
|
|
84
|
+
* 但 listen() 的 Promise 永远不 resolve,于是 MCP 不启动、横幅不打印,看上去就是「卡住」。
|
|
85
|
+
*/
|
|
86
|
+
async function freePort(start: number, host: string): Promise<number> {
|
|
87
|
+
for (let port = start; port < start + 100; port++) {
|
|
88
|
+
const ok = await new Promise<boolean>((resolve) => {
|
|
89
|
+
const probe = createNetServer();
|
|
90
|
+
probe.once('error', () => resolve(false));
|
|
91
|
+
probe.listen(port, host, () => probe.close(() => resolve(true)));
|
|
92
|
+
});
|
|
93
|
+
if (ok) return port;
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`${host}:${start} 起连续 100 个端口全被占了`);
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
export async function startGameServer(o: GameServerOptions): Promise<ViteDevServer> {
|
|
99
|
+
const port = await freePort(o.port, '127.0.0.1');
|
|
100
|
+
if (port !== o.port) process.stderr.write(`端口 ${o.port} 被占,改用 ${port}\n`);
|
|
79
101
|
const server = await createServer({
|
|
80
102
|
root: o.gameDir,
|
|
81
103
|
configFile: join(o.gameDir, 'vite.config.ts'),
|
|
@@ -87,8 +109,8 @@ export async function startGameServer(o: GameServerOptions): Promise<ViteDevServ
|
|
|
87
109
|
studioRoot: o.studioRoot,
|
|
88
110
|
})],
|
|
89
111
|
server: {
|
|
90
|
-
port
|
|
91
|
-
strictPort:
|
|
112
|
+
port,
|
|
113
|
+
strictPort: true,
|
|
92
114
|
host: '127.0.0.1',
|
|
93
115
|
/* 预览页嵌在 studio 页面的 iframe 里;同源,但照抄 apps/web 的做法放开——
|
|
94
116
|
sandbox 属性一加 Origin 就是 null,默认的 cors 会挡 */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 发布要用浏览器 cookie 打反代到 /v1。分开起第二个 server 这三件事全要绕。
|
|
5
5
|
*
|
|
6
6
|
* 不往游戏目录写任何文件:两个 html 从内存吐,入口用 /@fs 指回 studio 包。
|
|
7
|
-
*
|
|
7
|
+
* 不依赖游戏目录里有 index.html:脚手架出来的项目没有它,`bun run dev` 就是起 studio。 */
|
|
8
8
|
import type { Plugin } from 'vite';
|
|
9
9
|
import { API_PREFIX, createApi } from './api.ts';
|
|
10
10
|
import type { Engine } from './engine.ts';
|
|
@@ -18,10 +18,14 @@ export interface StudioPluginOptions {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const VIRTUAL_GAME = 'virtual:studio/game';
|
|
21
|
-
|
|
22
|
-
export const PAGE_PATH = '/
|
|
21
|
+
/** 根路径就是工作台;游戏目录里没有 index.html,`/` 不会和 vite 的 html 服务打架 */
|
|
22
|
+
export const PAGE_PATH = '/';
|
|
23
|
+
/** 只要预览机台(嵌在页面左边的那个 iframe) */
|
|
24
|
+
export const PREVIEW_PATH = '/preview';
|
|
23
25
|
/** 站点授权完带 code 回到这里,页面拿 code 换会话 */
|
|
24
|
-
export const CONNECT_PATH = '/
|
|
26
|
+
export const CONNECT_PATH = '/connect';
|
|
27
|
+
/** 早期版本的路径,留作别名 */
|
|
28
|
+
const LEGACY = { page: ['/__studio', '/__studio/', '/__studio/index.html'], preview: ['/__studio/preview.html'], connect: ['/__studio/connect'] };
|
|
25
29
|
|
|
26
30
|
const page = (title: string, entry: string): string => `<!doctype html>
|
|
27
31
|
<html lang="zh-CN">
|
|
@@ -52,9 +56,9 @@ export function studioPlugin(o: StudioPluginOptions): Plugin {
|
|
|
52
56
|
configureServer(server) {
|
|
53
57
|
server.middlewares.use(api);
|
|
54
58
|
server.middlewares.use((req, res, next) => {
|
|
55
|
-
const path = req.url?.split('?')[0];
|
|
56
|
-
const html = path === PREVIEW_PATH ? previewHtml
|
|
57
|
-
: path === PAGE_PATH || path ===
|
|
59
|
+
const path = req.url?.split('?')[0] ?? '/';
|
|
60
|
+
const html = path === PREVIEW_PATH || LEGACY.preview.includes(path) ? previewHtml
|
|
61
|
+
: path === PAGE_PATH || path === CONNECT_PATH || path === '/index.html' || LEGACY.page.includes(path) || LEGACY.connect.includes(path) ? appHtml
|
|
58
62
|
: null;
|
|
59
63
|
if (!html) return next();
|
|
60
64
|
void server.transformIndexHtml(req.url!, html).then((out) => {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
-
<title>__TITLE__</title>
|
|
7
|
-
<script type="module" src="/src/dev.ts"></script>
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
</body>
|
|
11
|
-
</html>
|