From 6976b55038910f97868bb0c0ed519b0d04360c0a Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 17:58:13 +0800 Subject: [PATCH 01/25] docs: keep Creator login work on main checkout branch --- .../plans/2026-09-05-local-platform-login.md | 8 ++++---- .../specs/2026-09-05-local-platform-login-design.md | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/plans/2026-09-05-local-platform-login.md b/docs/superpowers/plans/2026-09-05-local-platform-login.md index d5188af..5cbfae0 100644 --- a/docs/superpowers/plans/2026-09-05-local-platform-login.md +++ b/docs/superpowers/plans/2026-09-05-local-platform-login.md @@ -2,7 +2,7 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** 在隔离 Creator 工程中完成启动、真实资源加载、游客登录与本地服务器响应展示的验收。 +**Goal:** 在主 checkout 的联调分支中完成启动、真实资源加载、游客登录与本地服务器响应展示的验收。 **Architecture:** 复用 Runtime/Controller/WireClient 与 local sources,开发目录内组合严格 Transport、范围包装、诊断 GameEntry 和 Cocos 端口。无新 Store/Router。 **Tech Stack:** TypeScript ES2020、node:test/tsx、Cocos Creator 3.8.8、funplay MCP。 **Spec:** docs/superpowers/specs/2026-09-05-local-platform-login-design.md(用户已确认全文及第8节九项资源授权)。 @@ -13,7 +13,7 @@ - 不修改生产 GameEntry 契约,不选择真实子游戏,不接其他业务 RPC,不替换现有 Login/Launch/MainMenu 场景,不复做 3A。 - 代码新增只在 YouleNexus/assets/scripts/local-platform-login/,测试在 framework-tests/local-platform-login/;下文路径均相对 cocoscreator_projects/。 - .scene/.prefab/.anim/.meta 只能由 Creator/MCP 生成或保存;写入/提交仅限 spec 第8节,其他导入副产物保留且报告。 -- 先建 .worktrees/local-platform-login 与 codex/local-platform-login;Creator projectPath 必须精确指向该 worktree 才能写资源和运行预览。 +- 按用户最新授权,在 G:/Works/YouleGamesCocosCreator 使用 codex/local-platform-login;Creator projectPath 必须为该目录下 cocoscreator_projects/YouleNexus。原有50项改动按路径/状态/哈希保护,精确暂存,不自动 stash。 - 显式枚举纯 TS 测试及两项指定 architecture 测试;禁止 npm test、run-framework-tests.mjs、test:framework。tsx ENOMEM 精确升级运行,不改业务代码。 - TDD→任务规格/质量审查→修复复验→scoped commit;最后整分支审查,代码测试不能替代引擎/真实服务验收。 @@ -91,7 +91,7 @@ const entry = createDiagnosticGameEntry(context.config.identity.gameid, options. - [ ] 实现触摸绑定 group-2/游客登录;隐藏微信/手机/QQ/快速/隐藏快速入口;版本 Label 取有效 identity.version。加载节点 group-40/载入动画 用 Cocos update/tween 旋转,生命周期清理。不得持久改源 prefab。 - [ ] 组件 DEBUG 且 sys.isNative=false 限制;500ms 仅一处定义。start 调用宿主,configOptions 使用实际 location.search 与 window,禁止 query 默认;Storage 绑定 localStorage,random/now 用 Math.random/Date.now,sampleHost 显式 location=null 及存在时的 returnCitySN。 - [ ] loadResources 检查实际序列化 Prefab 与 SpriteFrame 依赖,实例化 Login、绑定节点与事件,等待真实 draw;minimum-display 自 Loading 首个 draw 计时剩余500ms,不能以 setTimeout 代替资源加载。帧回调使用 Cocos director 事件与真实取消。 -- [ ] 编辑器操作前核实精确 worktree projectPath。当前主工程无法通过 MCP 切换时,明确请求用户打开隔离路径,同时完成可独立工作的 TS;绝不在主工程代做。 +- [ ] 编辑器操作前用 MCP 核实固定主工程 projectPath。用户已取消 worktree 切换;在当前联调分支继续,不请求打开已清理的隔离路径,不保存未知场景。 - [ ] MCP 创建空 LocalPlatformLogin.scene,1600×720 Canvas/Camera/Host/roots/status/result/stop,实例化 Loading Prefab,挂新组件及序列化引用;Login Prefab 由运行实例加载。保存→重新打开→核实组件与引用真实持久,检查九个资源范围,不使用 edit_prefab_json。 - [ ] Cocos noEmit/诊断需使用实际引擎声明;不能把 cc 文件纳入无引擎 framework tsconfig。记录 engine diagnostics 与纯 TS 各自结果。MCP 预览新的场景,显式 profile=local;手工模拟点击前确认无旧 LoginFlow 活跃。 - [ ] 任务规格/质量审查覆盖代码及 MCP 绑定证据,再提交授权文件;若工具不能持久化挂载,报告具体阻塞而非手改序列化。 @@ -114,7 +114,7 @@ node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit node scripts/check-import-boundaries.mjs git diff --check ``` -- [ ] 审计基线到 HEAD 和所有未提交资源路径,主 checkout48项哈希不变;本批真实成功与纯测试成功分开记录。报告仅提交脱敏摘要与必要截图引用,认证密钥和原始账号设备标识不提交。 +- [ ] 审计基线到 HEAD 和所有未提交资源路径,主 checkout原有50项状态与哈希不变;本批真实成功与纯测试成功分开记录。报告仅提交脱敏摘要与必要截图引用,认证密钥和原始账号设备标识不提交。 - [ ] 任务规格/质量审查、完整分支审查、完成记录;不自动合并或推送。 ## 计划自审与执行 diff --git a/docs/superpowers/specs/2026-09-05-local-platform-login-design.md b/docs/superpowers/specs/2026-09-05-local-platform-login-design.md index 636866f..9b9a796 100644 --- a/docs/superpowers/specs/2026-09-05-local-platform-login-design.md +++ b/docs/superpowers/specs/2026-09-05-local-platform-login-design.md @@ -102,13 +102,15 @@ ResultRoot 为开发结果页,显示“登录成功”及玩家 ID/昵称/豆 若 Creator 自动导入生成清单以外的 meta,保留并报告,不能混入提交。若引用依赖非法或 MCP 不能持久保存脚本绑定,停止受影响资源步骤并说明具体原因,不以序列化文本编辑绕过。 -## 9. 隔离工作区与编辑器切换 +## 9. 主 checkout 分支与编辑器路径 -实施创建新 codex/local-platform-login 分支和 .worktrees/local-platform-login,基于包含本设计与后续计划的 master;不重建已清理的旧 worktree。先完成纯 TS TDD/审查再进入编辑器阶段。 +用户于2026-09-05明确改为主 checkout 上使用 Git 分支,覆盖此前 worktree 约定。实施路径固定为 G:/Works/YouleGamesCocosCreator,分支 codex/local-platform-login;Creator 工程固定为该目录下 cocoscreator_projects/YouleNexus,无需来回切换工程。 -Creator 资源操作只能对新 worktree 的 YouleNexus 执行。切换前记录主工程原有 48 项变更哈希,并确认当前编辑器是否存在未保存场景;不得自动保存或丢弃未知改动。将已授权且已验证的当前项目配置用于隔离工程时,不复制认证密钥,不共享 assets/library/temp。 +迁移时旧 worktree 无代码改动,任务记录已复制并校验哈希;临时 worktree 已清理,分支保留。仍先完成纯 TS TDD/审查,再进行已授权的编辑器资源操作。 -切换后必须通过 MCP get_project_info 证明 projectPath 精确为隔离路径;MCP 仍指向主 checkout 时不执行任何写入或预览。编辑器切换若无可用授权工具,由用户打开明确路径,不能用主工程测试隔离代码。依赖 node_modules 可复用,Creator 缓存由自身生成。 +开始资源操作前,通过 MCP get_project_info 核实上述固定 projectPath,并确认未保存内容;不自动保存或丢弃未知场景。主 checkout 原有48项 meta 状态以及后来新增的 scene-2d.scene/scene-2d.scene.meta,共50项文件状态和内容均纳入保护基线。只精确暂存本批文件,不使用 git add . 或自动 stash。 + +新增资源仍只能由 MCP/Creator 写入,范围严格遵循第8节。Creator 自动生成的清单外 meta 保留并报告,不混入提交。Git 切分支不能视为未提交文件隔离,必须在提交和合并前复核保护基线。 预览仅启动新场景,并明确携带 profile=local;不把默认启动场景或默认 profile 改成本地。不额外更改构建配置,也不发布包含诊断入口的正式包。 From a3cd74d31653632777b18691454e4119c12fb637 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 18:05:17 +0800 Subject: [PATCH 02/25] feat: add strict local login websocket transport --- .../local-login-transport.ts | 117 +++++++++ .../local-platform-login/transport.test.ts | 230 ++++++++++++++++++ 2 files changed, 347 insertions(+) create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts create mode 100644 cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts new file mode 100644 index 0000000..c30d760 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts @@ -0,0 +1,117 @@ +import { + assertLocalConnectionTarget, + type LocalStartupContext, +} from '../../framework/config/local-startup.ts'; +import type { Transport } from '../../framework/net/transport.ts'; + +export type LocalLoginEvidence = { kind: string; data: unknown }; +export type EvidenceSink = (event: LocalLoginEvidence) => void; + +interface LocalLoginTransportOptions { + context: LocalStartupContext; + createSocket: (url: string) => WebSocket; + onFault: (error: unknown) => void; + record: EvidenceSink; +} + +/** Local-only socket boundary; raw evidence precedes protocol decoding. */ +export class LocalLoginTransport implements Transport { + private socket: WebSocket | null = null; + private generation = 0; + private openCallback?: () => void; + private messageCallback?: (frame: string) => void; + private closeCallback?: () => void; + + constructor(private readonly options: LocalLoginTransportOptions) {} + + connect(url: string): void { + try { + assertLocalConnectionTarget(this.options.context, url); + } catch (error) { + this.options.onFault(error); + throw error; + } + this.close(); + let socket: WebSocket; + try { + socket = this.options.createSocket(url); + } catch (error) { + this.options.onFault(error); + throw error; + } + this.socket = socket; + const generation = this.generation; + socket.onopen = () => { + if (this.socket === socket) this.openCallback?.(); + }; + socket.onmessage = (event) => { + if (this.socket !== socket) return; + if (typeof event.data !== 'string') { + this.stopWithFault(new TypeError('Local login WebSocket requires text frames')); + return; + } + try { + this.options.record({ kind: 'transport-receive', data: event.data }); + } catch (error) { + this.stopWithFault(error); + throw error; + } + if (this.socket === socket) this.messageCallback?.(event.data); + }; + socket.onclose = () => { + if (this.socket !== socket) return; + this.socket = null; + this.closeCallback?.(); + }; + socket.onerror = (event) => { + if (this.socket !== socket) return; + this.stopWithFault(event); + if (this.generation === generation) this.closeCallback?.(); + }; + } + + send(frame: string): void { + const socket = this.socket; + if (!socket || socket.readyState !== 1) { + throw new Error('Local login WebSocket must be OPEN before send'); + } + try { + socket.send(frame); + this.options.record({ kind: 'transport-send', data: frame }); + } catch (error) { + this.stopWithFault(error); + throw error; + } + } + + close(): void { + this.generation++; + const socket = this.socket; + this.socket = null; + if (!socket) return; + try { + socket.close(); + } catch (error) { + this.options.onFault(error); + throw error; + } + } + + onOpen(callback: () => void): void { this.openCallback = callback; } + onMessage(callback: (frame: string) => void): void { this.messageCallback = callback; } + onClose(callback: () => void): void { this.closeCallback = callback; } + + private stopWithFault(error: unknown): void { + const socket = this.socket; + this.socket = null; + const cleanupErrors: unknown[] = []; + try { + socket?.close(); + } catch (cleanupError) { + cleanupErrors.push(cleanupError); + } + this.options.onFault(error); + // Preserve both independent failures, including non-Error thrown values. + for (const cleanupError of cleanupErrors) this.options.onFault(cleanupError); + } +} diff --git a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts new file mode 100644 index 0000000..8ffea5f --- /dev/null +++ b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts @@ -0,0 +1,230 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { resolveLocalStartup } from '../../YouleNexus/assets/framework/config/local-startup.ts'; +import { WireClient } from '../../YouleNexus/assets/framework/net/wire-client.ts'; +import type { Clock } from '../../YouleNexus/assets/framework/net/heartbeat.ts'; + +class FakeSocket { + readyState = 0; + sent: string[] = []; + closes = 0; + sendError?: Error; + closeError?: Error; + onopen: ((event: Event) => void) | null = null; + onmessage: ((event: MessageEvent) => void) | null = null; + onclose: ((event: CloseEvent) => void) | null = null; + onerror: ((event: Event) => void) | null = null; + send(frame: string): void { + if (this.sendError) throw this.sendError; + this.sent.push(frame); + } + close(): void { + this.closes++; + if (this.closeError) throw this.closeError; + this.readyState = 3; + this.onclose?.({} as CloseEvent); + } + open(): void { this.readyState = 1; this.onopen?.({} as Event); } + message(data: unknown): void { this.onmessage?.({ data } as MessageEvent); } +} + +async function setup(overrides: { + createSocket?: (url: string) => WebSocket; + record?: (event: { kind: string; data: unknown }) => void; + onFault?: (error: unknown) => void; +} = {}) { + const module = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts') + .catch((error: unknown) => assert.fail(`LocalLoginTransport implementation must exist: ${String(error)}`)); + const context = await resolveLocalStartup({ + mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', + fetcher: { async fetch(): Promise { throw new Error('unexpected fetch'); } }, + }); + const sockets: FakeSocket[] = []; + const urls: string[] = []; + const faults: unknown[] = []; + const evidence: { kind: string; data: unknown }[] = []; + const transport = new module.LocalLoginTransport({ + context, + createSocket: overrides.createSocket ?? ((url) => { + urls.push(url); + const socket = new FakeSocket(); + sockets.push(socket); + return socket as unknown as WebSocket; + }), + record: overrides.record ?? ((event) => evidence.push(event)), + onFault: (error) => { faults.push(error); overrides.onFault?.(error); }, + }); + return { transport, context, sockets, urls, faults, evidence, url: context.config.servers[0]! }; +} + +test('requires OPEN and records only frames successfully sent', async () => { + const h = await setup(); + assert.throws(() => h.transport.send('{}'), /open/i); + h.transport.connect(h.url); + assert.throws(() => h.transport.send('{}'), /open/i); + const socket = h.sockets[0]!; + socket.open(); + h.transport.send('frame'); + assert.deepEqual(socket.sent, ['frame']); + assert.deepEqual(h.evidence, [{ kind: 'transport-send', data: 'frame' }]); + const error = new Error('send failed'); + socket.sendError = error; + assert.throws(() => h.transport.send('unsent'), (caught) => caught === error); + assert.equal(h.evidence.length, 1); + assert.equal(h.faults[h.faults.length - 1], error); +}); + +test('validates every connection target before constructing and reports the thrown error', async () => { + const h = await setup(); + h.transport.connect(h.url); + assert.throws(() => h.transport.connect('wss://remote.example/'), (error) => { + assert.equal(h.faults[h.faults.length - 1], error); + return /loopback/i.test(String(error)); + }); + assert.deepEqual(h.urls, [h.url]); +}); + +test('socket constructor failure is reported before rethrowing the same object and cause', async () => { + const cause = new Error('root'); + const error = Object.assign(new Error('constructor failed'), { cause }); + const h = await setup({ createSocket: () => { throw error; } }); + assert.throws(() => h.transport.connect(h.url), (caught) => { + assert.equal(h.faults[0], error); + return caught === error && error.cause === cause; + }); +}); + +test('records raw handshake before message delivery', async () => { + const order: unknown[] = []; + const h = await setup({ record: (event) => order.push(event) }); + h.transport.onMessage((frame) => order.push(frame)); + h.transport.connect(h.url); + h.sockets[0]!.open(); + h.sockets[0]!.message('@toconcon raw handshake'); + assert.deepEqual(order, [ + { kind: 'transport-receive', data: '@toconcon raw handshake' }, '@toconcon raw handshake', + ]); +}); + +test('non-text frames stop, report TypeError and never reach consumers or reconnect callback', async () => { + const h = await setup(); + let delivered = 0; + let closed = 0; + h.transport.onMessage(() => delivered++); + h.transport.onClose(() => closed++); + h.transport.connect(h.url); + const socket = h.sockets[0]!; + socket.open(); + socket.message(new ArrayBuffer(2)); + socket.message('late frame'); + assert.equal(delivered, 0); + assert.equal(closed, 0); + assert.equal(socket.closes, 1); + assert.equal(h.faults.length, 1); + assert.ok(h.faults[0] instanceof TypeError); + assert.deepEqual(h.evidence, []); + assert.throws(() => h.transport.send('late'), /open/i); +}); + +for (const direction of ['receive', 'send']) { + test(`${direction} evidence failure preserves original error and stops without reconnect`, async () => { + const error = new Error('evidence storage failed'); + const h = await setup({ record: () => { throw error; } }); + let delivered = 0; + let closed = 0; + h.transport.onMessage(() => delivered++); + h.transport.onClose(() => closed++); + h.transport.connect(h.url); + const socket = h.sockets[0]!; + socket.open(); + const run = () => direction === 'receive' ? socket.message('frame') : h.transport.send('frame'); + assert.throws(run, (caught) => caught === error); + assert.equal(h.faults[0], error); + assert.equal(socket.closes, 1); + assert.equal(delivered, 0); + assert.equal(closed, 0); + assert.throws(() => h.transport.send('late'), /open/i); + }); +} + +test('error followed by close notifies once and retains original error event', async () => { + const h = await setup(); + let closed = 0; + h.transport.onClose(() => closed++); + h.transport.connect(h.url); + const socket = h.sockets[0]!; + const error = {} as Event; + socket.onerror?.(error); + socket.onclose?.({} as CloseEvent); + assert.equal(closed, 1); + assert.equal(h.faults[0], error); +}); + +test('explicit close and superseded sockets invalidate every old callback', async () => { + const h = await setup(); + const calls: string[] = []; + h.transport.onOpen(() => calls.push('open')); + h.transport.onMessage(() => calls.push('message')); + h.transport.onClose(() => calls.push('close')); + h.transport.connect(h.url); + const old = h.sockets[0]!; + h.transport.connect(h.url); + old.open(); old.message('old'); old.onerror?.({} as Event); old.onclose?.({} as CloseEvent); + assert.equal(old.closes, 1); + const current = h.sockets[1]!; + current.open(); + h.transport.close(); + current.open(); current.message('late'); current.onerror?.({} as Event); + assert.deepEqual(calls, ['open']); + assert.deepEqual(h.faults, []); + assert.deepEqual(h.evidence, []); +}); + +test('close failure is reported and thrown after invalidation', async () => { + const h = await setup(); + h.transport.connect(h.url); + const socket = h.sockets[0]!; + const error = new Error('close failed'); + socket.closeError = error; + assert.throws(() => h.transport.close(), (caught) => caught === error); + assert.equal(h.faults[0], error); + assert.throws(() => h.transport.send('late'), /open/i); +}); + +test('explicit stop from fault observer suppresses the pending error close callback', async () => { + const h = await setup({ onFault: () => h.transport.close() }); + let closed = 0; + h.transport.onClose(() => closed++); + h.transport.connect(h.url); + h.sockets[0]!.onerror?.({} as Event); + assert.equal(closed, 0); +}); + +test('a replacement connection from fault observer cannot receive the old close callback', async () => { + const h = await setup({ onFault: () => h.transport.connect(h.url) }); + let closed = 0; + h.transport.onClose(() => closed++); + h.transport.connect(h.url); + h.sockets[0]!.onerror?.({} as Event); + assert.equal(h.sockets.length, 2); + assert.equal(closed, 0); +}); + +test('natural close lets real WireClient reconnect while explicit stop cancels it', async () => { + const h = await setup(); + const timers = new Map void>(); + const clock: Clock = { + setTimeout: (fn) => { const key = {}; timers.set(key, fn); return key; }, + clearTimeout: (key) => { timers.delete(key); }, + }; + const wire = new WireClient({ servers: h.context.config.servers, transportFactory: () => h.transport, clock }); + wire.start(); + h.sockets[0]!.open(); + h.sockets[0]!.close(); + assert.equal(timers.size, 1); + const retry = [...timers.values()][0]!; + timers.clear(); retry(); + assert.equal(h.sockets.length, 2); + wire.stop(); + assert.equal(timers.size, 0); +}); From 9ad477f47489badca8908c88b752d9f3c87ec89f Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 18:09:56 +0800 Subject: [PATCH 03/25] fix: preserve reconnect after local socket errors --- .../local-login-transport.ts | 16 +++++- .../local-platform-login/transport.test.ts | 53 +++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts index c30d760..03571b8 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts @@ -65,7 +65,21 @@ export class LocalLoginTransport implements Transport { }; socket.onerror = (event) => { if (this.socket !== socket) return; - this.stopWithFault(event); + try { + this.options.record({ kind: 'transport-error', data: event }); + } catch (error) { + this.stopWithFault(error); + throw error; + } + if (this.socket !== socket || this.generation !== generation) return; + // Invalidate before close: browsers may dispatch close after error. + this.socket = null; + try { + socket.close(); + } catch (error) { + this.options.onFault(error); + throw error; + } if (this.generation === generation) this.closeCallback?.(); }; } diff --git a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts index 8ffea5f..ab53751 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts @@ -157,7 +157,8 @@ test('error followed by close notifies once and retains original error event', a socket.onerror?.(error); socket.onclose?.({} as CloseEvent); assert.equal(closed, 1); - assert.equal(h.faults[0], error); + assert.deepEqual(h.faults, []); + assert.deepEqual(h.evidence, [{ kind: 'transport-error', data: error }]); }); test('explicit close and superseded sockets invalidate every old callback', async () => { @@ -191,8 +192,8 @@ test('close failure is reported and thrown after invalidation', async () => { assert.throws(() => h.transport.send('late'), /open/i); }); -test('explicit stop from fault observer suppresses the pending error close callback', async () => { - const h = await setup({ onFault: () => h.transport.close() }); +test('explicit stop from error evidence observer suppresses the pending error close callback', async () => { + const h = await setup({ record: () => h.transport.close() }); let closed = 0; h.transport.onClose(() => closed++); h.transport.connect(h.url); @@ -200,8 +201,8 @@ test('explicit stop from fault observer suppresses the pending error close callb assert.equal(closed, 0); }); -test('a replacement connection from fault observer cannot receive the old close callback', async () => { - const h = await setup({ onFault: () => h.transport.connect(h.url) }); +test('a replacement connection from error evidence observer cannot receive the old close callback', async () => { + const h = await setup({ record: () => h.transport.connect(h.url) }); let closed = 0; h.transport.onClose(() => closed++); h.transport.connect(h.url); @@ -228,3 +229,45 @@ test('natural close lets real WireClient reconnect while explicit stop cancels i wire.stop(); assert.equal(timers.size, 0); }); + +test('natural error reconnects real WireClient even when terminal onFault stops the host', async () => { + const h = await setup({ onFault: () => wire.stop() }); + const timers = new Map void>(); + const clock: Clock = { + setTimeout: (fn) => { const key = {}; timers.set(key, fn); return key; }, + clearTimeout: (key) => { timers.delete(key); }, + }; + const wire = new WireClient({ servers: h.context.config.servers, transportFactory: () => h.transport, clock }); + wire.start(); + h.sockets[0]!.open(); + const error = {} as Event; + h.sockets[0]!.onerror?.(error); + h.sockets[0]!.onclose?.({} as CloseEvent); + assert.deepEqual(h.faults, []); + assert.deepEqual(h.evidence, [{ kind: 'transport-error', data: error }]); + assert.equal(timers.size, 1); + const retry = [...timers.values()][0]!; + timers.clear(); retry(); + assert.equal(h.sockets.length, 2); + wire.stop(); + assert.equal(timers.size, 0); +}); + +for (const failure of ['record', 'close']) { + test(`socket error ${failure} failure reports terminal original and suppresses reconnect`, async () => { + const error = new Error(`${failure} failed`); + const h = await setup(failure === 'record' ? { record: () => { throw error; } } : {}); + let closed = 0; + h.transport.onClose(() => closed++); + h.transport.connect(h.url); + const socket = h.sockets[0]!; + if (failure === 'close') socket.closeError = error; + const event = {} as Event; + assert.throws(() => socket.onerror?.(event), (caught) => caught === error); + assert.deepEqual(h.faults, [error]); + assert.equal(closed, 0); + socket.onclose?.({} as CloseEvent); + assert.equal(closed, 0); + assert.throws(() => h.transport.send('late'), /open/i); + }); +} From 1a3308ecd0511f50e296a8a8e8d9447c82f10e9e Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 18:19:11 +0800 Subject: [PATCH 04/25] feat: bound local platform login diagnostics and wire scope --- .../local-login-game-entry.ts | 33 ++++ .../local-platform-login/local-login-wire.ts | 120 +++++++++++++++ .../local-platform-login/game-entry.test.ts | 51 +++++++ .../local-platform-login/wire.test.ts | 144 ++++++++++++++++++ 4 files changed, 348 insertions(+) create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts create mode 100644 cocoscreator_projects/framework-tests/local-platform-login/game-entry.test.ts create mode 100644 cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts new file mode 100644 index 0000000..6e39db3 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts @@ -0,0 +1,33 @@ +import type { GameEntry } from '../../framework/sdk/contracts/game-entry.ts'; +import type { EvidenceSink } from './local-login-transport.ts'; + +/** Satisfies entry validation while making every actual game operation a scope error. */ +export function createDiagnosticGameEntry(gameId: string | number, record: EvidenceSink): GameEntry { + let nextInstanceId = 0; + function violation(operation: string, instanceId?: number): never { + record({ kind: 'scope-violation', data: { operation, instanceId } }); + throw new Error(`Local platform login scope forbids ${operation}`); + } + return { + key: 'local-platform-login', + route: 'local-platform-login', + gameId, + resolveSeatCount: () => violation('resolveSeatCount'), + createModule() { + const instanceId = ++nextInstanceId; + let disposed = false; + record({ kind: 'diagnostic-module', data: { instanceId, state: 'created' } }); + return { + attach: () => violation('attach', instanceId), + restore: () => violation('restore', instanceId), + handlePlatformEvent: () => violation('handlePlatformEvent', instanceId), + handleGameMessage: () => violation('handleGameMessage', instanceId), + dispose() { + if (disposed) return; + disposed = true; + record({ kind: 'diagnostic-module', data: { instanceId, state: 'disposed' } }); + }, + }; + }, + }; +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts new file mode 100644 index 0000000..d4e1f67 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts @@ -0,0 +1,120 @@ +import type { LocalStartupContext } from '../../framework/config/local-startup.ts'; +import type { Transport } from '../../framework/net/transport.ts'; +import { WireClient, type WireEvent } from '../../framework/net/wire-client.ts'; +import type { PlatformWireClient } from '../../framework/platform/runtime.ts'; +import { parseLoginResponse } from '../../framework/protocol/contracts/index.ts'; +import type { EvidenceSink } from './local-login-transport.ts'; + +interface LocalLoginWireOptions { + context: LocalStartupContext; + transportFactory: () => Transport; + onFault: (error: unknown) => void; + record: EvidenceSink; +} + +/** ES2020-compatible aggregate retaining original errors, including non-Error values. */ +class LocalLoginWireFault extends Error { + readonly errors: readonly unknown[]; + constructor(errors: readonly unknown[]) { + super('Local login Wire failed with additional cleanup or evidence errors'); + this.name = 'LocalLoginWireFault'; + this.errors = Object.freeze([...errors]); + } +} + +export function createLocalLoginWire(options: LocalLoginWireOptions): PlatformWireClient { + const wire = new WireClient({ servers: options.context.config.servers, transportFactory: options.transportFactory }); + const listeners = new Set<(event: WireEvent) => void>(); + let stopped = false; + let faulted = false; + let terminalDelivery = false; + + function stop(): void { + if (stopped) return; + stopped = true; + unsubscribe(); + listeners.clear(); + wire.stop(); + } + + function fail(error: unknown, operation?: string, event?: WireEvent): unknown { + if (faulted) return error; + faulted = true; + const errors = [error]; + try { stop(); } catch (cleanupError) { errors.push(cleanupError); } + if (operation !== undefined) { + try { options.record({ kind: 'scope-violation', data: { operation, event } }); } + catch (recordError) { errors.push(recordError); } + } + const fault = errors.length === 1 ? error : new LocalLoginWireFault(errors); + options.onFault(fault); + return fault; + } + + function scopeError(operation: string): Error { + return new Error(`Local platform login scope forbids ${operation}`); + } + + function assertActive(): void { + if (stopped || faulted || terminalDelivery) throw new Error('Local login Wire is stopped or terminal'); + } + + function deliver(event: WireEvent): void { + for (const listener of [...listeners]) { + if (stopped || faulted) break; + listener(event); + } + } + + const unsubscribe = wire.subscribe(event => { + if (stopped || faulted || terminalDelivery) return; + try { + options.record({ kind: 'wire-event', data: event }); + if (stopped || faulted) return; + if (event.type !== 'message') { deliver(event); return; } + const { route, rpc, data } = event.message; + if (rpc === 'connect_agentserver' || rpc === 'connect_roomserver' || (route !== 'agent' && route !== 'platform')) { + fail(scopeError(`${route}/${rpc}`), `${route}/${rpc}`, event); + return; + } + let terminalOperation: string | undefined; + if (route === 'agent' && rpc === 'player_login') { + const parsed = parseLoginResponse(data); + if (parsed.state === 0 && parsed.room !== null) { + fail(scopeError('login room recovery'), 'login room recovery', event); + return; + } + if (parsed.state !== 0) terminalOperation = 'login rejected'; + } + if (route === 'agent' && rpc === 'kick_server') terminalOperation = 'kick_server'; + if (terminalOperation !== undefined) { + // Existing runtime failure display runs first, but cannot initiate more traffic. + terminalDelivery = true; + try { deliver(event); } + catch (error) { fail(error); return; } + fail(scopeError(terminalOperation), terminalOperation, event); + return; + } + deliver(event); + } catch (error) { fail(error); } + }); + + return { + subscribe(listener) { + if (stopped || faulted) return () => {}; + listeners.add(listener); + return () => { listeners.delete(listener); }; + }, + start() { assertActive(); wire.start(); }, + send(envelope) { + assertActive(); + if (envelope.route !== 'agent' || envelope.rpc !== 'player_login') { + throw fail(scopeError(`outbound ${envelope.route}/${envelope.rpc}`), `outbound ${envelope.route}/${envelope.rpc}`); + } + wire.send(envelope); + }, + switchServer() { assertActive(); throw fail(scopeError('switchServer'), 'switchServer'); }, + reconnectCurrent() { assertActive(); wire.reconnectCurrent(); }, + stop, + }; +} diff --git a/cocoscreator_projects/framework-tests/local-platform-login/game-entry.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/game-entry.test.ts new file mode 100644 index 0000000..68aa08b --- /dev/null +++ b/cocoscreator_projects/framework-tests/local-platform-login/game-entry.test.ts @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { GameSessionHost } from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; +import type { GameHost, PlatformToGameEvent, GameServerMessage } from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; + +async function setup() { + const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts') + .catch(error => assert.fail(`Diagnostic GameEntry implementation must exist: ${String(error)}`)); + const evidence: { kind: string; data: unknown }[] = []; + return { entry: api.createDiagnosticGameEntry(101, event => evidence.push(event)), evidence }; +} + +test('real GameSessionHost validates and releases one diagnostic instance without entering a game', async () => { + const { entry, evidence } = await setup(); + const host = new GameSessionHost(entry); + assert.equal(host.state, 'idle'); + assert.equal(entry.key, 'local-platform-login'); + assert.equal(entry.route, 'local-platform-login'); + assert.equal(entry.gameId, 101); + assert.deepEqual(evidence, [ + { kind: 'diagnostic-module', data: { instanceId: 1, state: 'created' } }, + { kind: 'diagnostic-module', data: { instanceId: 1, state: 'disposed' } }, + ]); + host.dispose(); + host.dispose(); + assert.equal(evidence.length, 2); +}); + +test('every game behavior records its operation and throws; dispose is idempotent', async () => { + const { entry, evidence } = await setup(); + assert.throws(() => entry.resolveSeatCount([]), /resolveSeatCount/); + const module = entry.createModule(); + for (const [operation, invoke] of [ + ['attach', () => module.attach({} as GameHost)], + ['restore', () => module.restore({})], + ['handlePlatformEvent', () => module.handlePlatformEvent({} as PlatformToGameEvent)], + ['handleGameMessage', () => module.handleGameMessage({} as GameServerMessage)], + ] as const) { + assert.throws(invoke, new RegExp(operation)); + assert.equal(evidence[evidence.length - 1]?.kind, 'scope-violation'); + assert.equal((evidence[evidence.length - 1]?.data as { operation: string }).operation, operation); + } + module.dispose(); + module.dispose(); + entry.createModule().dispose(); + const lifecycle = evidence.filter(event => event.kind === 'diagnostic-module'); + assert.deepEqual(lifecycle.map(event => event.data), [ + { instanceId: 1, state: 'created' }, { instanceId: 1, state: 'disposed' }, + { instanceId: 2, state: 'created' }, { instanceId: 2, state: 'disposed' }, + ]); +}); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts new file mode 100644 index 0000000..080e640 --- /dev/null +++ b/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts @@ -0,0 +1,144 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { test } from 'node:test'; +import { resolveLocalStartup } from '../../YouleNexus/assets/framework/config/local-startup.ts'; +import { RECONNECT_INTERVAL_MS } from '../../YouleNexus/assets/framework/core/constants.ts'; +import type { WireEvent } from '../../YouleNexus/assets/framework/net/wire-client.ts'; +import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; +import { FakeTransport } from '../helpers/fake-transport.ts'; + +const login: OutboundEnvelope = { app: 'youle', route: 'agent', rpc: 'player_login', data: { gameid: 101 } }; +function fixture(name: string) { + return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')); +} +async function setup(recordHook?: (event: { kind: string; data: unknown }) => void) { + const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts') + .catch(error => assert.fail(`Local login Wire implementation must exist: ${String(error)}`)); + const context = await resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', + fetcher: { async fetch(): Promise { throw new Error('unexpected fetch'); } } }); + const transports: FakeTransport[] = []; + const events: WireEvent[] = []; + const evidence: { kind: string; data: unknown }[] = []; + const faults: unknown[] = []; + const order: string[] = []; + const wire = api.createLocalLoginWire({ context, + transportFactory: () => { const transport = new FakeTransport(); transports.push(transport); return transport; }, + record: event => { evidence.push(event); recordHook?.(event); }, + onFault: error => { order.push('fault'); faults.push(error); assert.equal(transports[0]?.closeCalls, 1); }, + }); + wire.subscribe(event => { events.push(event); order.push(event.type === 'message' ? event.message.rpc : event.type); }); + const push = async (message: unknown) => { transports[0]!.serverPush(JSON.stringify(message)); await transports[0]!.flush(); }; + return { wire, context, transports, events, evidence, faults, order, push }; +} + +test('start is not open; only original lobby and ordinary agent/platform events are forwarded', async t => { + const h = await setup(); t.after(() => h.wire.stop()); + h.wire.start(); + assert.equal(h.events.length, 0); + await h.transports[0]!.flush(); + let unsubscribed = 0; + const unsubscribe = h.wire.subscribe(() => unsubscribed++); unsubscribe(); unsubscribe(); + await h.push(fixture('player-login-success.json')); + await h.push({ route: 'agent', rpc: 'show_message', data: { msg: 'hello' } }); + await h.push({ route: 'platform', rpc: 'notice', data: {} }); + assert.equal(unsubscribed, 0); + assert.equal(h.events.filter(event => event.type === 'message').length, 3); + for (const event of h.events) assert.ok(h.evidence.some(item => item.kind === 'wire-event' && item.data === event)); + assert.deepEqual(h.faults, []); +}); + +for (const [name, message, error] of [ + ['room recovery', fixture('player-login-room.json'), /login room recovery/], + ['malformed login', { route: 'agent', rpc: 'player_login', data: { state: 0 } }, /\$\.data\.playerid/], + ['room route', { route: 'room', rpc: 'notice', data: {} }, /room/], + ['game route', { route: 'mahjong', rpc: 'notice', data: {} }, /mahjong/], + ['agent switch', { route: 'agent', rpc: 'connect_agentserver', data: {} }, /connect_agentserver/], + ['platform switch', { route: 'platform', rpc: 'connect_roomserver', data: {} }, /connect_roomserver/], +] as const) test(`${name} stops before downstream delivery and remains terminal`, async t => { + t.mock.timers.enable({ apis: ['setTimeout'] }); + const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); + await h.push(message); + assert.equal(h.faults.length, 1); assert.match(String(h.faults[0]), error); + assert.equal(h.events.length, 1); + await h.push(fixture('player-login-success.json')); + assert.throws(() => h.wire.reconnectCurrent(), /stopped|terminal/); + assert.throws(() => h.wire.start(), /stopped|terminal/); + h.wire.stop(); h.wire.stop(); t.mock.timers.tick(60000); + assert.equal(h.transports.length, 1); assert.equal(h.events.length, 1); assert.equal(h.faults.length, 1); +}); + +for (const message of [ + { route: 'agent', rpc: 'player_login', data: { state: 1 } }, + { route: 'agent', rpc: 'kick_server', data: { msg: 'bye' } }, +]) test(`${message.rpc} failure reaches existing handlers before host notification`, async t => { + const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); + h.wire.subscribe(event => { if (event.type === 'message') h.wire.stop(); }); + await h.push(message); + assert.deepEqual(h.order, ['open', message.rpc, 'fault']); + assert.equal(h.faults.length, 1); +}); + +test('outbound scope and explicit switch fail without sending or reconnecting', async t => { + for (const operation of ['send', 'switch'] as const) { + const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); + assert.throws(() => operation === 'send' + ? h.wire.send({ ...login, rpc: 'create_room' }) : h.wire.switchServer(h.context.config.servers[0]!), /scope|switchServer/); + assert.deepEqual(h.transports[0]!.sent, []); assert.equal(h.faults.length, 1); + } +}); + +test('normal network close reconnects with the same single login envelope', async t => { + t.mock.timers.enable({ apis: ['setTimeout'] }); + const h = await setup(); t.after(() => h.wire.stop()); + h.wire.subscribe(event => { if (event.type === 'open') h.wire.send(login); }); + h.wire.start(); await h.transports[0]!.flush(); + h.transports[0]!.serverError(); await h.transports[0]!.flush(); t.mock.timers.tick(RECONNECT_INTERVAL_MS); + assert.equal(h.transports.length, 2); await h.transports[1]!.flush(); + assert.equal(h.transports[0]!.sent[0], JSON.stringify(login)); + assert.deepEqual(h.transports[1]!.sent, h.transports[0]!.sent); assert.deepEqual(h.faults, []); +}); + +test('scope fault retains its primary error and independent cleanup error', async t => { + const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); + const cleanup = new Error('cleanup failed'); + h.transports[0]!.close = () => { h.transports[0]!.closeCalls++; throw cleanup; }; + await h.push({ route: 'room', rpc: 'notice', data: {} }); + assert.equal(h.faults.length, 1); + const errors = (h.faults[0] as { errors: unknown[] }).errors; + assert.match(String(errors[0]), /room/); assert.equal(errors[1], cleanup); + h.wire.stop(); assert.equal(h.transports[0]!.closeCalls, 1); +}); + +test('evidence failure preserves the exact thrown value plus cleanup error and prevents delivery', async t => { + const primary = { reason: 'evidence unavailable' }; + const h = await setup(event => { + if (event.kind === 'wire-event' && (event.data as WireEvent).type === 'message') throw primary; + }); + t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); + const cleanup = new Error('close failed'); + h.transports[0]!.close = () => { h.transports[0]!.closeCalls++; throw cleanup; }; + await h.push(fixture('player-login-success.json')); + assert.deepEqual((h.faults[0] as { errors: unknown[] }).errors, [primary, cleanup]); + assert.equal(h.events.length, 1); assert.equal(h.faults.length, 1); +}); + +test('listener error is reported unchanged, stops remaining delivery, and stops only once', async t => { + const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); + const primary = new Error('downstream failed'); + h.wire.subscribe(() => { throw primary; }); + let laterDeliveries = 0; h.wire.subscribe(() => laterDeliveries++); + await h.push(fixture('player-login-success.json')); + assert.equal(h.faults[0], primary); assert.equal(laterDeliveries, 0); + h.wire.stop(); assert.equal(h.transports[0]!.closeCalls, 1); +}); + +test('terminal callback cannot send or reconnect before runtime stop; host notification is single', async t => { + const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); + h.wire.subscribe(event => { + if (event.type !== 'message') return; + assert.throws(() => h.wire.send(login), /terminal/); + assert.throws(() => h.wire.reconnectCurrent(), /terminal/); + }); + await h.push({ route: 'agent', rpc: 'kick_server', data: { msg: 'bye' } }); + assert.equal(h.faults.length, 1); assert.deepEqual(h.transports[0]!.sent, []); +}); From e1b290ea22cb60ec9036983e4fa6f20781570938 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 18:31:16 +0800 Subject: [PATCH 05/25] feat: compose local platform login host --- .../local-platform-login/local-login-host.ts | 195 ++++++++++++++++++ .../local-platform-login/host.test.ts | 178 ++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts create mode 100644 cocoscreator_projects/framework-tests/local-platform-login/host.test.ts diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts new file mode 100644 index 0000000..00e6418 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts @@ -0,0 +1,195 @@ +import { resolveLocalStartup } from '../../framework/config/local-startup.ts'; +import type { ResolveRuntimeConfigOptions } from '../../framework/config/runtime-config.ts'; +import { createLocalLoginSources, type LocalHostSnapshot, type LocalLoginSources } from '../../framework/adapters/local/login-sources.ts'; +import type { LocalStoragePort } from '../../framework/adapters/local/local-storage.ts'; +import { PlatformRuntime } from '../../framework/platform/runtime.ts'; +import type { PlatformState } from '../../framework/platform/stores/platform-types.ts'; +import { PlatformUiController } from '../../framework/presentation/platform-ui-controller.ts'; +import type { PlatformViewPort, FrameScheduler } from '../../framework/presentation/ui-contracts.ts'; +import { createDiagnosticGameEntry } from './local-login-game-entry.ts'; +import { createLocalLoginWire } from './local-login-wire.ts'; +import { LocalLoginTransport, type EvidenceSink } from './local-login-transport.ts'; + +export interface LocalLoginHostOptions { + configOptions: ResolveRuntimeConfigOptions; + storage: LocalStoragePort; + random: () => number; + now: () => number; + sampleHost: () => LocalHostSnapshot; + view: PlatformViewPort; + scheduler: FrameScheduler; + loadResources: () => Promise; + waitForMinimumDisplay: () => Promise; + createSocket: (url: string) => WebSocket; + record: EvidenceSink; + onBusy: (busy: boolean) => void; + onFailure: (error: unknown) => void; +} + +export interface LocalLoginHost { + start(): Promise; + login(): Promise; + stop(): void; + getSnapshot(): { state: PlatformState | null; ready: boolean; busy: boolean }; +} + +class LocalLoginHostFault extends Error { + readonly errors: readonly unknown[]; + constructor(errors: readonly unknown[]) { + super('Local login host failed with additional observer or cleanup errors'); + this.name = 'LocalLoginHostFault'; + this.errors = Object.freeze([...errors]); + } +} + +function collected(errors: readonly unknown[]): unknown { + return errors.length === 1 ? errors[0] : new LocalLoginHostFault(errors); +} + +/** Development composition only; Runtime remains the sole business state owner. */ +export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLoginHost { + let runtime: PlatformRuntime | null = null; + let controller: PlatformUiController | null = null; + let sources: LocalLoginSources | null = null; + let startup: Promise | null = null; + let loginOperation: Promise | null = null; + let terminal = false; + let busy = false; + let cleanupErrors: unknown[] | null = null; + let serverFailure: { data: unknown } | null = null; + + function requireActive(): void { + if (terminal) throw new Error('Local login host is stopped or terminal'); + } + + function setBusy(value: boolean): void { + if (busy === value) return; + busy = value; + options.onBusy(value); + } + + function shutdown(errors: unknown[]): void { + terminal = true; + cleanupErrors = errors; + // Disable input before releasing either owner; dispose preserves drawn output. + try { setBusy(false); } catch (error) { errors.push(error); } + try { controller?.dispose(); } catch (error) { errors.push(error); } + try { runtime?.stop(); } catch (error) { errors.push(error); } + cleanupErrors = null; + } + + function fail(error: unknown): void { + if (cleanupErrors !== null) { + if (!cleanupErrors.includes(error)) cleanupErrors.push(error); + return; + } + if (terminal) return; + const errors = [error]; + shutdown(errors); + options.onFailure(collected(errors)); + } + + function wireFault(error: unknown): void { + if (serverFailure === null) { fail(error); return; } + // The Wire scope marker follows the existing denial/kick handler. Keep the + // original server payload, and retain any independent Wire cleanup errors. + const extras = error instanceof Error && 'errors' in error + ? (error as Error & { errors: readonly unknown[] }).errors.slice(1) : []; + fail(collected([serverFailure.data, ...extras])); + } + + function transportFault(error: unknown): void { + fail(serverFailure === null ? error : collected([serverFailure.data, error])); + } + + async function readyTask(task: () => Promise): Promise { + try { requireActive(); await task(); } + catch (error) { fail(error); throw error; } + } + + function start(): Promise { + if (startup !== null) return startup; + if (terminal) return Promise.reject(new Error('Local login host is stopped or terminal')); + startup = Promise.resolve().then(async () => { + requireActive(); + const context = await resolveLocalStartup(options.configOptions); + requireActive(); + options.record({ kind: 'startup-config', data: context.config }); + requireActive(); + const entry = createDiagnosticGameEntry(context.config.identity.gameid, options.record); + sources = createLocalLoginSources({ context, storage: options.storage, random: options.random, + now: options.now, sampleHost: options.sampleHost, + isSessionActive: () => busy || runtime?.state.app.phase === 'logged-in' }); + controller = new PlatformUiController({ + render: model => { + if (terminal) return; + try { options.view.render(model); } + catch (error) { fail(error); throw error; } + }, + activate: page => { + if (terminal) return; + try { + options.view.activate(page); + if (!terminal && page === 'lobby') setBusy(false); + } catch (error) { fail(error); throw error; } + }, + showOverlay: model => { + if (terminal) return; + if (model.kind === 'denial') serverFailure = { data: model.outcome.data }; + if (model.kind === 'kicked') serverFailure = { data: model.data }; + try { options.view.showOverlay(model); } + catch (error) { transportFault(error); throw error; } + if (model.kind === 'fatal') fail(model.error); + }, + }, options.scheduler, fail); + runtime = new PlatformRuntime({ gameEntry: entry, + resolveRuntimeConfig: async () => context.config, + createWireClient: () => createLocalLoginWire({ context, record: options.record, onFault: wireFault, + transportFactory: () => new LocalLoginTransport({ context, createSocket: options.createSocket, + record: options.record, onFault: transportFault }) }), + scene: controller, loadResources: () => readyTask(options.loadResources), + waitForMinimumDisplay: () => readyTask(options.waitForMinimumDisplay), + getLoginDeviceSnapshot: sources.getLoginDeviceSnapshot, + }); + controller.connect(runtime); + requireActive(); + await runtime.start(); + requireActive(); + }).catch(error => { fail(error); throw error; }); + return startup; + } + + function login(): Promise { + if (terminal) return Promise.reject(new Error('Local login host is stopped or terminal')); + if (busy && loginOperation !== null) return loginOperation; + if (runtime === null || controller === null || sources === null || !runtime.ready) { + return Promise.reject(new Error('Local login host is not ready for login')); + } + const activeSources = sources; + const activeController = controller; + loginOperation = Promise.resolve().then(async () => { + requireActive(); + const account = await activeSources.prepareLogin(); + requireActive(); + activeController.login(account); + }).catch(error => { fail(error); throw error; }); + try { setBusy(true); } catch (error) { fail(error); } + return loginOperation; + } + + return { + start, login, + stop() { + if (terminal) return; + const errors: unknown[] = []; + shutdown(errors); + if (errors.length > 0) { + const error = collected(errors); + options.onFailure(error); + throw error; + } + }, + getSnapshot: () => ({ state: runtime === null ? null : runtime.state, + ready: !terminal && runtime !== null && runtime.ready, busy }), + }; +} diff --git a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts new file mode 100644 index 0000000..9b244f0 --- /dev/null +++ b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts @@ -0,0 +1,178 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { test } from 'node:test'; +import type { PageModel, OverlayModel, PageId } from '../../YouleNexus/assets/framework/presentation/ui-contracts.ts'; +import type { ResolveRuntimeConfigOptions } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; + +function deferred() { + let resolve!: () => void; + let reject!: (error: unknown) => void; + const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); + return { promise, resolve, reject }; +} +async function flush() { for (let i = 0; i < 30; i++) await Promise.resolve(); } +function fixture(name = 'player-login-success.json') { + return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')); +} +class Socket { + readyState = 0; + sent: string[] = []; + closes = 0; + closeError?: Error; + onopen: (() => void) | null = null; + onclose: (() => void) | null = null; + onmessage: ((event: { data: string }) => void) | null = null; + onerror: (() => void) | null = null; + send(frame: string) { this.sent.push(frame); } + close() { this.closes++; if (this.closeError) throw this.closeError; this.readyState = 3; this.onclose?.(); } + open() { this.readyState = 1; this.onopen?.(); } + push(message: unknown) { this.onmessage?.({ data: JSON.stringify(message) }); } +} +async function setup(overrides: { config?: Partial; render?: (model: PageModel) => void; + record?: (event: { kind: string; data: unknown }) => void; onBusy?: (busy: boolean) => void } = {}) { + const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-host.ts') + .catch(error => assert.fail(`Local login host implementation must exist: ${String(error)}`)); + const resources = deferred(), minimum = deferred(); + const sockets: Socket[] = [], pages: PageModel[] = [], overlays: OverlayModel[] = [], activations: PageId[] = []; + const failures: unknown[] = [], busy: boolean[] = [], evidence: { kind: string; data: unknown }[] = []; + const values = new Map(), frames = new Set<() => void>(); + let fetches = 0; + const host = api.createLocalLoginHost({ + configOptions: { mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local&version=731', + fetcher: { async fetch(): Promise { fetches++; throw new Error('unexpected fetch'); } }, ...overrides.config }, + storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } }, + random: () => .5, now: () => 123456, sampleHost: () => ({ location: null }), + view: { render: model => { overrides.render?.(model); pages.push(model); }, + activate: page => { activations.push(page); }, showOverlay: model => { overlays.push(model); } }, + scheduler: { request: callback => { frames.add(callback); return () => { frames.delete(callback); }; } }, + loadResources: () => resources.promise, waitForMinimumDisplay: () => minimum.promise, + createSocket: () => { const socket = new Socket(); sockets.push(socket); return socket as unknown as WebSocket; }, + record: event => { evidence.push(event); overrides.record?.(event); }, + onBusy: value => { busy.push(value); overrides.onBusy?.(value); }, onFailure: error => { failures.push(error); }, + }); + const draw = () => { for (const frame of [...frames]) { frames.delete(frame); frame(); } }; + const ready = async () => { const starting = host.start(); resources.resolve(); minimum.resolve(); await flush(); sockets[0]!.open(); await starting; draw(); }; + return { host, resources, minimum, sockets, pages, overlays, activations, failures, busy, evidence, values, frames, draw, ready, fetches: () => fetches }; +} + +for (const missing of ['resources', 'minimum', 'socket'] as const) test(`login waits for ${missing}, and start composes only once`, async t => { + const h = await setup(); t.after(() => h.host.stop()); + const starting = h.host.start(); const repeated = h.host.start(); + assert.strictEqual(starting, repeated); + await flush(); + if (missing !== 'resources') h.resources.resolve(); + if (missing !== 'minimum') h.minimum.resolve(); + if (missing !== 'socket') h.sockets[0]!.open(); + await flush(); + assert.equal(h.host.getSnapshot().ready, false); + await assert.rejects(h.host.login(), /ready/i); + assert.equal(h.values.size, 0); assert.deepEqual(h.sockets[0]!.sent, []); + h.resources.resolve(); h.minimum.resolve(); if (missing === 'socket') h.sockets[0]!.open(); + await starting; + assert.equal(h.host.getSnapshot().ready, true); assert.equal(h.fetches(), 0); + assert.equal(h.sockets.length, 1); + assert.equal(h.evidence.filter(event => event.kind === 'startup-config').length, 1); + await h.host.login(); + assert.equal(JSON.parse(h.sockets[0]!.sent[0]!).data.version, 731); +}); + +test('duplicate clicks send once; response updates the sole Store and lobby projection and clears busy', async t => { + const h = await setup(); t.after(() => h.host.stop()); await h.ready(); + const first = h.host.login(), second = h.host.login(); assert.strictEqual(first, second); + assert.equal(h.host.getSnapshot().busy, true); + await first; await h.host.login(); assert.equal(h.sockets[0]!.sent.length, 1); + assert.deepEqual(h.busy, [true]); + h.sockets[0]!.push(fixture()); h.draw(); + assert.equal(h.host.getSnapshot().state?.app.phase, 'logged-in'); + assert.equal(h.host.getSnapshot().state?.room.kind, 'outside'); + assert.deepEqual(h.pages[h.pages.length - 1], { page: 'lobby', self: { playerid: 430511, nickname: '测试号', avatar: 'http://a', bean: 0 } }); + assert.deepEqual(h.busy, [true, false]); + const before = h.pages.length; h.host.stop(); h.draw(); assert.equal(h.pages.length, before); +}); + +for (const rpc of ['player_login', 'kick_server']) test(`${rpc} retains original server reason through terminal cleanup`, async t => { + const h = await setup(); t.after(() => h.host.stop()); await h.ready(); await h.host.login(); + const data = { state: 17, msg: '原始服务器原因' }; + h.sockets[0]!.push({ route: 'agent', rpc, data }); + assert.equal(h.failures.length, 1); assert.deepEqual(h.failures[0], data); + assert.equal(h.host.getSnapshot().ready, false); assert.equal(h.host.getSnapshot().busy, false); + assert.equal(h.sockets[0]!.closes, 1); + await assert.rejects(h.host.login(), /stop|terminal/i); +}); + +test('room recovery is refused before any player or room state enters Store', async t => { + const h = await setup(); t.after(() => h.host.stop()); await h.ready(); await h.host.login(); + h.sockets[0]!.push(fixture('player-login-room.json')); + assert.match(String(h.failures[0]), /room recovery/); + assert.equal(h.host.getSnapshot().state?.players.selfPlayerId, null); + assert.equal(h.host.getSnapshot().state?.room.kind, 'outside'); + assert.equal(h.pages.some(model => model.page === 'lobby'), false); +}); + +test('corrupt account cache prevents sending and preserves the cache', async t => { + const h = await setup(); t.after(() => h.host.stop()); await h.ready(); + // Discover the authority-owned key from a prior independent local login. + const prior = await setup(); t.after(() => prior.host.stop()); await prior.ready(); await prior.host.login(); + for (const [key, value] of prior.values) h.values.set(key, value); + const accountKey = [...h.values].find(([, value]) => value.includes('account'))![0]; + h.values.set(accountKey, '{broken'); + await assert.rejects(h.host.login(), /invalid JSON/); + assert.equal(h.sockets[0]!.sent.length, 0); assert.equal(h.values.get(accountKey), '{broken'); + assert.equal(h.failures.length, 1); +}); + +for (const phase of ['config', 'resources', 'prepare'] as const) test(`stop during ${phase} suppresses late connection, send and render`, async () => { + const h = await setup(); + let operation: Promise; + if (phase === 'prepare') { await h.ready(); operation = h.host.login(); } + else { operation = h.host.start(); if (phase === 'resources') await flush(); } + const settled = operation.catch(() => {}); + // Let config resolution / account preparation begin before terminal stop. + if (phase !== 'resources') await Promise.resolve(); + h.host.stop(); const pageCount = h.pages.length; + h.resources.resolve(); h.minimum.resolve(); await settled; await flush(); h.draw(); + assert.equal(h.pages.length, pageCount); assert.equal(h.frames.size, 0); + assert.equal(h.sockets.flatMap(socket => socket.sent).length, 0); + if (phase === 'config') assert.equal(h.sockets.length, 0); + assert.deepEqual(h.failures, []); +}); + +for (const config of [{ search: '?profile=prod' }, { hostKind: 'native-settings' as const }]) test('invalid local input cannot create an account or connection', async () => { + const h = await setup({ config }); + await assert.rejects(h.host.start(), /local startup/); + assert.equal(h.values.size, 0); assert.equal(h.sockets.length, 0); assert.equal(h.failures.length, 1); +}); + +test('resource failure is reported with original object after shutdown', async () => { + const h = await setup(); const error = new Error('resource dependency missing'); + const starting = h.host.start(); await flush(); h.resources.reject(error); h.minimum.resolve(); + await assert.rejects(starting); + assert.strictEqual(h.failures[0], error); assert.equal(h.sockets[0]!.closes, 1); +}); + +test('observer and socket cleanup failures both survive terminal reporting', async t => { + const observer = new Error('render observer failed'), cleanup = new Error('socket close failed'); + const h = await setup({ render: model => { if (model.page === 'lobby') throw observer; } }); + t.after(() => h.host.stop()); await h.ready(); await h.host.login(); h.sockets[0]!.closeError = cleanup; + h.sockets[0]!.push(fixture()); + const contains = (value: unknown, target: unknown): boolean => value === target || + (!!value && typeof value === 'object' && 'errors' in value && (value.errors as unknown[]).some(error => contains(error, target))); + assert.equal(contains(h.failures[0], observer), true); assert.equal(contains(h.failures[0], cleanup), true); +}); + +test('resource error remains primary when closing its socket also fails', async () => { + const h = await setup(); const primary = new Error('resource failed'), cleanup = new Error('close failed'); + const starting = h.host.start(); await flush(); h.sockets[0]!.closeError = cleanup; + h.resources.reject(primary); h.minimum.resolve(); await assert.rejects(starting); + const errors = (h.failures[0] as { errors: unknown[] }).errors; + assert.strictEqual(errors[0], primary); assert.ok(errors.includes(cleanup)); +}); + +test('kick data remains primary when closing its socket also fails', async () => { + const h = await setup(); await h.ready(); await h.host.login(); + const cleanup = new Error('kick close failed'); h.sockets[0]!.closeError = cleanup; + const data = { msg: 'original kick reason' }; + h.sockets[0]!.push({ route: 'agent', rpc: 'kick_server', data }); + const errors = (h.failures[0] as { errors: unknown[] }).errors; + assert.deepEqual(errors[0], data); assert.ok(errors.includes(cleanup)); +}); From 745b0a1aebe4d18080d1abe1c3ebf644ebea2aff Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 18:32:09 +0800 Subject: [PATCH 06/25] fix: report combined local transport failures once --- .../local-login-transport.ts | 31 +++++++----- .../local-platform-login/transport.test.ts | 50 +++++++++++++++++++ 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts index 03571b8..444026d 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts @@ -14,6 +14,16 @@ interface LocalLoginTransportOptions { record: EvidenceSink; } +/** ES2020 aggregate: retain originals before notifying a terminal observer once. */ +class LocalLoginTransportFault extends Error { + readonly errors: readonly unknown[]; + constructor(errors: readonly unknown[]) { + super('Local login transport failed with an additional cleanup error'); + this.name = 'LocalLoginTransportFault'; + this.errors = Object.freeze([...errors]); + } +} + /** Local-only socket boundary; raw evidence precedes protocol decoding. */ export class LocalLoginTransport implements Transport { private socket: WebSocket | null = null; @@ -53,8 +63,7 @@ export class LocalLoginTransport implements Transport { try { this.options.record({ kind: 'transport-receive', data: event.data }); } catch (error) { - this.stopWithFault(error); - throw error; + throw this.stopWithFault(error); } if (this.socket === socket) this.messageCallback?.(event.data); }; @@ -68,8 +77,7 @@ export class LocalLoginTransport implements Transport { try { this.options.record({ kind: 'transport-error', data: event }); } catch (error) { - this.stopWithFault(error); - throw error; + throw this.stopWithFault(error); } if (this.socket !== socket || this.generation !== generation) return; // Invalidate before close: browsers may dispatch close after error. @@ -93,8 +101,7 @@ export class LocalLoginTransport implements Transport { socket.send(frame); this.options.record({ kind: 'transport-send', data: frame }); } catch (error) { - this.stopWithFault(error); - throw error; + throw this.stopWithFault(error); } } @@ -115,17 +122,17 @@ export class LocalLoginTransport implements Transport { onMessage(callback: (frame: string) => void): void { this.messageCallback = callback; } onClose(callback: () => void): void { this.closeCallback = callback; } - private stopWithFault(error: unknown): void { + private stopWithFault(error: unknown): unknown { const socket = this.socket; this.socket = null; - const cleanupErrors: unknown[] = []; + const errors: unknown[] = [error]; try { socket?.close(); } catch (cleanupError) { - cleanupErrors.push(cleanupError); + errors.push(cleanupError); } - this.options.onFault(error); - // Preserve both independent failures, including non-Error thrown values. - for (const cleanupError of cleanupErrors) this.options.onFault(cleanupError); + const fault = errors.length === 1 ? error : new LocalLoginTransportFault(errors); + this.options.onFault(fault); + return fault; } } diff --git a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts index ab53751..320dda4 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts @@ -271,3 +271,53 @@ for (const failure of ['record', 'close']) { assert.throws(() => h.transport.send('late'), /open/i); }); } + +test('non-text and cleanup failures reach a single terminal observer together', async () => { + const observed: unknown[] = []; + const h = await setup({ onFault: (error) => { if (observed.length === 0) observed.push(error); } }); + h.transport.connect(h.url); + const socket = h.sockets[0]!; + const cleanupError = new Error('close failed'); + socket.closeError = cleanupError; + socket.message(new ArrayBuffer(1)); + assert.equal(h.faults.length, 1); + assert.equal(observed[0], h.faults[0]); + const fault = observed[0] as Error & { errors: unknown[] }; + assert.ok(fault instanceof Error); + assert.equal(fault.errors.length, 2); + assert.ok(fault.errors[0] instanceof TypeError); + assert.equal(fault.errors[1], cleanupError); + assert.throws(() => h.transport.send('late'), /open/i); +}); + +for (const operation of ['receive-record', 'error-record', 'send-record', 'send']) { + test(`${operation} and close failures are reported once and thrown as the same combined fault`, async () => { + const primary = new Error('primary failed'); + const cleanup = new Error('close failed'); + const observed: unknown[] = []; + const h = await setup({ + ...(operation === 'send' ? {} : { record: () => { throw primary; } }), + onFault: (error) => { if (observed.length === 0) observed.push(error); }, + }); + h.transport.connect(h.url); + const socket = h.sockets[0]!; + socket.open(); + socket.closeError = cleanup; + if (operation === 'send') socket.sendError = primary; + let thrown: unknown; + try { + if (operation === 'receive-record') socket.message('frame'); + else if (operation === 'error-record') socket.onerror?.({} as Event); + else h.transport.send('frame'); + } catch (error) { thrown = error; } + assert.equal(h.faults.length, 1); + assert.equal(thrown, observed[0]); + assert.equal(thrown, h.faults[0]); + assert.ok(thrown instanceof Error); + const errors = (thrown as Error & { errors: unknown[] }).errors; + assert.equal(errors.length, 2); + assert.equal(errors[0], primary); + assert.equal(errors[1], cleanup); + assert.throws(() => h.transport.send('late'), /open/i); + }); +} From fff48486548055594bde407eb64572eb57be92d8 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 18:36:52 +0800 Subject: [PATCH 07/25] fix: preserve local host scheduler and fatal errors --- .../local-platform-login/local-login-host.ts | 18 ++++++++- .../local-platform-login/host.test.ts | 38 ++++++++++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts index 00e6418..f7d1cf9 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts @@ -138,10 +138,24 @@ export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLogin if (model.kind === 'denial') serverFailure = { data: model.outcome.data }; if (model.kind === 'kicked') serverFailure = { data: model.data }; try { options.view.showOverlay(model); } - catch (error) { transportFault(error); throw error; } + catch (error) { + if (model.kind === 'fatal') fail(collected([model.error, error])); + else transportFault(error); + throw error; + } if (model.kind === 'fatal') fail(model.error); }, - }, options.scheduler, fail); + }, { + request: callback => { + try { + const cancel = options.scheduler.request(callback); + return () => { + try { cancel(); } + catch (error) { fail(error); throw error; } + }; + } catch (error) { fail(error); throw error; } + }, + }, fail); runtime = new PlatformRuntime({ gameEntry: entry, resolveRuntimeConfig: async () => context.config, createWireClient: () => createLocalLoginWire({ context, record: options.record, onFault: wireFault, diff --git a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts index 9b244f0..c3fcea9 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts @@ -29,6 +29,7 @@ class Socket { push(message: unknown) { this.onmessage?.({ data: JSON.stringify(message) }); } } async function setup(overrides: { config?: Partial; render?: (model: PageModel) => void; + cancelFrame?: () => void; showOverlay?: (model: OverlayModel) => void; record?: (event: { kind: string; data: unknown }) => void; onBusy?: (busy: boolean) => void } = {}) { const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-host.ts') .catch(error => assert.fail(`Local login host implementation must exist: ${String(error)}`)); @@ -43,8 +44,8 @@ async function setup(overrides: { config?: Partial; storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } }, random: () => .5, now: () => 123456, sampleHost: () => ({ location: null }), view: { render: model => { overrides.render?.(model); pages.push(model); }, - activate: page => { activations.push(page); }, showOverlay: model => { overlays.push(model); } }, - scheduler: { request: callback => { frames.add(callback); return () => { frames.delete(callback); }; } }, + activate: page => { activations.push(page); }, showOverlay: model => { overlays.push(model); overrides.showOverlay?.(model); } }, + scheduler: { request: callback => { frames.add(callback); return () => { frames.delete(callback); overrides.cancelFrame?.(); }; } }, loadResources: () => resources.promise, waitForMinimumDisplay: () => minimum.promise, createSocket: () => { const socket = new Socket(); sockets.push(socket); return socket as unknown as WebSocket; }, record: event => { evidence.push(event); overrides.record?.(event); }, @@ -176,3 +177,36 @@ test('kick data remains primary when closing its socket also fails', async () => const errors = (h.failures[0] as { errors: unknown[] }).errors; assert.deepEqual(errors[0], data); assert.ok(errors.includes(cleanup)); }); + +test('pending frame cancellation retains its primary error when socket cleanup also fails', async () => { + const primary = new Error('pending frame cancel failed'), cleanup = new Error('socket close failed'); + let armed = false; + const h = await setup({ cancelFrame: () => { if (armed) throw primary; } }); + await h.ready(); await h.host.login(); + armed = true; h.sockets[0]!.closeError = cleanup; + // The real login response schedules a Store frame, then showLobby flush cancels it. + h.sockets[0]!.push(fixture()); + assert.equal(h.failures.length, 1); + const fault = h.failures[0] as { errors?: unknown[] }; + assert.ok(fault.errors, 'primary and cleanup failures must both be retained'); + assert.strictEqual(fault.errors[0], primary); + assert.ok(fault.errors.includes(cleanup)); + assert.equal(h.host.getSnapshot().ready, false); assert.equal(h.frames.size, 0); + assert.equal(h.sockets[0]!.closes, 1); +}); + +test('fatal overlay observer failure retains the original Runtime error object', async () => { + const observer = new Error('fatal overlay observer failed'); + let primary: Error | undefined; + const h = await setup({ showOverlay: model => { + if (model.kind === 'fatal') { primary = model.error; throw observer; } + } }); + await h.ready(); + h.sockets[0]!.push({ route: 'agent', rpc: 'unsupported_test_rpc', data: {} }); + assert.ok(primary); assert.match(primary.message, /Unsupported platform rpc/); + assert.equal(h.failures.length, 1); + const fault = h.failures[0] as { errors?: unknown[] }; + assert.ok(fault.errors, 'Runtime error and overlay observer error must both be retained'); + assert.strictEqual(fault.errors[0], primary); assert.strictEqual(fault.errors[1], observer); + assert.equal(h.host.getSnapshot().ready, false); assert.equal(h.sockets[0]!.closes, 1); +}); From 690fdef78acb0d214dcdb404595f8736c7d469fa Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 18:53:44 +0800 Subject: [PATCH 08/25] feat: add local platform login Cocos scene and view --- .../assets/scenes/LocalPlatformLogin.scene | 1393 +++++++++++++++++ .../scenes/LocalPlatformLogin.scene.meta | 11 + .../assets/scripts/local-platform-login.meta | 9 + .../LocalPlatformLogin.ts | 207 +++ .../LocalPlatformLogin.ts.meta | 9 + .../local-login-game-entry.ts.meta | 9 + .../local-login-host.ts.meta | 9 + .../local-login-transport.ts.meta | 9 + .../local-platform-login/local-login-view.ts | 136 ++ .../local-login-view.ts.meta | 9 + .../local-login-wire.ts.meta | 9 + .../local-platform-login/view.test.ts | 145 ++ 12 files changed, 1955 insertions(+) create mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene create mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts.meta create mode 100644 cocoscreator_projects/framework-tests/local-platform-login/view.test.ts diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene new file mode 100644 index 0000000..2dd3d00 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene @@ -0,0 +1,1393 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "LocalPlatformLogin", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "scene": { + "__id__": 1 + } + }, + { + "__type__": "cc.Scene", + "_name": "LocalPlatformLogin", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 36 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "autoReleaseAssets": false, + "_globals": { + "__id__": 37 + }, + "_id": "67e69aac-9982-4e78-a44c-1d9c42f016ae" + }, + { + "__type__": "cc.Node", + "_name": "Canvas", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 5 + }, + { + "__id__": 7 + }, + { + "__id__": 17 + }, + { + "__id__": 19 + }, + { + "__id__": 24 + }, + { + "__id__": 29 + } + ], + "_active": true, + "_components": [ + { + "__id__": 34 + }, + { + "__id__": 35 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 800, + "y": 360, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "4fmPDGIcdPtKTxMtiB6nco" + }, + { + "__type__": "cc.Node", + "_name": "Camera", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 1000 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "56FfrqrBlPOJEcpxhwztIB" + }, + { + "__type__": "cc.Camera", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": null, + "_projection": 0, + "_priority": 0, + "_fov": 45, + "_fovAxis": 0, + "_orthoHeight": 360, + "_near": 0.1, + "_far": 2000, + "_color": { + "__type__": "cc.Color", + "r": 20, + "g": 29, + "b": 45, + "a": 255 + }, + "_depth": 1, + "_stencil": 0, + "_clearFlags": 7, + "_rect": { + "__type__": "cc.Rect", + "x": 0, + "y": 0, + "width": 1, + "height": 1 + }, + "_aperture": 19, + "_shutter": 7, + "_iso": 0, + "_screenScale": 1, + "_visibility": 33554432, + "_targetTexture": null, + "_postProcess": null, + "_usePostProcess": false, + "_cameraType": -1, + "_trackingType": 0, + "_id": "5fJx8MoDNLDruFvKpvUqV3" + }, + { + "__type__": "cc.Node", + "_name": "Host", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 6 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "273kGrcqlGnqgl7sJetcLH" + }, + { + "__type__": "25970YMrRZG8YntQr+ux0rZ", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 5 + }, + "_enabled": true, + "__prefab": null, + "loadingPrefab": { + "__uuid__": "4db0114a-f751-4931-bfaf-4fef31430583", + "__expectedType__": "cc.Prefab" + }, + "loginPrefab": { + "__uuid__": "b209cefb-b381-4c7c-b765-94a5ab75a89c", + "__expectedType__": "cc.Prefab" + }, + "loadingRoot": { + "__id__": 7 + }, + "loginRoot": { + "__id__": 17 + }, + "resultRoot": { + "__id__": 19 + }, + "statusOverlay": { + "__id__": 24 + }, + "statusLabel": { + "__id__": 27 + }, + "resultLabel": { + "__id__": 22 + }, + "stopButton": { + "__id__": 29 + }, + "_id": "94INvS4pdAnIe3wFZxabB7" + }, + { + "__type__": "cc.Node", + "_name": "LoadingRoot", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 8 + } + ], + "_active": true, + "_components": [ + { + "__id__": 16 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "40omEGvnFOKq2lXAwsqtP8" + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_prefab": { + "__id__": 9 + }, + "__editorExtras__": {} + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 8 + }, + "asset": { + "__uuid__": "4db0114a-f751-4931-bfaf-4fef31430583", + "__expectedType__": "cc.Prefab" + }, + "fileId": "RyJAROM7sGTy6Li8CWDPZA", + "instance": { + "__id__": 10 + }, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "ff8m2dc1lLAqLAsOz8udcb", + "prefabRootNode": null, + "mountedChildren": [], + "mountedComponents": [], + "propertyOverrides": [ + { + "__id__": 11 + }, + { + "__id__": 13 + }, + { + "__id__": 14 + }, + { + "__id__": 15 + } + ], + "removedComponents": [] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 12 + }, + "propertyPath": [ + "_name" + ], + "value": "Layer614_Loading" + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "RyJAROM7sGTy6Li8CWDPZA" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 12 + }, + "propertyPath": [ + "_lpos" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 12 + }, + "propertyPath": [ + "_lrot" + ], + "value": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 12 + }, + "propertyPath": [ + "_euler" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 7 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "12ZBzo5G9KBoOxPTXRKk/Q" + }, + { + "__type__": "cc.Node", + "_name": "LoginRoot", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": false, + "_components": [ + { + "__id__": 18 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "d6V5u3kY9ASbSWXectzlbS" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 17 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "40Uk4k/ONMnJXSfDjQDunZ" + }, + { + "__type__": "cc.Node", + "_name": "ResultRoot", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 20 + } + ], + "_active": false, + "_components": [ + { + "__id__": 23 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "4e40f7pcFAipccXYiKt1LW" + }, + { + "__type__": "cc.Node", + "_name": "ResultLabel", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 19 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 21 + }, + { + "__id__": 22 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "f9tsqXSTtDdqA0mcIgv5m8" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 20 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 1200, + "height": 380 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "44DJyd4DhIlI4pbBdfv/QM" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 20 + }, + "_enabled": true, + "__prefab": null, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 245, + "g": 246, + "b": 250, + "a": 255 + }, + "_string": "登录结果", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 0, + "_fontSize": 36, + "_fontFamily": "Arial", + "_lineHeight": 48, + "_overflow": 0, + "_enableWrapText": true, + "_font": null, + "_isSystemFontUsed": true, + "_spacingX": 0, + "_isItalic": false, + "_isBold": false, + "_isUnderline": false, + "_underlineHeight": 2, + "_cacheMode": 0, + "_enableOutline": false, + "_outlineColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_outlineWidth": 2, + "_enableShadow": false, + "_shadowColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_shadowOffset": { + "__type__": "cc.Vec2", + "x": 2, + "y": 2 + }, + "_shadowBlur": 2, + "_id": "01Oedc9UVL64Gj9z6SWzgN" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 19 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "fcIBurW2VOWo3ZMkOtLxIu" + }, + { + "__type__": "cc.Node", + "_name": "StatusOverlay", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 25 + } + ], + "_active": true, + "_components": [ + { + "__id__": 28 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "a6kdhP/8ZLMLeFOXf1x03K" + }, + { + "__type__": "cc.Node", + "_name": "StatusLabel", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 24 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 26 + }, + { + "__id__": 27 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -265, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "13eN54GNpEHIFD+205kDRk" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 25 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 363.8892470046937, + "height": 47.879999999999995 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "b2FWK2DwBNLL7XlUTSI/Nw" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 25 + }, + "_enabled": true, + "__prefab": null, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 245, + "g": 246, + "b": 250, + "a": 255 + }, + "_string": "正在加载资源和连接本地服务…", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 27.408333333333335, + "_fontSize": 26, + "_fontFamily": "Arial", + "_lineHeight": 38, + "_overflow": 0, + "_enableWrapText": true, + "_font": null, + "_isSystemFontUsed": true, + "_spacingX": 0, + "_isItalic": false, + "_isBold": false, + "_isUnderline": false, + "_underlineHeight": 2, + "_cacheMode": 0, + "_enableOutline": false, + "_outlineColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_outlineWidth": 2, + "_enableShadow": false, + "_shadowColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_shadowOffset": { + "__type__": "cc.Vec2", + "x": 2, + "y": 2 + }, + "_shadowBlur": 2, + "_id": "51RskzXn5Ata2xeyOVlNCY" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 24 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "a2g6t0veZNVry/oIZkFXdJ" + }, + { + "__type__": "cc.Node", + "_name": "StopButton", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 30 + } + ], + "_active": true, + "_components": [ + { + "__id__": 33 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 665, + "y": 310, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "00faTTaCpK+6dsBhx902Te" + }, + { + "__type__": "cc.Node", + "_name": "StopLabel", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 29 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 31 + }, + { + "__id__": 32 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "e4Tfs0ZLVBiaGDxp3kpgsI" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 30 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 103.96835628705533, + "height": 47.879999999999995 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "d0s+NCQGxLt5QFk5h0CZVa" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 30 + }, + "_enabled": true, + "__prefab": null, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 245, + "g": 246, + "b": 250, + "a": 255 + }, + "_string": "停止验证", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 27.408333333333335, + "_fontSize": 26, + "_fontFamily": "Arial", + "_lineHeight": 38, + "_overflow": 0, + "_enableWrapText": true, + "_font": null, + "_isSystemFontUsed": true, + "_spacingX": 0, + "_isItalic": false, + "_isBold": false, + "_isUnderline": false, + "_underlineHeight": 2, + "_cacheMode": 0, + "_enableOutline": false, + "_outlineColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_outlineWidth": 2, + "_enableShadow": false, + "_shadowColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_shadowOffset": { + "__type__": "cc.Vec2", + "x": 2, + "y": 2 + }, + "_shadowBlur": 2, + "_id": "eewB3DGj9MzpyN67ez8juQ" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 29 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 220, + "height": 64 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "52C95shHpLdqYKR29izOA+" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "ebjzNd6xRN6rCykuT0wir7" + }, + { + "__type__": "cc.Canvas", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_cameraComponent": { + "__id__": 4 + }, + "_alignCanvasWithScreen": true, + "_id": "793QSuOMFHY6zVWL61HGbM" + }, + { + "__type__": "cc.PrefabInfo", + "root": null, + "asset": null, + "fileId": "67e69aac-9982-4e78-a44c-1d9c42f016ae", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": [ + { + "__id__": 8 + } + ] + }, + { + "__type__": "cc.SceneGlobals", + "ambient": { + "__id__": 38 + }, + "shadows": { + "__id__": 39 + }, + "_skybox": { + "__id__": 40 + }, + "fog": { + "__id__": 41 + }, + "octree": { + "__id__": 42 + }, + "skin": { + "__id__": 43 + }, + "lightProbeInfo": { + "__id__": 44 + }, + "postSettings": { + "__id__": 45 + }, + "bakedWithStationaryMainLight": false, + "bakedWithHighpLightmap": false + }, + { + "__type__": "cc.AmbientInfo", + "_skyColorHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyColor": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyIllumHDR": 20000, + "_skyIllum": 20000, + "_groundAlbedoHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_groundAlbedo": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_skyColorLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 1 + }, + "_skyIllumLDR": 20000, + "_groundAlbedoLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + } + }, + { + "__type__": "cc.ShadowsInfo", + "_enabled": false, + "_type": 0, + "_normal": { + "__type__": "cc.Vec3", + "x": 0, + "y": 1, + "z": 0 + }, + "_distance": 0, + "_planeBias": 1, + "_shadowColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 76 + }, + "_maxReceived": 4, + "_size": { + "__type__": "cc.Vec2", + "x": 1024, + "y": 1024 + } + }, + { + "__type__": "cc.SkyboxInfo", + "_envLightingType": 0, + "_envmapHDR": null, + "_envmap": null, + "_envmapLDR": null, + "_diffuseMapHDR": null, + "_diffuseMapLDR": null, + "_enabled": false, + "_useHDR": true, + "_editableMaterial": null, + "_reflectionHDR": null, + "_reflectionLDR": null, + "_rotationAngle": 0 + }, + { + "__type__": "cc.FogInfo", + "_type": 0, + "_fogColor": { + "__type__": "cc.Color", + "r": 200, + "g": 200, + "b": 200, + "a": 255 + }, + "_enabled": false, + "_fogDensity": 0.3, + "_fogStart": 0.5, + "_fogEnd": 300, + "_fogAtten": 5, + "_fogTop": 1.5, + "_fogRange": 1.2, + "_accurate": false + }, + { + "__type__": "cc.OctreeInfo", + "_enabled": false, + "_minPos": { + "__type__": "cc.Vec3", + "x": -1024, + "y": -1024, + "z": -1024 + }, + "_maxPos": { + "__type__": "cc.Vec3", + "x": 1024, + "y": 1024, + "z": 1024 + }, + "_depth": 8 + }, + { + "__type__": "cc.SkinInfo", + "_enabled": true, + "_blurRadius": 0.01, + "_sssIntensity": 3 + }, + { + "__type__": "cc.LightProbeInfo", + "_giScale": 1, + "_giSamples": 1024, + "_bounces": 2, + "_reduceRinging": 0, + "_showProbe": true, + "_showWireframe": true, + "_showConvex": false, + "_data": null, + "_lightProbeSphereVolume": 1 + }, + { + "__type__": "cc.PostSettingsInfo", + "_toneMappingType": 0 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene.meta b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene.meta new file mode 100644 index 0000000..a88364b --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.1.50", + "importer": "scene", + "imported": true, + "uuid": "67e69aac-9982-4e78-a44c-1d9c42f016ae", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login.meta new file mode 100644 index 0000000..42f296d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "6072a7f3-9095-4e71-9dbd-c952d89a64a7", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts new file mode 100644 index 0000000..2f76a7a --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts @@ -0,0 +1,207 @@ +import { _decorator, Component, Label, Node, Prefab, Sprite, UITransform, instantiate, isValid, sys } from 'cc'; +import { DEBUG } from 'cc/env'; +import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts'; +import type { NativeHost } from '../../framework/config/sources/native-settings.ts'; +import type { RuntimeConfig } from '../../framework/config/runtime-config.ts'; +import type { PageModel } from '../../framework/presentation/ui-contracts.ts'; +import { createLocalLoginHost, type LocalLoginHost } from './local-login-host.ts'; +import type { LocalLoginEvidence } from './local-login-transport.ts'; +import { CocosLocalLoginView, createCocosFrameScheduler, requireNode } from './local-login-view.ts'; + +const { ccclass, property } = _decorator; +const MINIMUM_LOADING_MS = 500; + +/** Explicit DEBUG browser scene entry. All business state belongs to its host Runtime. */ +@ccclass('LocalPlatformLogin') +export class LocalPlatformLogin extends Component { + @property(Prefab) loadingPrefab: Prefab | null = null; + @property(Prefab) loginPrefab: Prefab | null = null; + @property(Node) loadingRoot: Node | null = null; + @property(Node) loginRoot: Node | null = null; + @property(Node) resultRoot: Node | null = null; + @property(Node) statusOverlay: Node | null = null; + @property(Label) statusLabel: Label | null = null; + @property(Label) resultLabel: Label | null = null; + @property(Node) stopButton: Node | null = null; + + private host: LocalLoginHost | null = null; + private view: CocosLocalLoginView | null = null; + private spinner: Node | null = null; + private loginInstance: Node | null = null; + private stopped = false; + private firstDrawAt: number | null = null; + private version: number | null = null; + private readonly pending = new Set<() => void>(); + private readonly evidence: (LocalLoginEvidence & { at: number })[] = []; + private pageModel: PageModel | null = null; + private readonly stopTouch = () => this.stop(); + + async start(): Promise { + // This guard precedes reading browser storage, preparing account or creating socket. + if (!DEBUG || sys.isNative) throw new Error('LocalPlatformLogin requires DEBUG browser preview'); + try { + const loadingRoot = this.required(this.loadingRoot, 'loadingRoot'); + const loginRoot = this.required(this.loginRoot, 'loginRoot'); + const stopButton = this.required(this.stopButton, 'stopButton'); + this.view = new CocosLocalLoginView({ loadingRoot, loginRoot, + resultRoot: this.required(this.resultRoot, 'resultRoot'), + statusOverlay: this.required(this.statusOverlay, 'statusOverlay'), + statusLabel: this.required(this.statusLabel, 'statusLabel'), + resultLabel: this.required(this.resultLabel, 'resultLabel') }); + this.required(this.loadingPrefab, 'loadingPrefab'); + this.required(this.loginPrefab, 'loginPrefab'); + this.spinner = requireNode(loadingRoot, 'Layer614_Loading/group-40/载入动画'); + this.validateSprites(loadingRoot); + if (this.spinner.getComponent(UITransform) === null || this.spinner.getComponent(Sprite) === null) { + throw new Error('Loading spinner requires UITransform and Sprite'); + } + stopButton.on(Node.EventType.TOUCH_END, this.stopTouch); + this.view.activate('loading'); + const firstDraw = this.nextDraw().then(() => { + this.firstDrawAt = performance.now(); + this.record({ kind: 'loading-first-draw', data: null }); + }); + // A configuration failure can cancel this frame before Runtime consumes its gate. + void firstDraw.catch(() => {}); + const view = this.view; + const browser = window as unknown as NativeHost & { returnCitySN?: unknown }; + this.host = createLocalLoginHost({ + configOptions: { mode: 'debug', hostKind: 'h5', search: window.location.search, + win: browser, fetcher: new HttpConfigFetcher() }, + storage: window.localStorage, random: Math.random, now: Date.now, + sampleHost: () => ({ location: null, + ...('returnCitySN' in browser ? { returnCitySN: browser.returnCitySN } : {}) }), + view: { + render: model => { this.pageModel = model; this.record({ kind: 'page-model', data: model }); view.render(model); }, + activate: page => { view.activate(page); this.record({ kind: 'page-active', data: page }); }, + showOverlay: model => view.showOverlay(model), + }, + scheduler: createCocosFrameScheduler(), + loadResources: () => this.loadResources(), + waitForMinimumDisplay: async () => { + await firstDraw; + const drawnAt = this.required(this.firstDrawAt, 'loading first draw'); + await this.delay(Math.max(0, MINIMUM_LOADING_MS - (performance.now() - drawnAt))); + this.record({ kind: 'minimum-display-ready', data: performance.now() - drawnAt }); + }, + createSocket: url => new WebSocket(url), record: event => this.record(event), + onBusy: busy => view.setBusy(busy), + onFailure: error => this.fail(error), + }); + await this.host.start(); + } catch (error) { this.fail(error); } + } + + private required(value: T | null, name: string): T { + if (value === null || value === undefined) throw new Error(`LocalPlatformLogin missing ${name}`); + return value; + } + + private record(event: LocalLoginEvidence): void { + if (event.kind === 'startup-config') this.version = (event.data as RuntimeConfig).identity.version; + this.evidence.push(Object.freeze({ ...event, at: performance.now() })); + } + + private validateSprites(root: Node): void { + for (const sprite of root.getComponentsInChildren(Sprite)) { + if (!sprite.node.activeInHierarchy) continue; + const frame = sprite.spriteFrame; + if (frame === null || !isValid(frame) || frame.texture === null || !isValid(frame.texture)) { + throw new Error(`Invalid SpriteFrame/texture dependency: ${sprite.node.name}`); + } + } + } + + private async loadResources(): Promise { + if (this.stopped) throw new Error('Local login resources cancelled'); + const prefab = this.required(this.loginPrefab, 'loginPrefab'); + const loading = this.required(this.loadingPrefab, 'loadingPrefab'); + if (!isValid(prefab) || !isValid(prefab.data) || !isValid(loading) || !isValid(loading.data)) { + throw new Error('Local login serialized Prefab dependency is invalid'); + } + const instance = instantiate(prefab); + this.loginInstance = instance; + instance.parent = this.required(this.loginRoot, 'loginRoot'); + const view = this.required(this.view, 'view'); + view.bindLogin(instance, () => { + const host = this.required(this.host, 'host'); + void host.login().catch(error => this.fail(error)); + }); + view.setVersion(this.required(this.version, 'resolved identity.version')); + // Validate active prefab content before the root is exposed as the login page. + for (const sprite of instance.getComponentsInChildren(Sprite)) { + if (!sprite.node.active) continue; + const frame = sprite.spriteFrame; + if (frame === null || !isValid(frame) || frame.texture === null || !isValid(frame.texture)) { + throw new Error(`Invalid login SpriteFrame/texture dependency: ${sprite.node.name}`); + } + } + await this.nextDraw(); + this.record({ kind: 'resources-ready', data: { login: prefab.uuid, loading: loading.uuid } }); + } + + private nextDraw(): Promise { + return new Promise((resolve, reject) => { + if (this.stopped) { reject(new Error('Local login frame cancelled')); return; } + const cancelFrame = createCocosFrameScheduler().request(() => { + this.pending.delete(cancel); resolve(); + }); + const cancel = () => { cancelFrame(); this.pending.delete(cancel); reject(new Error('Local login frame cancelled')); }; + this.pending.add(cancel); + }); + } + + private delay(milliseconds: number): Promise { + return new Promise((resolve, reject) => { + if (this.stopped) { reject(new Error('Local login timer cancelled')); return; } + const timer = window.setTimeout(() => { this.pending.delete(cancel); resolve(); }, milliseconds); + const cancel = () => { window.clearTimeout(timer); this.pending.delete(cancel); reject(new Error('Local login timer cancelled')); }; + this.pending.add(cancel); + }); + } + + update(deltaSeconds: number): void { + if (!this.stopped && this.spinner !== null && this.spinner.activeInHierarchy) this.spinner.angle -= deltaSeconds * 180; + } + + private release(): void { + this.stopped = true; + for (const cancel of [...this.pending]) cancel(); + this.stopButton?.off(Node.EventType.TOUCH_END, this.stopTouch); + this.view?.dispose(); + this.host?.stop(); + } + + private fail(error: unknown): void { + if (this.stopped) return; + this.record({ kind: 'component-failure', data: error }); + try { this.release(); } + catch (cleanup) { + this.record({ kind: 'cleanup-failure', data: cleanup }); + this.view?.showFailure({ error, cleanup }); + return; + } + this.view?.showFailure(error); + if (this.view === null && this.statusLabel !== null) this.statusLabel.string = `验证失败:${String(error)}`; + } + + stop(): void { + if (this.stopped) return; + this.view?.showStopped(); + try { this.release(); } + catch (error) { this.view?.showFailure(error); } + this.record({ kind: 'stopped', data: null }); + } + + onDestroy(): void { + this.stop(); + if (this.loginInstance !== null && isValid(this.loginInstance)) this.loginInstance.destroy(); + } + + /** Local memory only: callers must redact raw account/device IDs before sharing. */ + getDiagnostics() { + return Object.freeze({ events: Object.freeze([...this.evidence]), snapshot: this.host?.getSnapshot(), + pageModel: this.pageModel, stopped: this.stopped, pendingCount: this.pending.size, + firstDrawAt: this.firstDrawAt, spinnerAngle: this.spinner?.angle }); + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts.meta new file mode 100644 index 0000000..6737380 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "2597060c-ad16-46f1-89ed-42bfaec74ad9", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts.meta new file mode 100644 index 0000000..0f093bc --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "aea065db-0dfa-4da8-a2cb-f1fe0165d910", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts.meta new file mode 100644 index 0000000..53c7288 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "2c6ce55f-b5a3-4503-bc65-ca12c5537c8f", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts.meta new file mode 100644 index 0000000..dbeb6af --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "2bbf94d8-6aa1-4b5f-a94c-c3f12205f46c", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts new file mode 100644 index 0000000..85fba6c --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts @@ -0,0 +1,136 @@ +import { Director, Label, Node, Sprite, UITransform, director } from 'cc'; +import type { FrameScheduler, OverlayModel, PageId, PageModel, PlatformViewPort } from '../../framework/presentation/ui-contracts.ts'; + +interface ViewReferences { + loadingRoot: Node; loginRoot: Node; resultRoot: Node; + statusOverlay: Node; statusLabel: Label; resultLabel: Label; +} + +export function requireNode(root: Node, path: string): Node { + const node = root.getChildByPath(path); + if (node === null) throw new Error(`Local login binding missing node: ${path}`); + return node; +} + +function describeFailure(error: unknown): string { + if (error instanceof Error) { + const details = 'errors' in error ? (error as Error & { errors: unknown[] }).errors : []; + return [error.message, ...details.map(describeFailure)].join('\n'); + } + if (typeof error === 'string') return error; + return JSON.stringify(error, (_key, value: unknown) => value instanceof Error ? describeFailure(value) : value); +} + +/** Projection of Controller models; owns only Cocos bindings and input state. */ +export class CocosLocalLoginView implements PlatformViewPort { + private visitor: Node | null = null; + private version: Label | null = null; + private mask: Node | null = null; + private touch: (() => void) | null = null; + private canLogin = false; + private busy = false; + private terminal = false; + private page: PageId = 'loading'; + + constructor(private readonly refs: ViewReferences) { + for (const [name, value] of Object.entries(refs)) { + if (value === null || value === undefined) throw new Error(`Local login binding missing ${name}`); + } + } + + bindLogin(instance: Node, onLogin: () => void): void { + if (this.visitor !== null || this.terminal) throw new Error('Local login view cannot bind twice or after disposal'); + const visitor = requireNode(instance, 'group-2/游客登录'); + if (visitor.getComponent(Sprite) === null) throw new Error('游客登录 requires Sprite'); + if (visitor.getComponent(UITransform) === null) throw new Error('游客登录 requires UITransform'); + const version = requireNode(instance, 'group-2/版本显示').getComponent(Label); + if (version === null) throw new Error('版本显示 requires Label'); + const mask = requireNode(instance, 'group-2/登录中遮罩'); + const hidden = ['微信登录', '手机登录', 'qq登录', '快速登录', '隐藏快速登录-1', '隐藏快速登录-2', + '登录头像背景框', '登录中头像', '登录中昵称', '登录中动画', '登录中头像框'] + .map(name => requireNode(instance, `group-2/${name}`)); + hidden.forEach(node => { node.active = false; }); + this.visitor = visitor; this.version = version; this.mask = mask; + this.touch = () => { + if (!this.terminal && !this.busy && this.canLogin && this.page === 'login') onLogin(); + }; + visitor.on(Node.EventType.TOUCH_END, this.touch); + mask.active = this.busy; + } + + setVersion(version: number): void { + if (this.version === null) throw new Error('Login instance must be bound before version display'); + this.version.string = String(version); + } + + render(model: PageModel): void { + if (this.terminal) return; + switch (model.page) { + case 'loading': this.refs.statusLabel.string = '正在加载资源和连接本地服务…'; break; + case 'login': this.canLogin = model.canLogin; break; + case 'lobby': + this.refs.resultLabel.string = `登录成功\n玩家 ID:${model.self.playerid}\n昵称:${model.self.nickname}\n豆豆:${model.self.bean}`; + break; + case 'room': throw new Error('Local login view cannot present room'); + } + } + + activate(page: PageId): void { + if (this.terminal) return; + if (page === 'room') throw new Error('Local login view cannot activate room'); + this.page = page; + this.refs.loadingRoot.active = page === 'loading'; + this.refs.loginRoot.active = page === 'login'; + this.refs.resultRoot.active = page === 'lobby'; + this.refs.statusOverlay.active = page === 'loading' || this.busy; + } + + setBusy(busy: boolean): void { + if (this.terminal) return; + this.busy = busy; + if (this.mask !== null) this.mask.active = busy; + this.refs.statusOverlay.active = busy || this.page === 'loading'; + if (busy) this.refs.statusLabel.string = '正在登录,请稍候…'; + } + + showOverlay(model: OverlayModel): void { + if (this.terminal) return; + switch (model.kind) { + case 'none': this.refs.statusOverlay.active = this.busy || this.page === 'loading'; break; + case 'reconnect': + this.refs.statusOverlay.active = true; + this.refs.statusLabel.string = '连接中断,正在重连…'; break; + case 'denial': this.showFailure(model.outcome.data); break; + case 'kicked': this.showFailure(model.data); break; + case 'fatal': this.showFailure(model.error); break; + } + } + + showFailure(error: unknown): void { + this.dispose(); + this.refs.statusOverlay.active = true; + this.refs.statusLabel.string = `验证失败:${describeFailure(error)}`; + } + + showStopped(): void { + if (this.terminal) return; + this.dispose(); + this.refs.statusOverlay.active = true; + this.refs.statusLabel.string = '已停止本次验证'; + } + + dispose(): void { + this.terminal = true; this.canLogin = false; this.busy = false; + if (this.visitor !== null && this.touch !== null) this.visitor.off(Node.EventType.TOUCH_END, this.touch); + this.touch = null; + if (this.mask !== null) this.mask.active = false; + } +} + +export function createCocosFrameScheduler(): FrameScheduler { + return { request(callback) { + const onDraw = () => { director.off(Director.EVENT_AFTER_DRAW, onDraw); callback(); }; + director.on(Director.EVENT_AFTER_DRAW, onDraw); + return () => { director.off(Director.EVENT_AFTER_DRAW, onDraw); }; + } }; +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts.meta new file mode 100644 index 0000000..3eb9eab --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "5af2497c-15f2-4493-be9b-a6391d25b878", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts.meta new file mode 100644 index 0000000..5db90f4 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "96a339e3-f954-4e96-b07f-9fe124d457f0", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts new file mode 100644 index 0000000..3b7320b --- /dev/null +++ b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts @@ -0,0 +1,145 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync, existsSync } from 'node:fs'; +import { runInNewContext } from 'node:vm'; +import ts from 'typescript'; +import { EventEmitter } from 'node:events'; + +class Sprite {} +class UITransform {} +class Label { string = ''; } +class Node extends EventEmitter { + static EventType = { TOUCH_END: 'touch-end' }; + active = true; + children: Node[] = []; + components = new Map(); + constructor(public name = '') { super(); } + getComponent(type: unknown) { return this.components.get(type) ?? null; } + getChildByPath(path: string): Node | null { + const [head, ...tail] = path.split('/'); + const child = this.children.find(node => node.name === head); + return child ? (tail.length ? child.getChildByPath(tail.join('/')) : child) : null; + } +} +const director = new EventEmitter(); +const source = new URL('../../YouleNexus/assets/scripts/local-platform-login/local-login-view.ts', import.meta.url); +function load() { + assert.ok(existsSync(source), 'Cocos local login view implementation must exist'); + const exports = {}; + runInNewContext(ts.transpileModule(readFileSync(source, 'utf8'), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText, { exports, require: (name: string) => { + assert.equal(name, 'cc'); + return { Node, Sprite, UITransform, Label, director, Director: { EVENT_AFTER_DRAW: 'draw' } }; + }, Error }); + return exports as any; +} +function fixture() { + const refs = { loadingRoot: new Node(), loginRoot: new Node(), resultRoot: new Node(), + statusOverlay: new Node(), statusLabel: new Label(), resultLabel: new Label() }; + const instance = new Node(); const group = new Node('group-2'); instance.children.push(group); + for (const name of ['游客登录', '版本显示', '微信登录', '手机登录', 'qq登录', '快速登录', + '隐藏快速登录-1', '隐藏快速登录-2', '登录中遮罩', '登录头像背景框', '登录中头像', + '登录中昵称', '登录中动画', '登录中头像框']) { + const node = new Node(name); group.children.push(node); + node.components.set(Sprite, new Sprite()); node.components.set(UITransform, new UITransform()); + node.components.set(Label, new Label()); + } + return { refs, instance, group, visitor: instance.getChildByPath('group-2/游客登录')! }; +} +test('required binding fails explicitly, including missing Sprite', () => { + const { CocosLocalLoginView } = load(); const f = fixture(); + const view = new CocosLocalLoginView(f.refs); + assert.throws(() => view.bindLogin(new Node(), () => {}), /游客登录/); + f.visitor.components.delete(Sprite); + assert.throws(() => view.bindLogin(f.instance, () => {}), /Sprite/); +}); +test('PageModel drives result labels and blocks room presentation', () => { + const { CocosLocalLoginView } = load(); const f = fixture(); const view = new CocosLocalLoginView(f.refs); + view.render({ page: 'lobby', self: { playerid: 19, nickname: '测试玩家', bean: 72, avatar: 'unused' } }); + view.activate('lobby'); + assert.equal(f.refs.resultLabel.string, '登录成功\n玩家 ID:19\n昵称:测试玩家\n豆豆:72'); + assert.equal(f.refs.resultRoot.active, true); assert.equal(f.refs.loginRoot.active, false); + assert.throws(() => view.activate('room'), /room/); +}); +test('busy mask and readiness gate touch, terminal disposal removes ownership', () => { + const { CocosLocalLoginView } = load(); const f = fixture(); const view = new CocosLocalLoginView(f.refs); + let clicks = 0; view.bindLogin(f.instance, () => clicks++); view.setVersion(123); + assert.equal((f.instance.getChildByPath('group-2/版本显示')!.getComponent(Label) as Label).string, '123'); + assert.equal(f.instance.getChildByPath('group-2/微信登录')!.active, false); + view.render({ page: 'login', canLogin: false }); view.activate('login'); f.visitor.emit('touch-end'); + assert.equal(clicks, 0); + view.render({ page: 'login', canLogin: true }); view.setBusy(true); f.visitor.emit('touch-end'); + assert.equal(clicks, 0); assert.equal(f.instance.getChildByPath('group-2/登录中遮罩')!.active, true); + view.setBusy(false); f.visitor.emit('touch-end'); assert.equal(clicks, 1); + view.showFailure({ state: 7, msg: '服务器拒绝' }); view.showOverlay({ kind: 'none' }); + assert.match(f.refs.statusLabel.string, /服务器拒绝/); + f.visitor.emit('touch-end'); assert.equal(clicks, 1); + view.dispose(); assert.equal(f.visitor.listenerCount('touch-end'), 0); +}); +test('frame request is one shot and real cancellation removes pending draw callback', () => { + const { createCocosFrameScheduler } = load(); const scheduler = createCocosFrameScheduler(); let calls = 0; + const cancel = scheduler.request(() => calls++); cancel(); director.emit('draw'); assert.equal(calls, 0); + scheduler.request(() => calls++); director.emit('draw'); director.emit('draw'); assert.equal(calls, 1); + assert.equal(director.listenerCount('draw'), 0); +}); + +test('failure display preserves nested server and cleanup errors', () => { + const { CocosLocalLoginView } = load(); const f = fixture(); + const view = new CocosLocalLoginView(f.refs); + view.showFailure({ error: { state: 8, msg: '版本拒绝' }, cleanup: new Error('socket close failed') }); + assert.match(f.refs.statusLabel.string, /版本拒绝/); + assert.match(f.refs.statusLabel.string, /socket close failed/); +}); + +test('component rejects non-debug/native before creating host or writing browser storage', async () => { + const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url); + assert.ok(existsSync(path), 'LocalPlatformLogin component must exist'); + for (const [debug, native] of [[false, false], [true, true]]) { + let hosts = 0; const exports: any = {}; + class Component {} + const decorator = (..._args: unknown[]) => (target: unknown) => target; + runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, + }).outputText, { exports, require: (name: string) => { + if (name === 'cc/env') return { DEBUG: debug }; + if (name === 'cc') return { Component, Node, Label, Prefab: class {}, sys: { isNative: native }, + _decorator: { ccclass: decorator, property: () => () => {} } }; + if (name.endsWith('local-login-host.ts')) return { createLocalLoginHost: () => { hosts++; } }; + if (name.endsWith('local-login-view.ts')) return load(); + if (name.endsWith('remote-config-fetcher.ts')) return { HttpConfigFetcher: class {} }; + throw new Error(`Unexpected dependency ${name}`); + }, Error }); + const component = new exports.LocalPlatformLogin(); + await assert.rejects(async () => component.start(), /DEBUG.*browser/); + assert.equal(hosts, 0); + } +}); + +test('component stop cancels pending draw and timer work and disables spinner updates', async () => { + const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url); + const exports: any = {}; const timers = new Map void>(); let timerId = 0; + runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, + }).outputText, { exports, require: (name: string) => { + if (name === 'cc/env') return { DEBUG: true }; + if (name === 'cc') return { Component: class {}, Node, Label, Prefab: class {}, sys: { isNative: false }, + _decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } }; + if (name.endsWith('local-login-host.ts')) return {}; + if (name.endsWith('local-login-view.ts')) return load(); + if (name.endsWith('remote-config-fetcher.ts')) return {}; + throw new Error(`Unexpected dependency ${name}`); + }, Error, performance: { now: () => 123 }, window: { + setTimeout: (callback: () => void) => { timers.set(++timerId, callback); return timerId; }, + clearTimeout: (id: number) => timers.delete(id), + } }); + const component = new exports.LocalPlatformLogin(); + const draw = component.nextDraw(); const timer = component.delay(900); + const drawRejection = assert.rejects(draw, /cancelled/); const timerRejection = assert.rejects(timer, /cancelled/); + component.spinner = { angle: 50, activeInHierarchy: true }; + component.update(0.5); assert.equal(component.spinner.angle, -40); + component.stop(); await Promise.all([drawRejection, timerRejection]); + component.update(0.5); assert.equal(component.spinner.angle, -40); + assert.equal(timers.size, 0); assert.equal(director.listenerCount('draw'), 0); + assert.equal(component.getDiagnostics().pendingCount, 0); +}); From 266be19e76e70779c9af53c6ea2d607e4ccb2bd0 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 18:58:44 +0800 Subject: [PATCH 09/25] fix(sdk): inject assertions into Creator-loaded contract harness --- .../YouleNexus/assets/framework/README.md | 4 ++- .../sdk/testing/game-contract-harness.ts | 15 +++++++---- .../sdk/game-contract-harness.test.ts | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/cocoscreator_projects/YouleNexus/assets/framework/README.md b/cocoscreator_projects/YouleNexus/assets/framework/README.md index fd57e0d..cbbd344 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/README.md +++ b/cocoscreator_projects/YouleNexus/assets/framework/README.md @@ -38,7 +38,9 @@ SDK runtime 也不导入框架实现层,平台适配由 `platform/game-host-ad `GameEntry.resolveSeatCount` 只接收真实房间的 `roomtype`,预检只检查该方法存在; GameHost 在进房边界验证返回值是正整数并与服务器座位数组长度相同。 -`assertGameContract(entry, samples)` 的每个样本由游戏提供 `roomtype`、`expectedSeatCount` +`assertGameContract(entry, samples, assert)` 的 `assert` 由测试调用方从 `node:assert/strict` +显式导入并传入;harness 仅引用断言类型,避免 Creator 加载脚本时解析 Node 内置模块。 +每个样本由游戏提供 `roomtype`、`expectedSeatCount` 和对应的 `makeHost` 工厂,不要求游戏支持框架虚构的房间配置。 远程地址唯一取自 `data.urlserver`。配置请求使用空 body 的 POST,沿用无条件 `?` 缓存参数拼接。 diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts b/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts index 1a3bfd2..7c80bb3 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts @@ -1,4 +1,4 @@ -import { strict as assert } from 'node:assert'; +import type { strict } from 'node:assert'; import type { GameEntry, GameHost, @@ -21,8 +21,12 @@ export interface GameContractSample { readonly makeHost: GameContractHostFactory; } -/** Runs the shared lifecycle checks required of the app's sole GameEntry. */ -export function assertGameContract(entry: GameEntry, samples: readonly GameContractSample[]): void { +/** Runs lifecycle checks with assertions explicitly supplied by the test caller. */ +export function assertGameContract( + entry: GameEntry, + samples: readonly GameContractSample[], + assert: typeof strict, +): void { assert.ok(samples.length > 0, 'conformance requires game-owned roomtype samples'); assertGameEntry(entry); for (const route of ['platform', 'agent', 'room']) { @@ -48,8 +52,8 @@ export function assertGameContract(entry: GameEntry, samples: readonly GameContr const sessions = new GameSessionHost(entry); for (const sample of samples) { - runCycle(sessions, sample, entry, 1); - runCycle(sessions, sample, entry, 2); + runCycle(sessions, sample, entry, 1, assert); + runCycle(sessions, sample, entry, 2, assert); } assert.equal(sessions.state, 'idle'); } @@ -59,6 +63,7 @@ function runCycle( sample: GameContractSample, entry: GameEntry, cycle: number, + assert: typeof strict, ): void { const contractHost = sample.makeHost(); const snapshot = contractHost.host.getSnapshot(); diff --git a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts index 8e0531f..1b88668 100644 --- a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts +++ b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts @@ -1,6 +1,9 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; +import { runInNewContext } from 'node:vm'; +import ts from 'typescript'; +import * as gameSessionHost from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; import { createGameHostAdapter } from '../../YouleNexus/assets/framework/platform/game-host-adapter.ts'; @@ -21,6 +24,29 @@ const GAME_ROOMTYPE = JSON.parse(readFileSync( new URL('../fixtures/contracts/player-login-room.json', import.meta.url), 'utf8', )).data.roomtype as readonly unknown[]; +test('browser-like loader evaluates the emitted harness without Node builtin modules', () => { + const source = readFileSync(new URL( + '../../YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts', import.meta.url, + ), 'utf8'); + const { outputText } = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, + }); + const exports: Record = {}; + const loaded: string[] = []; + assert.doesNotThrow(() => runInNewContext(outputText, { + exports, + require(specifier: string) { + if (specifier !== '../runtime/game-session-host.ts') { + throw new Error(`Creator cannot load runtime module: ${specifier}`); + } + loaded.push(specifier); + return gameSessionHost; + }, + })); + assert.equal(typeof exports.assertGameContract, 'function'); + assert.deepEqual(loaded, ['../runtime/game-session-host.ts']); +}); + test('assertGameContract accepts a fixed-four game with its real nested roomtype and host fixture', () => { let invalidations = 0; let nextModuleId = 0; @@ -40,6 +66,7 @@ test('assertGameContract accepts a fixed-four game with its real nested roomtype expectedSeatCount: 4, makeHost: makeConformanceHost(entry, () => { invalidations++; }, restoredDeskinfo), }], + assert, )); assert.equal(invalidations, 2); assert.equal(restoredDeskinfo.length, 0); From 73d7826fbeb2865065f05bd48393d9607b452a3c Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 19:03:35 +0800 Subject: [PATCH 10/25] fix: render local login prefab layers in scene camera --- .../YouleNexus/assets/scenes/LocalPlatformLogin.scene | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene index 2dd3d00..d6180a9 100644 --- a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene +++ b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene @@ -211,7 +211,7 @@ "_shutter": 7, "_iso": 0, "_screenScale": 1, - "_visibility": 33554432, + "_visibility": 1107296256, "_targetTexture": null, "_postProcess": null, "_usePostProcess": false, From 4414e1bcabbc82e0f50fc929f829727c6a07e222 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 19:12:50 +0800 Subject: [PATCH 11/25] fix: enforce elapsed loading display minimum after timer wakes --- .../LocalPlatformLogin.ts | 6 ++- .../local-platform-login/view.test.ts | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts index 2f76a7a..337f498 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts @@ -81,7 +81,11 @@ export class LocalPlatformLogin extends Component { waitForMinimumDisplay: async () => { await firstDraw; const drawnAt = this.required(this.firstDrawAt, 'loading first draw'); - await this.delay(Math.max(0, MINIMUM_LOADING_MS - (performance.now() - drawnAt))); + let remaining = MINIMUM_LOADING_MS - (performance.now() - drawnAt); + while (remaining > 0) { + await this.delay(remaining); + remaining = MINIMUM_LOADING_MS - (performance.now() - drawnAt); + } this.record({ kind: 'minimum-display-ready', data: performance.now() - drawnAt }); }, createSocket: url => new WebSocket(url), record: event => this.record(event), diff --git a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts index 3b7320b..1dbed17 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts @@ -143,3 +143,57 @@ test('component stop cancels pending draw and timer work and disables spinner up assert.equal(timers.size, 0); assert.equal(director.listenerCount('draw'), 0); assert.equal(component.getDiagnostics().pendingCount, 0); }); + +test('minimum display ignores an early timer wake and cancellation clears its remaining wait', async () => { + const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url); + const exports: any = {}; const timers = new Map void>(); let timerId = 0; let clock = 100; + let hostOptions: any; + runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, + }).outputText, { exports, require: (name: string) => { + if (name === 'cc/env') return { DEBUG: true }; + if (name === 'cc') return { Component: class {}, Node, Label, Sprite, UITransform, Prefab: class {}, sys: { isNative: false }, + _decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } }; + if (name.endsWith('local-login-host.ts')) return { createLocalLoginHost: (options: unknown) => { + hostOptions = options; return { start: async () => {}, stop() {}, getSnapshot: () => ({}) }; + } }; + if (name.endsWith('local-login-view.ts')) return load(); + if (name.endsWith('remote-config-fetcher.ts')) return { HttpConfigFetcher: class {} }; + throw new Error(`Unexpected dependency ${name}`); + }, Error, performance: { now: () => clock }, window: { + location: { search: '?profile=local' }, localStorage: {}, + setTimeout: (callback: () => void) => { timers.set(++timerId, callback); return timerId; }, + clearTimeout: (id: number) => timers.delete(id), + } }); + async function started() { + const component = new exports.LocalPlatformLogin(); const f = fixture(); + Object.assign(component, f.refs, { stopButton: new Node(), loadingPrefab: {}, loginPrefab: {} }); + const loading = new Node('Layer614_Loading'); const group = new Node('group-40'); + const spinner = new Node('载入动画'); spinner.components.set(Sprite, new Sprite()); + spinner.components.set(UITransform, new UITransform()); group.children.push(spinner); + loading.children.push(group); component.loadingRoot.children.push(loading); + component.loadingRoot.getComponentsInChildren = () => []; + await component.start(); director.emit('draw'); await Promise.resolve(); + return component; + } + const drain = async () => { for (let i = 0; i < 5; i++) await Promise.resolve(); }; + function wake(at: number) { + clock = at; const [id, callback] = timers.entries().next().value!; + timers.delete(id); callback(); + } + const component = await started(); let completed = false; + const minimum = hostOptions.waitForMinimumDisplay().then(() => { completed = true; }); + await drain(); wake(599.8); await drain(); + assert.equal(completed, false, '499.8 ms elapsed must not open minimum display gate'); + assert.equal(component.getDiagnostics().events.some((event: any) => event.kind === 'minimum-display-ready'), false); + assert.equal(timers.size, 1); + wake(600); await minimum; assert.equal(completed, true); assert.equal(timers.size, 0); + component.stop(); + + clock = 1000; const cancelled = await started(); + const cancelledMinimum = hostOptions.waitForMinimumDisplay(); + const rejection = assert.rejects(cancelledMinimum, /cancelled/); + await drain(); wake(1499.8); await drain(); + assert.equal(timers.size, 1); cancelled.stop(); await rejection; + assert.equal(timers.size, 0); assert.equal(cancelled.getDiagnostics().pendingCount, 0); +}); From 556c47a1b64ca0230c6fbc635df2d6177cd570cc Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 19:22:37 +0800 Subject: [PATCH 12/25] docs: record verified local platform login acceptance --- .../plans/2026-09-05-local-platform-login.md | 64 +++++++++-------- ...6-09-05-local-platform-login-acceptance.md | 72 +++++++++++++++++++ .../2026-09-05-local-platform-login-design.md | 2 +- 3 files changed, 107 insertions(+), 31 deletions(-) create mode 100644 docs/superpowers/reports/2026-09-05-local-platform-login-acceptance.md diff --git a/docs/superpowers/plans/2026-09-05-local-platform-login.md b/docs/superpowers/plans/2026-09-05-local-platform-login.md index 5cbfae0..b47302b 100644 --- a/docs/superpowers/plans/2026-09-05-local-platform-login.md +++ b/docs/superpowers/plans/2026-09-05-local-platform-login.md @@ -1,6 +1,6 @@ # Local Platform Login Implementation Plan -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. **Goal:** 在主 checkout 的联调分支中完成启动、真实资源加载、游客登录与本地服务器响应展示的验收。 **Architecture:** 复用 Runtime/Controller/WireClient 与 local sources,开发目录内组合严格 Transport、范围包装、诊断 GameEntry 和 Cocos 端口。无新 Store/Router。 @@ -11,7 +11,7 @@ - 服务器、远程配置服务、settings/WVJB 契约零改动。默认 prod、BUILD_IDENTITY 及原生配置优先级保持不变。 - 不修改生产 GameEntry 契约,不选择真实子游戏,不接其他业务 RPC,不替换现有 Login/Launch/MainMenu 场景,不复做 3A。 -- 代码新增只在 YouleNexus/assets/scripts/local-platform-login/,测试在 framework-tests/local-platform-login/;下文路径均相对 cocoscreator_projects/。 +- 诊断代码新增只在 YouleNexus/assets/scripts/local-platform-login/,测试在 framework-tests/local-platform-login/;下文路径均相对 cocoscreator_projects/。实际 Creator 预览发现既有 SDK 测试辅助模块的 node:assert 阻断所有脚本加载,按最小来源修复扩展三个既有文本文件:sdk/testing/game-contract-harness.ts、对应测试和 framework/README.md,改为必需的调用方断言依赖;没有额外资源改动。 - .scene/.prefab/.anim/.meta 只能由 Creator/MCP 生成或保存;写入/提交仅限 spec 第8节,其他导入副产物保留且报告。 - 按用户最新授权,在 G:/Works/YouleGamesCocosCreator 使用 codex/local-platform-login;Creator projectPath 必须为该目录下 cocoscreator_projects/YouleNexus。原有50项改动按路径/状态/哈希保护,精确暂存,不自动 stash。 - 显式枚举纯 TS 测试及两项指定 architecture 测试;禁止 npm test、run-framework-tests.mjs、test:framework。tsx ENOMEM 精确升级运行,不改业务代码。 @@ -22,7 +22,7 @@ **Files:** Create YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts; Test framework-tests/local-platform-login/transport.test.ts。 **Interfaces:** export LocalLoginEvidence = {kind:string; data:unknown}; export EvidenceSink=(event:LocalLoginEvidence)=>void。export LocalLoginTransport implements Transport;constructor options {context:LocalStartupContext; createSocket:(url:string)=>WebSocket; onFault:(error:unknown)=>void; record:EvidenceSink}。注入真实浏览器构造器仅在组件根完成。 -- [ ] 写 RED 测试,FakeSocket 实现标准 open/message/close/error 回调与 readyState/send/close。导入缺失模块必须是语义 RED。 +- [x] 写 RED 测试,FakeSocket 实现标准 open/message/close/error 回调与 readyState/send/close。导入缺失模块必须是语义 RED。 ```ts assert.throws(() => transport.send('{}'), /open/i); transport.connect(context.config.servers[0]!); @@ -30,7 +30,7 @@ socket.readyState = 1; socket.onopen?.({} as Event); transport.send('frame'); assert.deepEqual(socket.sent, ['frame']); ``` -- [ ] 实现每次 connect 前 assertLocalConnectionTarget;构造器/校验失败先 onFault 再抛出。close 显式停止后回调失效;回调捕获 socket 实例,只交付当前连接。 +- [x] 实现每次 connect 前 assertLocalConnectionTarget;构造器/校验失败先 onFault 再抛出。close 显式停止后回调失效;回调捕获 socket 实例,只交付当前连接。 ```ts const socket = options.createSocket(url); this.socket = socket; @@ -41,28 +41,28 @@ socket.onmessage = event => { }; ``` 以上注释分支必须实现:非文本构造 TypeError→关闭/失效→报告,文本记录成功后交付;record 抛错按显式故障清理,不吞掉。 -- [ ] RED/GREEN:远程 URL 不构造;构造错误 cause/原对象保留;OPEN 前 send 失败;非文本失败不交付;error/close 单次 close 通知;自然 close 允许 Wire 重连;显式 close 无重连回调;旧实例事件无效;关闭抛错可定位。入站握手在 codec 前被 record 捕获,发送证据只记录成功 send。 -- [ ] `node --import tsx --test framework-tests/local-platform-login/transport.test.ts`、`node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit`、`node scripts/check-import-boundaries.mjs`、git diff --check。审查后只提交 source/test,无 meta。 +- [x] RED/GREEN:远程 URL 不构造;构造错误 cause/原对象保留;OPEN 前 send 失败;非文本失败不交付;error/close 单次 close 通知;自然 close 允许 Wire 重连;显式 close 无重连回调;旧实例事件无效;关闭抛错可定位。入站握手在 codec 前被 record 捕获,发送证据只记录成功 send。 +- [x] `node --import tsx --test framework-tests/local-platform-login/transport.test.ts`、`node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit`、`node scripts/check-import-boundaries.mjs`、git diff --check。审查后只提交 source/test,无 meta。 ## Task 2: 诊断 GameEntry 与 Wire 范围边界 **Files:** Create local-login-game-entry.ts、local-login-wire.ts 于上述脚本目录;Test framework-tests/local-platform-login/game-entry.test.ts、wire.test.ts。 **Interfaces:** createDiagnosticGameEntry(gameId:string|number,record:EvidenceSink):GameEntry;createLocalLoginWire({context,transportFactory,onFault,record}):PlatformWireClient,transportFactory:()=>Transport。只包装一个 new WireClient({servers:context.config.servers,transportFactory})。 -- [ ] RED:真实 new GameSessionHost(entry) 会创建/释放诊断对象;entry.resolveSeatCount([]) 必须抛错,module.attach/restore/events 均抛出操作名。dispose 状态转换记录一次,重复释放安全;key/route 为 local-platform-login,不是协议平台 route。 +- [x] RED:真实 new GameSessionHost(entry) 会创建/释放诊断对象;entry.resolveSeatCount([]) 必须抛错,module.attach/restore/events 均抛出操作名。dispose 状态转换记录一次,重复释放安全;key/route 为 local-platform-login,不是协议平台 route。 ```ts const host = new GameSessionHost(createDiagnosticGameEntry(context.config.identity.gameid, record)); assert.equal(host.state, 'idle'); assert.throws(() => entry.resolveSeatCount([]), /resolveSeatCount/); ``` -- [ ] RED/GREEN 工厂实现:模块实例编号递增,created→disposed,所有游戏行为 record violation 后 throw。不得返回座位数、空处理结果或真实子游戏模块。 -- [ ] Wire RED:真实 WireClient 配合 fake Transport 输入完整 login fixture;成功大厅原 event 引用交付;合法房间恢复在 Runtime 前终止;损坏响应保留解析错误;拒绝非 agent/player_login 出站且无 send。 +- [x] RED/GREEN 工厂实现:模块实例编号递增,created→disposed,所有游戏行为 record violation 后 throw。不得返回座位数、空处理结果或真实子游戏模块。 +- [x] Wire RED:真实 WireClient 配合 fake Transport 输入完整 login fixture;成功大厅原 event 引用交付;合法房间恢复在 Runtime 前终止;损坏响应保留解析错误;拒绝非 agent/player_login 出站且无 send。 ```ts const parsed = parseLoginResponse(event.message.data); if (parsed.state === 0 && parsed.room !== null) failScope('login room recovery'); ``` -- [ ] 实现 subscribe 返回注销函数,单个内部订阅转发;所有切服、room/子游戏 message 终止;agent/platform 普通推送原样交付。kick/非零登录回包先交付现有处理链,再通知宿主停止本次验收,以保留原始失败显示。故障单次锁存,先释放底层订阅与 Wire,后通知宿主,清理错误不能丢弃。实现 stop 幂等;生命周期 start/stop 不假装已经连接。 -- [ ] `node --import tsx --test framework-tests/local-platform-login/game-entry.test.ts framework-tests/local-platform-login/wire.test.ts`,覆盖范围禁止后零继续交付/零重连、正常重连同一信封。类型/导入/diff 检查,审查与 scoped commit。 +- [x] 实现 subscribe 返回注销函数,单个内部订阅转发;所有切服、room/子游戏 message 终止;agent/platform 普通推送原样交付。kick/非零登录回包先交付现有处理链,再通知宿主停止本次验收,以保留原始失败显示。故障单次锁存,先释放底层订阅与 Wire,后通知宿主,清理错误不能丢弃。实现 stop 幂等;生命周期 start/stop 不假装已经连接。 +- [x] `node --import tsx --test framework-tests/local-platform-login/game-entry.test.ts framework-tests/local-platform-login/wire.test.ts`,覆盖范围禁止后零继续交付/零重连、正常重连同一信封。类型/导入/diff 检查,审查与 scoped commit。 ## Task 3: 纯 TS 宿主组合与启动操作 @@ -70,41 +70,41 @@ if (parsed.state === 0 && parsed.room !== null) failScope('login room recovery') **Interfaces:** createLocalLoginHost(options): {start():Promise;login():Promise;stop():void;getSnapshot():{state:PlatformState|null;ready:boolean;busy:boolean}}。 options: configOptions:ResolveRuntimeConfigOptions、storage:LocalStoragePort、random:()=>number、now:()=>number、sampleHost:()=>LocalHostSnapshot、view:PlatformViewPort、scheduler:FrameScheduler、loadResources:()=>Promise、waitForMinimumDisplay:()=>Promise、createSocket:(url:string)=>WebSocket、record:EvidenceSink、onBusy:(busy:boolean)=>void、onFailure:(error:unknown)=>void;所有依赖必需。 -- [ ] 写真实 Runtime+Controller 联合 RED:控制 resources/minimum/socket 三个未完成输入,任何一个未就绪禁止 login;configOptions fetcher 计数为零,配置解析仅一次(从 record/身份解析输入探针确认)。首个 config 解析在 Runtime 构造前完成以提供诊断 gameId,场景 shell 显示此阶段。 -- [ ] 构造一次 local startup Promise,context→entry、sources、wire 工厂。Controller.connect(runtime)→runtime.start();Runtime 选项 resolveRuntimeConfig 返回已解析同一 config。 +- [x] 写真实 Runtime+Controller 联合 RED:控制 resources/minimum/socket 三个未完成输入,任何一个未就绪禁止 login;configOptions fetcher 计数为零,配置解析仅一次(从 record/身份解析输入探针确认)。首个 config 解析在 Runtime 构造前完成以提供诊断 gameId,场景 shell 显示此阶段。 +- [x] 构造一次 local startup Promise,context→entry、sources、wire 工厂。Controller.connect(runtime)→runtime.start();Runtime 选项 resolveRuntimeConfig 返回已解析同一 config。 ```ts const context = await resolveLocalStartup(options.configOptions); const entry = createDiagnosticGameEntry(context.config.identity.gameid, options.record); // createLocalLoginSources uses live closure for active login state. // Runtime creation and Controller.connect occur only once after context validation. ``` -- [ ] login 合并忙碌调用,prepare 成功才 controller.login;响应前保持 busy,onBusy 与 View 的登录遮罩一致。销毁期间异步 prepare/config 完成不能创建连接或发送;通过 generation/terminal 标记拒绝迟到结果,不新增业务状态机。失败先禁用输入再停止各所有者,onFailure 最后呈现错误;stop 保留已经绘制的成功/失败结果。 -- [ ] RED/GREEN:正常 player_login fixture→Store logged-in/outside→lobby model;state非零/kick 原错误;房间恢复无写入 Store;重复点击单 send;缓存损坏无 send;stop during prepare/config/resources 无后续渲染/连接;observer/清理错误保留;invalid query/native 零账号写入与连接。 -- [ ] `node --import tsx --test framework-tests/local-platform-login/host.test.ts`、类型/导入/diff 检查、任务审查提交。此步不真实联网,不实现空资源端口的生产入口。 +- [x] login 合并忙碌调用,prepare 成功才 controller.login;响应前保持 busy,onBusy 与 View 的登录遮罩一致。销毁期间异步 prepare/config 完成不能创建连接或发送;通过 generation/terminal 标记拒绝迟到结果,不新增业务状态机。失败先禁用输入再停止各所有者,onFailure 最后呈现错误;stop 保留已经绘制的成功/失败结果。 +- [x] RED/GREEN:正常 player_login fixture→Store logged-in/outside→lobby model;state非零/kick 原错误;房间恢复无写入 Store;重复点击单 send;缓存损坏无 send;stop during prepare/config/resources 无后续渲染/连接;observer/清理错误保留;invalid query/native 零账号写入与连接。 +- [x] `node --import tsx --test framework-tests/local-platform-login/host.test.ts`、类型/导入/diff 检查、任务审查提交。此步不真实联网,不实现空资源端口的生产入口。 ## Task 4: Cocos View、组件与授权场景 **Files:** Create local-login-view.ts、LocalPlatformLogin.ts;Test framework-tests/local-platform-login/view.test.ts(纯投影/绑定端口行为,可控 cc 模块测试注入只在测试环境);Create spec 第8节九个资源文件,由 MCP/Creator 完成。 **Interfaces:** CocosLocalLoginView implements PlatformViewPort,持有明确 roots/status/result Labels;createCocosFrameScheduler():FrameScheduler。View 提供 bindLogin(instance:Node,onLogin:()=>void)、setBusy(boolean)、showFailure(unknown)、dispose();所有必需节点缺失 throw。组件持有 Loading/Login Prefab、页面 roots、status/result Labels、StopButton 引用。 -- [ ] RED 先覆盖 view 的失败绑定、正确 PageModel→Label 显示、遮罩忙碌/终止、解绑行为;使用可控引擎端口而非在消费代码填默认 Node。必要的测试模块加载器只能位于 framework-tests,不能伪装 cc 生产实现。 -- [ ] 实现触摸绑定 group-2/游客登录;隐藏微信/手机/QQ/快速/隐藏快速入口;版本 Label 取有效 identity.version。加载节点 group-40/载入动画 用 Cocos update/tween 旋转,生命周期清理。不得持久改源 prefab。 -- [ ] 组件 DEBUG 且 sys.isNative=false 限制;500ms 仅一处定义。start 调用宿主,configOptions 使用实际 location.search 与 window,禁止 query 默认;Storage 绑定 localStorage,random/now 用 Math.random/Date.now,sampleHost 显式 location=null 及存在时的 returnCitySN。 -- [ ] loadResources 检查实际序列化 Prefab 与 SpriteFrame 依赖,实例化 Login、绑定节点与事件,等待真实 draw;minimum-display 自 Loading 首个 draw 计时剩余500ms,不能以 setTimeout 代替资源加载。帧回调使用 Cocos director 事件与真实取消。 -- [ ] 编辑器操作前用 MCP 核实固定主工程 projectPath。用户已取消 worktree 切换;在当前联调分支继续,不请求打开已清理的隔离路径,不保存未知场景。 -- [ ] MCP 创建空 LocalPlatformLogin.scene,1600×720 Canvas/Camera/Host/roots/status/result/stop,实例化 Loading Prefab,挂新组件及序列化引用;Login Prefab 由运行实例加载。保存→重新打开→核实组件与引用真实持久,检查九个资源范围,不使用 edit_prefab_json。 -- [ ] Cocos noEmit/诊断需使用实际引擎声明;不能把 cc 文件纳入无引擎 framework tsconfig。记录 engine diagnostics 与纯 TS 各自结果。MCP 预览新的场景,显式 profile=local;手工模拟点击前确认无旧 LoginFlow 活跃。 -- [ ] 任务规格/质量审查覆盖代码及 MCP 绑定证据,再提交授权文件;若工具不能持久化挂载,报告具体阻塞而非手改序列化。 +- [x] RED 先覆盖 view 的失败绑定、正确 PageModel→Label 显示、遮罩忙碌/终止、解绑行为;使用可控引擎端口而非在消费代码填默认 Node。必要的测试模块加载器只能位于 framework-tests,不能伪装 cc 生产实现。 +- [x] 实现触摸绑定 group-2/游客登录;隐藏微信/手机/QQ/快速/隐藏快速入口;版本 Label 取有效 identity.version。加载节点 group-40/载入动画 用 Cocos update/tween 旋转,生命周期清理。不得持久改源 prefab。 +- [x] 组件 DEBUG 且 sys.isNative=false 限制;500ms 仅一处定义。start 调用宿主,configOptions 使用实际 location.search 与 window,禁止 query 默认;Storage 绑定 localStorage,random/now 用 Math.random/Date.now,sampleHost 显式 location=null 及存在时的 returnCitySN。 +- [x] loadResources 检查实际序列化 Prefab 与 SpriteFrame 依赖,实例化 Login、绑定节点与事件,等待真实 draw;minimum-display 自 Loading 首个 draw 计时剩余500ms,不能以 setTimeout 代替资源加载。帧回调使用 Cocos director 事件与真实取消。 +- [x] 编辑器操作前用 MCP 核实固定主工程 projectPath。用户已取消 worktree 切换;在当前联调分支继续,不请求打开已清理的隔离路径,不保存未知场景。 +- [x] MCP 创建空 LocalPlatformLogin.scene,1600×720 Canvas/Camera/Host/roots/status/result/stop,实例化 Loading Prefab,挂新组件及序列化引用;Login Prefab 由运行实例加载。保存→重新打开→核实组件与引用真实持久,检查九个资源范围,不使用 edit_prefab_json。 +- [x] Cocos noEmit/诊断需使用实际引擎声明;不能把 cc 文件纳入无引擎 framework tsconfig。记录 engine diagnostics 与纯 TS 各自结果。MCP 预览新的场景,显式 profile=local;手工模拟点击前确认无旧 LoginFlow 活跃。 +- [x] 任务规格/质量审查覆盖代码及 MCP 绑定证据,再提交授权文件;若工具不能持久化挂载,报告具体阻塞而非手改序列化。 ## Task 5: 真实服务验收与整批回归 **Files:** Create docs/superpowers/reports/2026-09-05-local-platform-login-acceptance.md;Modify 本计划完成记录;失败时修复只能通过同任务 TDD 子代理与审查,且不得越资源授权。 **Interfaces:** 只消费已实现组件、只读证据与 MCP;不新增 RPC 或服务器接口。 -- [ ] 核实本地 gateway 进程与路由对应服务;只读定位版本/身份配置,脱敏记录来源和未证实项。不能依据历史 version=10000/41 改请求。 -- [ ] MCP 预览明确 profile=local,观察 Loading 实际旋转与≥500ms、ready四门、游客可操作;从 Transport 证据确认唯一 WS、本地 URL、握手,点击后唯一 agent/player_login。 -- [ ] 记录成功回包 state=0、Store logged-in/outside、PageModel 与结果页一致。若拒绝/无回包/房间恢复,记录事实并定位来源,不能宣告通过或去测其他 RPC。再独立预览同 origin 验证账号/机器缓存复用与登录成功;停止后无重连/监听。 -- [ ] 执行以下精确回归,所有 exit 非零即停止结果认定: +- [x] 核实本地 gateway 进程与路由对应服务;只读定位版本/身份配置,脱敏记录来源和未证实项。不能依据历史 version=10000/41 改请求。 +- [x] MCP 预览明确 profile=local,观察 Loading 实际旋转与≥500ms、ready四门、游客可操作;从 Transport 证据确认唯一 WS、本地 URL、握手,点击后唯一 agent/player_login。 +- [x] 记录成功回包 state=0、Store logged-in/outside、PageModel 与结果页一致。若拒绝/无回包/房间恢复,记录事实并定位来源,不能宣告通过或去测其他 RPC。再独立预览同 origin 验证账号/机器缓存复用与登录成功;停止后无重连/监听。 +- [x] 执行以下精确回归,所有 exit 非零即停止结果认定: ```powershell $loginTests = @(rg --files framework-tests -g '*.test.ts') if ($loginTests.Count -eq 0) { throw 'No TypeScript tests selected' } @@ -114,10 +114,14 @@ node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit node scripts/check-import-boundaries.mjs git diff --check ``` -- [ ] 审计基线到 HEAD 和所有未提交资源路径,主 checkout原有50项状态与哈希不变;本批真实成功与纯测试成功分开记录。报告仅提交脱敏摘要与必要截图引用,认证密钥和原始账号设备标识不提交。 -- [ ] 任务规格/质量审查、完整分支审查、完成记录;不自动合并或推送。 +- [x] 审计基线到 HEAD 和所有未提交资源路径,主 checkout原有50项状态与哈希不变;本批真实成功与纯测试成功分开记录。报告仅提交脱敏摘要与必要截图引用,认证密钥和原始账号设备标识不提交。 +- [x] 验收记录规格/质量审查与完整代码分支审查已通过;提交完成文档,不自动合并或推送。 ## 计划自审与执行 spec 1–6 对应 Tasks1–3;spec7–9 对应Task4;spec10–11对应Task5。诊断生命周期先创建再释放,不把工厂写成直接throw;最短展示从真实draw开始;View不能直接读原包。代码接口按上文导出,任务之间按依赖串行;测试存根只在测试目录。 用户已指定本会话 subagent-driven-development,无需再次选择执行方式。第8节资源授权已确认;额外资源不得暗中增加。 + +## 本批验收结果(2026-09-05) + +代码完成至4414e1b;两次独立真实预览登录state=0,账号与机器缓存复用,Store/PageModel/画面一致;实际加载旋转及修复后的501.1ms展示、忙碌反馈、停止后47秒无事件均通过。最终纯TS600/600、架构49/49、类型/导入与diff检查通过;原有50项状态/哈希不变。详细证据见../reports/2026-09-05-local-platform-login-acceptance.md。验收记录审查已通过;文档随本次收尾提交,分支保留,未合并或推送。 diff --git a/docs/superpowers/reports/2026-09-05-local-platform-login-acceptance.md b/docs/superpowers/reports/2026-09-05-local-platform-login-acceptance.md new file mode 100644 index 0000000..43d60e0 --- /dev/null +++ b/docs/superpowers/reports/2026-09-05-local-platform-login-acceptance.md @@ -0,0 +1,72 @@ +# 本地启动、加载、登录真实验收 + +日期:2026-09-05。分支:`codex/local-platform-login`。代码:`4414e1b`。状态:本批启动、真实加载和两次独立预览登录验收通过;未合并、未推送。 + +## 工作区与范围 + +仅在 `G:/Works/YouleGamesCocosCreator` 主 checkout 使用 Git 分支;Creator 固定打开其下 `cocoscreator_projects/YouleNexus`(3.8.8)。不再使用 worktree。保留原有 50 项未提交文件的状态和 SHA256;不自动 stash。 + +本批独立 `LocalPlatformLogin` 场景只验证启动、加载、游客登录及结果显示。未改生产启动场景、默认 prod、GameEntry 契约、服务器、远程配置或原生接口;未选择子游戏,未发起其他平台或房间 RPC。 + +序列化新增严格为设计第 8 节授权的九项资源,全部通过 funplay MCP/Creator 生成、绑定、保存和重开验证。源 Login/Loading Prefab 未修改。 + +## 配置与账号单源 + +- `assets/framework/config/profiles.ts`:debug/local 的直接连接目标为 `ws://127.0.0.1:3088`,跳过远程配置。 +- `assets/framework/config/sources/defaults.ts` 的 `BUILD_IDENTITY`:agentid、channelid、gameid、marketid、version 的构建来源。正式字段拼写为 `channelid`。 +- `runtime-config.ts` → `local-startup.ts`:解析真实 `location.search`,必须显式 `profile=local`;宿主只解析一次,将同一配置交给 Runtime、诊断 GameEntry 与本地账号来源。 +- `assets/framework/adapters/local/visitor-account.ts`:按身份/连接作用域读取严格缓存或生成游客;openid 为 `testopenid_<随机数>`,unionid 为 `ylgame<同一随机数>`,同时产生昵称、头像、性别和地区。 +- `assets/framework/adapters/local/login-sources.ts`:同次账号准备提供设备快照,机器标识从独立缓存读取或由时间戳与随机后缀生成;本地 location 显式为 null,其他可选输入遵循来源定义。消费方不补默认值。 +- 既有协议构造器产生唯一 `agent/player_login` 信封;未执行历史 `scripts/login-live.ts`。 + +本次真实请求使用未覆盖的 BUILD 身份,`version=1` 且类型为 number。下述 state=0 回包证明该组合被此次实际本地服务接受;不将源码配置或 TCP 可达单独当作身份匹配证据。 + +## 编辑器和运行环境 + +- 本地监听:3088,PID 20092,`G:/Works/JinXianProjects/gateway/game-gateway.exe`。只读复核,无服务端写入。 +- 场景 UUID:`67e69aac-9982-4e78-a44c-1d9c42f016ae`。1600×720,Host 仅挂新组件;九项组件引用经保存、切回原场景、重新打开验证有效。 +- Creator 返回预览端口 7456。通过 funplay MCP 创建独立 Electron Chromium 浏览器窗口,加载实际 Creator 页面 `http://127.0.0.1:7456/?profile=local`;关闭 Node integration、开启 context isolation/sandbox。未替换引擎或伪造编辑器内的浏览器 query。 +- 输入通过 MCP 向真实预览窗口发送鼠标事件。首次坐标偏移误解未命中节点,证据确认零发送;纠正为 game 面板中心偏移后命中游客按钮。 +- 原始收发仅保留于本地诊断内存。此报告不提交 openid、unionid、machineid、服务器认证/银行字段值或原始回包。 + +## 实际发现并修复的问题 + +1. Creator 的诊断工具默认选择 TypeScript 4.9.5,无法解析已有 TS 5 语法。使用仓库 TypeScript、真实 Cocos 声明及严格 ES2020 参数检查;不修改工程配置或原生桥。 +2. 首次实际预览因既有 SDK 测试辅助模块的运行时 `node:assert` 导入中断。最小 TS 范围扩展:该辅助函数改为必需的调用方断言参数,更新唯一测试调用和 README;所有原断言语义保留,无浏览器 shim。独立审查通过,MCP 刷新脚本后实际场景成功运行。 +3. 初次登录页不可见:新相机仅包含 UI_2D,原 Login Prefab 使用 DEFAULT。仅通过 MCP 修改授权新场景相机为 `UI_2D | DEFAULT`,重开和实际截图均通过;源 Prefab 不改。 +4. 第二个独立预览暴露单次浏览器计时器提前唤醒,记录 499.8ms。通过 RED/GREEN 加入每次唤醒后的单调时间阈值复核,未满 500ms 继续等待;保留取消能力。整分支审查的对应 Important 问题已复审关闭。该失败窗口未登录;修复后另开全新窗口验证为 501.1ms。 + +## 首次真实登录证据 + +- `fp_f5d718fac951802d`:Loading 首次 draw 为 861.3ms;865.9ms 与 1090.7ms 的两张真实截图均显示加载环,角度由 -47.682° 变为 -86.688°。登录页激活为 1368.3ms,实际加载展示约 507ms。 +- 本地唯一 open 目标为 `ws://127.0.0.1:3088`;资源、配置、连接和最短展示门完成后,PageModel 为 login/canLogin=true。 +- `fp_72e7cc8b8f6782a8`:一次游客点击仅发送一个 `agent/player_login`;服务器返回 `state=0`。首次业务响应耗时约 1694ms。后续 26 字节入站为非业务帧,未产生额外出站 RPC。 +- Store 为 logged-in/outside;服务器语义、Store、PageModel 的玩家 ID、昵称、豆豆一致;实际结果页显示登录成功。页面不直接用请求填充玩家结果。 +- 诊断 GameEntry 仅完成一次校验创建/释放,没有游戏 attach、restore 或消息调用。 +- `fp_e481ee99042a594f`:真实停止按钮触发后 ready=false、busy=false、pendingCount=0;观察 33.3419 秒,事件计数未变,无重连、继续渲染或浏览器异常。成功结果保留。 + +## 第二次独立预览与缓存复用 + +- `fp_9b9dc57c94603f6d`:关闭已停止的窗口,新建独立浏览器窗口 9,仍使用同一真实预览 origin 和 `profile=local`。修复后的 minimum-display-ready 为 501.1ms,login/canLogin=true,零浏览器异常。 +- `fp_ed01311f246331d4`:向实际游客按钮发送鼠标事件,35ms 后观测 busy=true、登录遮罩可见、提示“正在登录,请稍候…”,仅一个业务请求;已取得实际截图。 +- `fp_f75c9fb9806e6a5c`:服务器再次 state=0;openid、unionid、machineid 与首次请求均相同,返回相同玩家。Store logged-in/outside,Store、PageModel、回包和显示结果一致。比较在本地诊断内存完成,此处只记录布尔结论。 +- `fp_e58e3f88615487fb`:真实停止按钮后 ready=false、busy=false、pendingCount=0;观察 47.0203 秒无新增事件、重连或浏览器异常。保留成功结果供查看。 + +截图位于工程 `temp/mcp-captures/`(本地证据,不提交):`local-login-loading-first.png`、`local-login-loading-rotated.png`、`local-login-visible-ready.png`、`local-login-success-first.png`、`local-login-busy-second.png`、`local-login-success-second.png`。 + +## 回归与审查 + +在 `cocoscreator_projects` 执行显式枚举全部 `framework-tests/**/*.test.ts` 的 `node --import tsx --test --test-concurrency=1`:最终 600/600,通过,exit 0,对应 `4414e1b`。计时修复前为 599/599,新增早醒回归后为 600。 + +指定 `architecture/import-boundaries.test.mjs` 和 `architecture/presentation-boundaries.test.mjs`:49/49,通过。框架类型、导入边界检查通过。未使用 npm test、test:framework 或旧资源生成器。 + +全量输出含既有 `ui/store-binder.test.ts` 故意触发的 StoreBinder/reactive `boom` 日志;测试未失败,不宣称全量日志无噪声。 + +各实现任务与修复均完成独立规格/质量审查。整分支审查完成至 `73d7826`,计时修复 `4414e1b` 复审通过;没有未关闭的 Critical/Important 代码问题。 + +## 最终范围复核 + +- `4414e1b` 后复核原有 50 项文件的 porcelain 状态和 SHA256,全部不变。两项历史删除 meta 和用户 scene-2d 均保留。 +- 分支资源差异严格为九项授权新增文件。测试辅助模块兼容修复只修改三个既有文本文件,未增加资源授权范围。 +- `git diff --check` 通过。仅主 checkout;没有 worktree,未自动 stash、合并或推送。 +- 本结论仅为本批平台启动、加载、游客登录通过;不代表大厅其他 RPC、房间、真实子游戏、生产 profile 或原生 App 验收。 diff --git a/docs/superpowers/specs/2026-09-05-local-platform-login-design.md b/docs/superpowers/specs/2026-09-05-local-platform-login-design.md index 9b9a796..efff17d 100644 --- a/docs/superpowers/specs/2026-09-05-local-platform-login-design.md +++ b/docs/superpowers/specs/2026-09-05-local-platform-login-design.md @@ -1,7 +1,7 @@ # 本地真实启动、加载与登录联调设计 日期:2026-09-05。代码基线:master@66246c8。 -状态:用户已确认本文及第8节九项资源写入清单(2026-09-05),进入实施;实际验收尚未完成。 +状态:用户已确认本文及第8节九项资源写入清单(2026-09-05);启动、加载和两次真实登录已验收通过,详见 ../reports/2026-09-05-local-platform-login-acceptance.md。下文“已核实的基础”为设计时记录。 ## 1. 本批交付 From 62328b6fd62fdc6435c2fa51a84dc44b04c24c7c Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 21:08:31 +0800 Subject: [PATCH 13/25] docs: specify remote configuration memory flow --- .../plans/2026-09-05-remote-config-memory.md | 47 +++++++++++++++++++ .../2026-09-05-remote-config-memory-design.md | 43 +++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-05-remote-config-memory.md create mode 100644 docs/superpowers/specs/2026-09-05-remote-config-memory-design.md diff --git a/docs/superpowers/plans/2026-09-05-remote-config-memory.md b/docs/superpowers/plans/2026-09-05-remote-config-memory.md new file mode 100644 index 0000000..9c6d9e2 --- /dev/null +++ b/docs/superpowers/plans/2026-09-05-remote-config-memory.md @@ -0,0 +1,47 @@ +# Remote Config Memory Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox syntax for tracking. + +**Goal:** Both modes read remote JSON, resolve it like the legacy project, and keep it only in memory. + +**Architecture:** Existing remote-config.ts owns the hierarchy reader and server selection. Runtime configuration always awaits it; local selection overrides only the final connection targets. Existing local host preserves its lifecycle and loopback guards. + +**Tech Stack:** TypeScript, node:test/tsx, Cocos Creator 3.8.8 through funplay MCP. + +**Spec:** docs/superpowers/specs/2026-09-05-remote-config-memory-design.md + +## Global Constraints + +- Use branches in main checkout; never create worktrees. Current branch codex/local-platform-login. +- No server changes. No .scene/.prefab/.anim/.meta edits or generation. +- Both debug and release fetch remote data. Memory only: no storage or disk cache. +- Preserve legacy truthy hierarchy overrides, first loose identity match, whole-object replacement, game_server_tcp then player_server_tcp. +- Never connect production WebSockets during validation; local preview stays loopback only. +- Never run npm test, test:framework, or scripts/run-framework-tests.mjs. +- Do not stage unrelated files. Root controller owns spec/plan/protocol docs and live verification. + +### Task 1: Replace the obsolete remote contract and integrate every startup consumer + +**Files (all under cocoscreator_projects unless noted):** +- Modify YouleNexus/assets/framework/config/{remote-config.ts,runtime-config.ts,profiles.ts,local-startup.ts}. +- Modify existing YouleNexus/assets/scripts/local-platform-login files only if integration or sensitive diagnostic output needs correction. +- Test framework-tests/config/{remote-config.test.ts,runtime-config.test.ts,profiles.test.ts,local-startup.test.ts}; update dependent typed fixtures in framework-tests and old remote JSON fixture shapes to match new contract. +- Do not add new assets scripts. New test files outside assets are allowed if useful. + +**Interfaces:** Remote config view exposes readonly raw root and getValue(name: string): unknown; export a creator in remote-config.ts taking unknown root and ChannelIdentity. RuntimeConfig exposes the view as remoteConfig and keeps rawConfig as the same raw reference, source always remote and gameserver non-null. parseUrlServers receives the view and resolves game_server_tcp/player_server_tcp. Profiles retain unique servers for local override and all have remote gameserver. + +- [ ] Write failing tests first. Use the actual legacy get_paravalue extracted from 12_Logic.js in a node:vm sandbox as differential oracle; fixtures contain synthetic identities only. Assert values across all five scopes, false/0/empty values, object replacement, missing levels and first duplicate. Example: `assert.deepEqual(view.getValue('hall_config'), legacy(wrapper, 'hall_config'))`. +- [ ] Add startup behavior assertions: for debug local fake fetcher increment calls then return `{player_server_tcp:'remote.example:3088'}`; expect calls=1, source=remote, local servers, raw config retained. release with profile=local must still fetch and use remote candidates. Failed fetch or parse must leave local-host socket calls at zero. No persistence operation is added. +- [ ] Run exact focused tests from cocoscreator_projects: `node --import tsx --test --test-concurrency=1 framework-tests/config/remote-config.test.ts framework-tests/config/runtime-config.test.ts framework-tests/config/profiles.test.ts framework-tests/config/local-startup.test.ts`. Record expected assertion failures before production edits. If tsx ENOMEM occurs, repeat identical command with require_escalated. +- [ ] Implement the smallest integrated change. Loop scopes using first identity equality, update inherited value only if truthy, stop when scope is absent. Validate the JSON root and present hierarchy shape; normalize only the chosen server candidates. Runtime always awaits fetch, builds one memory view, parses candidates, then applies profile local servers. Preserve host cancellation guards and stop semantics. +- [ ] Update dependent tests/fixtures asserting the superseded direct contract; retain all unrelated assertions and add no runtime fallback. All test RuntimeConfig literals must supply the new view via a central test helper where appropriate. +- [ ] Run focused tests then full explicitly enumerated TS suite: `$testPaths = @(rg --files framework-tests -g '*.test.ts' | Sort-Object); node --import tsx --test --test-concurrency=1 @testPaths`. Run `node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit` and `node scripts/check-import-boundaries.mjs`. Discover and explicitly name the two platform architecture tests; do not invoke aggregate generators. +- [ ] Self-review, stage exact task files, commit. Report RED/GREEN commands, counts, changed files, and concerns in task-1-report.md. Do not touch root controller docs or existing dirty resources. +- [ ] Controller dispatches independent spec/quality reviewer; repair findings with failing regressions first, then scoped re-review. + +### Task 2: Documentation and real-boundary acceptance (controller) + +- [ ] Correct docs/protocol/01-传输层与架构.md remote-entry description to active get_config/getConfig_Succ, local wrapper and scoped server selection. Preserve unrelated protocol text. +- [ ] Read the live remote response through the production parser and current identity without storing response body or printing sensitive values. Record fetch/parse outcome and selected hierarchy depth only. +- [ ] Verify actual Cocos preview via funplay MCP, without asset mutations. Observe actual remote request and resolved local target; stop runtime after observation. If browser fetch fails, document status and exact origin/request facts before deciding a fix; do not disable security or proxy silently. +- [ ] Hash-check initial dirty files unchanged. Write a concise acceptance report with separate pure tests, live HTTP and real Creator evidence. Perform whole-change review before declaring completion. diff --git a/docs/superpowers/specs/2026-09-05-remote-config-memory-design.md b/docs/superpowers/specs/2026-09-05-remote-config-memory-design.md new file mode 100644 index 0000000..2d8e45f --- /dev/null +++ b/docs/superpowers/specs/2026-09-05-remote-config-memory-design.md @@ -0,0 +1,43 @@ +# 远程配置读取与内存解析 + +## 已确认目标 + +用户已确认:debug、release 都读取远程配置;按原工程解析;数据只存变量,不下载落盘、不写本地缓存。2026-09-05 用户回复“开始”,授权实施此方案。Login Layer/MainMenu 接线另批处理。 + +## 契约与来源 + +- 原工程 `projects/Game_Surface_3/js/00_Surface/12_Logic.js` 的实际入口是 `get_config`,POST 空 body,URL 为 `gameserver + '?' + timestamp`。`gameconfig` 注入仍按 `-`→`/`、`#`→`:` 后包成 `http://…txt`。 +- `getConfig_Succ` JSON.parse 后把根对象保存在 `GameData.serverConfig.data`。`data` 是客户端包装,不是远程响应信封。旧 `ServerUrl_Succ/data.urlserver` 是未启用路径,不能继续作为本次契约。 +- 当前项目配置源 DEFAULT_GAMESERVER 保持 `https://tsgames.daoqi88.cn/config/update_jsonv2.txt`。原模板旧 URL 在只读预检中返回非 JSON,不能因追求源码一致而切回失效地址;兼容的是读取与解析协议。 +- BUILD_IDENTITY、宿主与 query 身份链沿用已完成实现,不复制 agentid/gameid/channelid/marketid/version,也不改变测试账号与设备来源。 + +## 内存结构与解析 + +在现有 remote-config.ts 提供统一远程配置视图:持有原始根 JSON、已解析的身份,以及 `getValue(name)`。RuntimeConfig 持有此视图,rawConfig 引用同一根数据;所有配置值(含 hall_config、game_config、公告、登录图片、扩展字段)均保留。无 localStorage、文件、跨启动缓存或全局第二份配置。 + +getValue 严格复刻 get_paravalue:全局→匹配 agent→game→channel→market;每层首个宽松相等身份匹配;断层保留上层值;只有 truthy 值覆盖,0/false/空串不覆盖;对象和数组整体覆盖,不自动深合并;缺省返回 null。身份 marketid=0 时保留原函数不搜索该层的语义。非法结构显式 ConfigParseError,不修补。 + +WebSocket 候选按原 getConfig_Succ 选 truthy game_server_tcp,否则 player_server_tcp。支持原字符串/数组顺序及重复项;在协议适配入口转为 ws/wss URL。候选缺失或非法显式失败,不退回 data.urlserver。原工程明确的 game→player 选择属于来源契约。 + +本批不实现 Logic.setConfigInfo 内的 UI 布局、副作用及子游戏 Game_Modify 回调;其所需原始参数均可从统一 getValue 读取。后续界面接线时再按原工程执行各字段的显示转换和来源默认语义。 + +## 启动顺序 + +身份解析 → 远程读取 → 根结构及层级解析 → 服务器候选解析 → 显式本地服务器选择 → 发布 RuntimeConfig → 后续 Runtime 启动。 + +所有 profile 都是 remote。debug 与 release 共用解析器;isDebugger 保持构建模式语义,不控制是否读取。profile=local 是独立的本地服务器选择:仅 debug 使用,远程解析成功后覆盖连接目标为 PROFILES.local 唯一配置的回环地址。release 仍选 prod,不能因 URL 参数误接本地。 + +本地诊断入口继续严格验证 DEBUG/H5/profile=local、身份及回环目标;更新“必须 direct”的旧断言。远程读取/解析失败前不得创建 socket、准备账号或发送登录。停止发生在读取期间时,不得在读取完成后启动连接。 + +## 验收 + +1. 自动测试对照原 get_paravalue 实函数,覆盖五层、断层、重复身份、数字/字符串身份、truthy、对象覆盖及不存在字段。 +2. debug prod、debug local、release 均实际调用 fetcher;本地目标在读取解析完成后才生效;无缓存读写。 +3. 非 JSON、非法层级、地址缺失/非法、HTTP 失败均明确失败;本地 host 无 socket/登录副作用。 +4. 真实 HTTP 读取使用当前配置源与 BUILD_IDENTITY,输出脱敏结构/匹配情况;不输出原始配置及认证信息,不连接其生产 WS。 +5. 经 funplay MCP 验证实际 Creator 浏览器预览远程读取成功、配置在内存、最终目标仍为本地;若浏览器受 CORS 等阻止,记录真实阻塞,不能用 Node 成功代替浏览器通过。 +6. 显式枚举纯 TS 测试、指定架构测试、类型和导入边界检查。原有 dirty 资源不变。 + +## 实施边界 + +沿用主 checkout 的 codex/local-platform-login 分支,不创建 worktree。不改服务器、原生契约、资源序列化文件,不扩展 RPC/房间/子游戏。只修改已有 assets TS,避免新增脚本的 meta 生成要求。同步更正本次直接涉及的协议文档错误;已完成计划保留历史意义,不重新执行。 From 053302c5df08c2aeae96741e7518df571b8210ed Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 21:21:24 +0800 Subject: [PATCH 14/25] fix(config): resolve remote hierarchy in memory for every profile --- .../assets/framework/config/local-startup.ts | 7 +- .../assets/framework/config/profiles.ts | 7 +- .../assets/framework/config/remote-config.ts | 98 ++++++++++++----- .../assets/framework/config/runtime-config.ts | 43 ++++---- .../local-platform-login/local-login-host.ts | 3 +- .../adapters/local/login-sources.test.ts | 3 +- .../adapters/local/visitor-account.test.ts | 3 +- .../framework-tests/config/bootstrap.test.ts | 4 +- .../config/local-startup.test.ts | 18 +-- .../framework-tests/config/profiles.test.ts | 5 +- .../config/remote-config-fetcher.test.ts | 6 +- .../config/remote-config.test.ts | 104 ++++++++++-------- .../config/runtime-config.test.ts | 49 ++++++++- .../contracts/remote-config-array.json | 2 +- .../contracts/remote-config-single.json | 2 +- .../framework-tests/fixtures/remote-config.ts | 9 ++ .../integration/local-login-sources.test.ts | 2 +- .../integration/login-flow.test.ts | 3 +- .../platform-vertical-slice.test.ts | 2 +- .../local-platform-login/host.test.ts | 36 +++++- .../local-platform-login/transport.test.ts | 2 +- .../local-platform-login/wire.test.ts | 2 +- .../framework-tests/platform/commands.test.ts | 3 +- .../platform/game-host-adapter.test.ts | 3 +- .../framework-tests/platform/runtime.test.ts | 7 +- .../framework-tests/platform/startup.test.ts | 3 +- .../sdk/game-contract-harness.test.ts | 3 +- 27 files changed, 286 insertions(+), 143 deletions(-) create mode 100644 cocoscreator_projects/framework-tests/fixtures/remote-config.ts diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts index 5dfa233..4e77a4b 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts @@ -89,9 +89,10 @@ export function assertLocalStartupContext(context: LocalStartupContext): void { const { config } = context; if (config.mode !== 'debug') fail('mode', 'must be debug'); if (config.isDebugger !== true) fail('isDebugger', 'must be true'); - if (config.source !== 'direct') fail('source', 'must be direct'); - if (config.gameserver !== null) fail('gameserver', 'must be null'); - if (config.rawConfig !== null) fail('rawConfig', 'must be null'); + if (config.source !== 'remote') fail('source', 'must be remote'); + if (config.gameserver !== PROFILES.local!.gameserver) fail('gameserver', 'must equal local profile remote entry'); + if (!config.remoteConfig || typeof config.remoteConfig.getValue !== 'function') fail('remoteConfig', 'must provide memory view'); + if (!config.rawConfig || config.rawConfig !== config.remoteConfig.raw) fail('rawConfig', 'must share remoteConfig.raw'); try { resolveIdentity([() => config.identity]); diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts index b11b3e7..0be0355 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts @@ -9,12 +9,12 @@ export interface DebugAccount { unionid?: string | number; } -export type ProfileSource = 'remote' | 'direct'; +export type ProfileSource = 'remote'; export interface DebugProfile { readonly name: string; readonly source: ProfileSource; - readonly gameserver?: string; + readonly gameserver: string; readonly servers?: readonly string[]; readonly account?: DebugAccount; } @@ -27,7 +27,8 @@ export const PROFILES: Readonly> = Object.freeze({ staging: Object.freeze({ name: 'staging', source: 'remote', gameserver: DEFAULT_GAMESERVER }), local: Object.freeze({ name: 'local', - source: 'direct', + source: 'remote', + gameserver: DEFAULT_GAMESERVER, servers: Object.freeze(['ws://127.0.0.1:3088']), }), }); diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts index cb70717..0e7e18f 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts @@ -1,12 +1,11 @@ -/** 原配置服务的唯一服务器字段,逐字对齐 ServerUrl_Succ 的 data.urlserver。 */ +import type { ChannelIdentity } from './identity.ts'; + +/** One in-memory view over the JSON root received by legacy getConfig_Succ. */ export interface RemoteConfig { - readonly data: { - readonly urlserver: string | readonly string[]; - readonly [key: string]: unknown; - }; + readonly raw: Readonly>; + readonly getValue: (name: string) => unknown; } -/** 远程配置抓取抽象;cache bust 在 fetcher 内形成且只形成一次。 */ export interface ConfigFetcher { fetch(gameserver: string, cacheBust?: () => string): Promise; } @@ -14,33 +13,76 @@ export interface ConfigFetcher { export class ConfigFetchError extends Error {} export class ConfigParseError extends Error {} -function normalizeWebSocketUrl(value: string): string { - if (value.startsWith('ws://') || value.startsWith('wss://')) return value; - const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(value); - if (scheme) { - throw new ConfigParseError(`远程配置 data.urlserver 不支持 scheme: ${scheme[1]}`); +const SCOPES = [ + ['agentlist', 'agentid'], ['gamelist', 'gameid'], + ['channellist', 'channelid'], ['marketlist', 'marketid'], +] as const; + +type ConfigObject = Record; +function requireObject(value: unknown, path: string): ConfigObject { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new ConfigParseError(`Invalid remote config ${path}: expected object`); } - return `ws://${value}`; + return value as ConfigObject; } -/** - * 只接受 data.urlserver;不读取旧的 *_server_tcp,也不去重或改变配置顺序。 - */ -export function parseUrlServers(input: unknown): readonly string[] { - if (typeof input !== 'object' || input === null || !('data' in input)) { - throw new ConfigParseError('远程配置缺少 data.urlserver'); - } - const data = (input as { data?: unknown }).data; - if (typeof data !== 'object' || data === null || !('urlserver' in data)) { - throw new ConfigParseError('远程配置缺少 data.urlserver'); - } +function validateHierarchy(scope: ConfigObject, depth: number): void { + if (depth === SCOPES.length) return; + const [listName] = SCOPES[depth]; + if (!Object.prototype.hasOwnProperty.call(scope, listName)) return; + const list = scope[listName]; + if (!Array.isArray(list)) throw new ConfigParseError(`Invalid remote config ${listName}: expected array`); + for (const entry of list) validateHierarchy(requireObject(entry, listName), depth + 1); +} - const raw = (data as { urlserver?: unknown }).urlserver; +/** Preserve first loose identity match, truthy overrides and whole-object replacement. */ +export function createRemoteConfig(input: unknown, identity: Readonly): RemoteConfig { + const raw = requireObject(input, 'root'); + validateHierarchy(raw, 0); + const scopes = [raw]; + let scope = raw; + for (const [listName, key] of SCOPES) { + const list = scope[listName] as ConfigObject[] | undefined; + // Legacy do_get_para skips any falsy identity, including numeric marketid=0. + if (!list || !identity[key]) break; + const match = list.find(entry => entry[key] == identity[key]); + if (!match) break; + scopes.push(match); + scope = match; + } + return Object.freeze({ + raw, + getValue(name: string): unknown { + let value: unknown = null; + for (const current of scopes) { + if (Object.prototype.hasOwnProperty.call(current, name) && current[name]) value = current[name]; + } + return value; + }, + }); +} + +function normalizeWebSocketUrl(value: string): string { + const url = value.startsWith('ws://') || value.startsWith('wss://') ? value : `ws://${value}`; + if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(value) && !/^wss?:\/\//.test(value)) { + throw new ConfigParseError('Invalid remote server: unsupported scheme'); + } + try { + const parsed = new URL(url); + if (!value || /\s/.test(value) || !parsed.hostname || parsed.username || parsed.password + || (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:')) throw new Error('invalid'); + } catch { + throw new ConfigParseError('Invalid remote server: malformed WebSocket URL'); + } + return url; +} + +/** Legacy getConfig_Succ selects game_server_tcp when truthy, otherwise player_server_tcp. */ +export function parseUrlServers(config: RemoteConfig): readonly string[] { + const raw = config.getValue('game_server_tcp') || config.getValue('player_server_tcp'); const values = typeof raw === 'string' ? [raw] : raw; - if (!Array.isArray(values) || values.length === 0 - || values.some((value) => typeof value !== 'string' || value.length === 0)) { - throw new ConfigParseError('远程配置 data.urlserver 必须是非空字符串或非空字符串数组'); + if (!Array.isArray(values) || values.length === 0 || values.some(value => typeof value !== 'string' || !value)) { + throw new ConfigParseError('Remote game_server_tcp/player_server_tcp must be a nonempty string or string array'); } - return Object.freeze(values.map(normalizeWebSocketUrl)); } diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts index 0db44c4..f2e5c8e 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts @@ -8,6 +8,8 @@ import { } from './profiles.ts'; import { ConfigFetchError, + ConfigParseError, + createRemoteConfig, parseUrlServers, type ConfigFetcher, type RemoteConfig, @@ -29,9 +31,10 @@ export interface RuntimeConfig { readonly isDebugger: boolean; readonly identity: Readonly; readonly servers: readonly string[]; - readonly source: 'remote' | 'direct'; - readonly gameserver: string | null; - readonly rawConfig: RemoteConfig | null; + readonly source: 'remote'; + readonly gameserver: string; + readonly rawConfig: RemoteConfig['raw']; + readonly remoteConfig: RemoteConfig; } export interface ResolveRuntimeConfigOptions { @@ -78,24 +81,6 @@ export async function resolveRuntimeConfig( const profile = selectedProfile(options.mode, options.search); const identity = resolveHostIdentity(options); - if (profile.source === 'direct') { - if (options.mode !== 'debug' || profile.name !== 'local') { - throw new ConfigFetchError(`profile '${profile.name}' 不是允许的 local debug direct 模式`); - } - if (!profile.servers || profile.servers.length === 0) { - throw new ConfigFetchError(`profile '${profile.name}' 未配置完整 servers`); - } - return freezeResult({ - mode: options.mode, - isDebugger: true, - identity, - servers: Object.freeze([...profile.servers]), - source: 'direct', - gameserver: null, - rawConfig: null, - }); - } - if (!profile.gameserver) { throw new ConfigFetchError(`profile '${profile.name}' 未配置 gameserver`); } @@ -108,10 +93,19 @@ export async function resolveRuntimeConfig( try { raw = await options.fetcher.fetch(gameserver, options.cacheBust); } catch (error) { - if (error instanceof ConfigFetchError) throw error; + if (error instanceof ConfigFetchError || error instanceof ConfigParseError) throw error; throw new ConfigFetchError(`远程配置抓取失败: ${gameserver}`); } - const servers = parseUrlServers(raw); + const remoteConfig = createRemoteConfig(raw, identity); + let servers: readonly string[]; + if (profile.name === 'local') { + if (!profile.servers || profile.servers.length === 0) { + throw new ConfigFetchError('Local profile must define servers'); + } + servers = Object.freeze([...profile.servers]); + } else { + servers = parseUrlServers(remoteConfig); + } return freezeResult({ mode: options.mode, @@ -120,6 +114,7 @@ export async function resolveRuntimeConfig( servers, source: 'remote', gameserver, - rawConfig: raw as RemoteConfig, + rawConfig: remoteConfig.raw, + remoteConfig, }); } diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts index f7d1cf9..7ed15b0 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts @@ -114,7 +114,8 @@ export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLogin requireActive(); const context = await resolveLocalStartup(options.configOptions); requireActive(); - options.record({ kind: 'startup-config', data: context.config }); + const { mode, isDebugger, identity, servers, source, gameserver } = context.config; + options.record({ kind: 'startup-config', data: { mode, isDebugger, identity, servers, source, gameserver } }); requireActive(); const entry = createDiagnosticGameEntry(context.config.identity.gameid, options.record); sources = createLocalLoginSources({ context, storage: options.storage, random: options.random, diff --git a/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts b/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts index 9d116ca..effb8d7 100644 --- a/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts +++ b/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts @@ -1,3 +1,4 @@ +import { remoteFixture } from '../../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts'; @@ -7,7 +8,7 @@ import { LOCAL_MACHINE_KEY, localAccountKey, type LocalStoragePort } from '../.. import { createLocalLoginSources, type LocalHostSnapshot } from '../../../YouleNexus/assets/framework/adapters/local/login-sources.ts'; const context: LocalStartupContext = { hostKind: 'h5', profile: 'local', config: { - mode: 'debug', isDebugger: true, source: 'direct', gameserver: null, rawConfig: null, + mode: 'debug', isDebugger: true, source: 'remote', gameserver: PROFILES.local!.gameserver, ...remoteFixture(), servers: PROFILES.local!.servers!, identity: BUILD_IDENTITY, } }; class MapStorage implements LocalStoragePort { diff --git a/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts b/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts index 9a42819..f8a1ccd 100644 --- a/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts +++ b/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts @@ -1,3 +1,4 @@ +import { remoteFixture } from '../../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts'; @@ -15,7 +16,7 @@ class MapStorage implements LocalStoragePort { function context(overrides: Partial = {}): LocalStartupContext { return { hostKind: 'h5', profile: 'local', config: { - mode: 'debug', isDebugger: true, source: 'direct', gameserver: null, rawConfig: null, + mode: 'debug', isDebugger: true, source: 'remote', gameserver: PROFILES.local!.gameserver, ...remoteFixture(), servers: PROFILES.local!.servers!, identity: { ...BUILD_IDENTITY, ...overrides }, } }; } diff --git a/cocoscreator_projects/framework-tests/config/bootstrap.test.ts b/cocoscreator_projects/framework-tests/config/bootstrap.test.ts index 2d542a5..358a2e5 100644 --- a/cocoscreator_projects/framework-tests/config/bootstrap.test.ts +++ b/cocoscreator_projects/framework-tests/config/bootstrap.test.ts @@ -5,12 +5,12 @@ import type { ConfigFetcher } from '../../YouleNexus/assets/framework/config/rem test('resolveBootstrap:显式 hostKind 透传严格 RuntimeConfig,并保留 profile 元数据', async () => { const fetcher: ConfigFetcher = { - async fetch() { return { data: { urlserver: '5.5.5.5:5000' } }; }, + async fetch() { return { player_server_tcp: '5.5.5.5:5000' }; }, }; const result = await resolveBootstrap({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher, }); - assert.equal(result.source, 'direct'); + assert.equal(result.source, 'remote'); assert.equal(result.profile.name, 'local'); assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']); }); diff --git a/cocoscreator_projects/framework-tests/config/local-startup.test.ts b/cocoscreator_projects/framework-tests/config/local-startup.test.ts index 3026c73..542089d 100644 --- a/cocoscreator_projects/framework-tests/config/local-startup.test.ts +++ b/cocoscreator_projects/framework-tests/config/local-startup.test.ts @@ -1,3 +1,4 @@ +import { remoteFixture } from '../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { @@ -16,15 +17,15 @@ function options(search = '?profile=local'): ResolveRuntimeConfigOptions & { } { const fetcher = { calls: 0, - async fetch(): Promise { + async fetch() { this.calls += 1; - throw new Error('forbidden fetch'); + return { player_server_tcp: 'remote.example:3088' }; }, }; return { mode: 'debug', hostKind: 'h5', win: {}, search, fetcher }; } -test('local debug resolves the authoritative direct config without fetching', async () => { +test('local debug fetches remote config before choosing authoritative local servers', async () => { const input = options(); const context = await resolveLocalStartup(input); @@ -32,7 +33,8 @@ test('local debug resolves the authoritative direct config without fetching', as assert.equal(context.profile, 'local'); assert.equal(context.config.identity.gameid, BUILD_IDENTITY.gameid); assert.deepEqual(context.config.servers, PROFILES.local!.servers); - assert.equal(input.fetcher.calls, 0); + assert.equal(input.fetcher.calls, 1); + assert.equal(context.config.source, 'remote'); assert.equal(Object.isFrozen(context), true); }); @@ -107,9 +109,9 @@ function validContext(): LocalStartupContext { isDebugger: true, identity: BUILD_IDENTITY, servers: PROFILES.local!.servers!, - source: 'direct', - gameserver: null, - rawConfig: null, + source: 'remote', + gameserver: PROFILES.local!.gameserver, + ...remoteFixture(BUILD_IDENTITY), }, }; } @@ -119,7 +121,7 @@ test('context validator rejects forged config fields and malformed identity', () const configCases: Array<[keyof RuntimeConfig, unknown]> = [ ['mode', 'release'], ['isDebugger', false], - ['source', 'remote'], + ['source', 'direct'], ['gameserver', 'https://example.com/config.txt'], ['rawConfig', {}], ['servers', ['ws://127.0.0.1:9999']], diff --git a/cocoscreator_projects/framework-tests/config/profiles.test.ts b/cocoscreator_projects/framework-tests/config/profiles.test.ts index cb1e8cd..fbd5869 100644 --- a/cocoscreator_projects/framework-tests/config/profiles.test.ts +++ b/cocoscreator_projects/framework-tests/config/profiles.test.ts @@ -23,10 +23,11 @@ test('resolveGameServer:空 gameconfig 使用配置源,非空值使用解码 ); }); -test('默认 profile 是 ACTIVE_PROFILE,local 是显式 direct 源', () => { +test('默认 profile 是 ACTIVE_PROFILE,local 保持 remote 源与独立连接目标', () => { assert.equal(resolveActiveProfile('').name, ACTIVE_PROFILE); const local = resolveActiveProfile('?profile=local'); - assert.equal(local.source, 'direct'); + assert.equal(local.source, 'remote'); + assert.ok(local.gameserver); assert.deepEqual(local.servers, ['ws://127.0.0.1:3088']); }); diff --git a/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts b/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts index 6add1d1..1a892e6 100644 --- a/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts +++ b/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts @@ -8,7 +8,7 @@ test('HttpConfigFetcher:POST 空 body,并无条件以 ? 拼接 cache bust', const calls: Array<{ input: string; init?: RequestInit }> = []; const fetcher = new HttpConfigFetcher(async (input, init) => { calls.push({ input: String(input), init }); - return new Response('{"data":{"urlserver":"10.0.0.1:3088"}}'); + return new Response('{"player_server_tcp":"10.0.0.1:3088"}'); }); await fetcher.fetch('https://config.example/file.txt?existing=1', () => '123'); @@ -22,7 +22,7 @@ test('resolveRuntimeConfig:把 cacheBust 注入唯一的 HTTP 请求构造点' const calls: Array<{ input: string; init?: RequestInit }> = []; const fetcher = new HttpConfigFetcher(async (input, init) => { calls.push({ input: String(input), init }); - return new Response('{"data":{"urlserver":"10.0.0.1:3088"}}'); + return new Response('{"player_server_tcp":"10.0.0.1:3088"}'); }); await resolveRuntimeConfig({ @@ -49,7 +49,7 @@ test('HttpConfigFetcher:非法 JSON 显式抛 ConfigParseError', async () => { test('HttpConfigFetcher:非 200 响应即使 body 是合法 JSON 也拒绝', async () => { const fetcher = new HttpConfigFetcher(async () => new Response( - '{"data":{"urlserver":"10.0.0.1:3088"}}', + '{"player_server_tcp":"10.0.0.1:3088"}', { status: 503 }, )); await assert.rejects( diff --git a/cocoscreator_projects/framework-tests/config/remote-config.test.ts b/cocoscreator_projects/framework-tests/config/remote-config.test.ts index dc44c5c..2e998f4 100644 --- a/cocoscreator_projects/framework-tests/config/remote-config.test.ts +++ b/cocoscreator_projects/framework-tests/config/remote-config.test.ts @@ -1,54 +1,68 @@ import { readFileSync } from 'node:fs'; +import { runInNewContext } from 'node:vm'; import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { parseUrlServers } from '../../YouleNexus/assets/framework/config/remote-config.ts'; - -const single = JSON.parse(readFileSync( - new URL('../fixtures/contracts/remote-config-single.json', import.meta.url), - 'utf8', -)) as unknown; -const array = JSON.parse(readFileSync( - new URL('../fixtures/contracts/remote-config-array.json', import.meta.url), - 'utf8', -)) as unknown; - -test('parseUrlServers:读取 data.urlserver 字符串并补 ws://', () => { - assert.deepEqual(parseUrlServers(single), ['ws://10.0.0.1:3088']); +import * as api from '../../YouleNexus/assets/framework/config/remote-config.ts'; +const identity = { agentid: '11', gameid: '22', channelid: '33', marketid: 44, version: 1 }; +const source = readFileSync(new URL('../../../projects/Game_Surface_3/js/00_Surface/12_Logic.js', import.meta.url), 'utf8'); +const start = source.indexOf('function get_paravalue(o_config, paraname){'); +const legacySource = source.slice(start, source.indexOf('function getConfig_Succ', start)); +function view(raw: unknown, selected = identity) { + assert.equal(typeof api.createRemoteConfig, 'function', 'remote memory view must be exported'); + return api.createRemoteConfig(raw, selected); +} +function legacy(raw: unknown, name: string, selected = identity) { + return runInNewContext(`${legacySource}; get_paravalue(wrapper, name)`, { + wrapper: { data: raw }, name, + GameData: { AgentId: selected.agentid, GameId: selected.gameid, ChannelId: selected.channelid, marketID: selected.marketid }, + }); +} +test('memory view agrees with actual legacy function across all five scopes and preserves raw objects', () => { + const scopes: Record[] = [ + { hall_config: { root: true }, rootOnly: 'root', falsy: 'inherited' }, + { agentid: 11, hall_config: { agent: true }, agentOnly: 'agent', falsy: 0 }, + { gameid: 22, hall_config: { game: true }, gameOnly: 'game', falsy: false }, + { channelid: 33, hall_config: { channel: true }, channelOnly: 'channel', falsy: '' }, + { marketid: '44', hall_config: ['market'], marketOnly: 'market', falsy: null }, + ]; + const lists = ['agentlist', 'gamelist', 'channellist', 'marketlist']; + for (let depth = 0; depth < scopes.length; depth++) { + if (depth > 0) scopes[depth - 1][lists[depth - 1]] = [scopes[depth], { ...scopes[depth], hall_config: 'duplicate must lose' }]; + const result = view(scopes[0]); + assert.strictEqual(result.raw, scopes[0]); + for (const name of ['hall_config', 'rootOnly', 'agentOnly', 'gameOnly', 'channelOnly', 'marketOnly', 'falsy', 'missing']) { + assert.deepEqual(result.getValue(name), legacy(scopes[0], name)); + } + assert.strictEqual(result.getValue('hall_config'), scopes[depth].hall_config); + assert.equal(result.getValue('falsy'), 'inherited'); + } + assert.equal(view(scopes[0], { ...identity, marketid: 0 }).getValue('marketOnly'), null); + assert.deepEqual(view(scopes[0], { ...identity, agentid: 'missing' }).getValue('hall_config'), { root: true }); }); - -test('parseUrlServers:读取 data.urlserver 数组并保留已有 wss://', () => { - assert.deepEqual(parseUrlServers(array), ['ws://10.0.0.1:3088', 'wss://backup.example/ws']); +test('falsy root values remain null and missing identity levels stop inheritance', () => { + for (const value of [0, false, '', null]) assert.equal(view({ x: value }).getValue('x'), null); + assert.equal(view({ x: 'root', agentlist: [{ agentid: 'other', x: 'wrong' }] }).getValue('x'), 'root'); }); - -test('parseUrlServers:拒绝旧的 *_server_tcp 字段', () => { - assert.throws( - () => parseUrlServers({ data: { player_server_tcp: 'wrong:1' } }), - /data\.urlserver/, - ); +test('invalid root and present hierarchy fail explicitly', () => { + for (const raw of [null, [], 'json', { agentlist: {} }, { agentlist: [null] }, + { agentlist: [{ agentid: 11, gamelist: {} }] }, { agentlist: [{ agentid: 11, gamelist: [{ gameid: 22, channellist: [false] }] }] }]) { + assert.throws(() => view(raw), api.ConfigParseError); + } }); - -test('parseUrlServers:保留原顺序和重复项,并冻结结果', () => { - const servers = parseUrlServers({ data: { urlserver: ['same:1', 'same:1', 'ws://last:2'] } }); - assert.deepEqual(servers, ['ws://same:1', 'ws://same:1', 'ws://last:2']); - assert.equal(Object.isFrozen(servers), true); +test('server selection prefers scoped game then player and preserves order and duplicates', () => { + for (const [raw, expected] of [ + [{ player_server_tcp: 'remote.example:3088' }, ['ws://remote.example:3088']], + [{ player_server_tcp: 'unused', game_server_tcp: ['same:1', 'same:1', 'wss://last/ws'] }, ['ws://same:1', 'ws://same:1', 'wss://last/ws']], + [{ game_server_tcp: '', player_server_tcp: 'player:1' }, ['ws://player:1']], + [{ game_server_tcp: 'global:1', agentlist: [{ agentid: 11, game_server_tcp: 'agent:2' }] }, ['ws://agent:2']], + ] as const) { + const servers = api.parseUrlServers(view(raw)); + assert.deepEqual(servers, expected); assert.ok(Object.isFrozen(servers)); + } }); - -test('parseUrlServers:拒绝空字符串、空数组和非字符串数组项', () => { - assert.throws(() => parseUrlServers({ data: { urlserver: '' } }), /data\.urlserver/); - assert.throws(() => parseUrlServers({ data: { urlserver: [] } }), /data\.urlserver/); - assert.throws(() => parseUrlServers({ data: { urlserver: ['ok:1', ''] } }), /data\.urlserver/); - assert.throws(() => parseUrlServers({ data: { urlserver: ['ok:1', 2] } }), /data\.urlserver/); -}); - -test('parseUrlServers:显式拒绝已有的非 WebSocket scheme', () => { - for (const value of [ - 'http://config.example/ws', - 'https://config.example/ws', - 'ftp://config.example/ws', - ]) { - assert.throws( - () => parseUrlServers({ data: { urlserver: value } }), - /不支持.*scheme/i, - ); +test('missing or invalid chosen servers never fall back to obsolete envelope or player candidates', () => { + for (const raw of [{ data: { urlserver: 'old:1' } }, {}, + ...[[], ['ok:1', ''], ['ok:1', 2], true, ' ', 'http://wrong/ws', 'ws://', 'ws://user:pass@host:1', 'host:bad'].map(game_server_tcp => ({ game_server_tcp, player_server_tcp: 'valid:1' }))]) { + assert.throws(() => api.parseUrlServers(view(raw)), api.ConfigParseError); } }); diff --git a/cocoscreator_projects/framework-tests/config/runtime-config.test.ts b/cocoscreator_projects/framework-tests/config/runtime-config.test.ts index e27970c..58e03fe 100644 --- a/cocoscreator_projects/framework-tests/config/runtime-config.test.ts +++ b/cocoscreator_projects/framework-tests/config/runtime-config.test.ts @@ -12,7 +12,7 @@ function returning(value: unknown): ConfigFetcher & { calls: string[] } { }; } -const REMOTE = { data: { urlserver: ['5.5.5.5:5000', '5.5.5.5:5000'] } }; +const REMOTE = { player_server_tcp: ['5.5.5.5:5000', '5.5.5.5:5000'] }; test('h5:只读 query 身份,不访问原生宿主,并生成严格 remote 配置', async () => { const fetcher = returning(REMOTE); @@ -133,16 +133,17 @@ test('uAgent_3:拒绝宿主提供的非法身份字段类型', async () => { ); }); -test('显式 local debug:direct 字段不靠 null 推断,完整服务器来自 profile', async () => { +test('显式 local debug:远程配置保留内存,连接服务器来自 profile', async () => { const fetcher = returning(REMOTE); const result = await resolveRuntimeConfig({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher, }); - assert.equal(result.source, 'direct'); - assert.equal(result.gameserver, null); - assert.equal(result.rawConfig, null); + assert.equal(result.source, 'remote'); + assert.ok(result.gameserver); + assert.equal(result.rawConfig, REMOTE); + assert.strictEqual(result.remoteConfig.raw, result.rawConfig); assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']); - assert.deepEqual(fetcher.calls, []); + assert.equal(fetcher.calls.length, 1); }); test('远程抓取失败显式报错,不降级 direct', async () => { @@ -152,3 +153,39 @@ test('远程抓取失败显式报错,不降级 direct', async () => { /远程配置抓取失败/, ); }); + + +test('release ignores local profile but still fetches and selects remote servers', async () => { + const fetcher = returning(REMOTE); + const result = await resolveRuntimeConfig({ mode: 'release', hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); + assert.equal(fetcher.calls.length, 1); + assert.deepEqual(result.servers, ['ws://5.5.5.5:5000', 'ws://5.5.5.5:5000']); + assert.equal(result.isDebugger, false); +}); + + +test('JSON parse errors retain their classification through the runtime boundary', async () => { + const { ConfigParseError } = await import('../../YouleNexus/assets/framework/config/remote-config.ts'); + const error = new ConfigParseError('invalid JSON'); + const fetcher = { async fetch() { throw error; } }; + await assert.rejects(resolveRuntimeConfig({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher }), + actual => actual === error); +}); + +test('local consumes explicit loopback targets after valid remote JSON even when remote candidates are absent', async () => { + const raw = { visitor_server_tcp: 'unused.example:1234', notice: 'kept' }; + const fetcher = returning(raw); + const result = await resolveRuntimeConfig({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); + assert.equal(fetcher.calls.length, 1); + assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']); + assert.strictEqual(result.rawConfig, raw); + assert.equal(result.remoteConfig.getValue('notice'), 'kept'); +}); + +test('remote profiles reject missing game/player addresses without selecting visitor', async () => { + for (const mode of ['debug', 'release'] as const) { + const fetcher = returning({ visitor_server_tcp: 'unused.example:1234' }); + await assert.rejects(resolveRuntimeConfig({ mode, hostKind: 'h5', win: {}, search: '', fetcher }), /game_server_tcp\/player_server_tcp/); + assert.equal(fetcher.calls.length, 1); + } +}); diff --git a/cocoscreator_projects/framework-tests/fixtures/contracts/remote-config-array.json b/cocoscreator_projects/framework-tests/fixtures/contracts/remote-config-array.json index e00b44e..83b00f4 100644 --- a/cocoscreator_projects/framework-tests/fixtures/contracts/remote-config-array.json +++ b/cocoscreator_projects/framework-tests/fixtures/contracts/remote-config-array.json @@ -1 +1 @@ -{"data":{"urlserver":["10.0.0.1:3088","wss://backup.example/ws"]}} +{"player_server_tcp":["10.0.0.1:3088","wss://backup.example/ws"]} diff --git a/cocoscreator_projects/framework-tests/fixtures/contracts/remote-config-single.json b/cocoscreator_projects/framework-tests/fixtures/contracts/remote-config-single.json index 0d6e3d0..f08738d 100644 --- a/cocoscreator_projects/framework-tests/fixtures/contracts/remote-config-single.json +++ b/cocoscreator_projects/framework-tests/fixtures/contracts/remote-config-single.json @@ -1 +1 @@ -{"data":{"urlserver":"10.0.0.1:3088"}} +{"player_server_tcp":"10.0.0.1:3088"} diff --git a/cocoscreator_projects/framework-tests/fixtures/remote-config.ts b/cocoscreator_projects/framework-tests/fixtures/remote-config.ts new file mode 100644 index 0000000..85c6b7a --- /dev/null +++ b/cocoscreator_projects/framework-tests/fixtures/remote-config.ts @@ -0,0 +1,9 @@ +import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts'; +import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts'; +import type { ChannelIdentity } from '../../YouleNexus/assets/framework/config/identity.ts'; + +/** Test-only memory configuration shared by typed RuntimeConfig fixtures. */ +export function remoteFixture(identity: Readonly = BUILD_IDENTITY) { + const remoteConfig = createRemoteConfig({ player_server_tcp: 'fixture.example:3088' }, identity); + return { remoteConfig, rawConfig: remoteConfig.raw }; +} diff --git a/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts b/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts index 7cc20b7..40203b3 100644 --- a/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts +++ b/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts @@ -14,7 +14,7 @@ class MapStorage implements LocalStoragePort { function resolve(search = '?profile=local') { return resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search, - fetcher: { async fetch(): Promise { throw new Error('forbidden fetch'); } } }); + fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } }); } function sourcesFor(context: Awaited>, storage: LocalStoragePort, diff --git a/cocoscreator_projects/framework-tests/integration/login-flow.test.ts b/cocoscreator_projects/framework-tests/integration/login-flow.test.ts index 8abc880..43f0b2b 100644 --- a/cocoscreator_projects/framework-tests/integration/login-flow.test.ts +++ b/cocoscreator_projects/framework-tests/integration/login-flow.test.ts @@ -1,3 +1,4 @@ +import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; @@ -13,7 +14,7 @@ import { FakeTransport } from '../helpers/fake-transport.ts'; const CONFIG: RuntimeConfig = { mode: 'debug', isDebugger: true, identity: { agentid: 'A', channelid: 'C', gameid: '41', version: 1, marketid: 4 }, - source: 'direct', servers: ['ws://agent'], gameserver: null, rawConfig: null, + source: 'remote', servers: ['ws://agent'], gameserver: 'https://fixture/config', ...remoteFixture(), }; const ACCOUNT = { openid: 'o', nickname: 'n', avatar: 'a', sex: 0, province: '', city: '', unionid: 'u' }; const LOGIN = { diff --git a/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts b/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts index 2ac7c42..38d283f 100644 --- a/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts +++ b/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts @@ -137,7 +137,7 @@ test('real modern vertical replay preserves wire contracts, room ordering and te configRequests.push({ url: String(url), init }); return new Response(JSON.stringify({ urlserver: 'wrong-root.example:1', - data: { urlserver: ['agent.example:3088', 'backup.example:3088'] }, + player_server_tcp: ['agent.example:3088', 'backup.example:3088'], }), { status: 200 }); }), }), diff --git a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts index c3fcea9..754a066 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts @@ -40,7 +40,7 @@ async function setup(overrides: { config?: Partial; let fetches = 0; const host = api.createLocalLoginHost({ configOptions: { mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local&version=731', - fetcher: { async fetch(): Promise { fetches++; throw new Error('unexpected fetch'); } }, ...overrides.config }, + fetcher: { async fetch() { fetches++; return { player_server_tcp: 'remote.example:3088' }; } }, ...overrides.config }, storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } }, random: () => .5, now: () => 123456, sampleHost: () => ({ location: null }), view: { render: model => { overrides.render?.(model); pages.push(model); }, @@ -70,7 +70,7 @@ for (const missing of ['resources', 'minimum', 'socket'] as const) test(`login w assert.equal(h.values.size, 0); assert.deepEqual(h.sockets[0]!.sent, []); h.resources.resolve(); h.minimum.resolve(); if (missing === 'socket') h.sockets[0]!.open(); await starting; - assert.equal(h.host.getSnapshot().ready, true); assert.equal(h.fetches(), 0); + assert.equal(h.host.getSnapshot().ready, true); assert.equal(h.fetches(), 1); assert.equal(h.sockets.length, 1); assert.equal(h.evidence.filter(event => event.kind === 'startup-config').length, 1); await h.host.login(); @@ -210,3 +210,35 @@ test('fatal overlay observer failure retains the original Runtime error object', assert.strictEqual(fault.errors[0], primary); assert.strictEqual(fault.errors[1], observer); assert.equal(h.host.getSnapshot().ready, false); assert.equal(h.sockets[0]!.closes, 1); }); + +test('remote fetch and parse failures leave local sockets and account storage untouched', async () => { + for (const fetch of [async () => { throw new Error('network down'); }, async () => ({ agentlist: {} }), async () => null]) { + const h = await setup({ config: { fetcher: { fetch } } }); + const result = h.host.start().catch(error => error); + await flush(); + assert.equal(h.sockets.length, 0); + assert.equal(h.values.size, 0); + assert.ok(await result instanceof Error); + assert.equal(h.failures.length, 1); + } +}); + +test('stop while remote fetch is pending prevents late socket creation', async () => { + const pending = deferred(); + let fetched = false; + const h = await setup({ config: { fetcher: { async fetch() { + fetched = true; await pending.promise; return { player_server_tcp: 'remote.example:3088' }; + } } } }); + const result = h.host.start().catch(error => error); + await flush(); assert.equal(fetched, true); assert.equal(h.sockets.length, 0); + h.host.stop(); pending.resolve(); await result; + assert.equal(h.sockets.length, 0); assert.equal(h.values.size, 0); +}); + +test('startup diagnostics exclude raw remote fields', async t => { + const h = await setup({ config: { fetcher: { async fetch() { + return { player_server_tcp: 'remote.example:3088', privateConfig: 'DO_NOT_RECORD_REMOTE_SECRET' }; + } } } }); + t.after(() => h.host.stop()); await h.ready(); + assert.equal(JSON.stringify(h.evidence).includes('DO_NOT_RECORD_REMOTE_SECRET'), false); +}); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts index 320dda4..0630ce5 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts @@ -37,7 +37,7 @@ async function setup(overrides: { .catch((error: unknown) => assert.fail(`LocalLoginTransport implementation must exist: ${String(error)}`)); const context = await resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', - fetcher: { async fetch(): Promise { throw new Error('unexpected fetch'); } }, + fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } }, }); const sockets: FakeSocket[] = []; const urls: string[] = []; diff --git a/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts index 080e640..bd47479 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts @@ -15,7 +15,7 @@ async function setup(recordHook?: (event: { kind: string; data: unknown }) => vo const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts') .catch(error => assert.fail(`Local login Wire implementation must exist: ${String(error)}`)); const context = await resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', - fetcher: { async fetch(): Promise { throw new Error('unexpected fetch'); } } }); + fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } }); const transports: FakeTransport[] = []; const events: WireEvent[] = []; const evidence: { kind: string; data: unknown }[] = []; diff --git a/cocoscreator_projects/framework-tests/platform/commands.test.ts b/cocoscreator_projects/framework-tests/platform/commands.test.ts index 49a96b6..f9a07c8 100644 --- a/cocoscreator_projects/framework-tests/platform/commands.test.ts +++ b/cocoscreator_projects/framework-tests/platform/commands.test.ts @@ -1,3 +1,4 @@ +import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; @@ -22,7 +23,7 @@ const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ servers: Object.freeze(['ws://fixture']), source: 'remote', gameserver: 'https://fixture/config', - rawConfig: Object.freeze({ data: Object.freeze({ urlserver: 'fixture' }) }), + ...remoteFixture(), }); function fixture(name: string): { data: Record } { diff --git a/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts b/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts index bd22b1e..961ebfe 100644 --- a/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts +++ b/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts @@ -1,3 +1,4 @@ +import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; @@ -41,7 +42,7 @@ const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ servers: Object.freeze(['ws://fixture']), source: 'remote', gameserver: 'https://fixture/config', - rawConfig: Object.freeze({ data: Object.freeze({ urlserver: 'fixture' }) }), + ...remoteFixture(), }); function fixture(name: string): { data: Record } { diff --git a/cocoscreator_projects/framework-tests/platform/runtime.test.ts b/cocoscreator_projects/framework-tests/platform/runtime.test.ts index a39c23d..b29b45b 100644 --- a/cocoscreator_projects/framework-tests/platform/runtime.test.ts +++ b/cocoscreator_projects/framework-tests/platform/runtime.test.ts @@ -1,3 +1,4 @@ +import { remoteFixture } from '../fixtures/remote-config.ts'; import { readFileSync } from 'node:fs'; import assert from 'node:assert/strict'; import { test } from 'node:test'; @@ -57,9 +58,9 @@ function runtimeConfig(gameid: string | number = '41'): RuntimeConfig { version: 10000, }), servers: Object.freeze(['ws://agent']), - source: 'direct', - gameserver: null, - rawConfig: null, + source: 'remote', + gameserver: 'https://fixture/config', + ...remoteFixture(), }) as RuntimeConfig; } diff --git a/cocoscreator_projects/framework-tests/platform/startup.test.ts b/cocoscreator_projects/framework-tests/platform/startup.test.ts index 1c57888..93525e1 100644 --- a/cocoscreator_projects/framework-tests/platform/startup.test.ts +++ b/cocoscreator_projects/framework-tests/platform/startup.test.ts @@ -1,3 +1,4 @@ +import { remoteFixture } from '../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { StartupOrchestrator } from '../../YouleNexus/assets/framework/platform/startup.ts'; @@ -13,7 +14,7 @@ function bootstrapOf(): BootstrapResult { mode: 'debug', isDebugger: true, identity: IDENTITY, servers: ['ws://test'], - rawConfig: null, + ...remoteFixture(), profile: { name: 'debug' }, }; } diff --git a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts index 1b88668..61d9076 100644 --- a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts +++ b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts @@ -1,3 +1,4 @@ +import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; @@ -83,7 +84,7 @@ const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ servers: Object.freeze(['ws://fixture']), source: 'remote', gameserver: 'https://fixture/config', - rawConfig: Object.freeze({ data: Object.freeze({ urlserver: 'fixture' }) }), + ...remoteFixture(), }); function makeModule(id: number, onRestore: (deskinfo: unknown) => void): GameModule { From 58c974e114233cdc7b98b6cd6807b2dcd841fa8d Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 21:27:29 +0800 Subject: [PATCH 15/25] fix(config): reject WebSocket URL fragments at parse boundary --- .../YouleNexus/assets/framework/config/remote-config.ts | 2 +- .../framework-tests/config/remote-config.test.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts index 0e7e18f..8e7e5b7 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts @@ -69,7 +69,7 @@ function normalizeWebSocketUrl(value: string): string { } try { const parsed = new URL(url); - if (!value || /\s/.test(value) || !parsed.hostname || parsed.username || parsed.password + if (!value || /\s/.test(value) || url.includes('#') || !parsed.hostname || parsed.username || parsed.password || (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:')) throw new Error('invalid'); } catch { throw new ConfigParseError('Invalid remote server: malformed WebSocket URL'); diff --git a/cocoscreator_projects/framework-tests/config/remote-config.test.ts b/cocoscreator_projects/framework-tests/config/remote-config.test.ts index 2e998f4..29e977e 100644 --- a/cocoscreator_projects/framework-tests/config/remote-config.test.ts +++ b/cocoscreator_projects/framework-tests/config/remote-config.test.ts @@ -66,3 +66,11 @@ test('missing or invalid chosen servers never fall back to obsolete envelope or assert.throws(() => api.parseUrlServers(view(raw)), api.ConfigParseError); } }); + +test('WebSocket fragments including an empty trailing marker fail at the config boundary', () => { + for (const address of ['ws://127.0.0.1:3088/#invalid', 'wss://example.test/#', 'example.test:3088#']) { + assert.throws(() => api.parseUrlServers(view({ player_server_tcp: address })), api.ConfigParseError); + } + assert.deepEqual(api.parseUrlServers(view({ player_server_tcp: 'wss://example.test/path%23part' })), + ['wss://example.test/path%23part']); +}); From d79bed0253caab0d1eaf151812877dd705fa76d3 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 21:28:52 +0800 Subject: [PATCH 16/25] docs: record remote configuration source and browser evidence --- docs/protocol/01-传输层与架构.md | 12 ++++-- .../plans/2026-09-05-remote-config-memory.md | 2 +- ...6-09-05-remote-config-memory-acceptance.md | 37 +++++++++++++++++++ .../2026-09-05-remote-config-memory-design.md | 10 +++-- 4 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 docs/superpowers/reports/2026-09-05-remote-config-memory-acceptance.md diff --git a/docs/protocol/01-传输层与架构.md b/docs/protocol/01-传输层与架构.md index 5cb8595..5846f2a 100644 --- a/docs/protocol/01-传输层与架构.md +++ b/docs/protocol/01-传输层与架构.md @@ -10,10 +10,10 @@ ``` ┌─────────────┐ 配置请求(HTTP) ┌──────────────────┐ -│ 新前端 │ ─────────────────► │ 配置服务器 │ 返回 update_json (含 urlserver) +│ 新前端 │ ─────────────────► │ 配置服务器 │ 返回 update_json (身份层级配置) │ (Cocos) │ ◄───────────────── │ (gameserver 地址) │ └─────────────┘ └──────────────────┘ - │ ws://ip:port (拿到 urlserver 后) + │ ws://ip:port (解析服务器配置后) ▼ ┌─────────────────────────────────────────────────────┐ │ 大厅服务器(agent) ←切换→ 房间服务器(room) │ @@ -47,7 +47,10 @@ WebSocket 在 `min_tcp(config)`(`00_minhttp.js:256`)内创建: var ws = new WebSocket("ws://" + config.ipport); // 00_minhttp.js:258,例: ws://127.0.0.1:5414 ``` - `config.ipport` 来自 `GameData.Server`(`12_Logic.js:856 / 900 / 941` 等连接函数把 `server` 赋给 `config.ipport`)。 -- `GameData.Server` 的来源(netType==0 分支):配置服务器回包 `ServerUrl_Succ` 内 `GameData.Server=_msg.data.urlserver`(`12_Logic.js:549`)。 +- `GameData.Server` 的来源(netType==0 分支):启动实际调用 `get_config`(`12_Logic.js:521 / 1330`),以 POST 空 body 读取配置,URL 为 `gameserver + "?" + min_timestamp()`;`getConfig_Succ` 对响应文本 JSON.parse 后保存到内存 `GameData.serverConfig.data`。这里的 `data` 是客户端包装,远程 JSON 根对象本身不带这一层。 + - `get_paravalue` 按全局 → agent → game → channel → market 查找;每层第一个身份宽松相等的条目生效,只有 truthy 参数覆盖继承值,缺少层级即返回继承值,对象整体覆盖。 + - `getConfig_Succ` 选择 truthy `game_server_tcp`,否则 `player_server_tcp`;`serverType != 0` 时在配置解析之后覆盖为本地地址。debug/release 均读取配置,`isDebugger` 不控制读取开关。 + - 旧 `ServerUrl_Succ`(`12_Logic.js:533`)中的 `data.urlserver` 属于未启用的旧请求路径,不能作为当前启动配置契约。整份远程配置不写磁盘或 localStorage。 - ⚠️ 注意:`Logic.setGameServer()`(`12_Logic.js:1270`)只设置 `Game_Config.Debugger.gameserver`(配置文件 URL),**不**设置 `GameData.Server`;两者是两步、不可混淆。 - 切换房间/大厅服时,`GameData.Server` 被替换为 `_msg.data.roomserver` / `_msg.data.agentserver`(`09_Net.js:509 / 526`)。 - `GameData.Server` 可为**单个字符串,也可为数组**(多候选服务器)。为数组时按 `GameData.serverIndex` 轮询,详见第 7 节。 @@ -176,7 +179,8 @@ if (route === "platform" || route === "agent" || route === "room") { ``` 1. 启动 AppStart → HTTP 请求配置服务器(gameserver=Game_Config.Debugger.gameserver) - → 回包 ServerUrl_Succ → GameData.Server = _msg.data.urlserver (12_Logic.js:549) + → get_config POST 空 body → getConfig_Succ JSON.parse → 内存 GameData.serverConfig.data + → get_paravalue 按身份层级读取 → game_server_tcp 或 player_server_tcp → 显式本地覆盖 2. (netType==0 时) AppStart 启 GameData.TcpTimer = setInterval(..., 3*GameData.timer) = 30s 首连看门狗 (12_Logic.js:485-496):超时则 GameUI.OpenTips("网络状况不好...") 并 Net.ws_tcp.close() 首连成功(onopen)或拿到配置后会 clearTimeout 它。 diff --git a/docs/superpowers/plans/2026-09-05-remote-config-memory.md b/docs/superpowers/plans/2026-09-05-remote-config-memory.md index 9c6d9e2..b3ae160 100644 --- a/docs/superpowers/plans/2026-09-05-remote-config-memory.md +++ b/docs/superpowers/plans/2026-09-05-remote-config-memory.md @@ -33,7 +33,7 @@ - [ ] Write failing tests first. Use the actual legacy get_paravalue extracted from 12_Logic.js in a node:vm sandbox as differential oracle; fixtures contain synthetic identities only. Assert values across all five scopes, false/0/empty values, object replacement, missing levels and first duplicate. Example: `assert.deepEqual(view.getValue('hall_config'), legacy(wrapper, 'hall_config'))`. - [ ] Add startup behavior assertions: for debug local fake fetcher increment calls then return `{player_server_tcp:'remote.example:3088'}`; expect calls=1, source=remote, local servers, raw config retained. release with profile=local must still fetch and use remote candidates. Failed fetch or parse must leave local-host socket calls at zero. No persistence operation is added. - [ ] Run exact focused tests from cocoscreator_projects: `node --import tsx --test --test-concurrency=1 framework-tests/config/remote-config.test.ts framework-tests/config/runtime-config.test.ts framework-tests/config/profiles.test.ts framework-tests/config/local-startup.test.ts`. Record expected assertion failures before production edits. If tsx ENOMEM occurs, repeat identical command with require_escalated. -- [ ] Implement the smallest integrated change. Loop scopes using first identity equality, update inherited value only if truthy, stop when scope is absent. Validate the JSON root and present hierarchy shape; normalize only the chosen server candidates. Runtime always awaits fetch, builds one memory view, parses candidates, then applies profile local servers. Preserve host cancellation guards and stop semantics. +- [ ] Implement the smallest integrated change. Loop scopes using first identity equality, update inherited value only if truthy, stop when scope is absent. Validate the JSON root and present hierarchy shape; normalize only the chosen server candidates. Runtime always awaits fetch and builds one memory view, then selects explicit local servers or calls parseUrlServers(view) for remote profiles. Local does not require unused remote TCP fields. Preserve host cancellation guards and stop semantics. Test real-source-shaped `{visitor_server_tcp:'visitor.example:3088'}`: debug local succeeds only after fetch/parse; release fails for missing game/player TCP, never selects visitor. - [ ] Update dependent tests/fixtures asserting the superseded direct contract; retain all unrelated assertions and add no runtime fallback. All test RuntimeConfig literals must supply the new view via a central test helper where appropriate. - [ ] Run focused tests then full explicitly enumerated TS suite: `$testPaths = @(rg --files framework-tests -g '*.test.ts' | Sort-Object); node --import tsx --test --test-concurrency=1 @testPaths`. Run `node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit` and `node scripts/check-import-boundaries.mjs`. Discover and explicitly name the two platform architecture tests; do not invoke aggregate generators. - [ ] Self-review, stage exact task files, commit. Report RED/GREEN commands, counts, changed files, and concerns in task-1-report.md. Do not touch root controller docs or existing dirty resources. diff --git a/docs/superpowers/reports/2026-09-05-remote-config-memory-acceptance.md b/docs/superpowers/reports/2026-09-05-remote-config-memory-acceptance.md new file mode 100644 index 0000000..8e856a1 --- /dev/null +++ b/docs/superpowers/reports/2026-09-05-remote-config-memory-acceptance.md @@ -0,0 +1,37 @@ +# 远程配置读取与内存解析验收 + +日期:2026-09-05。分支:codex/local-platform-login。设计与计划提交:62328b6。 + +## 范围 + +debug、release 均读取远程 JSON,按原工程身份层级读取参数,仅保存在内存变量。无远程配置文件落盘、localStorage 快照或失败缓存兜底。Login Layer/MainMenu 和其他 RPC 接线不在本批。 + +## 实际浏览器预检 + +- funplay MCP `fp_c4e73df9a6e175c3`:Creator 打开 `G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus`,场景 dirty=false;已有预览窗口 9 位于 `http://127.0.0.1:7456/?profile=local`。 +- `fp_bc76fa976d3b5404`:在该真实 Chromium 预览 origin 使用 POST 空 body 读取源码 DEFAULT_GAMESERVER(附时间戳)。HTTP 200,JSON 文本字符数 31519、根字段 25,agentlist 为数组,无根 data 包装。未保存响应文件,也未输出配置内容。 +- `fp_aadb469bca47420b`:使用源码 BUILD_IDENTITY 比对该次真实响应,匹配 agentid;其下没有匹配的 gameid,故 channel/market 层未进入。按原 get_paravalue 应继承 agent/全局值,不更换身份。 +- 上述仅验证浏览器能读取配置及响应结构;新代码的完整启动验收在实现后单独记录。 + +## 新代码实际边界 + +实现提交 `053302c`。通过 MCP 逐个刷新五个既有 TS 资源,无序列化资源写入;重新加载已有本地预览窗口。 + +- `fp_dc324592cdfbe09c`:实际 LocalPlatformLogin 启动,loading-first-draw=848.4ms,startup-config=905ms,source=remote,最终唯一目标为 ws://127.0.0.1:3088,诊断数据无 rawConfig;resources-ready=907.4ms,最短展示门=1353.5ms,login 页激活=1354.5ms。ready=true,未显示错误。未点击登录,未验收其他 RPC。 +- `fp_5524451aad3e7ff5`:从 Creator import map 导入实际编译的 runtime-config 与 HttpConfigFetcher,在真实 Chromium 中调用两种模式,底层仍使用真实 fetch。debug/local 请求一次,HTTP 200,source=remote,唯一目标为本地;rawConfig===remoteConfig.raw,getValue 可用,游客服务器字段存在;前后 localStorage 内容未变化。 +- 同一调用中 release 也请求一次、HTTP 200;之后因 game_server_tcp/player_server_tcp 缺失明确报错,未创建生产 WebSocket,localStorage 未变化。说明读取和内存解析入口实际执行,不能称 release 正式启动已通过。 +- `fp_a0676df82b7d83ae`:停止实际本地预览后 ready=false、stopped=true、pendingCount=0。 + +## 当前配置来源限制 + +真实响应经新生产解析器确认:当前身份的 game_server_tcp、player_server_tcp 均为 null,visitor_server_tcp 为字符串;hall_config、logimage 为 null。debug/local 在读取与层级解析成功后按原 serverType 语义使用明确本地地址。release 保持显式缺失错误,未切换身份、未选择真实子游戏、未用游客服务器兜底。 + +因此:本地远程读取→内存解析→本地连接→登录页通过;当前 BUILD 身份对应的正式启动因远程配置缺少地址而未通过。本批不修改远程配置服务或服务器。 + +## 自动验证和工作区 + +- `053302c`:纯 TS 显式枚举 606/606;指定 import-boundaries/presentation-boundaries 架构测试 49/49;框架类型与导入边界检查通过。 +- TDD 包含原函数差分对照、local 必须等待读取、读取失败/停止期间零连接、正式地址缺失、不泄露远程配置到诊断等。保留既有错误路径测试的预期 boom 日志。 +- 原有 50 个未跟踪文件 SHA256 全部不变,两项历史 meta 删除仍保留,共 52 项原有改动未处理。没有新资源文件,没有 worktree,没有合并或推送。 +- 独立审查发现 URL 片段会通过 URL 解析却被 WebSocket 构造器拒绝;`58c974e` 已在配置入口拒绝字面 `#`(包含空片段),保留合法 `%23`。该修复 RED 为 5/6,GREEN 为 6/6;仅修改解析器及对应测试,未重复无关全量测试。 +- 最终复审正在进行,结论补充于审查完成后。 diff --git a/docs/superpowers/specs/2026-09-05-remote-config-memory-design.md b/docs/superpowers/specs/2026-09-05-remote-config-memory-design.md index 2d8e45f..653f832 100644 --- a/docs/superpowers/specs/2026-09-05-remote-config-memory-design.md +++ b/docs/superpowers/specs/2026-09-05-remote-config-memory-design.md @@ -17,15 +17,17 @@ getValue 严格复刻 get_paravalue:全局→匹配 agent→game→channel→market;每层首个宽松相等身份匹配;断层保留上层值;只有 truthy 值覆盖,0/false/空串不覆盖;对象和数组整体覆盖,不自动深合并;缺省返回 null。身份 marketid=0 时保留原函数不搜索该层的语义。非法结构显式 ConfigParseError,不修补。 -WebSocket 候选按原 getConfig_Succ 选 truthy game_server_tcp,否则 player_server_tcp。支持原字符串/数组顺序及重复项;在协议适配入口转为 ws/wss URL。候选缺失或非法显式失败,不退回 data.urlserver。原工程明确的 game→player 选择属于来源契约。 +远程 WebSocket 候选按原 getConfig_Succ 选 truthy game_server_tcp,否则 player_server_tcp。支持原字符串/数组顺序及重复项;在协议适配入口转为 ws/wss URL。实际采用远程服务器时,候选缺失或非法显式失败,不退回 data.urlserver 或 visitor_server_tcp。原工程明确的 game→player 选择属于来源契约。 本批不实现 Logic.setConfigInfo 内的 UI 布局、副作用及子游戏 Game_Modify 回调;其所需原始参数均可从统一 getValue 读取。后续界面接线时再按原工程执行各字段的显示转换和来源默认语义。 ## 启动顺序 -身份解析 → 远程读取 → 根结构及层级解析 → 服务器候选解析 → 显式本地服务器选择 → 发布 RuntimeConfig → 后续 Runtime 启动。 +身份解析 → 远程读取 → 根结构及层级解析 → 显式服务器来源选择 → 校验并规范化最终连接候选 → 发布 RuntimeConfig → 后续 Runtime 启动。 -所有 profile 都是 remote。debug 与 release 共用解析器;isDebugger 保持构建模式语义,不控制是否读取。profile=local 是独立的本地服务器选择:仅 debug 使用,远程解析成功后覆盖连接目标为 PROFILES.local 唯一配置的回环地址。release 仍选 prod,不能因 URL 参数误接本地。 +所有 profile 都是 remote。debug 与 release 共用解析器;isDebugger 保持构建模式语义,不控制是否读取。profile=local 是独立的本地服务器选择:仅 debug 使用,远程 JSON/层级解析成功后采用 PROFILES.local 唯一配置的回环地址,不要求未采用的远程 TCP 字段存在。原 getConfig_Succ 在赋值后以 serverType 覆盖地址,并未预先校验未使用的远程值。release 仍选 prod,不能因 URL 参数误接本地。 + +真实数据核对:当前 BUILD_IDENTITY 匹配 agent,但没有其下匹配 game;继承结果的 game/player TCP 均为 null,仅 visitor TCP 有值。这不授权更换身份或使用游客地址。release 应显式暴露缺失;debug local 在成功读取与层级解析后可用明确的本地地址。该规则是固定的来源选择,禁止捕获读取/解析错误后退回本地。 本地诊断入口继续严格验证 DEBUG/H5/profile=local、身份及回环目标;更新“必须 direct”的旧断言。远程读取/解析失败前不得创建 socket、准备账号或发送登录。停止发生在读取期间时,不得在读取完成后启动连接。 @@ -33,7 +35,7 @@ WebSocket 候选按原 getConfig_Succ 选 truthy game_server_tcp,否则 player 1. 自动测试对照原 get_paravalue 实函数,覆盖五层、断层、重复身份、数字/字符串身份、truthy、对象覆盖及不存在字段。 2. debug prod、debug local、release 均实际调用 fetcher;本地目标在读取解析完成后才生效;无缓存读写。 -3. 非 JSON、非法层级、地址缺失/非法、HTTP 失败均明确失败;本地 host 无 socket/登录副作用。 +3. 非 JSON、非法层级、HTTP 失败均明确失败;本地 host 无 socket/登录副作用。实际采用远程地址的模式在 game/player TCP 缺失或非法时明确失败;local 不消费这些字段。 4. 真实 HTTP 读取使用当前配置源与 BUILD_IDENTITY,输出脱敏结构/匹配情况;不输出原始配置及认证信息,不连接其生产 WS。 5. 经 funplay MCP 验证实际 Creator 浏览器预览远程读取成功、配置在内存、最终目标仍为本地;若浏览器受 CORS 等阻止,记录真实阻塞,不能用 Node 成功代替浏览器通过。 6. 显式枚举纯 TS 测试、指定架构测试、类型和导入边界检查。原有 dirty 资源不变。 From 5d90891b387c3cafded59c607663fb79cbcfc6eb Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 21:32:16 +0800 Subject: [PATCH 17/25] docs: finalize remote configuration acceptance --- .../plans/2026-09-05-remote-config-memory.md | 24 +++++++++---------- ...6-09-05-remote-config-memory-acceptance.md | 3 ++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/superpowers/plans/2026-09-05-remote-config-memory.md b/docs/superpowers/plans/2026-09-05-remote-config-memory.md index b3ae160..e0bbc13 100644 --- a/docs/superpowers/plans/2026-09-05-remote-config-memory.md +++ b/docs/superpowers/plans/2026-09-05-remote-config-memory.md @@ -30,18 +30,18 @@ **Interfaces:** Remote config view exposes readonly raw root and getValue(name: string): unknown; export a creator in remote-config.ts taking unknown root and ChannelIdentity. RuntimeConfig exposes the view as remoteConfig and keeps rawConfig as the same raw reference, source always remote and gameserver non-null. parseUrlServers receives the view and resolves game_server_tcp/player_server_tcp. Profiles retain unique servers for local override and all have remote gameserver. -- [ ] Write failing tests first. Use the actual legacy get_paravalue extracted from 12_Logic.js in a node:vm sandbox as differential oracle; fixtures contain synthetic identities only. Assert values across all five scopes, false/0/empty values, object replacement, missing levels and first duplicate. Example: `assert.deepEqual(view.getValue('hall_config'), legacy(wrapper, 'hall_config'))`. -- [ ] Add startup behavior assertions: for debug local fake fetcher increment calls then return `{player_server_tcp:'remote.example:3088'}`; expect calls=1, source=remote, local servers, raw config retained. release with profile=local must still fetch and use remote candidates. Failed fetch or parse must leave local-host socket calls at zero. No persistence operation is added. -- [ ] Run exact focused tests from cocoscreator_projects: `node --import tsx --test --test-concurrency=1 framework-tests/config/remote-config.test.ts framework-tests/config/runtime-config.test.ts framework-tests/config/profiles.test.ts framework-tests/config/local-startup.test.ts`. Record expected assertion failures before production edits. If tsx ENOMEM occurs, repeat identical command with require_escalated. -- [ ] Implement the smallest integrated change. Loop scopes using first identity equality, update inherited value only if truthy, stop when scope is absent. Validate the JSON root and present hierarchy shape; normalize only the chosen server candidates. Runtime always awaits fetch and builds one memory view, then selects explicit local servers or calls parseUrlServers(view) for remote profiles. Local does not require unused remote TCP fields. Preserve host cancellation guards and stop semantics. Test real-source-shaped `{visitor_server_tcp:'visitor.example:3088'}`: debug local succeeds only after fetch/parse; release fails for missing game/player TCP, never selects visitor. -- [ ] Update dependent tests/fixtures asserting the superseded direct contract; retain all unrelated assertions and add no runtime fallback. All test RuntimeConfig literals must supply the new view via a central test helper where appropriate. -- [ ] Run focused tests then full explicitly enumerated TS suite: `$testPaths = @(rg --files framework-tests -g '*.test.ts' | Sort-Object); node --import tsx --test --test-concurrency=1 @testPaths`. Run `node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit` and `node scripts/check-import-boundaries.mjs`. Discover and explicitly name the two platform architecture tests; do not invoke aggregate generators. -- [ ] Self-review, stage exact task files, commit. Report RED/GREEN commands, counts, changed files, and concerns in task-1-report.md. Do not touch root controller docs or existing dirty resources. -- [ ] Controller dispatches independent spec/quality reviewer; repair findings with failing regressions first, then scoped re-review. +- [x] Write failing tests first. Use the actual legacy get_paravalue extracted from 12_Logic.js in a node:vm sandbox as differential oracle; fixtures contain synthetic identities only. Assert values across all five scopes, false/0/empty values, object replacement, missing levels and first duplicate. Example: `assert.deepEqual(view.getValue('hall_config'), legacy(wrapper, 'hall_config'))`. +- [x] Add startup behavior assertions: for debug local fake fetcher increment calls then return `{player_server_tcp:'remote.example:3088'}`; expect calls=1, source=remote, local servers, raw config retained. release with profile=local must still fetch and use remote candidates. Failed fetch or parse must leave local-host socket calls at zero. No persistence operation is added. +- [x] Run exact focused tests from cocoscreator_projects: `node --import tsx --test --test-concurrency=1 framework-tests/config/remote-config.test.ts framework-tests/config/runtime-config.test.ts framework-tests/config/profiles.test.ts framework-tests/config/local-startup.test.ts`. Record expected assertion failures before production edits. If tsx ENOMEM occurs, repeat identical command with require_escalated. +- [x] Implement the smallest integrated change. Loop scopes using first identity equality, update inherited value only if truthy, stop when scope is absent. Validate the JSON root and present hierarchy shape; normalize only the chosen server candidates. Runtime always awaits fetch and builds one memory view, then selects explicit local servers or calls parseUrlServers(view) for remote profiles. Local does not require unused remote TCP fields. Preserve host cancellation guards and stop semantics. Test real-source-shaped `{visitor_server_tcp:'visitor.example:3088'}`: debug local succeeds only after fetch/parse; release fails for missing game/player TCP, never selects visitor. +- [x] Update dependent tests/fixtures asserting the superseded direct contract; retain all unrelated assertions and add no runtime fallback. All test RuntimeConfig literals must supply the new view via a central test helper where appropriate. +- [x] Run focused tests then full explicitly enumerated TS suite: `$testPaths = @(rg --files framework-tests -g '*.test.ts' | Sort-Object); node --import tsx --test --test-concurrency=1 @testPaths`. Run `node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit` and `node scripts/check-import-boundaries.mjs`. Discover and explicitly name the two platform architecture tests; do not invoke aggregate generators. +- [x] Self-review, stage exact task files, commit. Report RED/GREEN commands, counts, changed files, and concerns in task-1-report.md. Do not touch root controller docs or existing dirty resources. +- [x] Controller dispatches independent spec/quality reviewer; repair findings with failing regressions first, then scoped re-review. ### Task 2: Documentation and real-boundary acceptance (controller) -- [ ] Correct docs/protocol/01-传输层与架构.md remote-entry description to active get_config/getConfig_Succ, local wrapper and scoped server selection. Preserve unrelated protocol text. -- [ ] Read the live remote response through the production parser and current identity without storing response body or printing sensitive values. Record fetch/parse outcome and selected hierarchy depth only. -- [ ] Verify actual Cocos preview via funplay MCP, without asset mutations. Observe actual remote request and resolved local target; stop runtime after observation. If browser fetch fails, document status and exact origin/request facts before deciding a fix; do not disable security or proxy silently. -- [ ] Hash-check initial dirty files unchanged. Write a concise acceptance report with separate pure tests, live HTTP and real Creator evidence. Perform whole-change review before declaring completion. +- [x] Correct docs/protocol/01-传输层与架构.md remote-entry description to active get_config/getConfig_Succ, local wrapper and scoped server selection. Preserve unrelated protocol text. +- [x] Read the live remote response through the production parser and current identity without storing response body or printing sensitive values. Record fetch/parse outcome and selected hierarchy depth only. +- [x] Verify actual Cocos preview via funplay MCP, without asset mutations. Observe actual remote request and resolved local target; stop runtime after observation. If browser fetch fails, document status and exact origin/request facts before deciding a fix; do not disable security or proxy silently. +- [x] Hash-check initial dirty files unchanged. Write a concise acceptance report with separate pure tests, live HTTP and real Creator evidence. Perform whole-change review before declaring completion. diff --git a/docs/superpowers/reports/2026-09-05-remote-config-memory-acceptance.md b/docs/superpowers/reports/2026-09-05-remote-config-memory-acceptance.md index 8e856a1..30cbdb3 100644 --- a/docs/superpowers/reports/2026-09-05-remote-config-memory-acceptance.md +++ b/docs/superpowers/reports/2026-09-05-remote-config-memory-acceptance.md @@ -21,6 +21,7 @@ debug、release 均读取远程 JSON,按原工程身份层级读取参数, - `fp_5524451aad3e7ff5`:从 Creator import map 导入实际编译的 runtime-config 与 HttpConfigFetcher,在真实 Chromium 中调用两种模式,底层仍使用真实 fetch。debug/local 请求一次,HTTP 200,source=remote,唯一目标为本地;rawConfig===remoteConfig.raw,getValue 可用,游客服务器字段存在;前后 localStorage 内容未变化。 - 同一调用中 release 也请求一次、HTTP 200;之后因 game_server_tcp/player_server_tcp 缺失明确报错,未创建生产 WebSocket,localStorage 未变化。说明读取和内存解析入口实际执行,不能称 release 正式启动已通过。 - `fp_a0676df82b7d83ae`:停止实际本地预览后 ready=false、stopped=true、pendingCount=0。 +- `fp_115a6f611fcebfc0`:地址校验修复后再次刷新编译并重开实际预览,source=remote、ready=true、page=login;从 Creator 编译模块调用解析器确认空 `#` 片段抛出 ConfigParseError。停止后 stopped=true、pendingCount=0。 ## 当前配置来源限制 @@ -34,4 +35,4 @@ debug、release 均读取远程 JSON,按原工程身份层级读取参数, - TDD 包含原函数差分对照、local 必须等待读取、读取失败/停止期间零连接、正式地址缺失、不泄露远程配置到诊断等。保留既有错误路径测试的预期 boom 日志。 - 原有 50 个未跟踪文件 SHA256 全部不变,两项历史 meta 删除仍保留,共 52 项原有改动未处理。没有新资源文件,没有 worktree,没有合并或推送。 - 独立审查发现 URL 片段会通过 URL 解析却被 WebSocket 构造器拒绝;`58c974e` 已在配置入口拒绝字面 `#`(包含空片段),保留合法 `%23`。该修复 RED 为 5/6,GREEN 为 6/6;仅修改解析器及对应测试,未重复无关全量测试。 -- 最终复审正在进行,结论补充于审查完成后。 +- 独立规格/质量审查在片段修复后通过;最终整体复审覆盖 `556c47a..d79bed0`,无新增可操作缺陷。release 来源限制准确保留,未误报正式启动成功。 From 239af0aa398acb75aee47cfe4196ee85f7cb0224 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 22:40:24 +0800 Subject: [PATCH 18/25] docs: specify unified startup configuration --- .../2026-09-05-unified-startup-config.md | 44 +++++++++++++++ ...026-09-05-unified-startup-config-design.md | 54 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-05-unified-startup-config.md create mode 100644 docs/superpowers/specs/2026-09-05-unified-startup-config-design.md diff --git a/docs/superpowers/plans/2026-09-05-unified-startup-config.md b/docs/superpowers/plans/2026-09-05-unified-startup-config.md new file mode 100644 index 0000000..494b360 --- /dev/null +++ b/docs/superpowers/plans/2026-09-05-unified-startup-config.md @@ -0,0 +1,44 @@ +# Unified Startup Config Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. + +**Goal:** One profiles.ts source configures runtime mode, remote URL and local/remote server selection; browser testing requires no query switches. + +**Architecture:** STARTUP_CONFIG is the sole production source. Runtime takes an optional whole StartupConfig injection for tests and publishes a validated source snapshot. Local diagnostic consumers validate that source and derived targets, preserving account namespace and lifecycle. + +**Tech Stack:** TypeScript, node:test/tsx, Creator 3.8.8 via funplay MCP. + +**Spec:** docs/superpowers/specs/2026-09-05-unified-startup-config-design.md + +## Global Constraints + +- Main checkout codex/local-platform-login only; never worktrees. +- No server changes, serialized asset changes, new assets scripts, RPC expansion or subgame selection. +- mode and useLocalServer are independent. All four combinations read remote JSON into memory; no persistent cache. +- Source values appear once in STARTUP_CONFIG: mode debug, gameserver https://tsgames.daoqi88.cn/config/update_jsonv2.txt, useLocalServer true, localServers ws://127.0.0.1:3088. +- URL mode/profile cannot override or block startup. Preserve original gameconfig injection and identity sources. +- No npm test, test:framework, scripts/run-framework-tests.mjs or resource generators. +- Protect original52 dirty paths, stage exact task files only. No merge/push. + +### Task 1: Integrate the unified code configuration through all existing consumers + +**Files:** existing assets/framework/config/{profiles.ts,runtime-mode.ts,runtime-config.ts,local-startup.ts,bootstrap.ts}; existing assets/scripts/{LoginFlow.ts,local-platform-login/LocalPlatformLogin.ts}; dependent existing local-platform-login/adapters files only if required. Paths prefixed cocoscreator_projects/YouleNexus. Test files and fixtures are under cocoscreator_projects/framework-tests. + +**Interfaces:** export StartupConfig and STARTUP_CONFIG from profiles.ts. ResolveRuntimeConfigOptions replaces mode with optional startupConfig: Readonly; production default is the exported constant, test configuration is explicit. RuntimeConfig retains mode/isDebugger/identity/servers/remoteConfig/rawConfig and exposes validated startupConfig for local consumers to validate against the same source. No independent profile selector. runtime-mode.ts may export RuntimeMode and a pure reader but contains no environment or URL defaults. + +- [ ] Write RED tests in framework-tests/config/{profiles,runtime-mode,runtime-config,local-startup,bootstrap}.test.ts and local-platform-login/host.test.ts. For example, inject `{...STARTUP_CONFIG,mode:'release',useLocalServer:true}`, search `?mode=debug&profile=prod`, fake fetch `{visitor_server_tcp:'visitor:1'}`; assert one fetch and local servers with mode release. Repeat mode debug/release and useLocalServer true/false, using valid remote game/player candidates when false. Assert the default no-query local host starts, and conflicting/duplicate mode/profile parameters do not select another configuration. +- [ ] Run focused RED from cocoscreator_projects: `node --import tsx --test --test-concurrency=1 framework-tests/config/profiles.test.ts framework-tests/config/runtime-mode.test.ts framework-tests/config/runtime-config.test.ts framework-tests/config/local-startup.test.ts framework-tests/config/bootstrap.test.ts framework-tests/local-platform-login/host.test.ts`. Repeat identical command require_escalated on known sandbox ENOMEM. +- [ ] Implement source and validation: explicit mode enum, boolean useLocalServer, valid http(s) gameserver, valid loopback localServers. Freeze the source/array. Reject malformed values; no consumer defaults. Remove URL/environment mode selection and PROFILES selector. Preserve resolveGameServer original injection decoding. +- [ ] Integrate runtime: `const startupConfig = ...options.startupConfig or STARTUP_CONFIG` through one validated source function; resolve identity/injected remote URL, await fetch, create memory view, choose `startupConfig.useLocalServer ? startupConfig.localServers : parseUrlServers(remoteConfig)`. Preserve errors. Derive mode/isDebugger from startupConfig. +- [ ] Integrate diagnostic and facade: remove scalar mode in component; local-startup checks code source debug/local and loopback and accepts empty search. Ignore mode/profile query while retaining identity/gameconfig diagnostic restrictions. Use the same source snapshot in forged-context checks. Keep internal profile local namespace for account storage. Bootstrap uses same runtime; old LoginFlow stops after config error rather than assigning fallback identity/server, and forwards real location.search. Do not rewire the old UI or login contract. +- [ ] Update affected test literals via existing fixtures as appropriate; replace obsolete mode/profile expectations with new contract without weakening lifecycle/source tests. Add behavior tests for malformed local addresses and gameserver, remote failure, local diagnostic refusal of release/remote settings, and native injection compatibility. +- [ ] GREEN focused suite; explicit full suite: `$testPaths = @(rg --files framework-tests -g '*.test.ts' | Sort-Object); node --import tsx --test --test-concurrency=1 @testPaths`; architecture: `node --test framework-tests/architecture/import-boundaries.test.mjs framework-tests/architecture/presentation-boundaries.test.mjs`; type: `node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit`; boundaries: `node scripts/check-import-boundaries.mjs`. +- [ ] Self-review and exact task-only commit. Report RED/GREEN commands/results and file list in task-1-report.md; no full remote data/logged secrets. +- [ ] Independent task spec/quality review, fix loop with targeted regressions, then scoped re-review. + +### Task 2: Real browser and documentation acceptance (controller) + +- [ ] MCP verify actual main checkout Creator project and current dirty state; refresh only modified existing TS resources. +- [ ] Open actual LocalPlatformLogin preview without query parameters, verify remote config source, local target, login ready and no failure; stop afterward. Configuration-only browser calls may verify the other source selections without creating production sockets. +- [ ] Write docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md with exact results, final current configuration location, publishing instructions and source limitation. Keep prior completed reports as history. +- [ ] Whole-change independent review; verify original dirty hashes/status unchanged; mark plan progress and commit exact documents. No merge/push. diff --git a/docs/superpowers/specs/2026-09-05-unified-startup-config-design.md b/docs/superpowers/specs/2026-09-05-unified-startup-config-design.md new file mode 100644 index 0000000..14c41c4 --- /dev/null +++ b/docs/superpowers/specs/2026-09-05-unified-startup-config-design.md @@ -0,0 +1,54 @@ +# 单文件启动配置 + +## 已批准方案 + +用户要求把远程配置地址、debug/release、是否使用本地服务器集中在同一个代码文件,便于发布修改;已确认布尔字段命名 useLocalServer,并回复“可以”授权实施。 + +唯一可编辑配置入口为已有 `cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts`: + +```ts +export const STARTUP_CONFIG = { + mode: 'debug', + gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt', + useLocalServer: true, + localServers: ['ws://127.0.0.1:3088'], +} as const; +``` + +上述四项只定义一处。配置有明确 StartupConfig 类型、只读约束和入口验证;发布时仅修改这里的 mode 和 useLocalServer。应用 mode 不冒充 Creator 的编译 DEBUG 标志;引擎构建选项仍由 Creator 管理。 + +## 读取与来源 + +- 生产入口不再传 mode 常量;runtime-mode.ts 不读取 URL、不探测全局 DEBUG、不默认 debug。它保留类型和必要的纯模式读取功能,最终模式来自 STARTUP_CONFIG。 +- runtime-config.ts 默认消费 STARTUP_CONFIG。保留显式整体配置注入作为纯测试接缝(options.startupConfig),不保留单独 mode/profile/servers 的第二套来源。 +- URL 的 mode/profile 完全不参与模式和目标选择,包括冲突值或重复项,不因此拒绝网页启动。 +- 原生/H5 的既有 gameconfig 注入解码、身份 query/native/BUILD 来源保持兼容,不把身份搬迁或复制。正常网页无参数即可运行。 +- 旧 PROFILES/ACTIVE_PROFILE/resolveActiveProfile 不再作为生产配置来源;删除不再使用的选择逻辑,避免维护两套配置。仅用于旧 facade 或账号存储命名的派生 profile 标签不构成配置开关。 + +## 模式与服务器选择 + +debug/release 与 useLocalServer 是两个独立字段,四种组合都读取远程 JSON,按已完成的原工程层级规则解析并仅保存在内存。 + +- useLocalServer=true:成功读取并验证远程根/层级后采用配置中的 localServers,不要求未使用的远程 TCP 字段存在。 +- useLocalServer=false:使用 game_server_tcp 或 player_server_tcp,缺失/非法显式失败,不使用 visitor TCP 或本地兜底。 +- mode=release 不再隐式强制远程地址,否则两个字段不独立。实际发版应明确设 release/false。 +- 本地地址必须有效、无认证信息/片段且为 loopback(127.0.0.0/8、localhost 或 ::1);缺少配置显式失败,消费方不得补值。 + +## 入口和诊断边界 + +LocalPlatformLogin 不再硬编码 mode,也不要求 profile=local。保留 cc/env.DEBUG 与 H5 保护:它是本地诊断场景,未授权将它变成完整生产启动场景。进入诊断前验证配置的 mode=debug、useLocalServer=true,并沿已有严格回环目标、停止/失败生命周期检查工作。无参数 Creator 网页预览应进入 login/ready。 + +本地账号缓存的 namespace 保持原 local 标签,避免无关账号重建;标签可保留为内部常量,不再从 URL 读取。 + +旧 LoginFlow 仅做调用接口兼容调整:移除 mode/profile 硬编码和配置失败后的地址/身份兜底,失败时停止;不重做 UI 接线、不称旧场景已接入新 Runtime。其其他旧逻辑留在后续入口迁移批次。 + +## 验收及范围 + +1. 默认无参数读取统一配置;冲突的 mode/profile URL 不改变代码配置。 +2. 四种 mode/useLocalServer 组合、无效配置、远程读取失败、缺失远程地址和原生 gameconfig 注入有行为测试;任何失败无连接兜底。 +3. 原 get_paravalue、内存唯一引用与无远程持久化要求保持通过。 +4. 显式枚举纯 TS 测试,指定两项架构测试、类型和边界检查;每项实现 TDD、独立规格/质量和最终审查。 +5. funplay MCP 核实主 checkout 项目,刷新既有 TS 后用 `http://127.0.0.1:7456/`(实际端口为准)无参数预览,真实读取远程配置并进入可登录页,最终目标本地。验证后停止,不扩展 RPC 验收。 +6. 使用当前分支,不建 worktree。保留原有52项改动,不改服务器或任何 .scene/.prefab/.anim/.meta,不新增 assets 文件。 + +当前真实远程 JSON 对 BUILD 身份仍缺少 game/player TCP;useLocalServer=false 的实际启动限制保留,不通过改身份、选子游戏或修改远程服务来掩盖。 From facceeae9be963e08a250b2f4c65f192d3abaa64 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 22:52:14 +0800 Subject: [PATCH 19/25] feat(config): unify startup mode and server selection --- .../assets/framework/config/bootstrap.ts | 11 ++- .../assets/framework/config/local-startup.ts | 61 +++++----------- .../assets/framework/config/profiles.ts | 69 +++++++++++-------- .../assets/framework/config/runtime-config.ts | 35 +++------- .../assets/framework/config/runtime-mode.ts | 33 ++------- .../YouleNexus/assets/scripts/LoginFlow.ts | 25 +------ .../LocalPlatformLogin.ts | 2 +- .../adapters/local/login-sources.test.ts | 6 +- .../adapters/local/visitor-account.test.ts | 6 +- .../framework-tests/config/bootstrap.test.ts | 13 +++- .../config/local-startup.test.ts | 44 ++++++------ .../framework-tests/config/profiles.test.ts | 27 +++----- .../config/remote-config-fetcher.test.ts | 3 +- .../config/runtime-config.test.ts | 53 ++++++++++---- .../config/runtime-mode.test.ts | 15 +--- .../integration/local-login-sources.test.ts | 2 +- .../integration/login-flow.test.ts | 3 +- .../platform-vertical-slice.test.ts | 3 +- .../local-platform-login/host.test.ts | 12 +++- .../local-platform-login/transport.test.ts | 2 +- .../local-platform-login/wire.test.ts | 2 +- .../framework-tests/platform/commands.test.ts | 3 +- .../platform/game-host-adapter.test.ts | 3 +- .../framework-tests/platform/runtime.test.ts | 3 +- .../framework-tests/platform/startup.test.ts | 5 +- .../sdk/game-contract-harness.test.ts | 3 +- 26 files changed, 204 insertions(+), 240 deletions(-) diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts index 6623c00..ae9314f 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts @@ -1,5 +1,4 @@ -import type { DebugAccount, DebugProfile } from './profiles.ts'; -import { PROFILES, resolveActiveProfile } from './profiles.ts'; +import type { DebugAccount } from './profiles.ts'; import { resolveRuntimeConfig, type HostKind, @@ -24,7 +23,7 @@ export type BootstrapResult = Omit; + readonly profile: Readonly<{ name: 'local' | 'prod' }>; }; function bootstrapHostKind(options: BootstrapOptions): HostKind { @@ -34,9 +33,8 @@ function bootstrapHostKind(options: BootstrapOptions): HostKind { /** 兼容启动编排外观;所有运行契约均由唯一的 RuntimeConfig 边界产出。 */ export async function resolveBootstrap(options: BootstrapOptions): Promise { - const profile = options.mode === 'release' ? PROFILES.prod! : resolveActiveProfile(options.search); const runtime = await resolveRuntimeConfig({ - mode: options.mode, + startupConfig: options.startupConfig, hostKind: bootstrapHostKind(options), win: options.win, search: options.search, @@ -46,7 +44,6 @@ export async function resolveBootstrap(options: BootstrapOptions): Promise 1) fail(field, 'duplicate query control'); } - const profiles = query.getAll('profile'); - if (profiles.length !== 1 || profiles[0] !== 'local') { - fail('profile', 'must occur exactly once with value local'); - } - const modes = query.getAll('mode'); - if (modes.length === 1 && modes[0] !== 'debug') fail('mode', 'must be debug'); const gameconfigs = query.getAll('gameconfig'); if (gameconfigs.length === 1 && gameconfigs[0] !== '') { fail('gameconfig', 'must be empty'); @@ -72,11 +41,13 @@ function assertLocalQuery(search: string): void { export async function resolveLocalStartup( options: ResolveRuntimeConfigOptions, ): Promise { - if (options.mode !== 'debug') fail('mode', 'must be debug'); + const startupConfig = validateStartupConfig(options.startupConfig === undefined ? STARTUP_CONFIG : options.startupConfig); + if (startupConfig.mode !== 'debug') fail('mode', 'must be debug'); + if (!startupConfig.useLocalServer) fail('useLocalServer', 'must be true'); if (options.hostKind !== 'h5') fail('hostKind', 'must be h5'); assertLocalQuery(options.search); - const config = await resolveRuntimeConfig(options); + const config = await resolveRuntimeConfig({...options, startupConfig}); const context = Object.freeze({ hostKind: 'h5' as const, profile: 'local' as const, config }); assertLocalStartupContext(context); return context; @@ -87,10 +58,13 @@ export function assertLocalStartupContext(context: LocalStartupContext): void { if (context.profile !== 'local') fail('profile', 'must be local'); const { config } = context; + const startupConfig = validateStartupConfig(config.startupConfig); + if (startupConfig.mode !== 'debug') fail('mode', 'source must be debug'); + if (!startupConfig.useLocalServer) fail('useLocalServer', 'source must be true'); if (config.mode !== 'debug') fail('mode', 'must be debug'); if (config.isDebugger !== true) fail('isDebugger', 'must be true'); if (config.source !== 'remote') fail('source', 'must be remote'); - if (config.gameserver !== PROFILES.local!.gameserver) fail('gameserver', 'must equal local profile remote entry'); + if (config.gameserver !== startupConfig.gameserver) fail('gameserver', 'must equal startupConfig remote entry'); if (!config.remoteConfig || typeof config.remoteConfig.getValue !== 'function') fail('remoteConfig', 'must provide memory view'); if (!config.rawConfig || config.rawConfig !== config.remoteConfig.raw) fail('rawConfig', 'must share remoteConfig.raw'); @@ -105,26 +79,23 @@ export function assertLocalStartupContext(context: LocalStartupContext): void { throw wrapped; } - const authoritative = PROFILES.local?.servers; - if (!authoritative || authoritative.length === 0) { - fail('servers', 'PROFILES.local must define servers'); - } + const authoritative = startupConfig.localServers; if (config.servers.length !== authoritative.length) { - fail('servers', 'must equal PROFILES.local.servers'); + fail('servers', 'must equal startupConfig.localServers'); } for (let index = 0; index < authoritative.length; index += 1) { const expected = authoritative[index]; - validateLocalUrl(expected, `servers[${index}]`); + validateLocalServer(expected, `servers[${index}]`); if (config.servers[index] !== expected) { - fail('servers', 'must equal PROFILES.local.servers'); + fail('servers', 'must equal startupConfig.localServers'); } } } export function assertLocalConnectionTarget(context: LocalStartupContext, url: string): void { assertLocalStartupContext(context); - validateLocalUrl(url, 'url'); - if (!PROFILES.local?.servers?.includes(url)) { - fail('url', 'target must occur in PROFILES.local.servers'); + validateLocalServer(url, 'url'); + if (!context.config.startupConfig.localServers.includes(url)) { + fail('url', 'target must occur in startupConfig.localServers'); } } diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts index 0be0355..b19c9a1 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts @@ -1,3 +1,5 @@ +import { resolveRuntimeMode, type RuntimeMode } from './runtime-mode.ts'; + /** 调试用登录玩家档案;值只能由测试或本地启动配置显式注入。 */ export interface DebugAccount { openid: string; @@ -9,38 +11,49 @@ export interface DebugAccount { unionid?: string | number; } -export type ProfileSource = 'remote'; - -export interface DebugProfile { - readonly name: string; - readonly source: ProfileSource; +export interface StartupConfig { + readonly mode: RuntimeMode; readonly gameserver: string; - readonly servers?: readonly string[]; - readonly account?: DebugAccount; + readonly useLocalServer: boolean; + readonly localServers: readonly string[]; } -/** 正式 gameserver:原 00_SubGame_Config.js:11。 */ -export const DEFAULT_GAMESERVER = 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt'; - -export const PROFILES: Readonly> = Object.freeze({ - prod: Object.freeze({ name: 'prod', source: 'remote', gameserver: DEFAULT_GAMESERVER }), - staging: Object.freeze({ name: 'staging', source: 'remote', gameserver: DEFAULT_GAMESERVER }), - local: Object.freeze({ - name: 'local', - source: 'remote', - gameserver: DEFAULT_GAMESERVER, - servers: Object.freeze(['ws://127.0.0.1:3088']), - }), +/** 唯一发布配置入口;mode 与服务器选择互相独立。 */ +export const STARTUP_CONFIG: Readonly = Object.freeze({ + mode: 'debug', + gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt', + useLocalServer: true, + localServers: Object.freeze(['ws://127.0.0.1:3088']), }); -export const ACTIVE_PROFILE = 'prod'; +function invalid(field: string, reason: string): never { + throw new Error('Invalid startup config ' + field + ': ' + reason); +} -export function resolveActiveProfile(search: string): DebugProfile { - const requested = new URLSearchParams(search).get('profile'); - const name = requested || ACTIVE_PROFILE; - const profile = PROFILES[name]; - if (!profile) throw new Error(`未知 profile: ${name}`); - return profile; +export function validateLocalServer(value: string, field = 'localServers'): void { + if (typeof value !== 'string' || value.trim() !== value) invalid(field, 'must be an absolute URL'); + let url: URL; + try { url = new URL(value); } catch { invalid(field, 'must be an absolute URL'); } + if (url.protocol !== 'ws:' && url.protocol !== 'wss:') invalid(field, 'protocol must be ws: or wss:'); + if (url.username || url.password || value.includes('#')) invalid(field, 'credentials and fragments are forbidden'); + const host = url.hostname.toLowerCase(); + if (host !== 'localhost' && host !== '[::1]' && !/^127(?:\.[0-9]{1,3}){3}$/.test(host)) { + invalid(field, 'hostname must be loopback'); + } +} + +/** 验证后复制为不可变快照,异步抓取期间调用方不能改变配置。 */ +export function validateStartupConfig(source: Readonly): Readonly { + if (!source || typeof source !== 'object') invalid('source', 'must be an object'); + const mode = resolveRuntimeMode(source.mode); + if (typeof source.useLocalServer !== 'boolean') invalid('useLocalServer', 'must be boolean'); + if (typeof source.gameserver !== 'string' || source.gameserver.trim() !== source.gameserver) invalid('gameserver', 'must be http(s) URL'); + let url: URL; + try { url = new URL(source.gameserver); } catch { invalid('gameserver', 'must be http(s) URL'); } + if (url.protocol !== 'http:' && url.protocol !== 'https:') invalid('gameserver', 'must be http(s) URL'); + if (!Array.isArray(source.localServers) || source.localServers.length === 0) invalid('localServers', 'must be a nonempty array'); + for (let index = 0; index < source.localServers.length; index++) validateLocalServer(source.localServers[index], 'localServers[' + index + ']'); + return Object.freeze({mode, gameserver:source.gameserver, useLocalServer:source.useLocalServer, localServers:Object.freeze([...source.localServers])}); } /** 忠实旧 min_replaceAll 链:- → /,# → :,再包成 http://...txt。 */ @@ -55,11 +68,11 @@ export interface ResolveGameServerOptions { readonly injectedGameConfig: unknown; } -/** 注入的 gameconfig 非空时是唯一覆盖源;否则只使用 profile 配置源。 */ +/** 注入的 gameconfig 非空时是唯一覆盖源;否则只使用 启动配置源。 */ export function resolveGameServer(options: ResolveGameServerOptions): string { const injected = options.injectedGameConfig; if (injected === '' || injected === undefined || injected === null) { - if (!options.configured) throw new Error('profile 未配置 gameserver'); + if (!options.configured) throw new Error('启动配置未配置 gameserver'); return options.configured; } if (typeof injected !== 'string') throw new Error('gameconfig 必须是字符串'); diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts index f2e5c8e..6a8c41b 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts @@ -1,10 +1,10 @@ import type { ChannelIdentity, IdentitySource } from './identity.ts'; import { resolveIdentity } from './identity.ts'; import { - PROFILES, - resolveActiveProfile, + STARTUP_CONFIG, + validateStartupConfig, resolveGameServer, - type DebugProfile, + type StartupConfig, } from './profiles.ts'; import { ConfigFetchError, @@ -27,6 +27,7 @@ import { queryStringSource } from './sources/query-string.ts'; export type HostKind = 'h5' | NativeHostKind; export interface RuntimeConfig { + readonly startupConfig: Readonly; readonly mode: RuntimeMode; readonly isDebugger: boolean; readonly identity: Readonly; @@ -38,7 +39,7 @@ export interface RuntimeConfig { } export interface ResolveRuntimeConfigOptions { - readonly mode: RuntimeMode; + readonly startupConfig?: Readonly; readonly hostKind: HostKind; readonly win: NativeHost; readonly search: string; @@ -67,10 +68,6 @@ function resolveInjectedGameConfig(options: ResolveRuntimeConfigOptions): unknow return getothername(options.win, options.hostKind, 'gameconfig'); } -function selectedProfile(mode: RuntimeMode, search: string): DebugProfile { - return mode === 'release' ? PROFILES.prod! : resolveActiveProfile(search); -} - function freezeResult(result: RuntimeConfig): RuntimeConfig { return Object.freeze(result); } @@ -78,14 +75,11 @@ function freezeResult(result: RuntimeConfig): RuntimeConfig { export async function resolveRuntimeConfig( options: ResolveRuntimeConfigOptions, ): Promise { - const profile = selectedProfile(options.mode, options.search); + const startupConfig = validateStartupConfig(options.startupConfig === undefined ? STARTUP_CONFIG : options.startupConfig); const identity = resolveHostIdentity(options); - if (!profile.gameserver) { - throw new ConfigFetchError(`profile '${profile.name}' 未配置 gameserver`); - } const gameserver = resolveGameServer({ - configured: profile.gameserver, + configured: startupConfig.gameserver, injectedGameConfig: resolveInjectedGameConfig(options), }); @@ -97,19 +91,12 @@ export async function resolveRuntimeConfig( throw new ConfigFetchError(`远程配置抓取失败: ${gameserver}`); } const remoteConfig = createRemoteConfig(raw, identity); - let servers: readonly string[]; - if (profile.name === 'local') { - if (!profile.servers || profile.servers.length === 0) { - throw new ConfigFetchError('Local profile must define servers'); - } - servers = Object.freeze([...profile.servers]); - } else { - servers = parseUrlServers(remoteConfig); - } + const servers = startupConfig.useLocalServer ? startupConfig.localServers : parseUrlServers(remoteConfig); return freezeResult({ - mode: options.mode, - isDebugger: options.mode === 'debug', + startupConfig, + mode: startupConfig.mode, + isDebugger: startupConfig.mode === 'debug', identity, servers, source: 'remote', diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts index 220eb37..4e03b9e 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts @@ -1,32 +1,7 @@ -// 1. 先 export type export type RuntimeMode = 'debug' | 'release'; -declare const DEBUG: boolean | undefined; - -// 2. 函数引用 RuntimeMode(此时 type 已 export) -function readModeFromUrl(): RuntimeMode | null { - if (typeof window === 'undefined' || !window.location) return null; - const m = new URLSearchParams(window.location.search).get('mode'); - if (m === 'debug' || m === 'release') return m; - return null; -} - -function detectDebugBuild(): boolean { - if (typeof DEBUG !== 'undefined') return DEBUG; - return true; // Cocos 编辑器内 dev workflow 默认;生产 build 改 false -} - -// 3. 导出常量 -export const MODE_OVERRIDE: RuntimeMode | null = - readModeFromUrl() ?? (detectDebugBuild() ? 'debug' : 'release'); - -/** - * 解析运行模式:override 优先,否则由是否 debug 构建决定。 - */ -export function resolveRuntimeMode( - isDebugBuild: boolean, - override: RuntimeMode | null = MODE_OVERRIDE, -): RuntimeMode { - if (override) return override; - return isDebugBuild ? 'debug' : 'release'; +/** 应用模式仅来自显式配置,不读取 URL 或 Creator 编译标志。 */ +export function resolveRuntimeMode(mode: RuntimeMode): RuntimeMode { + if (mode !== 'debug' && mode !== 'release') throw new Error('Invalid startup config mode: must be debug or release'); + return mode; } diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts b/cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts index 7b9cc6b..952ee93 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts @@ -9,20 +9,7 @@ import type { Transport } from '../framework/net/transport.ts'; const { ccclass } = _decorator; -/** - * LoginFlow: 登录场景编排器(真机版,接入 PlatformSession 真实流)。 - * - * 调试期间: - * - mode='debug' + ?profile=local → 跳过远程配置,直连本地服务器 - * - 真实 WebSocket 传输(用浏览器原生 WebSocket API) - * - * 流程: - * 1. 构造 EventBus + PlatformSession + NetClient(WebSocket transport) - * 2. bootstrap() 跳过远程,直接拿 local server='ws://127.0.0.1:3088' - * 3. NetClient.start() → 真 WS connect → server 回 player_login - * 4. PlatformSession 落 3 Store → app.phase=loggedIn - * 5. director.loadScene('MainMenu') - */ +/** 旧登录编排兼容入口;启动配置由 profiles.ts 统一提供。 */ class WSTransport implements Transport { private onOpenCb?: () => void; @@ -73,11 +60,9 @@ export class LoginFlow extends Component { (async () => { try { - // 调试模式 + ?profile=local → 跳过远程,直连本地 ws://127.0.0.1:3088 const result = await resolveBootstrap({ - mode: 'debug', win: window as any, - search: '?profile=local', + search: window.location.search, fetcher: new HttpConfigFetcher(), isNative: false, }); @@ -86,11 +71,7 @@ export class LoginFlow extends Component { console.log('[LoginFlow] bootstrap OK, servers=' + result.servers[0] + ' profile=' + result.profile.name); } catch (e) { console.warn('[LoginFlow] bootstrap failed: ' + (e as Error).message); - session.app.setIdentity({ - agentid: 'A', channelid: 'C', gameid: 'G', marketid: 4, version: 10000, - }); - session.app.setServers(['ws://127.0.0.1:3088']); - console.log('[LoginFlow] fallback to ws://127.0.0.1:3088'); + return; } // 构造 NetClient + 真实 WS transport diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts index 337f498..718f156 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts @@ -66,7 +66,7 @@ export class LocalPlatformLogin extends Component { const view = this.view; const browser = window as unknown as NativeHost & { returnCitySN?: unknown }; this.host = createLocalLoginHost({ - configOptions: { mode: 'debug', hostKind: 'h5', search: window.location.search, + configOptions: { hostKind: 'h5', search: window.location.search, win: browser, fetcher: new HttpConfigFetcher() }, storage: window.localStorage, random: Math.random, now: Date.now, sampleHost: () => ({ location: null, diff --git a/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts b/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts index effb8d7..f6ee075 100644 --- a/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts +++ b/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts @@ -2,14 +2,14 @@ import { remoteFixture } from '../../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts'; -import { PROFILES } from '../../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG } from '../../../YouleNexus/assets/framework/config/profiles.ts'; import type { LocalStartupContext } from '../../../YouleNexus/assets/framework/config/local-startup.ts'; import { LOCAL_MACHINE_KEY, localAccountKey, type LocalStoragePort } from '../../../YouleNexus/assets/framework/adapters/local/local-storage.ts'; import { createLocalLoginSources, type LocalHostSnapshot } from '../../../YouleNexus/assets/framework/adapters/local/login-sources.ts'; const context: LocalStartupContext = { hostKind: 'h5', profile: 'local', config: { - mode: 'debug', isDebugger: true, source: 'remote', gameserver: PROFILES.local!.gameserver, ...remoteFixture(), - servers: PROFILES.local!.servers!, identity: BUILD_IDENTITY, + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, source: 'remote', gameserver: STARTUP_CONFIG.gameserver, ...remoteFixture(), + servers: STARTUP_CONFIG.localServers, identity: BUILD_IDENTITY, } }; class MapStorage implements LocalStoragePort { readonly values = new Map(); diff --git a/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts b/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts index f8a1ccd..cda6f2f 100644 --- a/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts +++ b/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts @@ -2,7 +2,7 @@ import { remoteFixture } from '../../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts'; -import { PROFILES } from '../../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG } from '../../../YouleNexus/assets/framework/config/profiles.ts'; import type { LocalStartupContext } from '../../../YouleNexus/assets/framework/config/local-startup.ts'; import { LOCAL_MACHINE_KEY, localAccountKey, type LocalStoragePort } from '../../../YouleNexus/assets/framework/adapters/local/local-storage.ts'; import { createLocalVisitorProvider } from '../../../YouleNexus/assets/framework/adapters/local/visitor-account.ts'; @@ -16,8 +16,8 @@ class MapStorage implements LocalStoragePort { function context(overrides: Partial = {}): LocalStartupContext { return { hostKind: 'h5', profile: 'local', config: { - mode: 'debug', isDebugger: true, source: 'remote', gameserver: PROFILES.local!.gameserver, ...remoteFixture(), - servers: PROFILES.local!.servers!, identity: { ...BUILD_IDENTITY, ...overrides }, + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, source: 'remote', gameserver: STARTUP_CONFIG.gameserver, ...remoteFixture(), + servers: STARTUP_CONFIG.localServers, identity: { ...BUILD_IDENTITY, ...overrides }, } }; } diff --git a/cocoscreator_projects/framework-tests/config/bootstrap.test.ts b/cocoscreator_projects/framework-tests/config/bootstrap.test.ts index 358a2e5..378eeda 100644 --- a/cocoscreator_projects/framework-tests/config/bootstrap.test.ts +++ b/cocoscreator_projects/framework-tests/config/bootstrap.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { resolveBootstrap } from '../../YouleNexus/assets/framework/config/bootstrap.ts'; @@ -8,9 +9,19 @@ test('resolveBootstrap:显式 hostKind 透传严格 RuntimeConfig,并保留 async fetch() { return { player_server_tcp: '5.5.5.5:5000' }; }, }; const result = await resolveBootstrap({ - mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher, + hostKind: 'h5', win: {}, search: '', fetcher, }); assert.equal(result.source, 'remote'); assert.equal(result.profile.name, 'local'); assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']); }); + +test('facade forwards the entire injected source and derives its compatibility label', async () => { + for (const useLocalServer of [true,false]) { + const startupConfig={...STARTUP_CONFIG,mode:'release' as const,useLocalServer,gameserver:'https://fixture.example/config.txt'}; + const result=await resolveBootstrap({startupConfig,isNative:false,win:{},search:'?mode=debug&profile=unknown&profile=local',fetcher:{async fetch(url){assert.equal(url,startupConfig.gameserver);return {player_server_tcp:'remote.example:3088'};}}}); + assert.deepEqual(result.startupConfig,startupConfig); assert.equal(result.mode,'release'); + assert.equal(result.profile.name,useLocalServer?'local':'prod'); + assert.deepEqual(result.servers,useLocalServer?STARTUP_CONFIG.localServers:['ws://remote.example:3088']); + } +}); diff --git a/cocoscreator_projects/framework-tests/config/local-startup.test.ts b/cocoscreator_projects/framework-tests/config/local-startup.test.ts index 542089d..3270bf3 100644 --- a/cocoscreator_projects/framework-tests/config/local-startup.test.ts +++ b/cocoscreator_projects/framework-tests/config/local-startup.test.ts @@ -7,12 +7,12 @@ import { resolveLocalStartup, type LocalStartupContext, } from '../../YouleNexus/assets/framework/config/local-startup.ts'; -import { PROFILES } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import type { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; import type { ConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config.ts'; import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts'; -function options(search = '?profile=local'): ResolveRuntimeConfigOptions & { +function options(search = ''): ResolveRuntimeConfigOptions & { fetcher: ConfigFetcher & { calls: number }; } { const fetcher = { @@ -22,7 +22,7 @@ function options(search = '?profile=local'): ResolveRuntimeConfigOptions & { return { player_server_tcp: 'remote.example:3088' }; }, }; - return { mode: 'debug', hostKind: 'h5', win: {}, search, fetcher }; + return { hostKind: 'h5', win: {}, search, fetcher }; } test('local debug fetches remote config before choosing authoritative local servers', async () => { @@ -32,7 +32,7 @@ test('local debug fetches remote config before choosing authoritative local serv assert.equal(context.hostKind, 'h5'); assert.equal(context.profile, 'local'); assert.equal(context.config.identity.gameid, BUILD_IDENTITY.gameid); - assert.deepEqual(context.config.servers, PROFILES.local!.servers); + assert.deepEqual(context.config.servers, STARTUP_CONFIG.localServers); assert.equal(input.fetcher.calls, 1); assert.equal(context.config.source, 'remote'); assert.equal(Object.isFrozen(context), true); @@ -40,7 +40,8 @@ test('local debug fetches remote config before choosing authoritative local serv test('rejects non-local startup modes and hosts before fetching', async () => { const cases: Array<[Partial, RegExp]> = [ - [{ mode: 'release' }, /mode.*debug/i], + [{ startupConfig: {...STARTUP_CONFIG, mode:'release'} }, /mode.*debug/i], + [{ startupConfig: {...STARTUP_CONFIG, useLocalServer:false} }, /useLocalServer/i], [{ hostKind: 'native-settings' }, /hostKind.*h5/i], [{ hostKind: 'uAgent_3' }, /hostKind.*h5/i], ]; @@ -52,21 +53,11 @@ test('rejects non-local startup modes and hosts before fetching', async () => { } }); -test('requires exactly one local profile and only an optional single debug mode', async () => { - const cases: Array<[string, RegExp]> = [ - ['', /profile/i], - ['?profile=prod', /profile/i], - ['?profile=staging', /profile/i], - ['?profile=local&profile=local', /profile/i], - ['?profile=local&mode=release', /mode.*debug/i], - ['?profile=local&mode=debug&mode=debug', /mode/i], - ]; - for (const [search, expected] of cases) { - const input = options(search); - await assert.rejects(resolveLocalStartup(input), expected); - assert.equal(input.fetcher.calls, 0); +test('ignores conflicting and duplicate mode/profile queries', async () => { + for (const search of ['', '?profile=prod', '?profile=local&profile=unknown&mode=release&mode=other']) { + const input=options(search); const result=await resolveLocalStartup(input); + assert.deepEqual(result.config.servers,STARTUP_CONFIG.localServers); assert.equal(input.fetcher.calls,1); } - await resolveLocalStartup(options('?profile=local&mode=debug')); }); test('rejects gameconfig and ambiguous identity controls before fetching', async () => { @@ -105,12 +96,13 @@ function validContext(): LocalStartupContext { hostKind: 'h5', profile: 'local', config: { + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, identity: BUILD_IDENTITY, - servers: PROFILES.local!.servers!, + servers: STARTUP_CONFIG.localServers, source: 'remote', - gameserver: PROFILES.local!.gameserver, + gameserver: STARTUP_CONFIG.gameserver, ...remoteFixture(BUILD_IDENTITY), }, }; @@ -172,3 +164,13 @@ test('connection target must be a valid URL from the authoritative local profile assert.throws(() => assertLocalConnectionTarget(context, url), expected); } }); + +test('local consumers validate the injected source snapshot and its exact targets', async () => { + const startupConfig={...STARTUP_CONFIG,gameserver:'https://fixture.example/config.txt',localServers:['ws://[::1]:4300']}; + const context=await resolveLocalStartup({...options(),startupConfig}); + assertLocalConnectionTarget(context,'ws://[::1]:4300'); + assert.throws(()=>assertLocalConnectionTarget(context,STARTUP_CONFIG.localServers[0]),/target/); + for (const patch of [{mode:'release'}, {useLocalServer:false}, {localServers:['ws://elsewhere:3088']}, {gameserver:'https://other.example/config.txt'}]) { + assert.throws(()=>assertLocalStartupContext({...context,config:{...context.config,startupConfig:{...context.config.startupConfig,...patch}}} as any),/mode|useLocalServer|localServers|gameserver/); + } +}); diff --git a/cocoscreator_projects/framework-tests/config/profiles.test.ts b/cocoscreator_projects/framework-tests/config/profiles.test.ts index fbd5869..1afbea5 100644 --- a/cocoscreator_projects/framework-tests/config/profiles.test.ts +++ b/cocoscreator_projects/framework-tests/config/profiles.test.ts @@ -1,10 +1,9 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { - ACTIVE_PROFILE, - PROFILES, + STARTUP_CONFIG, + validateStartupConfig, decodeGameConfig, - resolveActiveProfile, resolveGameServer, } from '../../YouleNexus/assets/framework/config/profiles.ts'; @@ -23,19 +22,13 @@ test('resolveGameServer:空 gameconfig 使用配置源,非空值使用解码 ); }); -test('默认 profile 是 ACTIVE_PROFILE,local 保持 remote 源与独立连接目标', () => { - assert.equal(resolveActiveProfile('').name, ACTIVE_PROFILE); - const local = resolveActiveProfile('?profile=local'); - assert.equal(local.source, 'remote'); - assert.ok(local.gameserver); - assert.deepEqual(local.servers, ['ws://127.0.0.1:3088']); +test('one frozen source defines the complete default startup configuration', () => { + assert.deepEqual(STARTUP_CONFIG, { mode: 'debug', gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt', useLocalServer: true, localServers: ['ws://127.0.0.1:3088'] }); + assert.ok(Object.isFrozen(STARTUP_CONFIG)); assert.ok(Object.isFrozen(STARTUP_CONFIG.localServers)); }); - -test('未知 profile 显式报错,不回退到 ACTIVE_PROFILE', () => { - assert.throws(() => resolveActiveProfile('?profile=nope'), /未知 profile/); -}); - -test('正常 build 可达 profile 不含占位调试账号', () => { - assert.equal(JSON.stringify(PROFILES).includes('REPLACE_ME'), false); - assert.equal(PROFILES.staging.account, undefined); +test('source validation rejects malformed configuration without defaults', () => { + for (const patch of [{mode:'other'}, {useLocalServer:undefined}, {gameserver:'ws://host'}, {gameserver:''}, {localServers:[]}, {localServers:['ws://example.com:3088']}, {localServers:['ws://user:pass@localhost']}, {localServers:['ws://localhost/#fragment']}, {localServers:['ws://127.999.0.1']}, {localServers:['not-a-url']}]) { + assert.throws(() => validateStartupConfig({...STARTUP_CONFIG,...patch} as any), /startup config/i); + } + for (const url of ['ws://127.4.3.2:3088','wss://localhost:3088','ws://[::1]:3088']) assert.deepEqual(validateStartupConfig({...STARTUP_CONFIG,localServers:[url]}).localServers,[url]); }); diff --git a/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts b/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts index 1a892e6..01f2382 100644 --- a/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts +++ b/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { HttpConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config-fetcher.ts'; @@ -26,7 +27,7 @@ test('resolveRuntimeConfig:把 cacheBust 注入唯一的 HTTP 请求构造点' }); await resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG,mode:'release',useLocalServer:false}, hostKind: 'h5', win: {}, search: '?agentid=A&channelid=C', diff --git a/cocoscreator_projects/framework-tests/config/runtime-config.test.ts b/cocoscreator_projects/framework-tests/config/runtime-config.test.ts index 58e03fe..32393d3 100644 --- a/cocoscreator_projects/framework-tests/config/runtime-config.test.ts +++ b/cocoscreator_projects/framework-tests/config/runtime-config.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts'; @@ -18,7 +19,7 @@ test('h5:只读 query 身份,不访问原生宿主,并生成严格 remote const fetcher = returning(REMOTE); const throwingSettings = new Proxy({}, { get() { throw new Error('native access forbidden'); } }); const result = await resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'h5', win: { settings: throwingSettings }, search: '?agentid=HA&channelid=HC', @@ -41,7 +42,7 @@ test('native-settings:使用四个原生方法,gameconfig 是唯一 gameserv const calls: string[] = []; const fetcher = returning(REMOTE); const result = await resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'native-settings', win: { settings: { @@ -65,7 +66,7 @@ test('native-settings:使用四个原生方法,gameconfig 是唯一 gameserv test('uAgent_3:只读 app_,settings 不参与身份或 gameserver', async () => { const fetcher = returning(REMOTE); const result = await resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'uAgent_3', win: { settings: new Proxy({}, { get() { throw new Error('settings forbidden'); } }), @@ -84,7 +85,7 @@ test('uAgent_3:只读 app_,settings 不参与身份或 gameserver', as test('native-settings:不完整最终身份显式拒绝,不拿 build identity 兜底', async () => { await assert.rejects( resolveRuntimeConfig({ - mode: 'release', hostKind: 'native-settings', win: {}, search: '', fetcher: returning(REMOTE), + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'native-settings', win: {}, search: '', fetcher: returning(REMOTE), }), /identity.*agentid.*channelid/i, ); @@ -103,14 +104,14 @@ test('uAgent_3:所有允许 number 的身份字段都拒绝 NaN 和无穷值', }; await assert.rejects( resolveRuntimeConfig({ - mode: 'release', hostKind: 'uAgent_3', win, search: '', fetcher: returning(REMOTE), + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'uAgent_3', win, search: '', fetcher: returning(REMOTE), }), new RegExp(`identity.*${sample.field}.*finite`, 'i'), ); } await assert.rejects( resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'h5', win: {}, search: '?agentid=A&channelid=C&version=NaN', @@ -123,7 +124,7 @@ test('uAgent_3:所有允许 number 的身份字段都拒绝 NaN 和无穷值', test('uAgent_3:拒绝宿主提供的非法身份字段类型', async () => { await assert.rejects( resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'uAgent_3', win: { app_agent: true, app_channel: 'C', app_market: 4, app_gameconfig: '' }, search: '', @@ -136,7 +137,7 @@ test('uAgent_3:拒绝宿主提供的非法身份字段类型', async () => { test('显式 local debug:远程配置保留内存,连接服务器来自 profile', async () => { const fetcher = returning(REMOTE); const result = await resolveRuntimeConfig({ - mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher, + startupConfig: STARTUP_CONFIG, hostKind: 'h5', win: {}, search: '?profile=local', fetcher, }); assert.equal(result.source, 'remote'); assert.ok(result.gameserver); @@ -149,7 +150,7 @@ test('显式 local debug:远程配置保留内存,连接服务器来自 prof test('远程抓取失败显式报错,不降级 direct', async () => { const fetcher: ConfigFetcher = { async fetch() { throw new Error('net down'); } }; await assert.rejects( - resolveRuntimeConfig({ mode: 'release', hostKind: 'h5', win: {}, search: '', fetcher }), + resolveRuntimeConfig({ startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'h5', win: {}, search: '', fetcher }), /远程配置抓取失败/, ); }); @@ -157,7 +158,7 @@ test('远程抓取失败显式报错,不降级 direct', async () => { test('release ignores local profile but still fetches and selects remote servers', async () => { const fetcher = returning(REMOTE); - const result = await resolveRuntimeConfig({ mode: 'release', hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); + const result = await resolveRuntimeConfig({ startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); assert.equal(fetcher.calls.length, 1); assert.deepEqual(result.servers, ['ws://5.5.5.5:5000', 'ws://5.5.5.5:5000']); assert.equal(result.isDebugger, false); @@ -168,14 +169,14 @@ test('JSON parse errors retain their classification through the runtime boundary const { ConfigParseError } = await import('../../YouleNexus/assets/framework/config/remote-config.ts'); const error = new ConfigParseError('invalid JSON'); const fetcher = { async fetch() { throw error; } }; - await assert.rejects(resolveRuntimeConfig({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher }), + await assert.rejects(resolveRuntimeConfig({ startupConfig: STARTUP_CONFIG, hostKind: 'h5', win: {}, search: '?profile=local', fetcher }), actual => actual === error); }); test('local consumes explicit loopback targets after valid remote JSON even when remote candidates are absent', async () => { const raw = { visitor_server_tcp: 'unused.example:1234', notice: 'kept' }; const fetcher = returning(raw); - const result = await resolveRuntimeConfig({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); + const result = await resolveRuntimeConfig({ startupConfig: STARTUP_CONFIG, hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); assert.equal(fetcher.calls.length, 1); assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']); assert.strictEqual(result.rawConfig, raw); @@ -185,7 +186,33 @@ test('local consumes explicit loopback targets after valid remote JSON even when test('remote profiles reject missing game/player addresses without selecting visitor', async () => { for (const mode of ['debug', 'release'] as const) { const fetcher = returning({ visitor_server_tcp: 'unused.example:1234' }); - await assert.rejects(resolveRuntimeConfig({ mode, hostKind: 'h5', win: {}, search: '', fetcher }), /game_server_tcp\/player_server_tcp/); + await assert.rejects(resolveRuntimeConfig({ startupConfig: {...STARTUP_CONFIG, mode, useLocalServer: false}, hostKind: 'h5', win: {}, search: '', fetcher }), /game_server_tcp\/player_server_tcp/); assert.equal(fetcher.calls.length, 1); } }); + +for (const mode of ['debug','release'] as const) for (const useLocalServer of [true,false]) test('independent source '+mode+'/'+useLocalServer+' ignores URL selectors', async () => { + const fetcher = returning(useLocalServer ? {visitor_server_tcp:'visitor:1'} : REMOTE); + const source = {...STARTUP_CONFIG,mode,useLocalServer}; + const result = await resolveRuntimeConfig({startupConfig:source,hostKind:'h5',win:{},search:'?mode=debug&mode=release&profile=prod&profile=unknown',fetcher}); + assert.equal(fetcher.calls.length,1); assert.equal(result.mode,mode); assert.equal(result.isDebugger,mode==='debug'); + assert.deepEqual(result.servers,useLocalServer ? STARTUP_CONFIG.localServers : ['ws://5.5.5.5:5000','ws://5.5.5.5:5000']); + assert.deepEqual(result.startupConfig,source); assert.ok(Object.isFrozen(result.startupConfig)); + source.localServers=['ws://localhost:9999']; assert.deepEqual(result.startupConfig.localServers,STARTUP_CONFIG.localServers); +}); +test('default runtime starts without query configuration', async () => { + const result=await resolveRuntimeConfig({hostKind:'h5',win:{},search:'',fetcher:returning({notice:'kept'})}); + assert.deepEqual(result.startupConfig,STARTUP_CONFIG); assert.deepEqual(result.servers,STARTUP_CONFIG.localServers); +}); + +test('invalid source fails before remote fetch, including null and missing fields', async () => { + for (const patch of [null, {useLocalServer: undefined}, {gameserver: ''}, {localServers: ['ws://elsewhere:3088']}]) { + const fetcher=returning(REMOTE); + await assert.rejects(resolveRuntimeConfig({startupConfig: patch === null ? null as any : {...STARTUP_CONFIG,...patch} as any,hostKind:'h5',win:{},search:'',fetcher}),/startup config/i); + assert.equal(fetcher.calls.length,0); + } +}); +for (const mode of ['debug','release'] as const) for (const useLocalServer of [true,false]) test('remote failure never falls back '+mode+'/'+useLocalServer, async () => { + const fetcher={async fetch(){throw new Error('remote unavailable');}}; + await assert.rejects(resolveRuntimeConfig({startupConfig:{...STARTUP_CONFIG,mode,useLocalServer},hostKind:'h5',win:{},search:'',fetcher}),/远程配置抓取失败/); +}); diff --git a/cocoscreator_projects/framework-tests/config/runtime-mode.test.ts b/cocoscreator_projects/framework-tests/config/runtime-mode.test.ts index b3138cf..fd90df1 100644 --- a/cocoscreator_projects/framework-tests/config/runtime-mode.test.ts +++ b/cocoscreator_projects/framework-tests/config/runtime-mode.test.ts @@ -1,16 +1,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { resolveRuntimeMode } from '../../YouleNexus/assets/framework/config/runtime-mode.ts'; - -test('override 优先于构建类型', () => { - assert.equal(resolveRuntimeMode(true, 'release'), 'release'); - assert.equal(resolveRuntimeMode(false, 'debug'), 'debug'); -}); - -test('无 override:debug 构建 → debug', () => { - assert.equal(resolveRuntimeMode(true, null), 'debug'); -}); - -test('无 override:release 构建 → release', () => { - assert.equal(resolveRuntimeMode(false, null), 'release'); +test('runtime mode is an explicit pure reader without environment defaults', () => { + assert.equal(resolveRuntimeMode('debug'),'debug'); assert.equal(resolveRuntimeMode('release'),'release'); + for (const value of [undefined,null,true,'other']) assert.throws(() => resolveRuntimeMode(value as any), /mode/i); }); diff --git a/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts b/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts index 40203b3..12d3543 100644 --- a/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts +++ b/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts @@ -13,7 +13,7 @@ class MapStorage implements LocalStoragePort { } function resolve(search = '?profile=local') { - return resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search, + return resolveLocalStartup({ hostKind: 'h5', win: {}, search, fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } }); } diff --git a/cocoscreator_projects/framework-tests/integration/login-flow.test.ts b/cocoscreator_projects/framework-tests/integration/login-flow.test.ts index 43f0b2b..174014a 100644 --- a/cocoscreator_projects/framework-tests/integration/login-flow.test.ts +++ b/cocoscreator_projects/framework-tests/integration/login-flow.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -12,7 +13,7 @@ import type { GameHost } from '../../YouleNexus/assets/framework/sdk/index.ts'; import { FakeTransport } from '../helpers/fake-transport.ts'; const CONFIG: RuntimeConfig = { - mode: 'debug', isDebugger: true, + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, identity: { agentid: 'A', channelid: 'C', gameid: '41', version: 1, marketid: 4 }, source: 'remote', servers: ['ws://agent'], gameserver: 'https://fixture/config', ...remoteFixture(), }; diff --git a/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts b/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts index 38d283f..db91afe 100644 --- a/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts +++ b/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; @@ -121,7 +122,7 @@ test('real modern vertical replay preserves wire contracts, room ordering and te const runtime = new PlatformRuntime({ gameEntry: entry, resolveRuntimeConfig: () => resolveRuntimeConfig({ - mode: 'release', hostKind: 'native-settings', search: '', + startupConfig: {...STARTUP_CONFIG,mode:'release',useLocalServer:false}, hostKind: 'native-settings', search: '', win: { settings: { getothername(name: string) { nativeCalls.push(`getothername:${name}`); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts index 754a066..5e43abf 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; @@ -39,7 +40,7 @@ async function setup(overrides: { config?: Partial; const values = new Map(), frames = new Set<() => void>(); let fetches = 0; const host = api.createLocalLoginHost({ - configOptions: { mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local&version=731', + configOptions: { hostKind: 'h5', win: {}, search: '?profile=local&version=731', fetcher: { async fetch() { fetches++; return { player_server_tcp: 'remote.example:3088' }; } }, ...overrides.config }, storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } }, random: () => .5, now: () => 123456, sampleHost: () => ({ location: null }), @@ -138,7 +139,7 @@ for (const phase of ['config', 'resources', 'prepare'] as const) test(`stop duri assert.deepEqual(h.failures, []); }); -for (const config of [{ search: '?profile=prod' }, { hostKind: 'native-settings' as const }]) test('invalid local input cannot create an account or connection', async () => { +for (const config of [{ startupConfig: {...STARTUP_CONFIG,mode:'release' as const} }, { startupConfig: {...STARTUP_CONFIG,useLocalServer:false} }, { hostKind: 'native-settings' as const }]) test('invalid local input cannot create an account or connection', async () => { const h = await setup({ config }); await assert.rejects(h.host.start(), /local startup/); assert.equal(h.values.size, 0); assert.equal(h.sockets.length, 0); assert.equal(h.failures.length, 1); @@ -242,3 +243,10 @@ test('startup diagnostics exclude raw remote fields', async t => { t.after(() => h.host.stop()); await h.ready(); assert.equal(JSON.stringify(h.evidence).includes('DO_NOT_RECORD_REMOTE_SECRET'), false); }); + +test('default no-query host reaches login ready and ignores conflicting selectors', async t => { + for (const search of ['', '?mode=release&mode=other&profile=prod&profile=unknown']) { + const h=await setup({config:{search}}); t.after(()=>h.host.stop()); await h.ready(); + assert.equal(h.fetches(),1); assert.equal(h.host.getSnapshot().ready,true); assert.ok(h.activations.includes('login')); assert.equal(h.failures.length,0); + } +}); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts index 0630ce5..752fbab 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts @@ -36,7 +36,7 @@ async function setup(overrides: { const module = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts') .catch((error: unknown) => assert.fail(`LocalLoginTransport implementation must exist: ${String(error)}`)); const context = await resolveLocalStartup({ - mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', + hostKind: 'h5', win: {}, search: '?profile=local', fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } }, }); const sockets: FakeSocket[] = []; diff --git a/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts index bd47479..339488a 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts @@ -14,7 +14,7 @@ function fixture(name: string) { async function setup(recordHook?: (event: { kind: string; data: unknown }) => void) { const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts') .catch(error => assert.fail(`Local login Wire implementation must exist: ${String(error)}`)); - const context = await resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', + const context = await resolveLocalStartup({ hostKind: 'h5', win: {}, search: '?profile=local', fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } }); const transports: FakeTransport[] = []; const events: WireEvent[] = []; diff --git a/cocoscreator_projects/framework-tests/platform/commands.test.ts b/cocoscreator_projects/framework-tests/platform/commands.test.ts index f9a07c8..1748b2f 100644 --- a/cocoscreator_projects/framework-tests/platform/commands.test.ts +++ b/cocoscreator_projects/framework-tests/platform/commands.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -11,7 +12,7 @@ import { } from '../../YouleNexus/assets/framework/protocol/contracts/index.ts'; const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release', isDebugger: false, identity: Object.freeze({ agentid: 'agent-from-config', diff --git a/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts b/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts index 961ebfe..e3e83c8 100644 --- a/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts +++ b/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -30,7 +31,7 @@ import { } from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release', isDebugger: false, identity: Object.freeze({ agentid: 17, diff --git a/cocoscreator_projects/framework-tests/platform/runtime.test.ts b/cocoscreator_projects/framework-tests/platform/runtime.test.ts index b29b45b..a826318 100644 --- a/cocoscreator_projects/framework-tests/platform/runtime.test.ts +++ b/cocoscreator_projects/framework-tests/platform/runtime.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import { readFileSync } from 'node:fs'; import assert from 'node:assert/strict'; @@ -48,7 +49,7 @@ const DEVICE = Object.freeze({ function runtimeConfig(gameid: string | number = '41'): RuntimeConfig { return Object.freeze({ - mode: 'debug', + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, identity: Object.freeze({ agentid: 'veRa0qrBf', diff --git a/cocoscreator_projects/framework-tests/platform/startup.test.ts b/cocoscreator_projects/framework-tests/platform/startup.test.ts index 93525e1..8630652 100644 --- a/cocoscreator_projects/framework-tests/platform/startup.test.ts +++ b/cocoscreator_projects/framework-tests/platform/startup.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; @@ -11,11 +12,11 @@ const IDENTITY = { agentid: 'A', channelid: 'C', gameid: 'G', marketid: 4, versi function bootstrapOf(): BootstrapResult { return { - mode: 'debug', isDebugger: true, + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, identity: IDENTITY, servers: ['ws://test'], ...remoteFixture(), - profile: { name: 'debug' }, + profile: { name: 'local' }, }; } diff --git a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts index 61d9076..11f20be 100644 --- a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts +++ b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -75,7 +76,7 @@ test('assertGameContract accepts a fixed-four game with its real nested roomtype }); const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release', isDebugger: false, identity: Object.freeze({ agentid: 'fixture-agent', channelid: 'fixture-channel', gameid: '41', From af059b40e9abc9b13034f8bbcfed3b20967878f1 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 23:23:04 +0800 Subject: [PATCH 20/25] docs: document unified startup settings and browser acceptance --- docs/guides/配置与调试指南.md | 564 +++--------------- .../2026-09-05-unified-startup-config.md | 2 +- ...09-05-unified-startup-config-acceptance.md | 43 ++ 3 files changed, 125 insertions(+), 484 deletions(-) create mode 100644 docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md diff --git a/docs/guides/配置与调试指南.md b/docs/guides/配置与调试指南.md index cb10d90..d86da2c 100644 --- a/docs/guides/配置与调试指南.md +++ b/docs/guides/配置与调试指南.md @@ -1,517 +1,115 @@ # YouleNexus 配置与调试指南 -本指南面向在新 Cocos 前端 `YouleNexus` 上做开发联调的工程师,讲清楚两件事: +本指南对应单文件启动配置设计:`docs/superpowers/specs/2026-09-05-unified-startup-config-design.md`。历史计划和验收报告记录当时行为,不作为当前配置步骤。 -1. **配置**——渠道身份(agentid/channelid/gameid/marketid/version)从哪来、远程配置文件怎么读、服务器地址怎么定。 -2. **调试**——如何一键切到本地/测试服、临时替换渠道身份、跑自动化测试、做真机端到端联调。 +## 修改启动配置 -对应实现位于 `cocoscreator_projects/YouleNexus/assets/framework/config/`,设计依据见 `docs/superpowers/specs/2026-06-28-config-channel-design.md`。 - -> **一句话心智模型**:启动时调用 `resolveBootstrap(...)`,它把「构建期默认 + 运行期来源 + 调试 profile + URL 覆盖」合并成最终 `identity`,再决定 `servers`(调试服直连 或 远程配置解析),产出 `{ identity, servers }` 喂给 `NetClient`。 - ---- - -## 目录 - -1. [快速上手(三个最常见场景)](#1-快速上手三个最常见场景) -2. [整体数据流](#2-整体数据流) -3. [渠道身份 ChannelIdentity](#3-渠道身份-channelidentity) -4. [一键切换 debug / release 模式](#4-一键切换-debug--release-模式) -5. [调试 Profiles](#5-调试-profiles) -6. [URL Query 参数清单](#6-url-query-参数清单) -7. [服务器地址是怎么定的](#7-服务器地址是怎么定的) -8. [远程配置文件](#8-远程配置文件) -9. [启动编排 resolveBootstrap 与接入 NetClient](#9-启动编排-resolvebootstrap-与接入-netclient) -10. [原生联调(window.settings / app_*)](#10-原生联调windowsettings--app_) -11. [跑测试与类型检查](#11-跑测试与类型检查) -12. [常见调试场景配方](#12-常见调试场景配方) -13. [真实服务器端到端联调](#13-真实服务器端到端联调) -14. [发布前检查清单](#14-发布前检查清单) -15. [红线:哪些字符串绝不能改](#15-红线哪些字符串绝不能改) -16. [API 速查表](#16-api-速查表) - ---- - -## 1. 快速上手(三个最常见场景) - -> 下面假设你在 Cocos 编辑器预览(浏览器/WebView 环境)下调试,地址栏可加 query 参数。 - -| 我想…… | 怎么做 | -| --- | --- | -| **连本地服务器**(`ws://127.0.0.1:3088`) | 预览 URL 加 `?profile=local`。直接连本地,不抓远程配置。 | -| **用测试配置服**(staging 的 gameserver) | 预览 URL 加 `?profile=staging`。 | -| **临时换一个渠道身份调一个 bug** | URL 加 `?agentid=xxx&channelid=yyy&marketid=2`,覆盖最高优先级,其余不变。 | - -更多组合见 [§12 常见调试场景配方](#12-常见调试场景配方)。 - ---- - -## 2. 整体数据流 - -``` - ┌─────────────────────────────────────────────┐ -启动调用 │ resolveBootstrap(opts) │ -resolveBootstrap ─────► │ │ - │ ① resolveActiveProfile(search) │ - │ └─ ACTIVE_PROFILE 或 ?profile= 切换 │ - │ │ - │ ② 合并身份 resolveIdentity([...]) │ - │ defaults < 运行时(native|query) │ - │ < profile.identity │ - │ < 显式 URL query (最高) │ - │ │ - │ ③ 决定服务器: │ - │ profile.server ? → 直连(跳过远程) │ - │ 否则 fetcher.fetch(gameserver) │ - │ → getParam 分层取参 │ - │ → resolveServers → ws:// 候选 │ - └───────────────┬─────────────────────────────┘ - │ { identity, servers, rawConfig, profile } - ▼ - net.setIdentity(identity 相关字段) - new NetClient({ servers }) → net.start() -``` - -**文件分工**(每个文件单一职责,纯逻辑与 IO 分离,均可在 Node 下测): - -| 文件 | 职责 | -| --- | --- | -| `config/identity.ts` | `ChannelIdentity` 类型、`IdentitySource` 接口、`resolveIdentity()` 纯合并 | -| `config/sources/defaults.ts` | `BUILD_IDENTITY` 构建期默认(旧 `version.js` 等价物) | -| `config/sources/query-string.ts` | `queryStringSource()`:H5 从 URL query 读身份 | -| `config/sources/native-settings.ts` | `nativeSettingsSource()`:原生从 `window.settings`/`app_*` 同步读身份 | -| `config/profiles.ts` | `DebugProfile` / `PROFILES` / `resolveActiveProfile()` 调试档位 | -| `config/remote-config.ts` | 远程配置类型、`ConfigFetcher` 接口、`getParam()` 分层取参、`resolveServers()` | -| `config/remote-config-fetcher.ts` | 生产用 `HttpConfigFetcher`(全局 `fetch` 抓取 txt) | -| `config/runtime-mode.ts` | `resolveRuntimeMode()` 运行模式(debug/release)解析,`MODE_OVERRIDE` 手动覆盖 | -| `config/bootstrap.ts` | `resolveBootstrap()` 启动编排,串起以上全部 | - ---- - -## 3. 渠道身份 ChannelIdentity - -最终要发给服务器 `player_login` 的渠道身份字段。**属性名就是协议字段名,逐字不可改**(见 [§15](#15-红线哪些字符串绝不能改))。 +只在 `cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts` 的 `STARTUP_CONFIG` 修改应用启动选项: ```ts -interface ChannelIdentity { - agentid: string | number; // 代理商 ID - channelid: string | number; // 渠道 ID - gameid: string; // 游戏标识(每个子游戏固定) - marketid: string | number; // 市场/包渠道 ID - version: string; // 版本号(旧 GameData.Version) - versionCode: number; // 版本码(旧 GameData.versionCode) -} +export const STARTUP_CONFIG = { + mode: 'debug', + gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt', + useLocalServer: true, + localServers: ['ws://127.0.0.1:3088'], +} as const; ``` -### 各字段从哪来 +- `mode`:应用运行模式,值为 `debug` 或 `release`。 +- `gameserver`:远程配置 JSON 的 HTTP(S) 地址;不是 WebSocket 服务器地址。 +- `useLocalServer`:true 使用本地地址,false 使用远程 JSON 按身份解析出的服务器地址。 +- `localServers`:本地服务器候选列表,必须是有效回环 WebSocket 地址。 -| 字段 | 主来源 | 说明 | -| --- | --- | --- | -| `gameid` | **编译期固定**(`BUILD_IDENTITY`,每子游戏不同) | 旧工程写死在 `version.js`。一般不随环境变。 | -| `agentid` | 运行期:H5 走 URL query,原生走 `window.settings.getothername('agent')` | 默认值在 `BUILD_IDENTITY`,运行期覆盖。 | -| `channelid` | 运行期:H5 走 URL query,原生走 `window.settings.getchannelName()` | 同上。 | -| `marketid` | 运行期:H5 走 URL query,原生走 `window.settings.getmarketname()`(默认 `4`) | 同上。 | -| `version` / `versionCode` | `BUILD_IDENTITY` 默认;H5 可用 `?version=` 覆盖 | — | +运行模式和服务器选择独立;release 不会偷偷改写 useLocalServer。发布时在这里明确设置 `mode: 'release'`、`useLocalServer: false`,并核对 gameserver。Creator 构建面板的 Debug 选项控制引擎编译,不能代替这里的应用配置。 -### 合并优先级(低 → 高,后者覆盖前者) +URL 的 mode/profile 参数不再参与启动选择,也不再要求它们存在。runtime-mode.ts 不自动读取 URL 或全局 DEBUG。生产调用方不再另传 mode、硬编码服务器或复制上述配置;测试可以整体注入 StartupConfig,以覆盖不同组合。 -``` -BUILD_IDENTITY(构建期默认) - < 运行时来源(原生环境=nativeSettings | H5 环境=queryString) - < profile.identity(当前调试档位的身份覆盖) - < 显式 URL query(最高,方便临时改单字段) -``` +## 网页本地测试 -`resolveIdentity(sources)` 按数组顺序合并,**只覆盖"有效值"字段**——`undefined`/`null`/`空字符串` 不覆盖(所以一个来源读不到某字段时,不会把已有值清空);注意 **`0` 是有效值**会覆盖(如 `marketid=0`)。 +1. 确认 STARTUP_CONFIG 为 debug、useLocalServer=true,且本地服务运行在配置的地址。 +2. Creator 打开主 checkout 的 YouleNexus 工程和 `assets/scenes/LocalPlatformLogin.scene`。 +3. 点击网页预览,使用编辑器生成的原始地址,例如 `http://127.0.0.1:7456/`。不需要追加查询参数,端口以实际预览为准。 +4. 观察远程配置读取、资源加载、本地连接完成后进入可登录页面。需要验收登录时再点击游客登录。 -> H5 模式下 `queryString` 同时担任「运行时来源」和「最高覆盖」两个角色,因此在 URL 里写 `?agentid=...` 永远生效且优先级最高。 +LocalPlatformLogin 是 DEBUG 浏览器下的本地诊断场景,要求应用配置 debug/useLocalServer=true,保留回环目标和生命周期保护。它不是正式生产入口;旧 Login.scene 也尚未接入新 Runtime,不用它替代验收。编辑器资源操作遵循仓库 AGENTS.md,通过 funplay MCP;未经授权不改序列化资源。 ---- +## 配置读取流程 -## 4. 一键切换 debug / release 模式 +统一配置 → 宿主身份 → 远程地址注入解析 → HTTP 读取 → JSON 根与身份层级解析 → 选择最终服务器 → Runtime 启动。 -运行模式是比 profile 更高一层的总开关,由 `config/runtime-mode.ts` 提供。 +无论 debug/release,也无论是否使用本地服务器,都必须先读取远程配置。`HttpConfigFetcher` 忠实使用 POST 空 body,URL 无条件拼接 `?` 和时间戳。HTTP/JSON/层级错误明确抛出,不使用旧缓存或连接兜底。 -| 模式 | 怎么进入 | profile | URL 覆盖(?profile=/?agentid=…) | 身份来源 | 收发包日志 | -| --- | --- | --- | --- | --- | --- | -| **debug** | debug/预览构建自动进入;或 `MODE_OVERRIDE='debug'` | `?profile=` 可切(默认 prod) | **全部生效** | 原生或 URL(按 `isNative`) | 开(`isDebugger=true`) | -| **release** | release 构建自动进入;或 `MODE_OVERRIDE='release'` | **强制 prod** | **全部忽略** | **只走原生 `window.settings`** | 关 | +远程响应本身是根 JSON;原工程 `getConfig_Succ` 将它放进客户端 `GameData.serverConfig.data`,data 不是远程信封。当前 `RuntimeConfig.rawConfig` 与 `RuntimeConfig.remoteConfig.raw` 引用同一根对象;`remoteConfig.getValue(name)` 提供统一取值。只存内存,无远程文件落盘或 localStorage 快照。 -### 三种切换姿势 +层级规则沿用原 get_paravalue:全局 → agent → game → channel → market;每层第一个身份宽松相等的条目生效;只有 truthy 值覆盖,0/false/空串不覆盖;缺少匹配层即继承已取得的值;对象和数组整体替换,不自动深合并。 -1. **什么都不动**:发 release 包自动 release,编辑器预览自动 debug。模式由 Cocos 全局 `DEBUG` 决定。 -2. **release 包里临时调试**:把 `config/runtime-mode.ts` 的 `MODE_OVERRIDE` 设为 `'debug'`。 -3. **预览里验证正式行为**:把 `MODE_OVERRIDE` 设为 `'release'`。 +useLocalServer=false 时,选择 truthy `game_server_tcp`,否则 `player_server_tcp`,支持字符串和数组,保留顺序及重复项。不使用 `visitor_server_tcp` 兜底。最终候选在配置边界验证,禁止认证信息、非法协议和 URL 片段。 -> release 之所以能放心锁死 URL:正式包是 Cocos 原生(jsb),渠道身份走 `window.settings`/`app_*`,本就不依赖 URL。 +useLocalServer=true 时,在远程根和层级解析完成后使用 localServers;未消费的远程 game/player TCP 可以缺失。该逻辑是显式来源选择,不是读取错误后的退路。 -### 接入 +公告、大厅配置、登录图片等字段由相同 getValue 读取。界面显示转换和原工程 UI 副作用在各自接线批次实现。 + +## 身份、账号与设备 + +身份唯一构建来源仍是 `config/sources/defaults.ts` 的 BUILD_IDENTITY,不在 profiles.ts 再复制一份。协议字段为 agentid、channelid、gameid、marketid、version;version 是 number,不是显示版本字符串。 + +- H5 使用 BUILD_IDENTITY,再应用 query-string.ts 支持的 agentid/channelid/marketid/version 覆盖;gameid 不从 URL 覆盖。 +- native-settings 使用构建 gameid/version 与原生提供的 agent/channel/market;不完整身份明确失败,不用 H5 身份填补。 +- uAgent_3 使用 app_agent/app_channel/app_market 等原生全局,按显式 hostKind 选择,不从浏览器 URL 猜测宿主。 + +原生接口名和源适配器中已定义的缺省语义保持原工程契约,不在下游重复实现。 + +本地游客账号由 adapters/local/visitor-account.ts 读取其已有作用域缓存或生成。openid 是 testopenid_ 加随机数,unionid 是 ylgame 加同一随机数,同时产生昵称、头像、性别和地区。设备字段由 login-sources.ts 等来源提供;UI 和协议消费方不补默认值。账号/机器标识的已有存储与“远程配置只存内存”是不同职责。 + +不运行历史 scripts/login-live.ts,不使用其占位账号或 staging 路径作为本次验证入口。 + +## 原生远程地址注入 + +保留原工程 gameconfig 契约:非空注入字符串按 `-`→`/`、`#`→`:` 解码,再形成 `http://…txt`。native-settings 读取 getothername('gameconfig'),uAgent_3 读取 app_gameconfig,普通 H5 的对应参数在统一配置解析边界处理。未注入时只使用 STARTUP_CONFIG.gameserver。 + +这是宿主契约的集中解析,不是另一份生产配置常量。本地诊断入口仍拒绝非空 gameconfig 和 gameid 覆盖,避免误用该诊断环境;普通运行配置解析仍保留注入兼容。 + +## 代码入口 + +- profiles.ts:唯一可编辑 STARTUP_CONFIG 及配置类型/验证,保留远程地址注入解码。 +- runtime-mode.ts:运行模式类型及纯读取逻辑,不拥有另一套默认配置。 +- runtime-config.ts:最终身份、服务器和内存远程配置的统一解析入口。 +- remote-config.ts:根/层级验证、getValue 与远程服务器候选解析。 +- remote-config-fetcher.ts:真实 HTTP 读取。 +- local-startup.ts:本地诊断边界验证,使用解析结果携带的配置来源。 +- bootstrap.ts:旧调用方的兼容外观,委托同一个 runtime-config;新流程直接使用 Runtime。 + +普通网页配置解析示例(由启动宿主继续接入 Runtime): ```ts -import { resolveRuntimeMode } from 'db://assets/framework/config/runtime-mode'; - -const isDebugBuild = typeof DEBUG !== 'undefined' ? DEBUG : true; // Cocos 全局 -const mode = resolveRuntimeMode(isDebugBuild); // 'debug' | 'release' -// 传给 resolveBootstrap({ mode, ... }),并把 result.isDebugger 传给 NetClient 的 debug 选项 +const config = await resolveRuntimeConfig({ + hostKind: 'h5', + win: window as unknown as NativeHost, + search: window.location.search, + fetcher: new HttpConfigFetcher(), +}); ``` ---- +不要在这个调用处再次写 mode 或服务器地址,也不要捕获配置错误后创建兜底连接。 -## 5. 调试 Profiles +## 自动化验证 -Profiles 取代了旧工程的 `Game_Config.Debugger`(`serverType` / `gameserver` / `isDebugger`)。一个 profile 打包了「连哪个服 + 用什么渠道身份 + 日志开关」。 +PowerShell 工作目录为 `cocoscreator_projects`: -```ts -interface DebugProfile { - name: string; - server?: string; // 显式 ws 调试服地址;给定则跳过远程抓取(纯直连) - gameserver?: string; // 远程配置 txt URL(不含防缓存串) - serverOverride?: string; // debug:抓远程配置走全链路后,把连接地址覆盖为此(联调本地游戏服) - identity?: Partial; // 调试用渠道身份覆盖 - account?: DebugAccount; // 调试用登录账号(openid 等),仅 debug 模式生效 - isDebugger?: boolean; // 收发包日志开关 -} - -// DebugAccount:player_login 中除渠道身份外的字段(openid 必填,其余可选) -// { openid, nickname?, avatar?, sex?, province?, city?, unionid? } -// 联调账号写在这里(profile 是唯一权威来源),不要散在调试脚本里。 +```powershell +$testPaths = @(rg --files framework-tests -g '*.test.ts' | Sort-Object) +node --import tsx --test --test-concurrency=1 @testPaths +node --test framework-tests/architecture/import-boundaries.test.mjs framework-tests/architecture/presentation-boundaries.test.mjs +node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit +node scripts/check-import-boundaries.mjs ``` -### 内置档位(`config/profiles.ts`) +禁止运行 npm test、test:framework 或 scripts/run-framework-tests.mjs;历史聚合入口含资源生成器。测试在 assets 外,不能用无引擎测试通过替代真实 Creator 界面验收。遇到已知 tsx 沙箱 uv_os_get_passwd ENOMEM,在获准环境执行同一精确命令,不改业务代码绕过。 -| profile | server(直连) | gameserver(远程配置) | isDebugger | 用途 | -| --- | --- | --- | --- | --- | -| `prod` | — | `http://ylyxservice1.0791ts.cn/config/update_json.txt` | `false` | **正式发布**。走远程配置解析服务器。 | -| `staging` | — | `http://testgame.youlehdyx.com/update_json/ceshi_json.txt` | `true` | 测试配置服。 | -| `local` | `ws://127.0.0.1:3088` | — | `true` | 本地服务器直连,**不抓远程**。 | +## 发布与实际来源限制 -### 如何切换 +发布前修改 STARTUP_CONFIG 中的应用模式和服务器选择,并核对远程配置是否为目标身份提供有效 game/player TCP。游戏身份、原生接口和账号来源按各自权威配置验证,不复制临时值到消费端。 -- **默认档位**:`ACTIVE_PROFILE`(当前为 `'prod'`),在 `config/profiles.ts` 顶部常量。 -- **临时切换**:URL 加 `?profile=`,例如 `?profile=local`、`?profile=staging`。未知名字会回退到 `ACTIVE_PROFILE`。 +2026-09-05 的真实配置对当前 BUILD 身份匹配 agent,但没有其下匹配 game,继承得到的 game/player TCP 均缺失。因此本地诊断可以在读取后使用明确本地地址;选择远程服务器的真实启动仍会显式报错。不要据此擅自选择子游戏、更换身份或使用游客服务器地址。 -```ts -resolveActiveProfile('?profile=local') // → PROFILES.local -resolveActiveProfile('') // → PROFILES.prod(ACTIVE_PROFILE) -resolveActiveProfile('?profile=xxx') // → PROFILES.prod(未知回退) -``` - -### 加一个自己的档位 - -直接在 `PROFILES` 里加一项即可,例如连组内某台联调机: - -```ts -export const PROFILES: Record = { - prod: { name: 'prod', gameserver: DEFAULT_GAMESERVER, isDebugger: false }, - staging: { name: 'staging', gameserver: STAGING_GAMESERVER, isDebugger: true }, - local: { name: 'local', server: 'ws://127.0.0.1:3088', isDebugger: true }, - // 新增:直连联调机 + 指定测试渠道身份 - lab: { name: 'lab', server: 'ws://192.168.1.50:3088', isDebugger: true, - identity: { agentid: 'test_agent', channelid: 'test_channel' } }, -}; -``` - -之后用 `?profile=lab` 即可。 - ---- - -## 6. URL Query 参数清单 - -预览/调试时可在地址栏拼接这些参数(H5 环境生效): - -| 参数 | 作用 | 示例 | -| --- | --- | --- | -| `profile` | 切换调试档位 | `?profile=local` | -| `agentid` | 覆盖代理商 ID(最高优先级) | `?agentid=00bA05...` | -| `channelid` | 覆盖渠道 ID | `?channelid=frdt0C...` | -| `marketid` | 覆盖市场 ID | `?marketid=2` | -| `version` | 覆盖版本号 | `?version=1.2` | - -多个参数用 `&` 连接:`?profile=staging&agentid=xxx&marketid=2`。 - -> `gameid` 不从 URL 取(按子游戏固定在 `BUILD_IDENTITY`)。 - ---- - -## 7. 服务器地址是怎么定的 - -`resolveBootstrap` 决定 `servers`(一个 `ws://` 地址数组,交给 `NetClient` 轮询)的顺序: - -1. **当前 profile 有 `server`** → 直接用它,**跳过远程配置抓取**(`local`/`lab` 这类纯直连档位走这条)。 -2. **否则**抓 profile 的 `gameserver` 远程配置文件,用 `getParam` 取出连接参数,`resolveServers` 组装候选列表。 - - **debug 且 profile 有 `serverOverride`**:仍抓远程配置走完整链路(`rawConfig` 照常返回),但最终连接地址**覆盖为 `serverOverride`**(联调时把游戏服连到本地测试服)。release 一律忽略 `serverOverride`,用远程解析结果。 -3. **不兜底**(CLAUDE.md 第二准则):profile 既无 `server` 又无 `gameserver`、远程抓取失败、或解析不出任何地址——一律抛 `ConfigFetchError` **显式暴露**,不猜默认、不降级。重试/重连归 `NetClient`,不在本层造兜底轮子。 - -`resolveServers` 取的连接参数键(**逐字一致**原工程,按此优先级): - -``` -player_server_tcp → visitor_server_tcp → game_server_tcp -``` - -值形如 `ip:port`,自动加 `ws://` 前缀(已带 `ws://`/`wss://` 的不重复加),并去重。 - -> **为什么只取 `*_tcp` 不取 `*_http`**:YouleNexus 是 WebSocket-only(对应旧 `netType==0` 分支)。旧工程的 `*_server_http` 属 HTTP 传输模式(`netType==1`),本期不在范围,故有意省略。 - ---- - -## 8. 远程配置文件 - -### 是什么 - -一个放在配置服务器上的 `.txt` 文件(内容是 JSON),URL 即 profile 的 `gameserver`。抓取时自动追加防缓存时间戳:`?`。 - -生产抓取实现 `HttpConfigFetcher`(`config/remote-config-fetcher.ts`)用全局 `fetch` GET 该 URL 并 `JSON.parse`,解析失败抛 `ConfigParseError`。 - -### 结构与分层取参 `getParam` - -远程配置是**分层覆盖**结构,越具体的层级优先级越高: - -``` -data ← 顶层默认 - └─ agentlist[ {agentid} ← 按 agentid 匹配 - gamelist[ {gameid} ← 再按 gameid 匹配 - channellist[ {channelid} ← 再按 channelid - marketlist[ {marketid} ] ← 最后按 marketid - ]]] -``` - -`getParam(config, key, id)` 用当前身份(agentid/gameid/channelid/marketid)逐层下钻,命中的最具体层若带该 `key` 就更新返回值,匹配链中断则返回当前已找到的最具体值(最终可能是顶层默认或 `null`)。这与旧工程 `get_paravalue` 语义逐字一致。 - -示例: - -```jsonc -{ - "data": { - "player_server_tcp": "1.1.1.1:1000", // 顶层默认 - "agentlist": [{ - "agentid": "A", - "player_server_tcp": "2.2.2.2:2000", // A 代理商覆盖 - "gamelist": [{ - "gameid": "G", - "channellist": [{ - "channelid": "C", - "player_server_tcp": "3.3.3.3:3000" // A/G/C 这条最具体 → 命中它 - }] - }] - }] - } -} -``` - -身份 `{agentid:'A', gameid:'G', channelid:'C', ...}` → `getParam(cfg,'player_server_tcp',id)` 得 `3.3.3.3:3000`;若 channelid 换成没配的值,则回退到 `2.2.2.2:2000`;agentid 也没配则回退顶层 `1.1.1.1:1000`。 - -> 平台/UI 类参数(公告 `menunotice`、跑马灯 `scrollmsg`、客服 `service_*`、登录图 `logimage`、排行榜 `rankList` 等)**复用同一个 `getParam`**,将在后续 platform 层接入;本期只解析了连接相关的服务器地址。 - ---- - -## 9. 启动编排 resolveBootstrap 与接入 NetClient - -### 签名 - -```ts -interface BootstrapOptions { - mode: 'debug' | 'release'; // 运行模式(由 resolveRuntimeMode 决定) - win: NativeHost; // 原生注入宿主(≈ window);H5 下传 globalThis/window 即可 - search: string; // location.search(如 '?profile=local') - fetcher: ConfigFetcher; // 远程配置抓取;生产传 new HttpConfigFetcher() - isNative: boolean; // 环境判定:原生 WebView=true,H5/浏览器=false(由调用方算) - cacheBust?: () => string; // 防缓存串注入(默认 String(Date.now())) -} - -interface BootstrapResult { - identity: ChannelIdentity; - account?: DebugAccount; // 调试登录账号(仅 debug 模式有值;release 恒 undefined) - servers: string[]; - rawConfig: RemoteConfig | null; // 直连档位为 null - profile: DebugProfile; - mode: 'debug' | 'release'; // 透传入参 mode - isDebugger: boolean; // 是否开启收发包日志(= mode === 'debug') -} - -function resolveBootstrap(opts: BootstrapOptions): Promise; -``` - -### 典型接入(生产 / H5 预览) - -```ts -import { resolveBootstrap } from 'db://assets/framework/config/bootstrap'; -import { resolveRuntimeMode } from 'db://assets/framework/config/runtime-mode'; -import { HttpConfigFetcher } from 'db://assets/framework/config/remote-config-fetcher'; -import { NetClient } from 'db://assets/framework/net/net-client'; -import { CocosWebSocketTransport } from 'db://assets/framework/net/cocos-transport'; - -async function startup() { - const href = location.href; - const isDebugBuild = typeof DEBUG !== 'undefined' ? DEBUG : true; - const mode = resolveRuntimeMode(isDebugBuild); - - const result = await resolveBootstrap({ - mode, - win: globalThis as any, - search: location.search, - fetcher: new HttpConfigFetcher(), - isNative: href.indexOf('index.html') > -1, // 等价旧 Logic.isH5Version() 反向 - }); - - const net = new NetClient({ - servers: result.servers, - transportFactory: () => new CocosWebSocketTransport(), - debug: result.isDebugger, - }); - - // 渠道身份 + 玩家档案合并成完整 player_login 数据 - // debug:玩家档案来自 result.account(profile 里配的调试账号,单一权威来源) - // release:result.account 为 undefined,玩家档案由 platform 层/原生提供(后续 Plan) - net.setIdentity({ - ...result.identity, // agentid/channelid/gameid/marketid/version... - ...(result.account ?? {}), // openid/nickname/avatar/... 调试账号或平台层登录信息 - }); - - net.on('login', (data) => { /* 解析 parseLoginResponse(data) */ }); - net.start(); -} -``` - -> `NetClient.setIdentity` 需要的是完整 `player_login` 字段(`LoginRequestData`)。`resolveBootstrap` 只负责其中的**渠道身份**部分;玩家档案字段(`openid/nickname/avatar/...`)由登录/微信信息提供,二者合并后再 `setIdentity`。 - -### 环境判定 `isNative` - -沿用旧工程 `Logic.isH5Version()` 的判据:**URL 含 `index.html` → 原生 WebView**(`isNative=true`);否则 H5/浏览器(`isNative=false`)。判定逻辑放在调用方,便于测试注入。 - ---- - -## 10. 原生联调(window.settings / app_*) - -原生模式(`isNative=true`)下,渠道身份从原生注入对象同步读取,**接口名/全局名逐字一致**旧工程: - -| 字段 | 主接口 | 全局回退 | -| --- | --- | --- | -| `agentid` | `window.settings.getothername('agent')` | `window.app_agent` | -| `channelid` | `window.settings.getchannelName()` | (无) | -| `marketid` | `window.settings.getmarketname()` | `window.app_market` | - -读取做了 `try/catch`:主接口抛错(原生桥未就绪)则尝试全局回退;都拿不到则该字段不覆盖(保留 `BUILD_IDENTITY` 默认)。 - -> 原生侧需要保证在 H5 启动前注入 `window.settings` 或上述 `app_*` 全局。完整的异步 WVJB 桥(分享/视频/语音/电量等)不在本子系统范围,属后续 sdk 层;本期只做**身份相关的同步读取**。 - ---- - -## 11. 跑测试与类型检查 - -配置/渠道子系统是纯逻辑 + IO 分离设计,全部可在 Node 下自动化测试(无需 Cocos 运行时)。 - -```bash -# 工作目录:cocoscreator_projects/ -cd cocoscreator_projects - -npm run test:framework # 跑全部框架测试(node:test + tsx) -npm run typecheck:framework # 类型检查(tsc --noEmit) -``` - -要点: - -- 测试文件在 `cocoscreator_projects/framework-tests/`(**Cocos `assets/` 之外**,不被编辑器编译),实现在 `YouleNexus/assets/framework/`。 -- `test:framework` 实际跑的是 `scripts/run-framework-tests.mjs`,递归收集 `framework-tests/**/*.test.ts`。**原因**:本机 Node 20.13 < 20.14,`node --test` 还不支持 glob,故用脚本枚举。 -- 依赖 `tsx`、`typescript`、`@types/node`(devDependencies)。 -- tsconfig(`tsconfig.framework.json`)开了 `allowImportingTsExtensions`,所以**框架内 import 必须带 `.ts` 后缀**。 - -> 写测试时注意一个 TS 6.x 坑:对「只在闭包内赋值、初值为 `null`」的变量,经 `assert.ok(x)` 收窄后会被推成 `never`。规避法:把变量声明为 `any`,或在断言处把数据传给纯函数再断言(参考 `framework-tests/integration/login-flow.test.ts`)。 - ---- - -## 12. 常见调试场景配方 - -### A. 连本地服务器跑一局 - -1. 本地起服务器在 `127.0.0.1:3088`(或改 `local` 档位的 `server`)。 -2. 预览 URL 加 `?profile=local`。 -3. 这条直连、不抓远程配置;`isDebugger=true` 会输出收发包日志。 - -### B. 用测试配置服 + 默认渠道 - -预览 URL 加 `?profile=staging`。走 staging 的 gameserver 解析出服务器地址。 - -### C. 复现某渠道专属 bug - -保持当前档位,URL 追加该渠道的身份: -``` -?agentid=<该渠道agentid>&channelid=<该渠道channelid>&marketid=<该渠道marketid> -``` -这些覆盖优先级最高,远程配置会据此分层取到该渠道的服务器/参数。 - -### D. 连组内某台联调机 - -在 `PROFILES` 里加一个档位(见 [§5](#5-调试-profiles) 的 `lab` 例子),用 `?profile=lab`。 - -### E. 想绕开远程配置服直连某地址 - -加一个带 `server` 的 profile(同 D 的 `lab` 例子)用 `?profile=lab` 直连——这是把"应急地址"写进**权威来源**(profile),而不是让 `resolveBootstrap` 兜底。远程配置服真的挂了,就是显式 `ConfigFetchError`,去修配置服或临时切到直连 profile,不在代码里降级(CLAUDE.md 第二准则)。 - ---- - -## 13. 真实服务器端到端联调 - -> 对应 Plan 2 的 Task 12 Step 4-5(自动化 mock 已全绿,真机联调需要外部输入)。 - -**前置**:① 可联调的服务器地址(`ws://ip:port` 或配置服 `gameserver` URL);② 一组后端认可的有效渠道身份(`agentid/channelid/marketid` + 目标子游戏的 `gameid`)。 - -**步骤**: - -1. 用 `funplay-cocos-mcp` 的 `execute_javascript`(context=editor)或 `execute_editor_script` 注入一段临时调试入口(或挂一个调试脚本),按 [§9](#9-启动编排-resolvebootstrap-与接入-netclient) 的接入示例:`resolveBootstrap` + `HttpConfigFetcher` + `NetClient` + `CocosWebSocketTransport`。 - - 想直连:在 `PROFILES` 临时加一档带 `server` 的 profile,或直接 `new NetClient({ servers: ['ws://<联调地址>'] })`。 - - 想验证远程配置链路:把 `gameserver` 指向联调配置服,走 `resolveBootstrap` 完整流程。 -2. `net.setIdentity({...result.identity, ...玩家档案})`,监听 `open`/`login`/`message`/`slow`/`reconnecting`。 -3. `net.start()` 连真机。 -4. 断言/观察:收到 `@toconcon` 握手不报错 → 发出 `player_login` → 收到 `player_login` 响应且 `parseLoginResponse(data).ok === true` → console 打印 `playerid`/资产。 -5. 用 `search_project_logs` / `capture_preview_screenshot` 留存联调证据。 -6. 把联调结论(成功/协议偏差)回填到 `docs/protocol/` 对应章节的「⚠️待服务器确认」项。 - ---- - -## 14. 发布前检查清单 - -- [ ] `config/runtime-mode.ts` 的 `MODE_OVERRIDE` 为 `null`(发布走自动模式,由 release 构建决定)。 -- [ ] `config/profiles.ts` 的 `ACTIVE_PROFILE` 为 `'prod'`。 -- [ ] `prod` 档位 `isDebugger: false`(不输出收发包日志)。 -- [ ] 不依赖任何 `?profile=`/`?agentid=` 之类 URL 覆盖(生产 URL 应是干净的)。 -- [ ] 目标子游戏的 `BUILD_IDENTITY.gameid` 正确(每子游戏不同)。 -- [ ] `gameserver`(`DEFAULT_GAMESERVER`)指向正式配置服。 -- [ ] `npm run test:framework` 全绿、`npm run typecheck:framework` exit 0。 - ---- - -## 15. 红线:哪些字符串绝不能改 - -跨边界到「服务器 / 原生 / 配置服务」的字符串契约**必须逐字不变**(CLAUDE.md 三条红线)。内部变量/类型/函数名可以现代化命名,但下面这些字面量不能动: - -- **协议字段名**:`agentid`、`channelid`、`gameid`、`marketid`、`openid`、`version`、`route`、`rpc` 等。 -- **原生接口名/全局名**:`window.settings.getothername`、`getchannelName`、`getmarketname`;全局 `app_agent`、`app_market`。 -- **远程配置键名**:`agentlist`、`gamelist`、`channellist`、`marketlist` 及其匹配键 `agentid/gameid/channelid/marketid`;连接参数 `player_server_tcp`、`visitor_server_tcp`、`game_server_tcp`(以及未来用到的 `*_http` 等)。 -- **gameserver URL 防缓存**:`?<时间戳>` 形式。 - -改这些之前先核对旧工程源码(`12_Logic.js` 的 `get_paravalue`/`getConfig_Succ`、`05_Func.js` 的 `getothername/getchannelName/getmarketname`、`version.js`、`00_SubGame_Config.js`),不要臆测。 - ---- - -## 16. API 速查表 - -| 符号 | 来自 | 签名 / 说明 | -| --- | --- | --- | -| `resolveRuntimeMode` | `config/runtime-mode.ts` | `(isDebugBuild, override?) => 'debug' | 'release'` 运行模式解析 | -| `resolveBootstrap` | `config/bootstrap.ts` | `(opts: BootstrapOptions) => Promise` 启动编排(入参含 mode,结果含 mode/isDebugger) | -| `BUILD_IDENTITY` | `config/sources/defaults.ts` | 构建期默认渠道身份常量 | -| `resolveIdentity` | `config/identity.ts` | `(sources: IdentitySource[]) => ChannelIdentity` 优先级合并 | -| `queryStringSource` | `config/sources/query-string.ts` | `(search: string) => IdentitySource` H5 URL 身份来源 | -| `nativeSettingsSource` | `config/sources/native-settings.ts` | `(win: NativeHost) => IdentitySource` 原生身份来源 | -| `PROFILES` / `ACTIVE_PROFILE` | `config/profiles.ts` | 档位表 / 当前默认档位名 | -| `resolveActiveProfile` | `config/profiles.ts` | `(search: string) => DebugProfile`,支持 `?profile=` | -| `DEFAULT_GAMESERVER` | `config/profiles.ts` | 正式远程配置 txt URL | -| `getParam` | `config/remote-config.ts` | `(config, key, id) => unknown` 分层取参 | -| `resolveServers` | `config/remote-config.ts` | `(config, id) => string[]` ws 候选地址 | -| `ConfigFetcher` | `config/remote-config.ts` | 抓取接口 `{ fetch(url): Promise }` | -| `HttpConfigFetcher` | `config/remote-config-fetcher.ts` | 生产用 `fetch` 实现 | -| `ConfigFetchError` / `ConfigParseError` | `config/remote-config.ts` | 抓取/解析错误类型 | - ---- - -*文档对应实现已合入 master;如接口有演进,请同步更新本指南。* +远程/原生/服务器契约仍以实际原工程及 docs/protocol 对应章节核对。此前指南中的 GET、远程 data 包装、URL profile 选择、直连跳过读取、版本字符串及默认身份兜底说明已失效,本版不再沿用。 diff --git a/docs/superpowers/plans/2026-09-05-unified-startup-config.md b/docs/superpowers/plans/2026-09-05-unified-startup-config.md index 494b360..2dc94ee 100644 --- a/docs/superpowers/plans/2026-09-05-unified-startup-config.md +++ b/docs/superpowers/plans/2026-09-05-unified-startup-config.md @@ -40,5 +40,5 @@ - [ ] MCP verify actual main checkout Creator project and current dirty state; refresh only modified existing TS resources. - [ ] Open actual LocalPlatformLogin preview without query parameters, verify remote config source, local target, login ready and no failure; stop afterward. Configuration-only browser calls may verify the other source selections without creating production sockets. -- [ ] Write docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md with exact results, final current configuration location, publishing instructions and source limitation. Keep prior completed reports as history. +- [ ] Update docs/guides/配置与调试指南.md to remove obsolete URL profile/remote-bypass/aggregate-test instructions. Write docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md with exact results, final current configuration location, publishing instructions and source limitation. Keep prior completed reports as history. - [ ] Whole-change independent review; verify original dirty hashes/status unchanged; mark plan progress and commit exact documents. No merge/push. diff --git a/docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md b/docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md new file mode 100644 index 0000000..640884f --- /dev/null +++ b/docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md @@ -0,0 +1,43 @@ +# 单文件启动配置验收 + +日期:2026-09-05。分支 codex/local-platform-login。设计/计划提交239af0a,实现提交facceea。独立审查正在进行。 + +## 范围 + +用户批准在 profiles.ts 单一 STARTUP_CONFIG 集中配置 mode、gameserver、useLocalServer、localServers。当前 debug/true;发布时在同一文件明确配置 release/false。URL mode/profile 不再选择模式或服务器,无参数网页可测试。本批不变更账号/身份协议、服务器或任何序列化资源,不扩展 RPC/UI 接线。 + +## 编辑器预检 + +- funplay MCP `fp_5838693755a937db`:主 checkout 的 `G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus`,场景 dirty=false。上批预览窗口已关闭。 +- `fp_da61681a45eaf2e1`:当前 LocalPlatformLogin 场景 UUID67e69aac-9982-4e78-a44c-1d9c42f016ae,Canvas/Host 挂 LocalPlatformLogin。未写入场景或组件。 + +## 实际浏览器 + +通过 MCP 逐一刷新七个已有 TS 文件后,run_project_preview 返回无参数 `http://localhost:7456/`(`fp_b0584ce226aea86b`)。另由 MCP 创建独立 Chromium 验证窗口,Node integration 关闭,context isolation/sandbox 开启,加载同一原始 URL(`fp_a5517175ea6d4b16`)。 + +- `fp_328c5953fca91a56`:真实 location.search 为空;startup-config 的 mode=debug、source=remote,唯一服务器为 ws://127.0.0.1:3088。loading-first-draw=1574.9ms,配置完成1768.8ms,资源完成1773.3ms,最短展示门2075.5ms,login 页面2077.8ms激活;ready=true,无错误提示。未点击游客登录或测试其他 RPC。 +- `fp_ec454dd740d8ce07`:通过 Creator import map 导入实际编译配置模块,保留真实 fetch,在冲突且重复的 mode/profile query 下读取。请求一次,HTTP200;结果仍为代码配置 debug/useLocalServer=true,配置快照内容等于 STARTUP_CONFIG,本地目标不变。rawConfig===remoteConfig.raw,前后 localStorage 内容不变。 +- `fp_7d52b3adb2c0ea9a`:独立验证窗口停止后 stopped=true、ready=false、pendingCount=0,然后关闭该窗口。编辑器启动的普通网页预览供用户自行测试。 + +## 工作区与使用指南 + +原有50个未跟踪文件 SHA256 无变化,两项历史 meta 删除仍保留;本批未修改任何序列化资源。继续使用同一主 checkout 分支,不创建 worktree。 + +同步更新 docs/guides/配置与调试指南.md:当前启动设置、无参数测试、原生注入、内存解析、发布修改和安全测试命令。旧指南中 URL profile、绕过远程读取和历史接口示例不再作为操作步骤。 + +## 自动回归 + +- TDD 首轮聚焦 RED:28项,4通过/24失败,包括无参数被拒绝、模式错误、旧profile影响目标及缺少新配置入口。 +- 最终聚焦62/62;全量显式枚举纯TS 618/618,无跳过或取消;指定架构测试49/49;框架类型与导入边界检查通过。全量包含既有负面路径的预期观察者异常日志。 +- 测试覆盖四种 mode/useLocalServer 组合、冲突与重复URL参数、无参数宿主、非法配置、原生gameconfig、读取失败、停止期间不连接,以及内存原始数据引用。没有执行聚合资源生成测试。 +- 七个生产TS文件在通知浏览器验证前已稳定;之后只补充测试,实际浏览器证据对应facceea的生产代码。 + +## 当前使用方式和限制 + +设置集中在 profiles.ts 的 STARTUP_CONFIG:mode=debug、useLocalServer=true;gameserver 与 localServers 同处定义。用户可直接打开 LocalPlatformLogin 并网页预览,不加mode/profile参数。 + +发布时在同一配置文件明确设置 release/false,同时选择Creator适当构建选项;LocalPlatformLogin仍是本地诊断场景,不是生产UI入口。当前真实远程配置对BUILD身份缺少game/player TCP,选择远程地址时仍会显式失败;不改身份、不用游客服务器或本地地址兜底。 + +独立Task1规格/质量审查覆盖239af0a..facceea,通过且无可操作问题;整体审查随后记录。 + +用户追加纠正:原工程启动资源加载界面应为 Layer1_Logo,当前诊断场景仍使用 Layer614_Loading。上述无参数启动证据只验收配置来源和就绪流程,不代表启动界面已忠实复刻。Logo 接线按下一批资源授权与原工程核对处理。 From 41fcd65464854b5d599f784c446aab713749b954 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 23:31:39 +0800 Subject: [PATCH 21/25] docs: finalize unified startup configuration acceptance --- .../2026-09-05-unified-startup-config.md | 26 +++++++++---------- ...09-05-unified-startup-config-acceptance.md | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/superpowers/plans/2026-09-05-unified-startup-config.md b/docs/superpowers/plans/2026-09-05-unified-startup-config.md index 2dc94ee..2f58e83 100644 --- a/docs/superpowers/plans/2026-09-05-unified-startup-config.md +++ b/docs/superpowers/plans/2026-09-05-unified-startup-config.md @@ -26,19 +26,19 @@ **Interfaces:** export StartupConfig and STARTUP_CONFIG from profiles.ts. ResolveRuntimeConfigOptions replaces mode with optional startupConfig: Readonly; production default is the exported constant, test configuration is explicit. RuntimeConfig retains mode/isDebugger/identity/servers/remoteConfig/rawConfig and exposes validated startupConfig for local consumers to validate against the same source. No independent profile selector. runtime-mode.ts may export RuntimeMode and a pure reader but contains no environment or URL defaults. -- [ ] Write RED tests in framework-tests/config/{profiles,runtime-mode,runtime-config,local-startup,bootstrap}.test.ts and local-platform-login/host.test.ts. For example, inject `{...STARTUP_CONFIG,mode:'release',useLocalServer:true}`, search `?mode=debug&profile=prod`, fake fetch `{visitor_server_tcp:'visitor:1'}`; assert one fetch and local servers with mode release. Repeat mode debug/release and useLocalServer true/false, using valid remote game/player candidates when false. Assert the default no-query local host starts, and conflicting/duplicate mode/profile parameters do not select another configuration. -- [ ] Run focused RED from cocoscreator_projects: `node --import tsx --test --test-concurrency=1 framework-tests/config/profiles.test.ts framework-tests/config/runtime-mode.test.ts framework-tests/config/runtime-config.test.ts framework-tests/config/local-startup.test.ts framework-tests/config/bootstrap.test.ts framework-tests/local-platform-login/host.test.ts`. Repeat identical command require_escalated on known sandbox ENOMEM. -- [ ] Implement source and validation: explicit mode enum, boolean useLocalServer, valid http(s) gameserver, valid loopback localServers. Freeze the source/array. Reject malformed values; no consumer defaults. Remove URL/environment mode selection and PROFILES selector. Preserve resolveGameServer original injection decoding. -- [ ] Integrate runtime: `const startupConfig = ...options.startupConfig or STARTUP_CONFIG` through one validated source function; resolve identity/injected remote URL, await fetch, create memory view, choose `startupConfig.useLocalServer ? startupConfig.localServers : parseUrlServers(remoteConfig)`. Preserve errors. Derive mode/isDebugger from startupConfig. -- [ ] Integrate diagnostic and facade: remove scalar mode in component; local-startup checks code source debug/local and loopback and accepts empty search. Ignore mode/profile query while retaining identity/gameconfig diagnostic restrictions. Use the same source snapshot in forged-context checks. Keep internal profile local namespace for account storage. Bootstrap uses same runtime; old LoginFlow stops after config error rather than assigning fallback identity/server, and forwards real location.search. Do not rewire the old UI or login contract. -- [ ] Update affected test literals via existing fixtures as appropriate; replace obsolete mode/profile expectations with new contract without weakening lifecycle/source tests. Add behavior tests for malformed local addresses and gameserver, remote failure, local diagnostic refusal of release/remote settings, and native injection compatibility. -- [ ] GREEN focused suite; explicit full suite: `$testPaths = @(rg --files framework-tests -g '*.test.ts' | Sort-Object); node --import tsx --test --test-concurrency=1 @testPaths`; architecture: `node --test framework-tests/architecture/import-boundaries.test.mjs framework-tests/architecture/presentation-boundaries.test.mjs`; type: `node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit`; boundaries: `node scripts/check-import-boundaries.mjs`. -- [ ] Self-review and exact task-only commit. Report RED/GREEN commands/results and file list in task-1-report.md; no full remote data/logged secrets. -- [ ] Independent task spec/quality review, fix loop with targeted regressions, then scoped re-review. +- [x] Write RED tests in framework-tests/config/{profiles,runtime-mode,runtime-config,local-startup,bootstrap}.test.ts and local-platform-login/host.test.ts. For example, inject `{...STARTUP_CONFIG,mode:'release',useLocalServer:true}`, search `?mode=debug&profile=prod`, fake fetch `{visitor_server_tcp:'visitor:1'}`; assert one fetch and local servers with mode release. Repeat mode debug/release and useLocalServer true/false, using valid remote game/player candidates when false. Assert the default no-query local host starts, and conflicting/duplicate mode/profile parameters do not select another configuration. +- [x] Run focused RED from cocoscreator_projects: `node --import tsx --test --test-concurrency=1 framework-tests/config/profiles.test.ts framework-tests/config/runtime-mode.test.ts framework-tests/config/runtime-config.test.ts framework-tests/config/local-startup.test.ts framework-tests/config/bootstrap.test.ts framework-tests/local-platform-login/host.test.ts`. Repeat identical command require_escalated on known sandbox ENOMEM. +- [x] Implement source and validation: explicit mode enum, boolean useLocalServer, valid http(s) gameserver, valid loopback localServers. Freeze the source/array. Reject malformed values; no consumer defaults. Remove URL/environment mode selection and PROFILES selector. Preserve resolveGameServer original injection decoding. +- [x] Integrate runtime: `const startupConfig = ...options.startupConfig or STARTUP_CONFIG` through one validated source function; resolve identity/injected remote URL, await fetch, create memory view, choose `startupConfig.useLocalServer ? startupConfig.localServers : parseUrlServers(remoteConfig)`. Preserve errors. Derive mode/isDebugger from startupConfig. +- [x] Integrate diagnostic and facade: remove scalar mode in component; local-startup checks code source debug/local and loopback and accepts empty search. Ignore mode/profile query while retaining identity/gameconfig diagnostic restrictions. Use the same source snapshot in forged-context checks. Keep internal profile local namespace for account storage. Bootstrap uses same runtime; old LoginFlow stops after config error rather than assigning fallback identity/server, and forwards real location.search. Do not rewire the old UI or login contract. +- [x] Update affected test literals via existing fixtures as appropriate; replace obsolete mode/profile expectations with new contract without weakening lifecycle/source tests. Add behavior tests for malformed local addresses and gameserver, remote failure, local diagnostic refusal of release/remote settings, and native injection compatibility. +- [x] GREEN focused suite; explicit full suite: `$testPaths = @(rg --files framework-tests -g '*.test.ts' | Sort-Object); node --import tsx --test --test-concurrency=1 @testPaths`; architecture: `node --test framework-tests/architecture/import-boundaries.test.mjs framework-tests/architecture/presentation-boundaries.test.mjs`; type: `node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit`; boundaries: `node scripts/check-import-boundaries.mjs`. +- [x] Self-review and exact task-only commit. Report RED/GREEN commands/results and file list in task-1-report.md; no full remote data/logged secrets. +- [x] Independent task spec/quality review, fix loop with targeted regressions, then scoped re-review. ### Task 2: Real browser and documentation acceptance (controller) -- [ ] MCP verify actual main checkout Creator project and current dirty state; refresh only modified existing TS resources. -- [ ] Open actual LocalPlatformLogin preview without query parameters, verify remote config source, local target, login ready and no failure; stop afterward. Configuration-only browser calls may verify the other source selections without creating production sockets. -- [ ] Update docs/guides/配置与调试指南.md to remove obsolete URL profile/remote-bypass/aggregate-test instructions. Write docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md with exact results, final current configuration location, publishing instructions and source limitation. Keep prior completed reports as history. -- [ ] Whole-change independent review; verify original dirty hashes/status unchanged; mark plan progress and commit exact documents. No merge/push. +- [x] MCP verify actual main checkout Creator project and current dirty state; refresh only modified existing TS resources. +- [x] Open actual LocalPlatformLogin preview without query parameters, verify remote config source, local target, login ready and no failure; stop afterward. Configuration-only browser calls may verify the other source selections without creating production sockets. +- [x] Update docs/guides/配置与调试指南.md to remove obsolete URL profile/remote-bypass/aggregate-test instructions. Write docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md with exact results, final current configuration location, publishing instructions and source limitation. Keep prior completed reports as history. +- [x] Whole-change independent review; verify original dirty hashes/status unchanged; mark plan progress and commit exact documents. No merge/push. diff --git a/docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md b/docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md index 640884f..34dab5a 100644 --- a/docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md +++ b/docs/superpowers/reports/2026-09-05-unified-startup-config-acceptance.md @@ -1,6 +1,6 @@ # 单文件启动配置验收 -日期:2026-09-05。分支 codex/local-platform-login。设计/计划提交239af0a,实现提交facceea。独立审查正在进行。 +日期:2026-09-05。分支 codex/local-platform-login。设计/计划提交239af0a,实现提交facceea。独立任务审查与整体审查均通过。 ## 范围 @@ -38,6 +38,6 @@ 发布时在同一配置文件明确设置 release/false,同时选择Creator适当构建选项;LocalPlatformLogin仍是本地诊断场景,不是生产UI入口。当前真实远程配置对BUILD身份缺少game/player TCP,选择远程地址时仍会显式失败;不改身份、不用游客服务器或本地地址兜底。 -独立Task1规格/质量审查覆盖239af0a..facceea,通过且无可操作问题;整体审查随后记录。 +独立Task1规格/质量审查覆盖239af0a..facceea,通过且无可操作问题;整体独立审查覆盖5d90891..af059b4,通过且无可操作P0/P1/P2/P3问题。 用户追加纠正:原工程启动资源加载界面应为 Layer1_Logo,当前诊断场景仍使用 Layer614_Loading。上述无参数启动证据只验收配置来源和就绪流程,不代表启动界面已忠实复刻。Logo 接线按下一批资源授权与原工程核对处理。 From 09e80e505215a5cba4b56cff3b902f466f1fc770 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 6 Sep 2026 00:00:19 +0800 Subject: [PATCH 22/25] feat(ui): wire Logo native loading progress --- .../ui/prefabs/widgets/Layer1_Logo.prefab | 319 +++++++++++++++--- .../assets/scenes/LocalPlatformLogin.scene | 12 +- .../LocalPlatformLogin.ts | 34 +- .../local-platform-login/local-login-view.ts | 42 +++ .../local-platform-login/view.test.ts | 65 +++- .../2026-09-05-logo-progress-acceptance.md | 35 ++ 6 files changed, 421 insertions(+), 86 deletions(-) create mode 100644 docs/superpowers/reports/2026-09-05-logo-progress-acceptance.md diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab index 112df3f..6650630 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab @@ -22,13 +22,13 @@ "__id__": 2 }, { - "__id__": 40 + "__id__": 50 } ], "_active": true, "_components": [], "_prefab": { - "__id__": 50 + "__id__": 60 }, "_lpos": { "__type__": "cc.Vec3", @@ -75,23 +75,23 @@ "__id__": 11 }, { - "__id__": 19 - }, - { - "__id__": 27 - } - ], - "_active": true, - "_components": [ - { - "__id__": 35 + "__id__": 29 }, { "__id__": 37 } ], + "_active": true, + "_components": [ + { + "__id__": 45 + }, + { + "__id__": 47 + } + ], "_prefab": { - "__id__": 39 + "__id__": 49 }, "_lpos": { "__type__": "cc.Vec3", @@ -305,21 +305,28 @@ "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 12 + } + ], "_active": true, "_components": [ { - "__id__": 12 + "__id__": 20 }, { - "__id__": 14 + "__id__": 22 }, { - "__id__": 16 + "__id__": 24 + }, + { + "__id__": 26 } ], "_prefab": { - "__id__": 18 + "__id__": 28 }, "_lpos": { "__type__": "cc.Vec3", @@ -350,6 +357,181 @@ }, "_id": "" }, + { + "__type__": "cc.Node", + "_name": "Fill", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 11 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 13 + }, + { + "__id__": 15 + }, + { + "__id__": 17 + } + ], + "_prefab": { + "__id__": 19 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 14 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 7 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "11wryNsjhNPqbXqeNtm4GL" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 16 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "04b97952-ff19-40c5-b2f0-d5a84ff93bb5@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 3, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "93ObiVr8BLmL02Gr+0Ygcn" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 18 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 416, + "_originalHeight": 7, + "_alignMode": 1, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "64uW+20zNAZ7vzhVMp3KqI" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "09/GP9iWJEppj1bGMyhJQK", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", @@ -360,7 +542,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 13 + "__id__": 21 }, "_contentSize": { "__type__": "cc.Size", @@ -388,7 +570,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 15 + "__id__": 23 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -433,7 +615,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 17 + "__id__": 25 }, "_alignFlags": 21, "_target": null, @@ -459,6 +641,31 @@ "__type__": "cc.CompPrefabInfo", "fileId": "w8o18qHP5Wem3Gb4ztYDwQ" }, + { + "__type__": "cc.ProgressBar", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 27 + }, + "_barSprite": { + "__id__": 15 + }, + "_mode": 2, + "_totalLength": 1, + "_progress": 0, + "_reverse": false, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "a0JQfp8UpBJbgzk6ZEyuhB" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -484,17 +691,17 @@ "_active": true, "_components": [ { - "__id__": 20 + "__id__": 30 }, { - "__id__": 22 + "__id__": 32 }, { - "__id__": 24 + "__id__": 34 } ], "_prefab": { - "__id__": 26 + "__id__": 36 }, "_lpos": { "__type__": "cc.Vec3", @@ -531,11 +738,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 21 + "__id__": 31 }, "_contentSize": { "__type__": "cc.Size", @@ -559,11 +766,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 23 + "__id__": 33 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -604,11 +811,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 25 + "__id__": 35 }, "_alignFlags": 16, "_target": null, @@ -659,17 +866,17 @@ "_active": true, "_components": [ { - "__id__": 28 + "__id__": 38 }, { - "__id__": 30 + "__id__": 40 }, { - "__id__": 32 + "__id__": 42 } ], "_prefab": { - "__id__": 34 + "__id__": 44 }, "_lpos": { "__type__": "cc.Vec3", @@ -706,11 +913,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 29 + "__id__": 39 }, "_contentSize": { "__type__": "cc.Size", @@ -734,11 +941,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 31 + "__id__": 41 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -802,11 +1009,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 43 }, "_alignFlags": 16, "_target": null, @@ -855,7 +1062,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 36 + "__id__": 46 }, "_contentSize": { "__type__": "cc.Size", @@ -883,7 +1090,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 38 + "__id__": 48 }, "_alignFlags": 0, "_target": null, @@ -932,13 +1139,13 @@ }, "_children": [ { - "__id__": 41 + "__id__": 51 } ], "_active": true, "_components": [], "_prefab": { - "__id__": 49 + "__id__": 59 }, "_lpos": { "__type__": "cc.Vec3", @@ -975,23 +1182,23 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 40 + "__id__": 50 }, "_children": [], "_active": true, "_components": [ { - "__id__": 42 + "__id__": 52 }, { - "__id__": 44 + "__id__": 54 }, { - "__id__": 46 + "__id__": 56 } ], "_prefab": { - "__id__": 48 + "__id__": 58 }, "_lpos": { "__type__": "cc.Vec3", @@ -1028,11 +1235,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 41 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 43 + "__id__": 53 }, "_contentSize": { "__type__": "cc.Size", @@ -1056,11 +1263,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 41 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 45 + "__id__": 55 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1101,11 +1308,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 41 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 57 }, "_alignFlags": 16, "_target": null, diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene index d6180a9..c591c27 100644 --- a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene +++ b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene @@ -275,7 +275,7 @@ "_enabled": true, "__prefab": null, "loadingPrefab": { - "__uuid__": "4db0114a-f751-4931-bfaf-4fef31430583", + "__uuid__": "3e5a41f3-b043-45a5-94f6-e299ea9acc61", "__expectedType__": "cc.Prefab" }, "loginPrefab": { @@ -371,10 +371,10 @@ "__id__": 8 }, "asset": { - "__uuid__": "4db0114a-f751-4931-bfaf-4fef31430583", + "__uuid__": "3e5a41f3-b043-45a5-94f6-e299ea9acc61", "__expectedType__": "cc.Prefab" }, - "fileId": "RyJAROM7sGTy6Li8CWDPZA", + "fileId": "D6NzGhQBg/6lYKr/OZsmJQ", "instance": { "__id__": 10 }, @@ -383,7 +383,7 @@ }, { "__type__": "cc.PrefabInstance", - "fileId": "ff8m2dc1lLAqLAsOz8udcb", + "fileId": "00Yyr3cfRNcZX1WLG7VLxz", "prefabRootNode": null, "mountedChildren": [], "mountedComponents": [], @@ -411,12 +411,12 @@ "propertyPath": [ "_name" ], - "value": "Layer614_Loading" + "value": "Layer1_Logo" }, { "__type__": "cc.TargetInfo", "localID": [ - "RyJAROM7sGTy6Li8CWDPZA" + "D6NzGhQBg/6lYKr/OZsmJQ" ] }, { diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts index 718f156..fb47160 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts @@ -1,4 +1,4 @@ -import { _decorator, Component, Label, Node, Prefab, Sprite, UITransform, instantiate, isValid, sys } from 'cc'; +import { _decorator, assetManager, Component, Label, Node, Prefab, ProgressBar, Sprite, instantiate, isValid, sys } from 'cc'; import { DEBUG } from 'cc/env'; import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts'; import type { NativeHost } from '../../framework/config/sources/native-settings.ts'; @@ -6,7 +6,7 @@ import type { RuntimeConfig } from '../../framework/config/runtime-config.ts'; import type { PageModel } from '../../framework/presentation/ui-contracts.ts'; import { createLocalLoginHost, type LocalLoginHost } from './local-login-host.ts'; import type { LocalLoginEvidence } from './local-login-transport.ts'; -import { CocosLocalLoginView, createCocosFrameScheduler, requireNode } from './local-login-view.ts'; +import { CocosLocalLoginView, createAssetLoadTask, createCocosFrameScheduler, requireNode } from './local-login-view.ts'; const { ccclass, property } = _decorator; const MINIMUM_LOADING_MS = 500; @@ -26,7 +26,7 @@ export class LocalPlatformLogin extends Component { private host: LocalLoginHost | null = null; private view: CocosLocalLoginView | null = null; - private spinner: Node | null = null; + private resourceProgress: ProgressBar | null = null; private loginInstance: Node | null = null; private stopped = false; private firstDrawAt: number | null = null; @@ -50,11 +50,11 @@ export class LocalPlatformLogin extends Component { resultLabel: this.required(this.resultLabel, 'resultLabel') }); this.required(this.loadingPrefab, 'loadingPrefab'); this.required(this.loginPrefab, 'loginPrefab'); - this.spinner = requireNode(loadingRoot, 'Layer614_Loading/group-40/载入动画'); + const logo = requireNode(loadingRoot, 'Layer1_Logo'); + this.resourceProgress = requireNode(logo, 'group-1/Spirit254').getComponent(ProgressBar); + if (this.resourceProgress === null) throw new Error('Layer1_Logo/group-1/Spirit254 requires ProgressBar'); + for (const path of ['group-1/Spirit3103', 'group-1/Spirit3104', 'group-32']) requireNode(logo, path).active = false; this.validateSprites(loadingRoot); - if (this.spinner.getComponent(UITransform) === null || this.spinner.getComponent(Sprite) === null) { - throw new Error('Loading spinner requires UITransform and Sprite'); - } stopButton.on(Node.EventType.TOUCH_END, this.stopTouch); this.view.activate('loading'); const firstDraw = this.nextDraw().then(() => { @@ -118,11 +118,21 @@ export class LocalPlatformLogin extends Component { private async loadResources(): Promise { if (this.stopped) throw new Error('Local login resources cancelled'); - const prefab = this.required(this.loginPrefab, 'loginPrefab'); + const serializedPrefab = this.required(this.loginPrefab, 'loginPrefab'); const loading = this.required(this.loadingPrefab, 'loadingPrefab'); - if (!isValid(prefab) || !isValid(prefab.data) || !isValid(loading) || !isValid(loading.data)) { + if (!isValid(serializedPrefab) || !isValid(serializedPrefab.data) || !isValid(loading) || !isValid(loading.data)) { throw new Error('Local login serialized Prefab dependency is invalid'); } + const progress = this.required(this.resourceProgress, 'resource ProgressBar'); + const task = createAssetLoadTask((onProgress, onComplete) => { + assetManager.loadAny(serializedPrefab.uuid, onProgress, (error, asset) => onComplete(error, asset as Prefab)); + }, value => { progress.progress = value; }); + this.pending.add(task.cancel); + let prefab: Prefab; + try { prefab = await task.promise; } + finally { this.pending.delete(task.cancel); } + if (this.stopped) throw new Error('Local login resources cancelled'); + if (!isValid(prefab) || !isValid(prefab.data)) throw new Error('Loaded login Prefab dependency is invalid'); const instance = instantiate(prefab); this.loginInstance = instance; instance.parent = this.required(this.loginRoot, 'loginRoot'); @@ -164,10 +174,6 @@ export class LocalPlatformLogin extends Component { }); } - update(deltaSeconds: number): void { - if (!this.stopped && this.spinner !== null && this.spinner.activeInHierarchy) this.spinner.angle -= deltaSeconds * 180; - } - private release(): void { this.stopped = true; for (const cancel of [...this.pending]) cancel(); @@ -206,6 +212,6 @@ export class LocalPlatformLogin extends Component { getDiagnostics() { return Object.freeze({ events: Object.freeze([...this.evidence]), snapshot: this.host?.getSnapshot(), pageModel: this.pageModel, stopped: this.stopped, pendingCount: this.pending.size, - firstDrawAt: this.firstDrawAt, spinnerAngle: this.spinner?.angle }); + firstDrawAt: this.firstDrawAt, resourceProgress: this.resourceProgress?.progress }); } } diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts index 85fba6c..d1484f3 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts @@ -6,6 +6,48 @@ interface ViewReferences { statusOverlay: Node; statusLabel: Label; resultLabel: Label; } +export interface AssetLoadTask { + readonly promise: Promise; + cancel(): void; +} + +type AssetLoadStarter = ( + onProgress: (completed: number, total: number) => void, + onComplete: (error: unknown, asset?: T) => void, +) => void; + +/** Owns loader callback lifetime so stop/failure cannot revive the login flow. */ +export function createAssetLoadTask(start: AssetLoadStarter, setProgress: (progress: number) => void): AssetLoadTask { + let settled = false; + let resolveTask!: (asset: T) => void; + let rejectTask!: (error: unknown) => void; + const promise = new Promise((resolve, reject) => { resolveTask = resolve; rejectTask = reject; }); + const reject = (error: unknown) => { + if (settled) return; + settled = true; + rejectTask(error); + }; + try { + setProgress(0); + start((completed, total) => { + if (settled) return; + if (!Number.isInteger(completed) || !Number.isInteger(total) || completed < 0 || total <= 0 || completed > total) { + reject(new Error(`Invalid asset load progress: completed=${completed}, total=${total}`)); + return; + } + try { setProgress(completed / total); } + catch (error) { reject(error); } + }, (error, asset) => { + if (settled) return; + if (error !== null && error !== undefined) { reject(error); return; } + if (asset === null || asset === undefined) { reject(new Error('Asset loader completed without an asset')); return; } + settled = true; + resolveTask(asset); + }); + } catch (error) { reject(error); } + return { promise, cancel() { reject(new Error('Asset load cancelled')); } }; +} + export function requireNode(root: Node, path: string): Node { const node = root.getChildByPath(path); if (node === null) throw new Error(`Local login binding missing node: ${path}`); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts index 1dbed17..97ea59f 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts @@ -7,6 +7,7 @@ import { EventEmitter } from 'node:events'; class Sprite {} class UITransform {} +class ProgressBar { progress = 0; } class Label { string = ''; } class Node extends EventEmitter { static EventType = { TOUCH_END: 'touch-end' }; @@ -84,6 +85,53 @@ test('frame request is one shot and real cancellation removes pending draw callb assert.equal(director.listenerCount('draw'), 0); }); +test('asset load task reports validated native loader progress from zero through completion', async () => { + const { createAssetLoadTask } = load(); + const progress: number[] = []; + let onProgress!: (completed: number, total: number) => void; + let onComplete!: (error: unknown, asset?: unknown) => void; + const task = createAssetLoadTask((progressCallback: typeof onProgress, completeCallback: typeof onComplete) => { + onProgress = progressCallback; onComplete = completeCallback; + }, (value: number) => progress.push(value)); + + assert.deepEqual(progress, [0]); + onProgress(1, 2); onProgress(2, 2); onComplete(null, { uuid: 'login' }); + assert.deepEqual(progress, [0, 0.5, 1]); + assert.deepEqual(await task.promise, { uuid: 'login' }); +}); + +test('asset load task rejects invalid counts and loader failures without reporting guessed progress', async () => { + const { createAssetLoadTask } = load(); + for (const invalid of [[0, 0], [-1, 2], [3, 2], [0.5, 2], [1, Number.NaN]]) { + const progress: number[] = []; + let onProgress!: (completed: number, total: number) => void; + const task = createAssetLoadTask((progressCallback: typeof onProgress) => { onProgress = progressCallback; }, + (value: number) => progress.push(value)); + onProgress(invalid[0], invalid[1]); + await assert.rejects(task.promise, /asset load progress/i); + assert.deepEqual(progress, [0]); + } + + let complete!: (error: unknown, asset?: unknown) => void; + const failed = createAssetLoadTask((_progress: unknown, callback: typeof complete) => { complete = callback; }, () => {}); + complete(new Error('decoder failed')); + await assert.rejects(failed.promise, /decoder failed/); +}); + +test('asset load task cancellation settles pending work and ignores late loader callbacks', async () => { + const { createAssetLoadTask } = load(); + const progress: number[] = []; + let onProgress!: (completed: number, total: number) => void; + let onComplete!: (error: unknown, asset?: unknown) => void; + const task = createAssetLoadTask((progressCallback: typeof onProgress, completeCallback: typeof onComplete) => { + onProgress = progressCallback; onComplete = completeCallback; + }, (value: number) => progress.push(value)); + task.cancel(); + await assert.rejects(task.promise, /cancelled/i); + onProgress(1, 2); onComplete(null, { uuid: 'late' }); + assert.deepEqual(progress, [0]); +}); + test('failure display preserves nested server and cleanup errors', () => { const { CocosLocalLoginView } = load(); const f = fixture(); const view = new CocosLocalLoginView(f.refs); @@ -116,14 +164,14 @@ test('component rejects non-debug/native before creating host or writing browser } }); -test('component stop cancels pending draw and timer work and disables spinner updates', async () => { +test('component stop cancels pending draw and timer work', async () => { const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url); const exports: any = {}; const timers = new Map void>(); let timerId = 0; runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, }).outputText, { exports, require: (name: string) => { if (name === 'cc/env') return { DEBUG: true }; - if (name === 'cc') return { Component: class {}, Node, Label, Prefab: class {}, sys: { isNative: false }, + if (name === 'cc') return { Component: class {}, Node, Label, Prefab: class {}, ProgressBar, sys: { isNative: false }, _decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } }; if (name.endsWith('local-login-host.ts')) return {}; if (name.endsWith('local-login-view.ts')) return load(); @@ -136,10 +184,7 @@ test('component stop cancels pending draw and timer work and disables spinner up const component = new exports.LocalPlatformLogin(); const draw = component.nextDraw(); const timer = component.delay(900); const drawRejection = assert.rejects(draw, /cancelled/); const timerRejection = assert.rejects(timer, /cancelled/); - component.spinner = { angle: 50, activeInHierarchy: true }; - component.update(0.5); assert.equal(component.spinner.angle, -40); component.stop(); await Promise.all([drawRejection, timerRejection]); - component.update(0.5); assert.equal(component.spinner.angle, -40); assert.equal(timers.size, 0); assert.equal(director.listenerCount('draw'), 0); assert.equal(component.getDiagnostics().pendingCount, 0); }); @@ -152,7 +197,7 @@ test('minimum display ignores an early timer wake and cancellation clears its re compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, }).outputText, { exports, require: (name: string) => { if (name === 'cc/env') return { DEBUG: true }; - if (name === 'cc') return { Component: class {}, Node, Label, Sprite, UITransform, Prefab: class {}, sys: { isNative: false }, + if (name === 'cc') return { Component: class {}, Node, Label, Sprite, UITransform, ProgressBar, Prefab: class {}, sys: { isNative: false }, _decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } }; if (name.endsWith('local-login-host.ts')) return { createLocalLoginHost: (options: unknown) => { hostOptions = options; return { start: async () => {}, stop() {}, getSnapshot: () => ({}) }; @@ -168,10 +213,10 @@ test('minimum display ignores an early timer wake and cancellation clears its re async function started() { const component = new exports.LocalPlatformLogin(); const f = fixture(); Object.assign(component, f.refs, { stopButton: new Node(), loadingPrefab: {}, loginPrefab: {} }); - const loading = new Node('Layer614_Loading'); const group = new Node('group-40'); - const spinner = new Node('载入动画'); spinner.components.set(Sprite, new Sprite()); - spinner.components.set(UITransform, new UITransform()); group.children.push(spinner); - loading.children.push(group); component.loadingRoot.children.push(loading); + const logo = new Node('Layer1_Logo'); const group = new Node('group-1'); + const bar = new Node('Spirit254'); bar.components.set(ProgressBar, new ProgressBar()); + group.children.push(bar, new Node('Spirit3103'), new Node('Spirit3104')); + logo.children.push(group, new Node('group-32')); component.loadingRoot.children.push(logo); component.loadingRoot.getComponentsInChildren = () => []; await component.start(); director.emit('draw'); await Promise.resolve(); return component; diff --git a/docs/superpowers/reports/2026-09-05-logo-progress-acceptance.md b/docs/superpowers/reports/2026-09-05-logo-progress-acceptance.md new file mode 100644 index 0000000..6d7ab60 --- /dev/null +++ b/docs/superpowers/reports/2026-09-05-logo-progress-acceptance.md @@ -0,0 +1,35 @@ +# Logo 原生进度条接线验收 + +日期:2026-09-05。分支 codex/local-platform-login,基线41fcd65。用户批准修改现有Layer1_Logo.prefab与LocalPlatformLogin.scene;全部资源操作通过funplay MCP,未手改序列化文本。 + +## 最终行为 + +LocalPlatformLogin 的启动加载实例与loadingPrefab引用改为Layer1_Logo。group-1/Spirit254挂载原生cc.ProgressBar;原有00096_1底图保留,新建Fill子节点引用已有00096_2黄色前景。ProgressBar以FILLED模式控制前景水平填充,totalLength=1、reverse=false、初始progress=0。父节点保持1600×7、原位置和Widget;Fill居中、四边Widget偏移0,避免进度改变宽度与布局竞争。 + +原工程11_GameUI.js:2910的case254按LoadCount裁剪image96第二行。MCP读图确认00096_1是底图、00096_2是前景;因此没有把黑色底图误当填充图。独立资源审查发现过Fill偏移-800的问题,已通过编辑器修正并复审通过。 + +代码通过assetManager.loadAny读取序列化loginPrefab.uuid,使用真实completed/total回调更新进度。缓存命中可能很快从0到1,未伪造中间进度。loader失败、非法计数、停止取消均显式终止,迟到回调不再更新。资源就绪仅在实例化、绑定、Sprite依赖校验与下一帧之后记录;100%不会绕过原有配置、连接和最短展示门。 + +## 边界 + +本批是Logo替换与原生进度条优化,不是完整广告启动流程迁移。诊断实例隐藏Spirit3103/Spirit3104和group-32;保留已有500ms诊断展示门,它不产生资源进度。广告图片、远程launchWaitTime倒计时和跳过仍需后续接线。未改配置来源、协议、服务器、账号、房间或子游戏;未新增RPC验收。LocalPlatformLogin仍为本地诊断入口。 + +## 验证 + +- TDD:新增3个行为测试先RED(8通过/3失败,缺少createAssetLoadTask),实现后聚焦11/11通过;覆盖0/.5/1、非法总数/计数、失败、取消与迟到回调。tsx沙箱ENOMEM后使用相同精确命令在获准环境运行。 +- 显式枚举framework-tests/**/*.test.ts:621/621通过;指定架构测试49/49通过;框架TypeScript与导入边界检查通过。没有运行聚合测试或资源生成器。 +- MCP实际Creator3.8.8主checkout;开始时prefab编辑模式dirty=false。现有50个未跟踪文件SHA256不变,两项历史meta删除保留。 +- Prefab/scene通过编辑器保存。MCP fp_3f9e3aa7b3ad6b20复读保存后的Fill位置0和Widget偏移0;资源引用检查无缺失。 +- 无参数http://localhost:7456/实际网页:fp_d74b6f92c22912ae记录resources-ready先于login页面;最终资源修正后fp_ccac4dd12986039a再次验证ready=true、resourceProgress=1、pendingCount=0、无组件失败,Fill位置0、尺寸1600×7。未点击登录。 +- 正常流程验证后停止host,另做视觉夹具,不将人工设置进度当真实加载证据。1600×765窗口半条与1920×945窗口整条截图均检查;0/.5/1填充比例正确。浏览器遮挡会暂停引擎,验收时恢复可见并等待真实AFTER_DRAW后截图,fp_82f3e3cae88af817确认paused=false、hidden=false、progress=fillRange=1。 +- 临时截图在YouleNexus/temp/mcp-captures/logo-progress-half.png与logo-progress-full-wide.png;未提交生成图。 + +独立任务规格/质量审查及资源修正复审通过。最终整体独立审查通过,无可操作问题。 + +## 补充类型与收尾证据 + +Creator自带run_script_diagnostics(fp_6d1508df1f89941a)在既有webview-javascript-bridge.ts:80的const泛型上报13条语法错误;git show基线41fcd65确认该语法早已存在,本批未改。仓库现代TypeScript直接套Creator旧默认配置另有引擎声明/旧lib兼容诊断,不能将它们当作本批代码回归或宣称默认Creator诊断通过。 + +为覆盖实际Cocos脚本(不只framework),使用现有Creator项目及引擎类型,以命令行指定现代语言检查选项:`node node_modules/typescript/bin/tsc -p YouleNexus/tsconfig.json --noEmit --allowImportingTsExtensions --ignoreDeprecations 6.0 --target ES2020 --lib ES2020,DOM --strict --skipLibCheck --types node`,退出0。没有修改任何tsconfig或引擎文件。 + +最终独立验证窗口停止后stopped=true、ready=false、pendingCount=0并关闭,编辑器dirty=false(fp_eef13b93f09005af)。重新核对原有50个未跟踪文件SHA256不变,2项历史meta删除保留。 \ No newline at end of file From 84fa739ddd2da359c920275839301f7b025b34df Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 6 Sep 2026 00:32:37 +0800 Subject: [PATCH 23/25] feat(ui): restore Logo countdown and gated skip --- .../assets/framework/config/remote-config.ts | 16 +++ .../framework/config/sources/defaults.ts | 3 + .../LocalPlatformLogin.ts | 40 ++++-- .../local-platform-login/local-login-host.ts | 27 +++- .../local-platform-login/local-login-view.ts | 116 ++++++++++++++- .../config/launch-wait-time.test.ts | 18 +++ .../local-platform-login/host.test.ts | 59 +++++++- .../local-platform-login/view.test.ts | 134 +++++++++++++++++- .../2026-09-06-logo-countdown-acceptance.md | 29 ++++ 9 files changed, 415 insertions(+), 27 deletions(-) create mode 100644 cocoscreator_projects/framework-tests/config/launch-wait-time.test.ts create mode 100644 docs/superpowers/reports/2026-09-06-logo-countdown-acceptance.md diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts index 8e7e5b7..3e44ffa 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts @@ -1,4 +1,5 @@ import type { ChannelIdentity } from './identity.ts'; +import { DEFAULT_LAUNCH_WAIT_TIME_MS } from './sources/defaults.ts'; /** One in-memory view over the JSON root received by legacy getConfig_Succ. */ export interface RemoteConfig { @@ -10,6 +11,21 @@ export interface ConfigFetcher { fetch(gameserver: string, cacheBust?: () => string): Promise; } +/** Legacy initial value -> hall_config merge -> scoped top-level override. */ +export function resolveLaunchWaitTime(remote: RemoteConfig): number { + let value: unknown = DEFAULT_LAUNCH_WAIT_TIME_MS; + const hall = remote.getValue('hall_config'); + if (typeof hall === 'object' && hall !== null && Object.prototype.hasOwnProperty.call(hall, 'launchWaitTime')) { + value = (hall as Record).launchWaitTime; + } + const override = remote.getValue('launchWaitTime'); + if (override !== null && override !== undefined) value = override; + if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { + throw new Error('Invalid launchWaitTime: expected finite nonnegative milliseconds'); + } + return value; +} + export class ConfigFetchError extends Error {} export class ConfigParseError extends Error {} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts index f92d7f3..2dcf69e 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts @@ -1,5 +1,8 @@ import type { ChannelIdentity } from '../identity.ts'; +/** Legacy GameData launchWaitTime (04_Data.js): timer units are milliseconds. */ +export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10; + /** * 构建期默认渠道身份(旧 version.js 等价物)。 * gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。 diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts index fb47160..31d926b 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts @@ -1,15 +1,14 @@ import { _decorator, assetManager, Component, Label, Node, Prefab, ProgressBar, Sprite, instantiate, isValid, sys } from 'cc'; import { DEBUG } from 'cc/env'; import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts'; +import { resolveLaunchWaitTime } from '../../framework/config/remote-config.ts'; import type { NativeHost } from '../../framework/config/sources/native-settings.ts'; -import type { RuntimeConfig } from '../../framework/config/runtime-config.ts'; import type { PageModel } from '../../framework/presentation/ui-contracts.ts'; import { createLocalLoginHost, type LocalLoginHost } from './local-login-host.ts'; import type { LocalLoginEvidence } from './local-login-transport.ts'; -import { CocosLocalLoginView, createAssetLoadTask, createCocosFrameScheduler, requireNode } from './local-login-view.ts'; +import { CocosLocalLoginView, CocosLogoCountdownView, createLogoCountdown, createAssetLoadTask, createCocosFrameScheduler, requireNode } from './local-login-view.ts'; const { ccclass, property } = _decorator; -const MINIMUM_LOADING_MS = 500; /** Explicit DEBUG browser scene entry. All business state belongs to its host Runtime. */ @ccclass('LocalPlatformLogin') @@ -31,6 +30,8 @@ export class LocalPlatformLogin extends Component { private stopped = false; private firstDrawAt: number | null = null; private version: number | null = null; + private countdown: ReturnType | null = null; + private countdownView: CocosLogoCountdownView | null = null; private readonly pending = new Set<() => void>(); private readonly evidence: (LocalLoginEvidence & { at: number })[] = []; private pageModel: PageModel | null = null; @@ -53,7 +54,18 @@ export class LocalPlatformLogin extends Component { const logo = requireNode(loadingRoot, 'Layer1_Logo'); this.resourceProgress = requireNode(logo, 'group-1/Spirit254').getComponent(ProgressBar); if (this.resourceProgress === null) throw new Error('Layer1_Logo/group-1/Spirit254 requires ProgressBar'); - for (const path of ['group-1/Spirit3103', 'group-1/Spirit3104', 'group-32']) requireNode(logo, path).active = false; + requireNode(logo, 'group-32').active = false; + const countdownLabels = logo.getComponentsInChildren(Label).filter(label => label.node.name === 'Spirit3104'); + if (countdownLabels.length !== 1) throw new Error('Logo requires exactly one Spirit3104 Label'); + this.countdownView = new CocosLogoCountdownView(requireNode(logo, 'group-1/Spirit3103'), countdownLabels[0].node, + () => this.countdown?.skip()); + this.countdown = createLogoCountdown({ now: () => performance.now(), + schedule: (callback, ms) => { + const timer = window.setTimeout(() => { + try { callback(); } catch (error) { this.fail(error); } + }, ms); + return () => window.clearTimeout(timer); + }, render: state => this.countdownView?.render(state) }); this.validateSprites(loadingRoot); stopButton.on(Node.EventType.TOUCH_END, this.stopTouch); this.view.activate('loading'); @@ -77,15 +89,20 @@ export class LocalPlatformLogin extends Component { showOverlay: model => view.showOverlay(model), }, scheduler: createCocosFrameScheduler(), + onConfigResolved: config => { + this.version = config.identity.version; + this.required(this.countdown, 'Logo countdown').start(resolveLaunchWaitTime(config.remoteConfig)); + }, + onResourcesReady: () => { + this.countdown?.resourcesReady(); + this.required(this.resourceProgress, 'resource ProgressBar').node.active = false; + }, + onSocketReady: ready => this.countdown?.socketReady(ready), + onStopped: () => { this.countdown?.cancel(); this.countdownView?.dispose(); }, loadResources: () => this.loadResources(), waitForMinimumDisplay: async () => { - await firstDraw; + await Promise.all([firstDraw, this.required(this.countdown, 'Logo countdown').promise]); const drawnAt = this.required(this.firstDrawAt, 'loading first draw'); - let remaining = MINIMUM_LOADING_MS - (performance.now() - drawnAt); - while (remaining > 0) { - await this.delay(remaining); - remaining = MINIMUM_LOADING_MS - (performance.now() - drawnAt); - } this.record({ kind: 'minimum-display-ready', data: performance.now() - drawnAt }); }, createSocket: url => new WebSocket(url), record: event => this.record(event), @@ -102,7 +119,6 @@ export class LocalPlatformLogin extends Component { } private record(event: LocalLoginEvidence): void { - if (event.kind === 'startup-config') this.version = (event.data as RuntimeConfig).identity.version; this.evidence.push(Object.freeze({ ...event, at: performance.now() })); } @@ -176,6 +192,8 @@ export class LocalPlatformLogin extends Component { private release(): void { this.stopped = true; + this.countdown?.cancel(); + this.countdownView?.dispose(); for (const cancel of [...this.pending]) cancel(); this.stopButton?.off(Node.EventType.TOUCH_END, this.stopTouch); this.view?.dispose(); diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts index 7ed15b0..68cda18 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts @@ -1,5 +1,5 @@ import { resolveLocalStartup } from '../../framework/config/local-startup.ts'; -import type { ResolveRuntimeConfigOptions } from '../../framework/config/runtime-config.ts'; +import type { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../framework/config/runtime-config.ts'; import { createLocalLoginSources, type LocalHostSnapshot, type LocalLoginSources } from '../../framework/adapters/local/login-sources.ts'; import type { LocalStoragePort } from '../../framework/adapters/local/local-storage.ts'; import { PlatformRuntime } from '../../framework/platform/runtime.ts'; @@ -18,8 +18,12 @@ export interface LocalLoginHostOptions { sampleHost: () => LocalHostSnapshot; view: PlatformViewPort; scheduler: FrameScheduler; - loadResources: () => Promise; - waitForMinimumDisplay: () => Promise; + loadResources: (config: RuntimeConfig) => Promise; + waitForMinimumDisplay: (config: RuntimeConfig) => Promise; + onConfigResolved?: (config: RuntimeConfig) => void; + onResourcesReady?: () => void; + onSocketReady?: (ready: boolean) => void; + onStopped?: () => void; createSocket: (url: string) => WebSocket; record: EvidenceSink; onBusy: (busy: boolean) => void; @@ -57,6 +61,7 @@ export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLogin let busy = false; let cleanupErrors: unknown[] | null = null; let serverFailure: { data: unknown } | null = null; + let unsubscribeState: (() => void) | null = null; function requireActive(): void { if (terminal) throw new Error('Local login host is stopped or terminal'); @@ -71,6 +76,8 @@ export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLogin function shutdown(errors: unknown[]): void { terminal = true; cleanupErrors = errors; + try { options.onStopped?.(); } catch (error) { errors.push(error); } + try { unsubscribeState?.(); unsubscribeState = null; } catch (error) { errors.push(error); } // Disable input before releasing either owner; dispose preserves drawn output. try { setBusy(false); } catch (error) { errors.push(error); } try { controller?.dispose(); } catch (error) { errors.push(error); } @@ -103,7 +110,7 @@ export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLogin } async function readyTask(task: () => Promise): Promise { - try { requireActive(); await task(); } + try { requireActive(); await task(); requireActive(); } catch (error) { fail(error); throw error; } } @@ -114,6 +121,8 @@ export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLogin requireActive(); const context = await resolveLocalStartup(options.configOptions); requireActive(); + options.onConfigResolved?.(context.config); + requireActive(); const { mode, isDebugger, identity, servers, source, gameserver } = context.config; options.record({ kind: 'startup-config', data: { mode, isDebugger, identity, servers, source, gameserver } }); requireActive(); @@ -162,10 +171,16 @@ export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLogin createWireClient: () => createLocalLoginWire({ context, record: options.record, onFault: wireFault, transportFactory: () => new LocalLoginTransport({ context, createSocket: options.createSocket, record: options.record, onFault: transportFault }) }), - scene: controller, loadResources: () => readyTask(options.loadResources), - waitForMinimumDisplay: () => readyTask(options.waitForMinimumDisplay), + scene: controller, loadResources: () => readyTask(async () => { + await options.loadResources(context.config); + requireActive(); options.onResourcesReady?.(); + }), + waitForMinimumDisplay: () => readyTask(() => options.waitForMinimumDisplay(context.config)), getLoginDeviceSnapshot: sources.getLoginDeviceSnapshot, }); + unsubscribeState = runtime.subscribeState(state => { + if (!terminal) options.onSocketReady?.(state.app.phase === 'connected' || state.app.phase === 'logged-in'); + }, fail); controller.connect(runtime); requireActive(); await runtime.start(); diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts index d1484f3..527d856 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts @@ -1,4 +1,4 @@ -import { Director, Label, Node, Sprite, UITransform, director } from 'cc'; +import { Director, Label, Node, Sprite, UITransform, Vec3, Widget, director } from 'cc'; import type { FrameScheduler, OverlayModel, PageId, PageModel, PlatformViewPort } from '../../framework/presentation/ui-contracts.ts'; interface ViewReferences { @@ -6,6 +6,120 @@ interface ViewReferences { statusOverlay: Node; statusLabel: Label; resultLabel: Label; } +export interface LogoCountdownState { readonly visible: boolean; readonly text: string; } + +/** Native input and measured label layout, independent of serialized label parentage. */ +export class CocosLogoCountdownView { + private readonly label: Label; + private readonly buttonTransform: UITransform; + private readonly textTransform: UITransform; + private readonly parentTransform: UITransform; + private disposed = false; + + constructor(private readonly button: Node, private readonly text: Node, private readonly onSkip: () => void) { + const label = text.getComponent(Label), buttonTransform = button.getComponent(UITransform); + const textTransform = text.getComponent(UITransform), parentTransform = text.parent?.getComponent(UITransform); + if (!label || !buttonTransform || !textTransform || !parentTransform) { + throw new Error('Logo countdown requires Label and native UITransform bindings'); + } + this.label = label; this.buttonTransform = buttonTransform; + this.textTransform = textTransform; this.parentTransform = parentTransform; + // The label follows the actual button center, so its old absolute Widget must not fight layout. + const widget = text.getComponent(Widget); if (widget) widget.enabled = false; + this.label.overflow = Label.Overflow.NONE; + button.active = false; text.active = false; + button.on(Node.EventType.TOUCH_END, onSkip); + } + + render(state: LogoCountdownState): void { + if (this.disposed) return; + this.button.active = state.visible; this.text.active = state.visible; + this.label.string = state.text; + if (!state.visible) return; + this.button.getComponent(Widget)?.updateAlignment(); + this.label.updateRenderData(true); + const button = this.buttonTransform, text = this.textTransform; + const center = button.convertToWorldSpaceAR(new Vec3((0.5 - button.anchorX) * button.width, (0.5 - button.anchorY) * button.height, 0)); + const position = this.parentTransform.convertToNodeSpaceAR(center); + position.x += (text.anchorX - 0.5) * text.width; + position.y += (text.anchorY - 0.5) * text.height; + this.text.setPosition(position); + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + this.button.off(Node.EventType.TOUCH_END, this.onSkip); + this.button.active = false; this.text.active = false; + } +} + +/** Display gate only. Runtime still independently owns resource/config/socket gates. */ +export function createLogoCountdown(options: { + now(): number; + schedule(callback: () => void, milliseconds: number): () => void; + render(state: LogoCountdownState): void; +}) { + let started = false, finished = false, resources = false, socket = false; + let remaining = 0, deadline = 0; + let cancelDeadline: (() => void) | null = null, cancelTick: (() => void) | null = null; + let resolve!: () => void, reject!: (error: unknown) => void; + const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); + // Cancellation may precede the host attaching the display gate. + void promise.catch(() => {}); + const render = () => options.render({ visible: started && !finished && remaining > 0, + text: `倒计时${Math.ceil(Math.max(0, remaining) / 1000)}s${resources ? ' 跳过' : ''}` }); + const finish = (error?: unknown) => { + if (finished) return; + finished = true; + const failures: unknown[] = error === undefined ? [] : [error]; + for (const cancel of [cancelDeadline, cancelTick]) { + try { cancel?.(); } catch (failure) { failures.push(failure); } + } + cancelDeadline = null; cancelTick = null; + try { render(); } catch (failure) { failures.push(failure); } + if (failures.length > 0) reject(failures[0]); else resolve(); + }; + const guard = (operation: () => void) => { + if (finished) return; + try { operation(); } catch (error) { finish(error); } + }; + const checkDeadline = () => { + cancelDeadline = null; + if (finished) return; + const wait = deadline - options.now(); + if (wait <= 0) { finish(); return; } + guard(() => { cancelDeadline = options.schedule(checkDeadline, wait); }); + }; + const tick = () => { + cancelTick = null; + if (finished) return; + guard(() => { + remaining -= 1000; + render(); + if (remaining > 1000) cancelTick = options.schedule(tick, 1000); + }); + }; + render(); + return { + promise, + start(milliseconds: number) { + if (finished) return; + if (started) throw new Error('Logo countdown already started'); + if (!Number.isFinite(milliseconds) || milliseconds < 0) throw new Error('Invalid launchWaitTime'); + started = true; remaining = milliseconds; deadline = options.now() + milliseconds; + guard(() => { + render(); checkDeadline(); + if (!finished && remaining > 1000) cancelTick = options.schedule(tick, 1000); + }); + }, + resourcesReady() { guard(() => { resources = true; render(); }); }, + socketReady(value: boolean) { if (!finished) socket = value; }, + skip() { if (!finished && started && resources && socket) finish(); }, + cancel() { finish(new Error('Logo countdown cancelled')); }, + }; +} + export interface AssetLoadTask { readonly promise: Promise; cancel(): void; diff --git a/cocoscreator_projects/framework-tests/config/launch-wait-time.test.ts b/cocoscreator_projects/framework-tests/config/launch-wait-time.test.ts new file mode 100644 index 0000000..96ea427 --- /dev/null +++ b/cocoscreator_projects/framework-tests/config/launch-wait-time.test.ts @@ -0,0 +1,18 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import * as config from '../../YouleNexus/assets/framework/config/remote-config.ts'; +import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts'; + +test('launchWaitTime follows legacy default, hall own field and truthy hierarchy precedence', () => { + const resolve = (config as any).resolveLaunchWaitTime; + assert.equal(typeof resolve, 'function'); + for (const [raw, expected] of [ + [{}, 10], [{ hall_config: { launchWaitTime: 0 } }, 0], + [{ hall_config: { launchWaitTime: 4000 }, launchWaitTime: 0 }, 4000], + [{ hall_config: { launchWaitTime: 4000 }, launchWaitTime: 2500 }, 2500], + [{ launchWaitTime: 2500, agentlist: [{ agentid: BUILD_IDENTITY.agentid, launchWaitTime: 3200 }] }, 3200], + ] as const) assert.equal(resolve(config.createRemoteConfig(raw, BUILD_IDENTITY)), expected); + for (const value of [-1, Infinity, NaN, '5000', false, null, undefined]) { + assert.throws(() => resolve(config.createRemoteConfig({ hall_config: { launchWaitTime: value } }, BUILD_IDENTITY)), /launchWaitTime/); + } +}); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts index 5e43abf..5c55540 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; import type { PageModel, OverlayModel, PageId } from '../../YouleNexus/assets/framework/presentation/ui-contracts.ts'; -import type { ResolveRuntimeConfigOptions } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; +import type { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; function deferred() { let resolve!: () => void; @@ -31,7 +31,9 @@ class Socket { } async function setup(overrides: { config?: Partial; render?: (model: PageModel) => void; cancelFrame?: () => void; showOverlay?: (model: OverlayModel) => void; - record?: (event: { kind: string; data: unknown }) => void; onBusy?: (busy: boolean) => void } = {}) { + record?: (event: { kind: string; data: unknown }) => void; onBusy?: (busy: boolean) => void; + onConfigResolved?: (config: RuntimeConfig) => void; onResourcesReady?: () => void; + onSocketReady?: (ready: boolean) => void; onStopped?: () => void } = {}) { const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-host.ts') .catch(error => assert.fail(`Local login host implementation must exist: ${String(error)}`)); const resources = deferred(), minimum = deferred(); @@ -40,6 +42,8 @@ async function setup(overrides: { config?: Partial; const values = new Map(), frames = new Set<() => void>(); let fetches = 0; const host = api.createLocalLoginHost({ + onConfigResolved: overrides.onConfigResolved, onResourcesReady: overrides.onResourcesReady, + onSocketReady: overrides.onSocketReady, onStopped: overrides.onStopped, configOptions: { hostKind: 'h5', win: {}, search: '?profile=local&version=731', fetcher: { async fetch() { fetches++; return { player_server_tcp: 'remote.example:3088' }; } }, ...overrides.config }, storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } }, @@ -57,6 +61,57 @@ async function setup(overrides: { config?: Partial; return { host, resources, minimum, sockets, pages, overlays, activations, failures, busy, evidence, values, frames, draw, ready, fetches: () => fetches }; } +test('host exposes resolved config and independent socket/resources readiness before display, and stops late events', async () => { + const configs: RuntimeConfig[] = [], socket: boolean[] = []; let resources = 0, stopped = 0; + const h = await setup({ onConfigResolved: config => configs.push(config), onResourcesReady: () => resources++, + onSocketReady: ready => socket.push(ready), onStopped: () => stopped++ }); + const starting = h.host.start(); await flush(); + assert.equal(configs.length, 1); assert.equal(configs[0].identity.version, 731); + assert.equal(configs[0].remoteConfig.getValue('player_server_tcp'), 'remote.example:3088'); + h.sockets[0].open(); await flush(); assert.equal(socket[socket.length - 1], true); + assert.equal(h.host.getSnapshot().ready, false); + h.resources.resolve(); await flush(); assert.equal(resources, 1); + assert.equal(h.host.getSnapshot().ready, false); + h.minimum.resolve(); await starting; h.host.stop(); assert.equal(stopped, 1); + const events = socket.length; h.sockets[0].open(); await flush(); assert.equal(socket.length, events); +}); + +test('host stop cancels presentation and ignores a resource completion still pending', async () => { + let resources = 0, stopped = 0; + const h = await setup({ onResourcesReady: () => resources++, onStopped: () => stopped++ }); + const starting = h.host.start(); const rejection = assert.rejects(starting, /stop|terminal/); + await flush(); h.host.stop(); h.resources.resolve(); h.minimum.resolve(); + await rejection; assert.equal(resources, 0); assert.equal(stopped, 1); +}); + +for (const order of [ + ['resources', 'socket', 'minimum'], ['resources', 'minimum', 'socket'], + ['socket', 'resources', 'minimum'], ['socket', 'minimum', 'resources'], + ['minimum', 'resources', 'socket'], ['minimum', 'socket', 'resources'], +] as const) test(`all startup gates remain independent in ${order.join(' -> ')} order`, async t => { + const h = await setup(); t.after(() => h.host.stop()); + const starting = h.host.start(); await flush(); + for (let index = 0; index < order.length; index++) { + const gate = order[index]; + if (gate === 'socket') h.sockets[0].open(); else h[gate].resolve(); + await flush(); + assert.equal(h.host.getSnapshot().ready, index === order.length - 1); + assert.equal(h.activations.includes('login'), index === order.length - 1); + } + await starting; h.draw(); assert.equal(h.activations.includes('login'), true); +}); + +test('pending config cannot be bypassed by previously completed resource/display tasks', async t => { + const config = deferred(); let resolved = 0; + const h = await setup({ config: { fetcher: { async fetch() { await config.promise; return {}; } } }, + onConfigResolved: () => resolved++ }); + t.after(() => h.host.stop()); const starting = h.host.start(); + h.resources.resolve(); h.minimum.resolve(); await flush(); + assert.equal(resolved, 0); assert.equal(h.sockets.length, 0); assert.equal(h.host.getSnapshot().ready, false); + config.resolve(); await flush(); assert.equal(resolved, 1); assert.equal(h.host.getSnapshot().ready, false); + h.sockets[0].open(); await starting; h.draw(); assert.equal(h.host.getSnapshot().ready, true); +}); + for (const missing of ['resources', 'minimum', 'socket'] as const) test(`login waits for ${missing}, and start composes only once`, async t => { const h = await setup(); t.after(() => h.host.stop()); const starting = h.host.start(); const repeated = h.host.start(); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts index 97ea59f..680f208 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts @@ -4,18 +4,35 @@ import { readFileSync, existsSync } from 'node:fs'; import { runInNewContext } from 'node:vm'; import ts from 'typescript'; import { EventEmitter } from 'node:events'; +import { resolveLaunchWaitTime } from '../../YouleNexus/assets/framework/config/remote-config.ts'; class Sprite {} -class UITransform {} +class Vec3 { constructor(public x = 0, public y = 0, public z = 0) {} } +class Widget { enabled = true; updateAlignment() {} } +class UITransform { + width = 100; height = 30; anchorX = 0.5; anchorY = 0.5; + convertToWorldSpaceAR(value: Vec3) { return new Vec3(value.x + 250, value.y + 70, value.z); } + convertToNodeSpaceAR(value: Vec3) { return new Vec3(value.x - 20, value.y - 10, value.z); } +} class ProgressBar { progress = 0; } -class Label { string = ''; } +class Label { + static Overflow = { NONE: 0 }; overflow = 1; + string = ''; updateRenderData() {} +} class Node extends EventEmitter { static EventType = { TOUCH_END: 'touch-end' }; active = true; children: Node[] = []; + parent: Node | null = null; + position = new Vec3(); + setPosition(position: Vec3) { this.position = position; } components = new Map(); constructor(public name = '') { super(); } getComponent(type: unknown) { return this.components.get(type) ?? null; } + getComponentsInChildren(type: unknown): any[] { + const own = this.getComponent(type); + return [...(own ? [own] : []), ...this.children.flatMap(child => child.getComponentsInChildren(type))]; + } getChildByPath(path: string): Node | null { const [head, ...tail] = path.split('/'); const child = this.children.find(node => node.name === head); @@ -23,6 +40,79 @@ class Node extends EventEmitter { } } const director = new EventEmitter(); +function countdownFixture() { + const { createLogoCountdown } = load(); + assert.equal(typeof createLogoCountdown, 'function'); + let now = 0; let id = 0; + const timers = new Map void }>(); + const states: { visible: boolean; text: string }[] = []; + const gate = createLogoCountdown({ now: () => now, + schedule: (callback: () => void, ms: number) => { + const key = ++id; timers.set(key, { at: now + ms, callback }); return () => timers.delete(key); + }, render: (state: { visible: boolean; text: string }) => states.push(state) }); + const advance = (ms: number) => { + const end = now + ms; + while (true) { + const next = [...timers].filter(([, timer]) => timer.at <= end).sort((a, b) => a[1].at - b[1].at)[0]; + if (!next) break; + now = next[1].at; timers.delete(next[0]); next[1].callback(); + } + now = end; + }; + return { gate, timers, states, advance, last: () => states[states.length - 1]! }; +} + +test('Logo countdown ticks legacy milliseconds and skip requires config, resource and socket readiness', async () => { + const f = countdownFixture(); let complete = false; void f.gate.promise.then(() => { complete = true; }); + assert.equal(f.last().visible, false); + f.gate.skip(); f.gate.start(2500); + assert.equal(f.last().text, '倒计时3s'); + f.advance(1000); assert.equal(f.last().text, '倒计时2s'); + f.gate.resourcesReady(); assert.equal(f.last().text, '倒计时2s 跳过'); + f.gate.skip(); await Promise.resolve(); assert.equal(complete, false); + f.gate.socketReady(true); await Promise.resolve(); assert.equal(complete, false, 'premature click must not queue'); + f.gate.skip(); await f.gate.promise; + assert.equal(f.last().visible, false); assert.equal(f.timers.size, 0); +}); + +test('Logo exact deadline is independent from resources and socket; terminal cancellation ignores late events', async () => { + const f = countdownFixture(); f.gate.start(10); f.advance(9); + assert.equal(f.last().visible, true); f.advance(1); await f.gate.promise; + assert.equal(f.last().visible, false); assert.equal(f.timers.size, 0); + const cancelled = countdownFixture(); cancelled.gate.start(5000); + const rejection = assert.rejects(cancelled.gate.promise, /cancelled/); + cancelled.gate.cancel(); await rejection; + const count = cancelled.states.length; + cancelled.gate.start(9999); cancelled.gate.resourcesReady(); cancelled.gate.socketReady(true); cancelled.gate.skip(); cancelled.advance(10000); + assert.equal(cancelled.states.length, count); assert.equal(cancelled.timers.size, 0); +}); + +test('Logo render failure at completion rejects the gate and clears all timers', async () => { + const { createLogoCountdown } = load(); let now = 0; let failRender = false; + const callbacks = new Set<() => void>(); + const gate = createLogoCountdown({ now: () => now, + schedule: (callback: () => void) => { callbacks.add(callback); return () => callbacks.delete(callback); }, + render: () => { if (failRender) throw new Error('label rendering failed'); } }); + gate.start(2500); failRender = true; now = 2500; + const rejection = assert.rejects(gate.promise, /label rendering failed/); + assert.doesNotThrow(() => { const callback = [...callbacks][0]; callbacks.delete(callback); callback(); }); + await rejection; assert.equal(callbacks.size, 0); +}); + +test('Logo scheduling and tick render failures settle pending display promises', async () => { + const { createLogoCountdown } = load(); + const failed = createLogoCountdown({ now: () => 0, schedule: () => { throw new Error('timer failed'); }, render() {} }); + const scheduling = assert.rejects(failed.promise, /timer failed/); + assert.doesNotThrow(() => failed.start(2500)); await scheduling; + let failRender = false; const callbacks = new Set<() => void>(); + const gate = createLogoCountdown({ now: () => 0, + schedule: (callback: () => void) => { callbacks.add(callback); return () => callbacks.delete(callback); }, + render: () => { if (failRender) throw new Error('tick rendering failed'); } }); + gate.start(2500); failRender = true; + const rendering = assert.rejects(gate.promise, /tick rendering failed/); + assert.doesNotThrow(() => { const callback = [...callbacks][1]; callbacks.delete(callback); callback(); }); + await rendering; assert.equal(callbacks.size, 0); +}); const source = new URL('../../YouleNexus/assets/scripts/local-platform-login/local-login-view.ts', import.meta.url); function load() { assert.ok(existsSync(source), 'Cocos local login view implementation must exist'); @@ -31,10 +121,26 @@ function load() { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText, { exports, require: (name: string) => { assert.equal(name, 'cc'); - return { Node, Sprite, UITransform, Label, director, Director: { EVENT_AFTER_DRAW: 'draw' } }; + return { Node, Sprite, UITransform, Label, Widget, Vec3, director, Director: { EVENT_AFTER_DRAW: 'draw' } }; }, Error }); return exports as any; } + +test('Logo native binding measures label around button center and releases its touch listener', () => { + const { CocosLogoCountdownView } = load(); assert.equal(typeof CocosLogoCountdownView, 'function'); + const button = new Node('Spirit3103'), text = new Node('Spirit3104'), parent = new Node('group'); + text.parent = parent; + for (const node of [button, text, parent]) node.components.set(UITransform, new UITransform()); + const label = new Label(); text.components.set(Label, label); + const widget = new Widget(); text.components.set(Widget, widget); + let clicks = 0; const view = new CocosLogoCountdownView(button, text, () => clicks++); + assert.equal(button.active, false); assert.equal(text.active, false); + view.render({ visible: true, text: '倒计时3s 跳过' }); + assert.equal(label.string, '倒计时3s 跳过'); assert.equal(label.overflow, Label.Overflow.NONE); + assert.equal(widget.enabled, false); assert.equal(text.position.x, 230); assert.equal(text.position.y, 60); + button.emit('touch-end'); assert.equal(clicks, 1); + view.dispose(); assert.equal(button.listenerCount('touch-end'), 0); assert.equal(button.active, false); +}); function fixture() { const refs = { loadingRoot: new Node(), loginRoot: new Node(), resultRoot: new Node(), statusOverlay: new Node(), statusLabel: new Label(), resultLabel: new Label() }; @@ -155,7 +261,8 @@ test('component rejects non-debug/native before creating host or writing browser _decorator: { ccclass: decorator, property: () => () => {} } }; if (name.endsWith('local-login-host.ts')) return { createLocalLoginHost: () => { hosts++; } }; if (name.endsWith('local-login-view.ts')) return load(); - if (name.endsWith('remote-config-fetcher.ts')) return { HttpConfigFetcher: class {} }; + if (name.endsWith('remote-config.ts')) return { resolveLaunchWaitTime }; + if (name.endsWith('remote-config-fetcher.ts')) return { HttpConfigFetcher: class {} }; throw new Error(`Unexpected dependency ${name}`); }, Error }); const component = new exports.LocalPlatformLogin(); @@ -175,6 +282,7 @@ test('component stop cancels pending draw and timer work', async () => { _decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } }; if (name.endsWith('local-login-host.ts')) return {}; if (name.endsWith('local-login-view.ts')) return load(); + if (name.endsWith('remote-config.ts')) return { resolveLaunchWaitTime }; if (name.endsWith('remote-config-fetcher.ts')) return {}; throw new Error(`Unexpected dependency ${name}`); }, Error, performance: { now: () => 123 }, window: { @@ -203,6 +311,7 @@ test('minimum display ignores an early timer wake and cancellation clears its re hostOptions = options; return { start: async () => {}, stop() {}, getSnapshot: () => ({}) }; } }; if (name.endsWith('local-login-view.ts')) return load(); + if (name.endsWith('remote-config.ts')) return { resolveLaunchWaitTime }; if (name.endsWith('remote-config-fetcher.ts')) return { HttpConfigFetcher: class {} }; throw new Error(`Unexpected dependency ${name}`); }, Error, performance: { now: () => clock }, window: { @@ -210,15 +319,18 @@ test('minimum display ignores an early timer wake and cancellation clears its re setTimeout: (callback: () => void) => { timers.set(++timerId, callback); return timerId; }, clearTimeout: (id: number) => timers.delete(id), } }); - async function started() { + async function started(emitDraw = true) { const component = new exports.LocalPlatformLogin(); const f = fixture(); Object.assign(component, f.refs, { stopButton: new Node(), loadingPrefab: {}, loginPrefab: {} }); const logo = new Node('Layer1_Logo'); const group = new Node('group-1'); const bar = new Node('Spirit254'); bar.components.set(ProgressBar, new ProgressBar()); - group.children.push(bar, new Node('Spirit3103'), new Node('Spirit3104')); + const button = new Node('Spirit3103'), text = new Node('Spirit3104'); + for (const node of [button, text, group]) node.components.set(UITransform, new UITransform()); + const label = new Label() as Label & { node: Node }; label.node = text; text.components.set(Label, label); text.parent = group; + group.children.push(bar, button, text); logo.children.push(group, new Node('group-32')); component.loadingRoot.children.push(logo); component.loadingRoot.getComponentsInChildren = () => []; - await component.start(); director.emit('draw'); await Promise.resolve(); + await component.start(); if (emitDraw) director.emit('draw'); await Promise.resolve(); return component; } const drain = async () => { for (let i = 0; i < 5; i++) await Promise.resolve(); }; @@ -227,6 +339,7 @@ test('minimum display ignores an early timer wake and cancellation clears its re timers.delete(id); callback(); } const component = await started(); let completed = false; + hostOptions.onConfigResolved({ identity: { version: 731 }, remoteConfig: { getValue: (name: string) => name === 'launchWaitTime' ? 500 : null } }); const minimum = hostOptions.waitForMinimumDisplay().then(() => { completed = true; }); await drain(); wake(599.8); await drain(); assert.equal(completed, false, '499.8 ms elapsed must not open minimum display gate'); @@ -236,9 +349,16 @@ test('minimum display ignores an early timer wake and cancellation clears its re component.stop(); clock = 1000; const cancelled = await started(); + hostOptions.onConfigResolved({ identity: { version: 731 }, remoteConfig: { getValue: (name: string) => name === 'launchWaitTime' ? 500 : null } }); const cancelledMinimum = hostOptions.waitForMinimumDisplay(); const rejection = assert.rejects(cancelledMinimum, /cancelled/); await drain(); wake(1499.8); await drain(); assert.equal(timers.size, 1); cancelled.stop(); await rejection; assert.equal(timers.size, 0); assert.equal(cancelled.getDiagnostics().pendingCount, 0); + + const undrawn = await started(false); let zeroComplete = false; + hostOptions.onConfigResolved({ identity: { version: 731 }, remoteConfig: { getValue: (name: string) => name === 'hall_config' ? { launchWaitTime: 0 } : null } }); + const zeroMinimum = hostOptions.waitForMinimumDisplay().then(() => { zeroComplete = true; }); + await drain(); assert.equal(zeroComplete, false, 'zero display deadline cannot bypass first native frame'); + director.emit('draw'); await zeroMinimum; assert.equal(zeroComplete, true); undrawn.stop(); }); diff --git a/docs/superpowers/reports/2026-09-06-logo-countdown-acceptance.md b/docs/superpowers/reports/2026-09-06-logo-countdown-acceptance.md new file mode 100644 index 0000000..8c72d72 --- /dev/null +++ b/docs/superpowers/reports/2026-09-06-logo-countdown-acceptance.md @@ -0,0 +1,29 @@ +# Logo 倒计时与跳过验收 + +日期:2026-09-06。基线09e80e5,主checkout分支codex/local-platform-login。用户批准恢复Spirit3103/Spirit3104的原工程行为。 + +## 原工程依据与实现边界 + +- 04_Data.js:181:hallConfig.launchWaitTime初始值10。12_Logic.js:1790起:先合并hall_config,再以get_paravalue逐项覆盖。来源默认仅在配置层定义,消费UI不兜底。 +- 12_Logic.js:1469起:配置成功后设置Logo计时、显示倒计时并初始化连接。gameabc.min.js:3370设置ontime,1876起按当前毫秒时间差触发;1319以Date.getTime取时间。10是毫秒,不擅自扩大为10秒。 +- 11_GameUI.js:8821:显示3103/3104,文字为倒计时ceil(remaining/1000)s,资源100%时追加两个空格和跳过;每1000ms递减,到期隐藏两者并清理定时器。7937的adaptPositionSpid只做文字水平居中,可用Cocos布局表达。 +- 11_GameUI.js:2182:点击3103必须同时满足firstConnect_Succ、getJSONState及LoadCount==100,才清理计时并进入登录。自动切换同样不能忽略配置/资源/连接状态。 +- save/Layer00001.xml:210:原文字字号14、白色;保留当前节点字号/外观。 + +现代实现使用一次性的启动就绪门表达这些条件,替换旧引擎重复绘制计时轮询,不复刻配置回调顺序造成的偶然提前跳转或轮询延迟。保留Logo原生资源进度条;只接倒计时与跳过,不扩展广告图替换、房间、子游戏或其他RPC。 + +## 验证记录 + +- 生产代码完成,独立规格/质量审查通过。TDD新增配置、协调器、宿主回调、原生绑定、异常路径均有RED→GREEN记录;最终聚焦50/50。全量显式枚举TS636/636,指定架构49/49;framework tsc、现代Creator项目严格类型检查、导入边界和diff-check通过。未运行聚合测试/资源生成器。 +- 配置解析保持默认10毫秒→hall_config自有字段→scoped单项覆盖。UI从明确的config回调获取来源;不从diagnostics取业务数据。倒计时使用精确截止时间,firstDraw保持独立门。结束/失败/停止均清理计时与点击,当前连接状态由Runtime订阅给出;早点击不排队。 +- 五个既有TS经MCP刷新,编辑器主checkout/currentLocalPlatformLogin,无资源写入。创建独立sandbox Chromium窗口12(fp_ad50e68505262f18),未点击登录。 +- 实际无参数网页fp_ce914709f5aedc1e:ready=true、stopped=false、pending=0、资源progress=1。3104父节点为Spirit3103,文字倒计时1s但控件已隐藏。firstDraw1078.7ms、展示门结束1239.5ms、资源就绪1248ms、登录页1249.4ms;配置默认短等待未绕过资源门。无component-failure。 +- 长倒计时视觉夹具尝试仅在独立窗口内装饰HttpConfigFetcher返回数据并重载同一场景,未写生产配置,未记录成功结果。该MCP调用没有返回,后续只读编辑器查询也挂起;已停止工具等待。HTTP8765/health仍返回ok,不能据此认定编辑器调用可用。已请求用户关闭独立验证窗口/恢复Funplay扩展。 +- 待恢复MCP后完成:长倒计时文字逐秒变化、实际点击跳过与到期隐藏的网页验收;恢复临时fetcher装饰(若安装成功)并停止/关闭独立窗口。此项未完成,不以50项模拟测试冒充网页验收。 + +## 用户资源改动保护 + +实施期间只读检查发现Logo.prefab新增未提交差异:Spirit3104已移为Spirit3103子节点,局部位置为0。该差异不是本批工具产生,单独记录SHA256并保留,不随代码提交。代码应唯一定位目标Label并按实际父节点布局,避免绑定旧的同级路径;支持这一现有用户布局不需要修改序列化资源。 + + +整体独立代码审查通过,无可操作问题;结论附带上述网页长倒计时验收未完成的限定。原有50个未跟踪文件与外部Logo.prefab快照SHA256均不变,两项历史meta删除保留。 From f2a948f059fc5119f3c47acabb530b2e411addc5 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 6 Sep 2026 00:45:44 +0800 Subject: [PATCH 24/25] fix(ui): keep diagnostic stop clear of Logo skip --- .../LocalPlatformLogin.ts | 6 ++-- .../local-platform-login/local-login-view.ts | 25 ++++++++++++++++ .../local-platform-login/view.test.ts | 29 ++++++++++++++++++- .../2026-09-06-logo-countdown-acceptance.md | 20 +++++++++++-- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts index 31d926b..ba09187 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts @@ -6,7 +6,7 @@ import type { NativeHost } from '../../framework/config/sources/native-settings. import type { PageModel } from '../../framework/presentation/ui-contracts.ts'; import { createLocalLoginHost, type LocalLoginHost } from './local-login-host.ts'; import type { LocalLoginEvidence } from './local-login-transport.ts'; -import { CocosLocalLoginView, CocosLogoCountdownView, createLogoCountdown, createAssetLoadTask, createCocosFrameScheduler, requireNode } from './local-login-view.ts'; +import { CocosLocalLoginView, CocosLogoCountdownView, createLogoCountdown, createAssetLoadTask, createCocosFrameScheduler, positionDiagnosticStopButtonBelow, requireNode } from './local-login-view.ts'; const { ccclass, property } = _decorator; @@ -57,7 +57,9 @@ export class LocalPlatformLogin extends Component { requireNode(logo, 'group-32').active = false; const countdownLabels = logo.getComponentsInChildren(Label).filter(label => label.node.name === 'Spirit3104'); if (countdownLabels.length !== 1) throw new Error('Logo requires exactly one Spirit3104 Label'); - this.countdownView = new CocosLogoCountdownView(requireNode(logo, 'group-1/Spirit3103'), countdownLabels[0].node, + const logoButton = requireNode(logo, 'group-1/Spirit3103'); + positionDiagnosticStopButtonBelow(stopButton, logoButton); + this.countdownView = new CocosLogoCountdownView(logoButton, countdownLabels[0].node, () => this.countdown?.skip()); this.countdown = createLogoCountdown({ now: () => performance.now(), schedule: (callback, ms) => { diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts index 527d856..9486a02 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts @@ -8,6 +8,31 @@ interface ViewReferences { export interface LogoCountdownState { readonly visible: boolean; readonly text: string; } +const DIAGNOSTIC_STOP_GAP = 12; + +/** Keeps the scene diagnostic control clear of the native Logo click region. */ +export function positionDiagnosticStopButtonBelow(stopButton: Node, logoButton: Node): void { + const stopTransform = stopButton.getComponent(UITransform); + const logoTransform = logoButton.getComponent(UITransform); + const stopParentTransform = stopButton.parent?.getComponent(UITransform); + if (!stopTransform || !logoTransform || !stopParentTransform) { + throw new Error('Diagnostic stop layout requires native UITransform bindings'); + } + const widget = stopButton.getComponent(Widget); + if (widget) { + widget.updateAlignment(); + widget.enabled = false; + } + const logoBottom = logoTransform.convertToWorldSpaceAR( + new Vec3((0.5 - logoTransform.anchorX) * logoTransform.width, -logoTransform.anchorY * logoTransform.height, 0), + ); + const bottomInStopParent = stopParentTransform.convertToNodeSpaceAR(logoBottom); + const current = stopButton.position; + stopButton.setPosition(new Vec3(current.x, + bottomInStopParent.y - DIAGNOSTIC_STOP_GAP - (1 - stopTransform.anchorY) * stopTransform.height, + current.z)); +} + /** Native input and measured label layout, independent of serialized label parentage. */ export class CocosLogoCountdownView { private readonly label: Label; diff --git a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts index 680f208..2272c98 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts @@ -141,6 +141,30 @@ test('Logo native binding measures label around button center and releases its t button.emit('touch-end'); assert.equal(clicks, 1); view.dispose(); assert.equal(button.listenerCount('touch-end'), 0); assert.equal(button.active, false); }); + +test('diagnostic stop button is placed below Logo across different parent coordinates without changing its right-side x', () => { + const { positionDiagnosticStopButtonBelow } = load(); + assert.equal(typeof positionDiagnosticStopButtonBelow, 'function'); + const logo = new Node('Spirit3103'), logoParent = new Node('logo-parent'); + const stop = new Node('StopButton'), canvas = new Node('Canvas'); + logo.parent = logoParent; stop.parent = canvas; stop.position = new Vec3(665, 310, 7); + const logoTransform = new UITransform(); + Object.assign(logoTransform, { width: 208, height: 59, anchorX: 0.5, anchorY: 0.5 }); + logoTransform.convertToWorldSpaceAR = value => new Vec3(value.x + 671.37, value.y + 311.5, value.z); + const stopTransform = new UITransform(); + Object.assign(stopTransform, { width: 220, height: 64, anchorX: 0.5, anchorY: 0.5 }); + const canvasTransform = new UITransform(); + canvasTransform.convertToNodeSpaceAR = value => new Vec3(value.x - 100, value.y - 40, value.z); + logo.components.set(UITransform, logoTransform); stop.components.set(UITransform, stopTransform); + canvas.components.set(UITransform, canvasTransform); + const widget = new Widget(); stop.components.set(Widget, widget); + + positionDiagnosticStopButtonBelow(stop, logo); + + assert.equal(widget.enabled, false, 'runtime position must not be overwritten by the native Widget'); + assert.equal(stop.position.x, 665); assert.equal(stop.position.z, 7); + assert.equal(stop.position.y, 198, 'button top is 12 px below the logo bottom in Canvas coordinates'); +}); function fixture() { const refs = { loadingRoot: new Node(), loginRoot: new Node(), resultRoot: new Node(), statusOverlay: new Node(), statusLabel: new Label(), resultLabel: new Label() }; @@ -321,7 +345,10 @@ test('minimum display ignores an early timer wake and cancellation clears its re } }); async function started(emitDraw = true) { const component = new exports.LocalPlatformLogin(); const f = fixture(); - Object.assign(component, f.refs, { stopButton: new Node(), loadingPrefab: {}, loginPrefab: {} }); + const stopButton = new Node('StopButton'), canvas = new Node('Canvas'); + stopButton.parent = canvas; stopButton.components.set(UITransform, new UITransform()); + canvas.components.set(UITransform, new UITransform()); + Object.assign(component, f.refs, { stopButton, loadingPrefab: {}, loginPrefab: {} }); const logo = new Node('Layer1_Logo'); const group = new Node('group-1'); const bar = new Node('Spirit254'); bar.components.set(ProgressBar, new ProgressBar()); const button = new Node('Spirit3103'), text = new Node('Spirit3104'); diff --git a/docs/superpowers/reports/2026-09-06-logo-countdown-acceptance.md b/docs/superpowers/reports/2026-09-06-logo-countdown-acceptance.md index 8c72d72..e520f69 100644 --- a/docs/superpowers/reports/2026-09-06-logo-countdown-acceptance.md +++ b/docs/superpowers/reports/2026-09-06-logo-countdown-acceptance.md @@ -1,6 +1,6 @@ # Logo 倒计时与跳过验收 -日期:2026-09-06。基线09e80e5,主checkout分支codex/local-platform-login。用户批准恢复Spirit3103/Spirit3104的原工程行为。 +状态:网页补验已完成,含实际鼠标点击与清理。日期:2026-09-06。基线09e80e5,主checkout分支codex/local-platform-login。用户批准恢复Spirit3103/Spirit3104的原工程行为。 ## 原工程依据与实现边界 @@ -19,11 +19,25 @@ - 五个既有TS经MCP刷新,编辑器主checkout/currentLocalPlatformLogin,无资源写入。创建独立sandbox Chromium窗口12(fp_ad50e68505262f18),未点击登录。 - 实际无参数网页fp_ce914709f5aedc1e:ready=true、stopped=false、pending=0、资源progress=1。3104父节点为Spirit3103,文字倒计时1s但控件已隐藏。firstDraw1078.7ms、展示门结束1239.5ms、资源就绪1248ms、登录页1249.4ms;配置默认短等待未绕过资源门。无component-failure。 - 长倒计时视觉夹具尝试仅在独立窗口内装饰HttpConfigFetcher返回数据并重载同一场景,未写生产配置,未记录成功结果。该MCP调用没有返回,后续只读编辑器查询也挂起;已停止工具等待。HTTP8765/health仍返回ok,不能据此认定编辑器调用可用。已请求用户关闭独立验证窗口/恢复Funplay扩展。 -- 待恢复MCP后完成:长倒计时文字逐秒变化、实际点击跳过与到期隐藏的网页验收;恢复临时fetcher装饰(若安装成功)并停止/关闭独立窗口。此项未完成,不以50项模拟测试冒充网页验收。 +- 首次验收时待恢复MCP后完成(已由下方补验完成):长倒计时文字逐秒变化、实际点击跳过与到期隐藏的网页验收;恢复临时fetcher装饰(若安装成功)并停止/关闭独立窗口。当时未完成,未以50项模拟测试冒充网页验收。 ## 用户资源改动保护 实施期间只读检查发现Logo.prefab新增未提交差异:Spirit3104已移为Spirit3103子节点,局部位置为0。该差异不是本批工具产生,单独记录SHA256并保留,不随代码提交。代码应唯一定位目标Label并按实际父节点布局,避免绑定旧的同级路径;支持这一现有用户布局不需要修改序列化资源。 -整体独立代码审查通过,无可操作问题;结论附带上述网页长倒计时验收未完成的限定。原有50个未跟踪文件与外部Logo.prefab快照SHA256均不变,两项历史meta删除保留。 +整体独立代码审查通过,无可操作问题;首次审查时附带网页长倒计时待验收的限定,现已完成下列补验。原有50个未跟踪文件与外部Logo.prefab快照SHA256均不变,两项历史meta删除保留。 + +## MCP恢复后的最终补验 + +- MCP恢复,旧窗口12已关闭;新独立窗口13,主checkout和Creator3.8.8不变。避免再次重载场景,使用当前实际编译LocalPlatformLogin类创建临时运行时实例,复用场景引用。只在该浏览器内装饰fetcher返回内存配置,验证有效launchWaitTime等于夹具指定值;不改发布配置、资源、远程服务或持久化数据。 +- 实际默认配置仍为10ms,正常页面ready=true、无错误(fp_e983ffe0d6c28f91);修复后再次正常启动通过(fp_cf9a986deb3363ee)。 +- 15秒夹具:文字从倒计时15s 跳过变为倒计时14s 跳过;资源progress=1但ready=false,Label在父背景中心(fp_aab5629e8b3f7d8a)。 +- 实际鼠标验收发现诊断StopButton与3103重叠,点击被停止按钮接收(fp_4e244b818e107608)。修复只在运行时将StopButton放到Logo背景底边下12单位,保持原x/z;应用Widget布局后关闭其运行时覆盖。两处TS与一个测试变更,无序列化资源修改。独立修复复审通过。 +- 修复后实鼠点击屏幕(1467,98):30秒倒计时提前约0.55秒完成展示门并进入login,ready=true、stopped=false、控件隐藏、pending=0(fp_0f0778181f3dc1cb)。截图已检查:两个按钮不再重叠,倒计时文字居中。临时截图temp/mcp-captures/logo-countdown-ready.png未提交。 +- 2.2秒自然到期夹具:ready=false/控件可见变为ready=true/控件隐藏(fp_991219d94f45c3c1)。 +- 暂缓真实资源加载任务完成的夹具:点击后ready=false、stopped=false,显示倒计时10s;释放资源任务后显示倒计时10s 跳过,仍未提前进入login,证明早点击不排队(fp_7a2ec06865f205b3)。 +- 提前停止后文字不变、控件隐藏、ready=false、pending=0(fp_544552041e1a4dce)。诊断StopButton实际鼠标点击仍有效。最终恢复fetcher和临时资源方法,stopped=true、ready=false、pending=0,然后关闭窗口13;编辑器dirty=false(fp_be8fcfffbf985067)。 +- 修复TDD先16通过/1失败,后17/17通过;最终完整显式枚举TS637/637。框架及现代Creator严格类型检查通过;此前指定架构49/49通过,本次未改变架构边界。没有重复生成资源或测试其他RPC。 + +本批启动倒计时、资源完成显示跳过、就绪后跳过、到期隐藏、停止清理网页验收完成。原生广告图片替换、其他平台RPC不在本批范围。 \ No newline at end of file From 695d3be25cd30efa4de4427123104f6341d5388b Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 6 Sep 2026 09:06:00 +0800 Subject: [PATCH 25/25] feat: migrate platform login scenes and native login flows --- .../adapters.meta} | 2 +- .../assets/framework/adapters/local.meta | 9 + .../local/local-storage.ts.meta} | 2 +- .../local/login-sources.ts.meta} | 2 +- .../adapters/local/visitor-account.ts | 2 +- .../adapters/local/visitor-account.ts.meta} | 2 +- .../assets/framework/adapters/native.meta | 9 + .../adapters/native/login-accounts.ts | 169 ++ .../adapters/native/login-accounts.ts.meta} | 2 +- .../adapters/native/native-contracts.ts.meta | 9 + .../native/native-settings-adapter.ts.meta | 9 + .../framework/adapters/native/phone-code.ts | 9 + .../adapters/native/phone-code.ts.meta | 9 + .../native/webview-javascript-bridge.ts.meta | 9 + .../assets/framework/config/local-startup.ts | 5 +- .../framework/config/local-startup.ts.meta | 9 + .../assets/framework/config/profiles.ts | 16 +- .../assets/framework/config/remote-config.ts | 57 +- .../assets/framework/config/runtime-config.ts | 17 +- .../framework/config/runtime-config.ts.meta | 9 + .../framework/config/sources/defaults.ts | 5 +- .../assets/framework/net/wire-client.ts.meta | 9 + .../framework/platform/commands.ts.meta | 9 + .../platform/connection-intent.ts.meta | 9 + .../platform/game-host-adapter.ts.meta | 9 + .../framework/platform/ready-gate.ts.meta | 9 + .../platform/runtime-session.ts.meta | 9 + .../assets/framework/platform/runtime.ts.meta | 9 + .../framework/platform/scene-port.ts.meta | 9 + .../platform/stores/platform-store.ts.meta | 9 + .../platform/stores/platform-types.ts.meta | 9 + .../platform/stores/selectors.ts.meta | 9 + .../assets/framework/presentation.meta | 9 + .../presentation/frame-renderer.ts.meta | 9 + .../framework/presentation/login-policy.ts | 42 + .../presentation/login-policy.ts.meta | 9 + .../framework/presentation/page-model.ts.meta | 9 + .../platform-ui-controller.ts.meta | 9 + .../presentation/ui-contracts.ts.meta | 9 + .../assets/framework/protocol/contracts.meta | 9 + .../protocol/contracts/index.ts.meta | 9 + .../protocol/contracts/login-contract.ts | 8 +- .../protocol/contracts/login-contract.ts.meta | 9 + .../protocol/contracts/room-contracts.ts.meta | 9 + .../protocol/contracts/validation.ts.meta | 9 + .../protocol/first-slice-routes.ts.meta | 9 + .../protocol/platform-handlers.ts.meta | 9 + .../assets/framework/sdk/contracts.meta | 9 + .../sdk/contracts/game-entry.ts.meta | 9 + .../framework/sdk/contracts/game-host.ts.meta | 9 + .../sdk/contracts/game-module.ts.meta | 9 + .../sdk/contracts/game-seat-mapper.ts.meta | 9 + .../framework/sdk/contracts/index.ts.meta | 9 + .../sdk/contracts/platform-events.ts.meta | 9 + .../framework/sdk/contracts/snapshots.ts.meta | 9 + .../assets/framework/sdk/runtime.meta | 9 + .../sdk/runtime/game-session-host.ts.meta | 9 + .../assets/framework/sdk/testing.meta | 9 + .../sdk/testing/game-contract-harness.ts.meta | 9 + .../assets/framework/ui/animations.meta | 9 + .../framework/ui/animations/LoginLoading.anim | 86 + .../ui/animations/LoginLoading.anim.meta | 13 + .../framework/ui/prefabs/Login_Layer.prefab | 731 +++++--- .../ui/prefabs/widgets/Layer10_Protol.prefab | 381 +++-- .../ui/prefabs/widgets/Layer1_Logo.prefab | 313 ++-- .../prefabs/widgets/Layer29_PhoneInfo.prefab | 416 ++--- .../YouleNexus/assets/resources.meta | 14 + .../assets/resources/dev-config.meta | 9 + .../resources/dev-config/update_jsonv2.txt | 1500 +++++++++++++++++ .../dev-config/update_jsonv2.txt.meta | 11 + .../assets/scenes/LocalPlatformLogin.scene | 1393 --------------- .../assets/scenes/PlatformLoading.scene | 416 +++++ ....scene.meta => PlatformLoading.scene.meta} | 2 +- .../assets/scenes/PlatformLogin.scene | 416 +++++ .../assets/scenes/PlatformLogin.scene.meta | 11 + .../assets/scenes/PlatformStartup.scene | 316 ++++ .../assets/scenes/PlatformStartup.scene.meta | 11 + .../YouleNexus/assets/scenes/scene-2d.scene | 449 +++++ .../assets/scenes/scene-2d.scene.meta | 11 + .../LocalPlatformLogin.ts | 237 --- .../local-platform-login/local-login-host.ts | 225 --- .../local-login-host.ts.meta | 9 - .../local-login-transport.ts | 138 -- .../local-login-transport.ts.meta | 9 - .../local-platform-login/local-login-view.ts | 317 ---- .../local-login-view.ts.meta | 9 - .../local-platform-login/local-login-wire.ts | 120 -- .../local-login-wire.ts.meta | 9 - .../assets/scripts/platform-login.meta | 9 + .../platform-login/PhoneLoginAssets.ts | 9 + .../platform-login/PhoneLoginAssets.ts.meta | 9 + .../scripts/platform-login/PlatformStartup.ts | 217 +++ .../platform-login/PlatformStartup.ts.meta | 9 + .../platform-login/local-config-fetcher.ts | 29 + .../local-config-fetcher.ts.meta | 9 + .../scripts/platform-login/login-panel.ts | 166 ++ .../platform-login/login-panel.ts.meta | 9 + .../login-stage-game-entry.ts} | 10 +- .../login-stage-game-entry.ts.meta | 9 + .../scripts/platform-login/logo-countdown.ts | 116 ++ .../platform-login/logo-countdown.ts.meta | 9 + .../scripts/platform-login/node-bindings.ts | 8 + .../platform-login/node-bindings.ts.meta | 9 + .../scripts/platform-login/scene-flow.ts | 18 + .../scripts/platform-login/scene-flow.ts.meta | 9 + .../legacy-migration/BackHall_Layer.prefab | 20 +- .../out/legacy-migration/Protol_Layer.prefab | 44 +- .../YouleNexus/tsconfig.json | 3 +- .../adapters/local/login-sources.test.ts | 4 +- .../adapters/local/visitor-account.test.ts | 6 +- .../framework-tests/config/bootstrap.test.ts | 6 +- .../config/local-config-fetcher.test.ts | 47 + .../config/local-startup.test.ts | 15 +- .../framework-tests/config/profiles.test.ts | 14 +- .../config/remote-config-fetcher.test.ts | 6 +- .../config/remote-config.test.ts | 13 + .../config/runtime-config.test.ts | 45 +- .../integration/local-login-sources.test.ts | 3 +- .../integration/login-flow.test.ts | 4 +- .../platform-vertical-slice.test.ts | 4 +- .../local-platform-login/host.test.ts | 307 ---- .../local-platform-login/transport.test.ts | 323 ---- .../local-platform-login/view.test.ts | 391 ----- .../local-platform-login/wire.test.ts | 144 -- .../game-entry.test.ts | 4 +- .../platform-login/loading-animation.test.ts | 32 + .../platform-login/login-accounts.test.ts | 76 + .../platform-login/login-policy.test.ts | 40 + .../platform-login/logo-countdown.test.ts | 144 ++ .../platform-login/phone-code.test.ts | 15 + .../platform-login/scene-flow.test.ts | 12 + .../framework-tests/platform/commands.test.ts | 4 +- .../platform/game-host-adapter.test.ts | 4 +- .../framework-tests/platform/runtime.test.ts | 4 +- .../framework-tests/platform/startup.test.ts | 6 +- .../sdk/game-contract-harness.test.ts | 4 +- docs/development-local-config.md | 33 + docs/protocol/07-RPC全量清单.md | 833 +++++++++ .../2026-09-06-login-scenes-and-contract.md | 67 + ...6-09-05-platform-ui-function-comparison.md | 348 ++++ 140 files changed, 7599 insertions(+), 4397 deletions(-) rename cocoscreator_projects/YouleNexus/assets/{scripts/local-platform-login.meta => framework/adapters.meta} (70%) create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/adapters/local.meta rename cocoscreator_projects/YouleNexus/assets/framework/{platform/native-bridge.ts.meta => adapters/local/local-storage.ts.meta} (70%) rename cocoscreator_projects/YouleNexus/assets/framework/{protocol/active-game.ts.meta => adapters/local/login-sources.ts.meta} (70%) rename cocoscreator_projects/YouleNexus/assets/{scripts/local-platform-login/local-login-game-entry.ts.meta => framework/adapters/local/visitor-account.ts.meta} (70%) create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/adapters/native.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/adapters/native/login-accounts.ts rename cocoscreator_projects/YouleNexus/assets/{scripts/local-platform-login/LocalPlatformLogin.ts.meta => framework/adapters/native/login-accounts.ts.meta} (70%) create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/adapters/native/native-contracts.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/adapters/native/native-settings-adapter.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/adapters/native/phone-code.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/adapters/native/phone-code.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/adapters/native/webview-javascript-bridge.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/net/wire-client.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/connection-intent.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/game-host-adapter.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/ready-gate.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/runtime-session.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/platform/stores/selectors.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/presentation.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/presentation/frame-renderer.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/presentation/login-policy.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/presentation/login-policy.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/presentation/page-model.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/presentation/platform-ui-controller.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/presentation/ui-contracts.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-contracts.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/validation.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/protocol/first-slice-routes.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-entry.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-host.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-module.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-seat-mapper.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/platform-events.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/snapshots.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-session-host.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/testing.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/animations.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/animations/LoginLoading.anim create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/animations/LoginLoading.anim.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/resources.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/resources/dev-config.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt create mode 100644 cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt.meta delete mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene create mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/PlatformLoading.scene rename cocoscreator_projects/YouleNexus/assets/scenes/{LocalPlatformLogin.scene.meta => PlatformLoading.scene.meta} (72%) create mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/PlatformLogin.scene create mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/PlatformLogin.scene.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene create mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/scene-2d.scene create mode 100644 cocoscreator_projects/YouleNexus/assets/scenes/scene-2d.scene.meta delete mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts delete mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts delete mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts.meta delete mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts delete mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts.meta delete mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts delete mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts.meta delete mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts delete mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PhoneLoginAssets.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PhoneLoginAssets.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/local-config-fetcher.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/local-config-fetcher.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-panel.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-panel.ts.meta rename cocoscreator_projects/YouleNexus/assets/scripts/{local-platform-login/local-login-game-entry.ts => platform-login/login-stage-game-entry.ts} (71%) create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-stage-game-entry.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/logo-countdown.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/logo-countdown.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/node-bindings.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/node-bindings.ts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/scene-flow.ts create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/platform-login/scene-flow.ts.meta create mode 100644 cocoscreator_projects/framework-tests/config/local-config-fetcher.test.ts delete mode 100644 cocoscreator_projects/framework-tests/local-platform-login/host.test.ts delete mode 100644 cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts delete mode 100644 cocoscreator_projects/framework-tests/local-platform-login/view.test.ts delete mode 100644 cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts rename cocoscreator_projects/framework-tests/{local-platform-login => platform-login}/game-entry.test.ts (93%) create mode 100644 cocoscreator_projects/framework-tests/platform-login/loading-animation.test.ts create mode 100644 cocoscreator_projects/framework-tests/platform-login/login-accounts.test.ts create mode 100644 cocoscreator_projects/framework-tests/platform-login/login-policy.test.ts create mode 100644 cocoscreator_projects/framework-tests/platform-login/logo-countdown.test.ts create mode 100644 cocoscreator_projects/framework-tests/platform-login/phone-code.test.ts create mode 100644 cocoscreator_projects/framework-tests/platform-login/scene-flow.test.ts create mode 100644 docs/development-local-config.md create mode 100644 docs/protocol/07-RPC全量清单.md create mode 100644 docs/superpowers/plans/2026-09-06-login-scenes-and-contract.md create mode 100644 docs/superpowers/reports/2026-09-05-platform-ui-function-comparison.md diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters.meta similarity index 70% rename from cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login.meta rename to cocoscreator_projects/YouleNexus/assets/framework/adapters.meta index 42f296d..6ac2cb6 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters.meta @@ -2,7 +2,7 @@ "ver": "1.2.0", "importer": "directory", "imported": true, - "uuid": "6072a7f3-9095-4e71-9dbd-c952d89a64a7", + "uuid": "aea92450-9c09-40ed-8a4d-5cced7a56b3b", "files": [], "subMetas": {}, "userData": {} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/local.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local.meta new file mode 100644 index 0000000..b587a55 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "c1b8189d-1134-4ff3-9426-d2668fd9ab6f", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/native-bridge.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/local-storage.ts.meta similarity index 70% rename from cocoscreator_projects/YouleNexus/assets/framework/platform/native-bridge.ts.meta rename to cocoscreator_projects/YouleNexus/assets/framework/adapters/local/local-storage.ts.meta index 5992bba..c6f04a6 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/platform/native-bridge.ts.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/local-storage.ts.meta @@ -2,7 +2,7 @@ "ver": "4.0.24", "importer": "typescript", "imported": true, - "uuid": "2eb9409f-115d-4a7d-bdc4-30a333b57a28", + "uuid": "0693ece5-bed1-4b21-8ae7-7393c2eee8a7", "files": [], "subMetas": {}, "userData": {} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/active-game.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/login-sources.ts.meta similarity index 70% rename from cocoscreator_projects/YouleNexus/assets/framework/protocol/active-game.ts.meta rename to cocoscreator_projects/YouleNexus/assets/framework/adapters/local/login-sources.ts.meta index 5c2c120..72215d8 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/protocol/active-game.ts.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/login-sources.ts.meta @@ -2,7 +2,7 @@ "ver": "4.0.24", "importer": "typescript", "imported": true, - "uuid": "d8981b12-16b5-49a7-95d6-a51eb735462a", + "uuid": "4deb8dec-916d-4dda-b709-ac5f38fa0253", "files": [], "subMetas": {}, "userData": {} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/visitor-account.ts b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/visitor-account.ts index 8e33121..262dd60 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/visitor-account.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/visitor-account.ts @@ -70,7 +70,7 @@ function sample(random: () => number, key: string): number { return sampleLocalRandom(random, (reason, cause) => wrap(key, 'random', reason, cause)); } -function generate(random: () => number, key: string): LoginAccountIdentity { +export function generate(random: () => number, key: string): LoginAccountIdentity { const id = Math.floor(sample(random, key) * 100000); const avatar = AVATARS[Math.floor(sample(random, key) * AVATARS.length)]!; const sex = Math.floor(sample(random, key) * 2) + 1; diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/visitor-account.ts.meta similarity index 70% rename from cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts.meta rename to cocoscreator_projects/YouleNexus/assets/framework/adapters/local/visitor-account.ts.meta index 0f093bc..db7f163 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/local/visitor-account.ts.meta @@ -2,7 +2,7 @@ "ver": "4.0.24", "importer": "typescript", "imported": true, - "uuid": "aea065db-0dfa-4da8-a2cb-f1fe0165d910", + "uuid": "bb7c9b58-c08f-4dc9-b8d1-57c2b20f9c28", "files": [], "subMetas": {}, "userData": {} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native.meta new file mode 100644 index 0000000..4cea785 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "8499ef07-6051-4d8f-aca6-11996e448b7f", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/login-accounts.ts b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/login-accounts.ts new file mode 100644 index 0000000..22deb2b --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/login-accounts.ts @@ -0,0 +1,169 @@ +import type { LoginAccountIdentity, DeviceLoginMode } from '../../protocol/contracts/login-contract.ts'; +import type { LoginPolicy } from '../../presentation/login-policy.ts'; +import { generate } from '../local/visitor-account.ts'; +import { readPhoneCode } from './phone-code.ts'; + +export interface LoginNativeSettings { + accreditlogin(provider: number): void; + getcompareCode(): number | string; +} +interface Options { + storage: {getItem(key:string):string|null;setItem(key:string,value:string):void}; + settings?:LoginNativeSettings; random():number; now():number; + gameid:string|number;agentid:string|number;policy:LoginPolicy; + submit(account:LoginAccountIdentity,device:DeviceLoginMode):void;tip(message:string):void; +} + +/** MD5 is the legacy nickname derivation, not a security primitive. */ +export function nicknameHash(input:string):string { + const bytes=Array.from(new TextEncoder().encode(input));const bitLength=bytes.length*8; + bytes.push(128);while(bytes.length%64!==56)bytes.push(0); + for(let i=0;i<8;i++)bytes.push(i<4?(bitLength>>>(8*i))&255:0); + const shifts=[7,12,17,22,5,9,14,20,4,11,16,23,6,10,15,21]; + let state=[0x67452301,0xefcdab89,0x98badcfe,0x10325476]; + for(let offset=0;offsetbytes[offset+j*4]|bytes[offset+j*4+1]<<8|bytes[offset+j*4+2]<<16|bytes[offset+j*4+3]<<24); + let [a,b,c,d]=state; + for(let i=0;i<64;i++){ + const round=Math.floor(i/16);let f:number,g:number; + if(round===0){f=(b&c)|(~b&d);g=i;}else if(round===1){f=(d&b)|(~d&c);g=(5*i+1)%16;} + else if(round===2){f=b^c^d;g=(3*i+5)%16;}else{f=c^(b|~d);g=(7*i)%16;} + const sum=(a+f+Math.floor(Math.abs(Math.sin(i+1))*4294967296)+words[g])|0; + const shift=shifts[round*4+i%4];const next=(b+((sum<>>(32-shift))))|0; + a=d;d=c;c=b;b=next; + } + state=state.map((v,i)=>(v+[a,b,c,d][i])|0); + } + return state.map(v=>[0,8,16,24].map(s=>((v>>>s)&255).toString(16).padStart(2,'0')).join('')).join(''); +} +function record(value:unknown):Record{ + if(typeof value==='string')value=JSON.parse(value); + if(!value||typeof value!=='object'||Array.isArray(value))throw Error('Invalid sharelogin account'); + return value as Record; +} +function accountFrom(value:unknown):LoginAccountIdentity { + const data=record(value); + for(const field of ['openid','headimgurl','nickname','city','Province'])if(typeof data[field]!=='string')throw Error(`Invalid sharelogin.${field}`); + if(typeof data.unionid!=='string'&&typeof data.unionid!=='number')throw Error('Invalid sharelogin.unionid'); + const sex=data.sex?data.sex:1; // Func.sharelogin explicitly owns this default. + if(typeof sex!=='number'||!Number.isFinite(sex))throw Error('Invalid sharelogin.sex'); + if(!data.nickname&&!data.headimgurl)throw Error('授权失败,请重新授权!'); + return {openid:data.openid as string,avatar:data.headimgurl as string,nickname:data.nickname as string, + city:data.city as string,province:data.Province as string,unionid:data.unionid as string|number,sex}; +} + +/** Exact native/cached account boundary; UI never invents account or server fields. */ +/** 账户统一入口:协议确认、缓存选择、原生授权及提交模式均由此处管理。 */ +export class LoginAccounts { + readonly versionState:0|1; + private hiddenClicks=0; + /** 每次启动默认同意;按钮、缓存登录及原生回调读取同一份状态。 */ + private agreementAccepted=true; + get agreed():boolean{return this.agreementAccepted;} + setAgreement(accepted:boolean):void{ + if(accepted&&!this.agreementAccepted)this.options.tip(''); + this.agreementAccepted=accepted; + } + /** 未同意时只提示,不调用授权、不写缓存、不提交请求。 */ + requireAgreement():boolean{ + if(this.agreementAccepted)return true; + this.options.tip('未同意协议');return false; + } + constructor(private readonly options:Options){ + // Browser source explicitly uses the legacy getVersionState default of zero. + const state=options.settings?Number(options.settings.getcompareCode()):0; + if(state!==0&&state!==1)throw Error('Invalid getcompareCode result');this.versionState=state; + } + private read(key:string,cookie:boolean):Record|null { + if(cookie&&this.versionState===0){ + const expiry=this.options.storage.getItem('tsgame_validtime'); + if(expiry===null)return null; + if(!Number.isFinite(Number(expiry)))throw Error('Invalid tsgame_validtime'); + if(this.options.now()>=Number(expiry))return null; + } + const raw=this.options.storage.getItem(key);return raw===null?null:record(raw); + } + private save(account:LoginAccountIdentity,key:string,telphone?:string){ + const data={openid:account.openid,headimgurl:account.avatar,nickname:account.nickname,sex:account.sex, + city:account.city,Province:account.province,unionid:account.unionid,...(telphone===undefined?{}:{telphone})}; + this.options.storage.setItem('tsgame_validtime',String(this.options.now()+48*60*60*1000)); + this.options.storage.setItem(key,JSON.stringify(data)); + } + /** 保留原工程缓存优先级,游客开关先于手机模式判断。 */ + cached():LoginAccountIdentity|null { + if(this.versionState!==0)return null; + const policy=this.options.policy; + const data=this.read(policy.showVisitor?'tsgame_visitorinfo':policy.deviceLogin?'tsgame_devicePlayer':'tsgame_wxinfo',true); + return data===null?null:accountFrom(data); + } + autoLogin():void { + if(!this.requireAgreement())return; + const account=this.cached();if(account===null)throw Error('No cached login'); + // showVisitor selects the visitor cache above, even when the phone button is enabled. + if(!this.options.policy.showVisitor&&this.options.policy.deviceLogin){ + const data=this.read('tsgame_devicePlayer',true)!; + if(typeof data.telphone!=='string')throw Error('Cached device account missing telphone'); + this.options.submit(account,{enabled:true,telphone:data.telphone}); + }else this.options.submit(account,{enabled:false}); + } + authorize(provider:'wechat'|'qq'):void { + if(!this.requireAgreement())return; + if(!this.options.settings){this.options.tip('当前宿主未提供原生登录授权接口');return;} + if(provider==='wechat')this.options.storage.setItem(`tsgame_playerid_${this.options.gameid}${this.options.agentid}`,''); + this.options.settings.accreditlogin(provider==='wechat'?2:1); + } + acceptNative(data:unknown,channel:'global'|'bridge'):void { + if(!this.requireAgreement())return; + const account=accountFrom(data); + this.save(account,channel==='global'&&this.options.policy.showVisitor?'tsgame_visitorinfo':'tsgame_wxinfo'); + this.options.submit(account,{enabled:false}); + } + /** 仅本次手机登录要求 tel,不能依据按钮显示配置要求游客响应携带手机号。 */ + loginSucceeded(value:unknown,mode:DeviceLoginMode):void { + const data=record(value); + if(data.state!==0)throw Error('Cannot cache an unsuccessful login'); + if(typeof data.playerid!=='string'&&(typeof data.playerid!=='number'||!Number.isFinite(data.playerid)))throw Error('Invalid login playerid'); + // Button availability is not the authentication mode of this request. + if(mode.enabled){ + if(typeof data.tel!=='string')throw Error('Device login response missing tel'); + const account=accountFrom({...data,headimgurl:data.avatar,Province:data.province}); + this.save(account,'tsgame_devicePlayer',data.tel); + this.options.storage.setItem('tsgame_dLogin','1'); + } + this.options.storage.setItem(`tsgame_playerid_${this.options.gameid}${this.options.agentid}`,String(data.playerid)); + } + visitor():void { + if(!this.requireAgreement())return; + const cached=this.read('tsgame_visitorinfo',false); + const account=cached===null?generate(this.options.random,'tsgame_visitorinfo'):accountFrom(cached); + this.save(account,this.options.policy.showVisitor?'tsgame_visitorinfo':'tsgame_wxinfo'); + this.options.submit(account,{enabled:false}); + } + hiddenFirst():void{this.hiddenClicks++;} + phonePanelState(){return readPhoneCode(this.options.storage.getItem('sgame_phoneCode'),this.options.now());} + hiddenSecond():void{if(this.hiddenClicks>=5&&this.options.policy.hiddenVisitor){this.hiddenClicks=0;this.visitor();}} + private deviceAccount(phone?:string):LoginAccountIdentity { + const cached=this.read('tsgame_devicePlayer',true);if(cached)return accountFrom(cached); + let unionid:string; + if(phone!==undefined)unionid=`phone_${phone}`; + else { + const info=this.read('tsgame_phoneInfo1',false); + if(info&&typeof info.PhoneIMEI==='string'&&info.PhoneIMEI)unionid=info.PhoneIMEI; + else { + const sets=['0123456789','qwertyuioplkjhgfdsazxcvbnm','QWERTYUIOPLKJHGFDSAZXCVBNM']; + unionid=Array.from({length:25},()=>{const chars=sets[Math.round(this.options.random()*2)];return chars[Math.ceil(this.options.random()*100000000)%chars.length];}).join(''); + this.options.storage.setItem('tsgame_phoneInfo1',JSON.stringify({...info,PhoneIMEI:unionid})); + } + } + const {avatarUrl,avatarRange}=this.options.policy; + return {openid:unionid,unionid,nickname:`User_${nicknameHash(unionid).slice(0,6)}`, + avatar:avatarUrl?`${avatarUrl}${avatarRange[0]+Math.round(this.options.random()*(avatarRange[1]-avatarRange[0]))}.jpg`:'', + sex:Math.floor(this.options.random()*2)+1,city:'China',province:phone===undefined?'HongKong':'ShangHai'}; + } + device():void{if(!this.requireAgreement())return;this.options.submit(this.deviceAccount(),{enabled:false});} + phone(phone:string,code:string):void { + if(!this.requireAgreement())return; + if(!phone||!code){this.options.tip('手机号与验证码均不能为空!');return;} + this.options.submit(this.deviceAccount(phone),{enabled:true,telphone:phone,smmcode:code}); + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/login-accounts.ts.meta similarity index 70% rename from cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts.meta rename to cocoscreator_projects/YouleNexus/assets/framework/adapters/native/login-accounts.ts.meta index 6737380..1e263cb 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts.meta +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/login-accounts.ts.meta @@ -2,7 +2,7 @@ "ver": "4.0.24", "importer": "typescript", "imported": true, - "uuid": "2597060c-ad16-46f1-89ed-42bfaec74ad9", + "uuid": "57d7618f-3e88-4036-83de-96a6e63be657", "files": [], "subMetas": {}, "userData": {} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/native-contracts.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/native-contracts.ts.meta new file mode 100644 index 0000000..695aad1 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/native-contracts.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "27e9345c-5f75-4829-be9f-174141dec6e6", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/native-settings-adapter.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/native-settings-adapter.ts.meta new file mode 100644 index 0000000..18d341a --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/native-settings-adapter.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "df3fb4b3-bb92-40f7-b661-05a158dec617", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/phone-code.ts b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/phone-code.ts new file mode 100644 index 0000000..eceeb80 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/phone-code.ts @@ -0,0 +1,9 @@ +/** Logic.readPhoneCode: sgame_phoneCode owns the phone number and 30-second expiry. */ +export function readPhoneCode(raw:string|null,now:number):{phoneNumber:string;restTime:number}|null { + if(raw===null)return null; + const data=JSON.parse(raw); + if(!data||typeof data!=='object'||typeof data.phoneNumber!=='string')throw Error('Invalid phoneCode.phoneNumber'); + if(!Object.prototype.hasOwnProperty.call(data,'codeTime')||!['string','number'].includes(typeof data.codeTime)||!Number.isFinite(Number(data.codeTime)))throw Error('Invalid phoneCode.codeTime'); + const elapsed=now-Number(data.codeTime); + return {phoneNumber:data.phoneNumber,restTime:elapsed>30000?0:Math.trunc(30-elapsed/1000)}; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/phone-code.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/phone-code.ts.meta new file mode 100644 index 0000000..0a49fb4 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/phone-code.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "bb46066c-73af-4603-9f7a-5579a2ad030b", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/webview-javascript-bridge.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/webview-javascript-bridge.ts.meta new file mode 100644 index 0000000..fab20bb --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/adapters/native/webview-javascript-bridge.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "588b5341-c0f6-4eef-bf31-b47c815db2b0", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts index 3bb72b6..382e283 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts @@ -63,8 +63,9 @@ export function assertLocalStartupContext(context: LocalStartupContext): void { if (!startupConfig.useLocalServer) fail('useLocalServer', 'source must be true'); if (config.mode !== 'debug') fail('mode', 'must be debug'); if (config.isDebugger !== true) fail('isDebugger', 'must be true'); - if (config.source !== 'remote') fail('source', 'must be remote'); - if (config.gameserver !== startupConfig.gameserver) fail('gameserver', 'must equal startupConfig remote entry'); + if (config.source !== startupConfig.configSource) fail('source', 'must equal startupConfig.configSource'); + const configEntry = startupConfig.configSource === 'local' ? startupConfig.localConfigResource : startupConfig.gameserver; + if (config.gameserver !== configEntry) fail('gameserver', 'must equal startupConfig selected entry'); if (!config.remoteConfig || typeof config.remoteConfig.getValue !== 'function') fail('remoteConfig', 'must provide memory view'); if (!config.rawConfig || config.rawConfig !== config.remoteConfig.raw) fail('rawConfig', 'must share remoteConfig.raw'); diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts.meta new file mode 100644 index 0000000..5fccd1e --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/local-startup.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "1d920343-a60b-4b40-8eba-f29b4a7d53fa", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts index b19c9a1..8b72a41 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts @@ -13,6 +13,10 @@ export interface DebugAccount { export interface StartupConfig { readonly mode: RuntimeMode; + /** 配置内容来源;local 仅允许 debug,独立于 WebSocket 服务器选择。 */ + readonly configSource: 'remote' | 'local'; + /** resources 下的 txt 路径,不含扩展名。 */ + readonly localConfigResource: string; readonly gameserver: string; readonly useLocalServer: boolean; readonly localServers: readonly string[]; @@ -21,7 +25,10 @@ export interface StartupConfig { /** 唯一发布配置入口;mode 与服务器选择互相独立。 */ export const STARTUP_CONFIG: Readonly = Object.freeze({ mode: 'debug', - gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt', + // 开发时改为 local,编辑 assets/resources/dev-config/update_jsonv2.txt 后重新预览。 + configSource: 'local', + localConfigResource: 'dev-config/update_jsonv2', + gameserver: 'https://tsgames.daoqi88.cn/config_test/update_jsonv2.txt', useLocalServer: true, localServers: Object.freeze(['ws://127.0.0.1:3088']), }); @@ -46,6 +53,11 @@ export function validateLocalServer(value: string, field = 'localServers'): void export function validateStartupConfig(source: Readonly): Readonly { if (!source || typeof source !== 'object') invalid('source', 'must be an object'); const mode = resolveRuntimeMode(source.mode); + if (source.configSource !== 'remote' && source.configSource !== 'local') invalid('configSource', 'must be remote or local'); + if (source.configSource === 'local' && mode !== 'debug') invalid('configSource', 'local is only allowed in debug mode'); + if (typeof source.localConfigResource !== 'string' || !/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/.test(source.localConfigResource)) { + invalid('localConfigResource', 'must be a resources path without extension or traversal'); + } if (typeof source.useLocalServer !== 'boolean') invalid('useLocalServer', 'must be boolean'); if (typeof source.gameserver !== 'string' || source.gameserver.trim() !== source.gameserver) invalid('gameserver', 'must be http(s) URL'); let url: URL; @@ -53,7 +65,7 @@ export function validateStartupConfig(source: Readonly): Readonly if (url.protocol !== 'http:' && url.protocol !== 'https:') invalid('gameserver', 'must be http(s) URL'); if (!Array.isArray(source.localServers) || source.localServers.length === 0) invalid('localServers', 'must be a nonempty array'); for (let index = 0; index < source.localServers.length; index++) validateLocalServer(source.localServers[index], 'localServers[' + index + ']'); - return Object.freeze({mode, gameserver:source.gameserver, useLocalServer:source.useLocalServer, localServers:Object.freeze([...source.localServers])}); + return Object.freeze({mode, configSource:source.configSource, localConfigResource:source.localConfigResource, gameserver:source.gameserver, useLocalServer:source.useLocalServer, localServers:Object.freeze([...source.localServers])}); } /** 忠实旧 min_replaceAll 链:- → /,# → :,再包成 http://...txt。 */ diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts index 3e44ffa..d581246 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts @@ -51,29 +51,52 @@ function validateHierarchy(scope: ConfigObject, depth: number): void { for (const entry of list) validateHierarchy(requireObject(entry, listName), depth + 1); } -/** Preserve first loose identity match, truthy overrides and whole-object replacement. */ +/** + * Port of Game_Surface_3/12_Logic.js get_paravalue. Only GameData globals are + * replaced with the resolved identity argument; lookup order and truthiness stay exact. + * createRemoteConfig validates the downloaded JSON before exposing this reader. + */ +export function get_paravalue( + o_config: { readonly data?: Readonly | null }, + paraname: string, + identity: Readonly, +): unknown { + const {agentid, gameid, channelid, marketid} = identity; + let paravalue: unknown = null; + const do_get_para = (arylist: unknown, keyname: string, keyvalue: string | number): ConfigObject | null => { + if (arylist && keyname && keyvalue) { + const list = arylist as ConfigObject[]; + for (let i = 0; i < list.length; i++) { + if (list[i][keyname] == keyvalue) { + if (list[i][paraname]) paravalue = list[i][paraname]; + return list[i]; + } + } + } + return null; + }; + if (!o_config.data) return paravalue; + if (o_config.data[paraname]) paravalue = o_config.data[paraname]; + const _o_agent = do_get_para(o_config.data.agentlist, 'agentid', agentid); + if (!_o_agent) return paravalue; + const _o_game = do_get_para(_o_agent.gamelist, 'gameid', gameid); + if (!_o_game) return paravalue; + const _o_channel = do_get_para(_o_game.channellist, 'channelid', channelid); + if (!_o_channel) return paravalue; + const _o_market = do_get_para(_o_channel.marketlist, 'marketid', marketid); + if (!_o_market) return paravalue; + return paravalue; +} + +/** All consumers share get_paravalue; no separate hierarchy or override rules. */ export function createRemoteConfig(input: unknown, identity: Readonly): RemoteConfig { const raw = requireObject(input, 'root'); validateHierarchy(raw, 0); - const scopes = [raw]; - let scope = raw; - for (const [listName, key] of SCOPES) { - const list = scope[listName] as ConfigObject[] | undefined; - // Legacy do_get_para skips any falsy identity, including numeric marketid=0. - if (!list || !identity[key]) break; - const match = list.find(entry => entry[key] == identity[key]); - if (!match) break; - scopes.push(match); - scope = match; - } + const wrapper = {data: raw}; return Object.freeze({ raw, getValue(name: string): unknown { - let value: unknown = null; - for (const current of scopes) { - if (Object.prototype.hasOwnProperty.call(current, name) && current[name]) value = current[name]; - } - return value; + return get_paravalue(wrapper, name, identity); }, }); } diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts index 6a8c41b..7acb047 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts @@ -32,7 +32,8 @@ export interface RuntimeConfig { readonly isDebugger: boolean; readonly identity: Readonly; readonly servers: readonly string[]; - readonly source: 'remote'; + readonly source: 'remote' | 'local'; + /** 实际读取入口:remote 为最终 URL,local 为 resources 资源路径。 */ readonly gameserver: string; readonly rawConfig: RemoteConfig['raw']; readonly remoteConfig: RemoteConfig; @@ -44,6 +45,8 @@ export interface ResolveRuntimeConfigOptions { readonly win: NativeHost; readonly search: string; readonly fetcher: ConfigFetcher; + /** 由引擎层注入项目 txt 读取器;仅 local 模式需要,缺失直接报错。 */ + readonly localFetcher?: ConfigFetcher; readonly cacheBust?: () => string; } @@ -78,17 +81,21 @@ export async function resolveRuntimeConfig( const startupConfig = validateStartupConfig(options.startupConfig === undefined ? STARTUP_CONFIG : options.startupConfig); const identity = resolveHostIdentity(options); - const gameserver = resolveGameServer({ + // 先选择唯一来源;本地模式不读取或应用宿主的远程 gameconfig 覆盖。 + const local = startupConfig.configSource === 'local'; + const gameserver = local ? startupConfig.localConfigResource : resolveGameServer({ configured: startupConfig.gameserver, injectedGameConfig: resolveInjectedGameConfig(options), }); + const fetcher = local ? options.localFetcher : options.fetcher; + if (!fetcher) throw new ConfigFetchError('本地配置缺少 localFetcher'); let raw: unknown; try { - raw = await options.fetcher.fetch(gameserver, options.cacheBust); + raw = await fetcher.fetch(gameserver, options.cacheBust); } catch (error) { if (error instanceof ConfigFetchError || error instanceof ConfigParseError) throw error; - throw new ConfigFetchError(`远程配置抓取失败: ${gameserver}`); + throw new ConfigFetchError(`${local ? '本地配置读取失败' : '远程配置抓取失败'}: ${gameserver}`); } const remoteConfig = createRemoteConfig(raw, identity); const servers = startupConfig.useLocalServer ? startupConfig.localServers : parseUrlServers(remoteConfig); @@ -99,7 +106,7 @@ export async function resolveRuntimeConfig( isDebugger: startupConfig.mode === 'debug', identity, servers, - source: 'remote', + source: startupConfig.configSource, gameserver, rawConfig: remoteConfig.raw, remoteConfig, diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts.meta new file mode 100644 index 0000000..f664b2a --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "4e90f0c8-b271-40a4-9909-68aa8f7d5974", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts index 2dcf69e..56e2e53 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/sources/defaults.ts @@ -8,8 +8,9 @@ export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10; * gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。 */ export const BUILD_IDENTITY: ChannelIdentity = { - agentid: 'veRa0qrBf0df2K1G4de2tgfmVxB2jxpv', - channelid: 'FtJf073aa0d6rI1xD8J1Y42fINTm0ziK', + // This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel. + agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy', + channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG', gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl', marketid: 4, version: 1, // 数字 versionCode(联调时改成本地服要求的版本号) diff --git a/cocoscreator_projects/YouleNexus/assets/framework/net/wire-client.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/net/wire-client.ts.meta new file mode 100644 index 0000000..1307294 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/net/wire-client.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "613b0457-be57-44e3-a1fb-e5d086b69468", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts.meta new file mode 100644 index 0000000..2416586 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "4098ba4c-c444-459f-9427-0412f1a15bb6", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/connection-intent.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/connection-intent.ts.meta new file mode 100644 index 0000000..8b8ad20 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/connection-intent.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "1a6fd8e5-5d0c-4fbb-a035-ef3a4f90badd", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/game-host-adapter.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/game-host-adapter.ts.meta new file mode 100644 index 0000000..83e4702 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/game-host-adapter.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "92bf4247-2b4b-460d-8992-8922baf78df2", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/ready-gate.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/ready-gate.ts.meta new file mode 100644 index 0000000..1c6a98d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/ready-gate.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "ac23132b-03ac-47c4-b6f8-93602c94f080", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime-session.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime-session.ts.meta new file mode 100644 index 0000000..021ff3e --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime-session.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "3e0b81db-7986-47f4-9e23-fbc9b47d8a36", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts.meta new file mode 100644 index 0000000..756bd7e --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "9f495e9b-65ae-4b3e-b44c-9c9a91f6679f", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts.meta new file mode 100644 index 0000000..d5ae7e6 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "4f5169b6-e140-4a3a-9eed-e155fecf24df", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts.meta new file mode 100644 index 0000000..7c8be57 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "3b11f970-2e0d-4784-8d65-c0fcc5e4ac6a", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts.meta new file mode 100644 index 0000000..3644b3e --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "68da9cca-3c8e-400b-ba7e-539b49994ed4", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/selectors.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/selectors.ts.meta new file mode 100644 index 0000000..a270936 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/selectors.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "8eb5d9a1-cfd3-4df4-b311-c67cb7db4423", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation.meta new file mode 100644 index 0000000..ce03bbb --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "717f5c0e-b085-47f8-879c-1b605e4fc773", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/frame-renderer.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation/frame-renderer.ts.meta new file mode 100644 index 0000000..6db2fe9 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/frame-renderer.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "8650f2a7-9471-4a3e-bec9-0aecf6405cdb", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/login-policy.ts b/cocoscreator_projects/YouleNexus/assets/framework/presentation/login-policy.ts new file mode 100644 index 0000000..0d9a01c --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/login-policy.ts @@ -0,0 +1,42 @@ +import type { RemoteConfig } from '../config/remote-config.ts'; + +/** Defaults owned here reproduce 04_Data.js and Game_Surface_3 Debugger. */ +const LOGIN_DEFAULTS = Object.freeze({ deviceLogin:0, showVisitor:0, qqLogin:0, + avatarUrl:'https://projectimage.tscce.cn/image_132/', avatarRange:[100,10000] as readonly number[], + hiddenVisitor:true }); +export interface LoginPolicy { + deviceLogin:boolean; showVisitor:boolean; qqLogin:boolean; hiddenVisitor:boolean; + avatarUrl:string; avatarRange:readonly number[]; +} +function flag(value:unknown, name:string):boolean { + if (![0,1,false,true].includes(value as number)) throw Error(`Invalid login config ${name}`); + return Boolean(value); +} +export function resolveLoginPolicy(remote:RemoteConfig):LoginPolicy { + const hallValue=remote.getValue('hall_config'); + if (hallValue != null && (typeof hallValue!=='object' || Array.isArray(hallValue))) throw Error('Invalid hall_config'); + const hall=hallValue as Record | null; + // Legacy hall_config merge precedes hallConfigName top-level overrides. + const hallField=(name:'showVisitor'|'qqLogin')=> { + const override=remote.getValue(name); + if(override!=null) return override; + return hall && Object.prototype.hasOwnProperty.call(hall,name)?hall[name]:LOGIN_DEFAULTS[name]; + }; + const sourceField=(name:'deviceLogin'|'avatarUrl'|'avatarRange')=> { + const value=remote.getValue(name);return value==null?LOGIN_DEFAULTS[name]:value; + }; + const avatarUrl=sourceField('avatarUrl'),range=sourceField('avatarRange'); + if(typeof avatarUrl!=='string')throw Error('Invalid login config avatarUrl'); + if(!Array.isArray(range)||range.length!==2||range.some(v=>!Number.isInteger(v))||range[1]<=range[0])throw Error('Invalid login config avatarRange'); + return Object.freeze({deviceLogin:flag(sourceField('deviceLogin'),'deviceLogin'), + showVisitor:flag(hallField('showVisitor'),'showVisitor'),qqLogin:flag(hallField('qqLogin'),'qqLogin'), + hiddenVisitor:LOGIN_DEFAULTS.hiddenVisitor,avatarUrl,avatarRange:Object.freeze([...range])}); +} +/** Assignment order is significant: JumpWxAuth applies showVisitor/QQ after cache hiding. */ +export function loginButtons(policy:LoginPolicy, versionState:0|1, cached:boolean) { + const account=versionState===0&&cached; + let wechat=!policy.deviceLogin&&!account; + let visitor=!account; + if(versionState===0){visitor=false;if(policy.showVisitor){wechat=false;visitor=true;}} + return {wechat,visitor,phone:policy.deviceLogin&&!account,quick:false,qq:policy.qqLogin,account}; +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/login-policy.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation/login-policy.ts.meta new file mode 100644 index 0000000..4e6ce14 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/login-policy.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "fe78702a-5e02-4006-844c-15123d950d80", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/page-model.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation/page-model.ts.meta new file mode 100644 index 0000000..6196110 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/page-model.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "093506bf-570f-47f8-9f4d-0a87d78cba2e", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/platform-ui-controller.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation/platform-ui-controller.ts.meta new file mode 100644 index 0000000..1410f4d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/platform-ui-controller.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "024324ab-1239-4568-9b72-5f957e32ba15", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/presentation/ui-contracts.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/presentation/ui-contracts.ts.meta new file mode 100644 index 0000000..b17b110 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/presentation/ui-contracts.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "e6f7adfb-42be-43ed-a658-73aac741e2f2", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts.meta b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts.meta new file mode 100644 index 0000000..84d067d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "65fb20da-5c43-4171-a53a-66ae24890751", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts.meta new file mode 100644 index 0000000..264cf2e --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "0b2dae51-cba0-4d30-95f6-a6ca50bc2f83", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts index 3711fb0..119c6fa 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts @@ -42,7 +42,7 @@ export interface LoginAccountIdentity { export type DeviceLoginMode = | { readonly enabled: false } - | { readonly enabled: true; readonly telphone: string }; + | { readonly enabled: true; readonly telphone: string; readonly smmcode?: string }; export type LoginPlayerIdMode = | { readonly enabled: false } @@ -112,7 +112,11 @@ export function buildLoginRequest( const deviceLogin = requireRecord(deviceRecord.deviceLogin, '$.device.deviceLogin'); if (requireBoolean(deviceLogin.enabled, '$.device.deviceLogin.enabled')) { data.telphone = requireString(deviceLogin.telphone, '$.device.deviceLogin.telphone'); - data.telphoneAuto = true; + // Explicit phone verification (sharelogin) differs from cached device login. + if (hasOwn(deviceLogin, 'smmcode')) { + data.smmcode = requireString(deviceLogin.smmcode, '$.device.deviceLogin.smmcode'); + data.telphoneAuto = false; + } else data.telphoneAuto = true; } const loginPlayerId = requireRecord(deviceRecord.loginPlayerId, '$.device.loginPlayerId'); diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts.meta new file mode 100644 index 0000000..f38dbb0 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "e473d40d-15cb-4ba0-a9dd-2aee7db34288", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-contracts.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-contracts.ts.meta new file mode 100644 index 0000000..3bbe24d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-contracts.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "c39d248f-79e2-4e7e-935c-8640d7ba2c1d", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/validation.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/validation.ts.meta new file mode 100644 index 0000000..0245aec --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/validation.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "464618de-1031-4d9d-b47a-5abfc3884b2a", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/first-slice-routes.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/protocol/first-slice-routes.ts.meta new file mode 100644 index 0000000..a4edb99 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/first-slice-routes.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "396594e0-d025-4f59-8f09-55f5563149fc", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts.meta new file mode 100644 index 0000000..f152a7b --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "bc28a4de-e56a-4ff3-9793-4a358b7f40e1", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts.meta new file mode 100644 index 0000000..2b7eb2d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "d8033485-9f99-48f1-8c69-80173036f33c", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-entry.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-entry.ts.meta new file mode 100644 index 0000000..1f6a409 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-entry.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "7e0c41b9-1036-46cd-9401-98c4ae86794e", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-host.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-host.ts.meta new file mode 100644 index 0000000..2dde16c --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-host.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "f657f863-7e72-4697-827c-11240096d8be", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-module.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-module.ts.meta new file mode 100644 index 0000000..13ef97e --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-module.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "3ad6eade-cc0c-4971-bdfb-453f0016df2c", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-seat-mapper.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-seat-mapper.ts.meta new file mode 100644 index 0000000..1589867 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-seat-mapper.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "bd3872ba-100e-4b2a-838f-2098366df9fb", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts.meta new file mode 100644 index 0000000..d902197 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "03a2c16b-b059-4ef9-8c26-3fe3a9761720", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/platform-events.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/platform-events.ts.meta new file mode 100644 index 0000000..238e5ae --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/platform-events.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "a61c62b7-e973-4fb6-97be-69db3ed6fd90", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/snapshots.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/snapshots.ts.meta new file mode 100644 index 0000000..8989dac --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/snapshots.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "5e7d1166-152b-4697-a882-032da5c60f10", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime.meta new file mode 100644 index 0000000..1a67c14 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "f6eae679-99d7-45f3-b301-319b961625f1", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-session-host.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-session-host.ts.meta new file mode 100644 index 0000000..a5cc0f9 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-session-host.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "650981bd-d942-4bb9-8e68-3efb07dda4dd", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing.meta new file mode 100644 index 0000000..8c7ed76 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "f1d3ef88-5577-4be2-853b-27900231ff29", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts.meta b/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts.meta new file mode 100644 index 0000000..9d0e601 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "754c5d58-de20-4a2d-9a62-406126c1cf93", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/animations.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations.meta new file mode 100644 index 0000000..65c8ad8 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "2ade8276-e1d3-46ca-9c77-46b3c5f71413", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/LoginLoading.anim b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/LoginLoading.anim new file mode 100644 index 0000000..2871465 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/LoginLoading.anim @@ -0,0 +1,86 @@ +[ + { + "__type__": "cc.AnimationClip", + "_name": "LoginLoading", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "sample": 3, + "speed": 1, + "wrapMode": 2, + "enableTrsBlending": false, + "_duration": 1, + "_hash": 0, + "_tracks": [ + { + "__id__": 1 + } + ], + "_exoticAnimation": null, + "_events": [], + "_embeddedPlayers": [], + "_additiveSettings": { + "__id__": 6 + }, + "_auxiliaryCurveEntries": [] + }, + { + "__type__": "cc.animation.ObjectTrack", + "_binding": { + "__type__": "cc.animation.TrackBinding", + "path": { + "__id__": 2 + }, + "proxy": null + }, + "_channel": { + "__id__": 4 + } + }, + { + "__type__": "cc.animation.TrackPath", + "_paths": [ + { + "__id__": 3 + }, + "spriteFrame" + ] + }, + { + "__type__": "cc.animation.ComponentPath", + "component": "cc.Sprite" + }, + { + "__type__": "cc.animation.Channel", + "_curve": { + "__id__": 5 + } + }, + { + "__type__": "cc.ObjectCurve", + "_times": [ + 0, + 0.3333333333333333, + 0.6666666666666666 + ], + "_values": [ + { + "__uuid__": "41c0f42d-631a-4a3f-abf1-a36ed787e563@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "bb80821b-0b3f-460e-a74a-1b1a7eaad560@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + { + "__uuid__": "b39af9d6-4d70-43ca-a312-f993de9f961c@f9941", + "__expectedType__": "cc.SpriteFrame" + } + ] + }, + { + "__type__": "cc.AnimationClipAdditiveSettings", + "enabled": false, + "refClip": null + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/LoginLoading.anim.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/LoginLoading.anim.meta new file mode 100644 index 0000000..6ffc96e --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/animations/LoginLoading.anim.meta @@ -0,0 +1,13 @@ +{ + "ver": "2.0.4", + "importer": "animation-clip", + "imported": true, + "uuid": "d7f4b280-dc40-47c7-bfe5-9421350459fe", + "files": [ + ".bin" + ], + "subMetas": {}, + "userData": { + "name": "LoginLoading" + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab index 7d76acc..4def164 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab @@ -25,14 +25,14 @@ "_active": true, "_components": [ { - "__id__": 152 + "__id__": 166 }, { - "__id__": 154 + "__id__": 168 } ], "_prefab": { - "__id__": 156 + "__id__": 170 }, "_lpos": { "__type__": "cc.Vec3", @@ -85,59 +85,44 @@ "__id__": 27 }, { - "__id__": 35 + "__id__": 41 }, { - "__id__": 43 + "__id__": 49 }, { - "__id__": 51 + "__id__": 57 }, { - "__id__": 59 + "__id__": 65 }, { - "__id__": 67 + "__id__": 73 }, { - "__id__": 75 + "__id__": 81 }, { - "__id__": 83 + "__id__": 89 }, { - "__id__": 91 + "__id__": 97 }, { - "__id__": 99 - }, - { - "__id__": 107 - }, - { - "__id__": 115 - }, - { - "__id__": 123 - }, - { - "__id__": 131 - }, - { - "__id__": 139 + "__id__": 105 } ], "_active": true, "_components": [ { - "__id__": 147 + "__id__": 161 }, { - "__id__": 149 + "__id__": 163 } ], "_prefab": { - "__id__": 151 + "__id__": 165 }, "_lpos": { "__type__": "cc.Vec3", @@ -585,8 +570,8 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 84.51171875, - "height": 50.4 + "width": 63.37372911777421, + "height": 50.39999999999999 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -624,8 +609,8 @@ "_string": "label", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 40, - "_fontSize": 40, + "_actualFontSize": 31.625, + "_fontSize": 30, "_fontFamily": "Arial", "_lineHeight": 40, "_overflow": 0, @@ -681,9 +666,9 @@ }, "_alignFlags": 9, "_target": null, - "_left": 16, + "_left": 26.568994816112895, "_right": 0, - "_top": 5, + "_top": 5.0000000000000036, "_bottom": 0, "_horizontalCenter": 0, "_verticalCenter": 0, @@ -724,26 +709,80 @@ "_parent": { "__id__": 2 }, + "_children": [ + { + "__id__": 28 + } + ], + "_active": true, + "_components": [ + { + "__id__": 34 + }, + { + "__id__": 36 + }, + { + "__id__": 38 + } + ], + "_prefab": { + "__id__": 40 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -76.807, + "y": -207.5, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "check", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 27 + }, "_children": [], "_active": true, "_components": [ { - "__id__": 28 + "__id__": 29 }, { - "__id__": 30 - }, - { - "__id__": 32 + "__id__": 31 } ], "_prefab": { - "__id__": 34 + "__id__": 33 }, "_lpos": { "__type__": "cc.Vec3", - "x": -135, - "y": -207.5, + "x": 0, + "y": 0, "z": 0 }, "_lrot": { @@ -769,6 +808,92 @@ }, "_id": "" }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 28 + }, + "_enabled": true, + "__prefab": { + "__id__": 30 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 40, + "height": 40 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "b6bRc0cFBLhrtni64efWOT" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 28 + }, + "_enabled": true, + "__prefab": { + "__id__": 32 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "83e400a9-4b21-47e0-ab49-9ba2fe18ad54@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "a0BP4H5odKxZFOrqZfmFMw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "844Kue3d9DIq0010JH7pFx", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", @@ -779,7 +904,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 29 + "__id__": 35 }, "_contentSize": { "__type__": "cc.Size", @@ -807,7 +932,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 31 + "__id__": 37 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -852,7 +977,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 39 }, "_alignFlags": 16, "_target": null, @@ -860,7 +985,7 @@ "_right": 0, "_top": 0, "_bottom": 0, - "_horizontalCenter": -135, + "_horizontalCenter": -76.807, "_verticalCenter": 0, "_isAbsLeft": true, "_isAbsRight": true, @@ -887,8 +1012,6 @@ "__id__": 0 }, "fileId": "7bOUfugfZGRJARCABMOobI", - "instance": null, - "targetOverrides": null, "nestedPrefabInstanceRoots": null }, { @@ -903,21 +1026,21 @@ "_active": true, "_components": [ { - "__id__": 36 + "__id__": 42 }, { - "__id__": 38 + "__id__": 44 }, { - "__id__": 40 + "__id__": 46 } ], "_prefab": { - "__id__": 42 + "__id__": 48 }, "_lpos": { "__type__": "cc.Vec3", - "x": 35, + "x": 82.11, "y": -210.5, "z": 0 }, @@ -950,11 +1073,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 41 }, "_enabled": true, "__prefab": { - "__id__": 37 + "__id__": 43 }, "_contentSize": { "__type__": "cc.Size", @@ -978,11 +1101,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 41 }, "_enabled": true, "__prefab": { - "__id__": 39 + "__id__": 45 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1023,11 +1146,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 35 + "__id__": 41 }, "_enabled": true, "__prefab": { - "__id__": 41 + "__id__": 47 }, "_alignFlags": 16, "_target": null, @@ -1035,7 +1158,7 @@ "_right": 0, "_top": 0, "_bottom": 0, - "_horizontalCenter": 35, + "_horizontalCenter": 82.11, "_verticalCenter": 0, "_isAbsLeft": true, "_isAbsRight": true, @@ -1062,8 +1185,6 @@ "__id__": 0 }, "fileId": "aa6JBxpmNMqbBB+2K20Gpm", - "instance": null, - "targetOverrides": null, "nestedPrefabInstanceRoots": null }, { @@ -1078,17 +1199,17 @@ "_active": true, "_components": [ { - "__id__": 44 + "__id__": 50 }, { - "__id__": 46 + "__id__": 52 }, { - "__id__": 48 + "__id__": 54 } ], "_prefab": { - "__id__": 50 + "__id__": 56 }, "_lpos": { "__type__": "cc.Vec3", @@ -1125,11 +1246,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 49 }, "_enabled": true, "__prefab": { - "__id__": 45 + "__id__": 51 }, "_contentSize": { "__type__": "cc.Size", @@ -1153,11 +1274,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 49 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 53 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1198,11 +1319,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 43 + "__id__": 49 }, "_enabled": true, "__prefab": { - "__id__": 49 + "__id__": 55 }, "_alignFlags": 32, "_target": null, @@ -1253,17 +1374,17 @@ "_active": true, "_components": [ { - "__id__": 52 + "__id__": 58 }, { - "__id__": 54 + "__id__": 60 }, { - "__id__": 56 + "__id__": 62 } ], "_prefab": { - "__id__": 58 + "__id__": 64 }, "_lpos": { "__type__": "cc.Vec3", @@ -1300,11 +1421,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 57 }, "_enabled": true, "__prefab": { - "__id__": 53 + "__id__": 59 }, "_contentSize": { "__type__": "cc.Size", @@ -1328,11 +1449,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 57 }, "_enabled": true, "__prefab": { - "__id__": 55 + "__id__": 61 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1373,11 +1494,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 57 }, "_enabled": true, "__prefab": { - "__id__": 57 + "__id__": 63 }, "_alignFlags": 8, "_target": null, @@ -1428,17 +1549,17 @@ "_active": true, "_components": [ { - "__id__": 60 + "__id__": 66 }, { - "__id__": 62 + "__id__": 68 }, { - "__id__": 64 + "__id__": 70 } ], "_prefab": { - "__id__": 66 + "__id__": 72 }, "_lpos": { "__type__": "cc.Vec3", @@ -1475,11 +1596,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 65 }, "_enabled": true, "__prefab": { - "__id__": 61 + "__id__": 67 }, "_contentSize": { "__type__": "cc.Size", @@ -1503,11 +1624,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 65 }, "_enabled": true, "__prefab": { - "__id__": 63 + "__id__": 69 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1548,11 +1669,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 59 + "__id__": 65 }, "_enabled": true, "__prefab": { - "__id__": 65 + "__id__": 71 }, "_alignFlags": 16, "_target": null, @@ -1603,17 +1724,17 @@ "_active": true, "_components": [ { - "__id__": 68 + "__id__": 74 }, { - "__id__": 70 + "__id__": 76 }, { - "__id__": 72 + "__id__": 78 } ], "_prefab": { - "__id__": 74 + "__id__": 80 }, "_lpos": { "__type__": "cc.Vec3", @@ -1650,11 +1771,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 73 }, "_enabled": true, "__prefab": { - "__id__": 69 + "__id__": 75 }, "_contentSize": { "__type__": "cc.Size", @@ -1678,11 +1799,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 73 }, "_enabled": true, "__prefab": { - "__id__": 71 + "__id__": 77 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1723,11 +1844,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 67 + "__id__": 73 }, "_enabled": true, "__prefab": { - "__id__": 73 + "__id__": 79 }, "_alignFlags": 16, "_target": null, @@ -1778,17 +1899,17 @@ "_active": true, "_components": [ { - "__id__": 76 + "__id__": 82 }, { - "__id__": 78 + "__id__": 84 }, { - "__id__": 80 + "__id__": 86 } ], "_prefab": { - "__id__": 82 + "__id__": 88 }, "_lpos": { "__type__": "cc.Vec3", @@ -1825,11 +1946,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 81 }, "_enabled": true, "__prefab": { - "__id__": 77 + "__id__": 83 }, "_contentSize": { "__type__": "cc.Size", @@ -1853,11 +1974,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 81 }, "_enabled": true, "__prefab": { - "__id__": 79 + "__id__": 85 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1898,11 +2019,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 75 + "__id__": 81 }, "_enabled": true, "__prefab": { - "__id__": 81 + "__id__": 87 }, "_alignFlags": 16, "_target": null, @@ -1953,17 +2074,17 @@ "_active": true, "_components": [ { - "__id__": 84 + "__id__": 90 }, { - "__id__": 86 + "__id__": 92 }, { - "__id__": 88 + "__id__": 94 } ], "_prefab": { - "__id__": 90 + "__id__": 96 }, "_lpos": { "__type__": "cc.Vec3", @@ -2000,11 +2121,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 89 }, "_enabled": true, "__prefab": { - "__id__": 85 + "__id__": 91 }, "_contentSize": { "__type__": "cc.Size", @@ -2028,11 +2149,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 89 }, "_enabled": true, "__prefab": { - "__id__": 87 + "__id__": 93 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2073,11 +2194,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 83 + "__id__": 89 }, "_enabled": true, "__prefab": { - "__id__": 89 + "__id__": 95 }, "_alignFlags": 16, "_target": null, @@ -2128,17 +2249,17 @@ "_active": true, "_components": [ { - "__id__": 92 + "__id__": 98 }, { - "__id__": 94 + "__id__": 100 }, { - "__id__": 96 + "__id__": 102 } ], "_prefab": { - "__id__": 98 + "__id__": 104 }, "_lpos": { "__type__": "cc.Vec3", @@ -2175,11 +2296,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 97 }, "_enabled": true, "__prefab": { - "__id__": 93 + "__id__": 99 }, "_contentSize": { "__type__": "cc.Size", @@ -2203,11 +2324,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 97 }, "_enabled": true, "__prefab": { - "__id__": 95 + "__id__": 101 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2248,11 +2369,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 91 + "__id__": 97 }, "_enabled": true, "__prefab": { - "__id__": 97 + "__id__": 103 }, "_alignFlags": 16, "_target": null, @@ -2293,27 +2414,96 @@ }, { "__type__": "cc.Node", - "_name": "登录中遮罩", + "_name": "Logining", "_objFlags": 0, "__editorExtras__": {}, "_parent": { "__id__": 2 }, + "_children": [ + { + "__id__": 106 + }, + { + "__id__": 114 + }, + { + "__id__": 122 + }, + { + "__id__": 130 + }, + { + "__id__": 138 + }, + { + "__id__": 148 + } + ], + "_active": true, + "_components": [ + { + "__id__": 156 + }, + { + "__id__": 158 + } + ], + "_prefab": { + "__id__": 160 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "登录中遮罩", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 105 + }, "_children": [], "_active": true, "_components": [ { - "__id__": 100 + "__id__": 107 }, { - "__id__": 102 + "__id__": 109 }, { - "__id__": 104 + "__id__": 111 } ], "_prefab": { - "__id__": 106 + "__id__": 113 }, "_lpos": { "__type__": "cc.Vec3", @@ -2350,11 +2540,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 106 }, "_enabled": true, "__prefab": { - "__id__": 101 + "__id__": 108 }, "_contentSize": { "__type__": "cc.Size", @@ -2378,11 +2568,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 106 }, "_enabled": true, "__prefab": { - "__id__": 103 + "__id__": 110 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2423,11 +2613,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 99 + "__id__": 106 }, "_enabled": true, "__prefab": { - "__id__": 105 + "__id__": 112 }, "_alignFlags": 45, "_target": null, @@ -2472,23 +2662,23 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 105 }, "_children": [], "_active": true, "_components": [ { - "__id__": 108 + "__id__": 115 }, { - "__id__": 110 + "__id__": 117 }, { - "__id__": 112 + "__id__": 119 } ], "_prefab": { - "__id__": 114 + "__id__": 121 }, "_lpos": { "__type__": "cc.Vec3", @@ -2525,11 +2715,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 114 }, "_enabled": true, "__prefab": { - "__id__": 109 + "__id__": 116 }, "_contentSize": { "__type__": "cc.Size", @@ -2553,11 +2743,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 114 }, "_enabled": true, "__prefab": { - "__id__": 111 + "__id__": 118 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2598,11 +2788,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 107 + "__id__": 114 }, "_enabled": true, "__prefab": { - "__id__": 113 + "__id__": 120 }, "_alignFlags": 16, "_target": null, @@ -2647,23 +2837,23 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 105 }, "_children": [], "_active": true, "_components": [ { - "__id__": 116 + "__id__": 123 }, { - "__id__": 118 + "__id__": 125 }, { - "__id__": 120 + "__id__": 127 } ], "_prefab": { - "__id__": 122 + "__id__": 129 }, "_lpos": { "__type__": "cc.Vec3", @@ -2700,11 +2890,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 122 }, "_enabled": true, "__prefab": { - "__id__": 117 + "__id__": 124 }, "_contentSize": { "__type__": "cc.Size", @@ -2728,11 +2918,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 122 }, "_enabled": true, "__prefab": { - "__id__": 119 + "__id__": 126 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2773,11 +2963,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 115 + "__id__": 122 }, "_enabled": true, "__prefab": { - "__id__": 121 + "__id__": 128 }, "_alignFlags": 16, "_target": null, @@ -2822,27 +3012,27 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 105 }, "_children": [], "_active": true, "_components": [ { - "__id__": 124 + "__id__": 131 }, { - "__id__": 126 + "__id__": 133 }, { - "__id__": 128 + "__id__": 135 } ], "_prefab": { - "__id__": 130 + "__id__": 137 }, "_lpos": { "__type__": "cc.Vec3", - "x": 67.5, + "x": 53.645, "y": -46, "z": 0 }, @@ -2875,16 +3065,16 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 130 }, "_enabled": true, "__prefab": { - "__id__": 125 + "__id__": 132 }, "_contentSize": { "__type__": "cc.Size", - "width": 84.51171875, - "height": 50.4 + "width": 63.37372911777421, + "height": 50.39999999999999 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -2903,27 +3093,27 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 130 }, "_enabled": true, "__prefab": { - "__id__": 127 + "__id__": 134 }, "_customMaterial": null, "_srcBlendFactor": 2, "_dstBlendFactor": 4, "_color": { "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, + "r": 49, + "g": 10, + "b": 10, "a": 255 }, "_string": "label", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 40, - "_fontSize": 40, + "_actualFontSize": 31.625, + "_fontSize": 30, "_fontFamily": "Arial", "_lineHeight": 40, "_overflow": 0, @@ -2971,11 +3161,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 123 + "__id__": 130 }, "_enabled": true, "__prefab": { - "__id__": 129 + "__id__": 136 }, "_alignFlags": 16, "_target": null, @@ -2983,7 +3173,7 @@ "_right": 0, "_top": 0, "_bottom": 0, - "_horizontalCenter": 67.5, + "_horizontalCenter": 53.645, "_verticalCenter": 0, "_isAbsLeft": true, "_isAbsRight": true, @@ -3020,27 +3210,30 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 105 }, "_children": [], "_active": true, "_components": [ { - "__id__": 132 + "__id__": 139 }, { - "__id__": 134 + "__id__": 141 }, { - "__id__": 136 + "__id__": 143 + }, + { + "__id__": 145 } ], "_prefab": { - "__id__": 138 + "__id__": 147 }, "_lpos": { "__type__": "cc.Vec3", - "x": 73, + "x": 59.144, "y": -106, "z": 0 }, @@ -3073,11 +3266,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 138 }, "_enabled": true, "__prefab": { - "__id__": 133 + "__id__": 140 }, "_contentSize": { "__type__": "cc.Size", @@ -3101,11 +3294,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 138 }, "_enabled": true, "__prefab": { - "__id__": 135 + "__id__": 142 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3146,11 +3339,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 131 + "__id__": 138 }, "_enabled": true, "__prefab": { - "__id__": 137 + "__id__": 144 }, "_alignFlags": 16, "_target": null, @@ -3158,7 +3351,7 @@ "_right": 0, "_top": 0, "_bottom": 0, - "_horizontalCenter": 73, + "_horizontalCenter": 59.144, "_verticalCenter": 0, "_isAbsLeft": true, "_isAbsRight": true, @@ -3176,6 +3369,35 @@ "__type__": "cc.CompPrefabInfo", "fileId": "uEwYXwzNJK+j9XvosozgjD" }, + { + "__type__": "cc.Animation", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 138 + }, + "_enabled": true, + "__prefab": { + "__id__": 146 + }, + "playOnLoad": false, + "_clips": [ + { + "__uuid__": "d7f4b280-dc40-47c7-bfe5-9421350459fe", + "__expectedType__": "cc.AnimationClip" + } + ], + "_defaultClip": { + "__uuid__": "d7f4b280-dc40-47c7-bfe5-9421350459fe", + "__expectedType__": "cc.AnimationClip" + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "b7dBKCl1ZCMZL20vV4j20y" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -3195,23 +3417,23 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 105 }, "_children": [], "_active": true, "_components": [ { - "__id__": 140 + "__id__": 149 }, { - "__id__": 142 + "__id__": 151 }, { - "__id__": 144 + "__id__": 153 } ], "_prefab": { - "__id__": 146 + "__id__": 155 }, "_lpos": { "__type__": "cc.Vec3", @@ -3248,11 +3470,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 148 }, "_enabled": true, "__prefab": { - "__id__": 141 + "__id__": 150 }, "_contentSize": { "__type__": "cc.Size", @@ -3276,11 +3498,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 148 }, "_enabled": true, "__prefab": { - "__id__": 143 + "__id__": 152 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -3321,11 +3543,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 139 + "__id__": 148 }, "_enabled": true, "__prefab": { - "__id__": 145 + "__id__": 154 }, "_alignFlags": 16, "_target": null, @@ -3364,6 +3586,83 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 105 + }, + "_enabled": true, + "__prefab": { + "__id__": 157 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "basldfhxdJQrqpNsve4d7+" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 105 + }, + "_enabled": true, + "__prefab": { + "__id__": 159 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 687.479, + "_originalHeight": 180, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "06PjqbFMhCfYvaP6IN87ad" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "00RsNEn29JXZu+dl3t3qBQ", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", @@ -3374,7 +3673,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 148 + "__id__": 162 }, "_contentSize": { "__type__": "cc.Size", @@ -3402,7 +3701,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 150 + "__id__": 164 }, "_alignFlags": 45, "_target": null, @@ -3437,8 +3736,6 @@ "__id__": 0 }, "fileId": "d4P87gqa5AIJltQ5D4Vthy", - "instance": null, - "targetOverrides": null, "nestedPrefabInstanceRoots": null }, { @@ -3451,7 +3748,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 153 + "__id__": 167 }, "_contentSize": { "__type__": "cc.Size", @@ -3479,7 +3776,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 155 + "__id__": 169 }, "_alignFlags": 45, "_target": null, diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protol.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protol.prefab index 42f7a1e..d72bee5 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protol.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protol.prefab @@ -25,14 +25,14 @@ "_active": true, "_components": [ { - "__id__": 40 + "__id__": 50 }, { - "__id__": 42 + "__id__": 52 } ], "_prefab": { - "__id__": 44 + "__id__": 54 }, "_lpos": { "__type__": "cc.Vec3", @@ -61,7 +61,7 @@ "y": 0, "z": 0 }, - "_id": "acrZGZbf1NerIwhXMce5C9" + "_id": "" }, { "__type__": "cc.Node", @@ -79,23 +79,23 @@ "__id__": 11 }, { - "__id__": 19 - }, - { - "__id__": 27 - } - ], - "_active": true, - "_components": [ - { - "__id__": 35 + "__id__": 21 }, { "__id__": 37 } ], + "_active": true, + "_components": [ + { + "__id__": 45 + }, + { + "__id__": 47 + } + ], "_prefab": { - "__id__": 39 + "__id__": 49 }, "_lpos": { "__type__": "cc.Vec3", @@ -124,7 +124,7 @@ "y": 0, "z": 0 }, - "_id": "e6VeYSc7NPirZdAaN4p44e" + "_id": "" }, { "__type__": "cc.Node", @@ -177,7 +177,7 @@ "y": 0, "z": 0 }, - "_id": "f1pHSoD4dILLRVFTIObRtT" + "_id": "" }, { "__type__": "cc.UITransform", @@ -201,7 +201,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "a9XLkwiTVFH68tpCqSosju" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -246,7 +246,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "15y0dNzphPKaTgoMZ3YkkU" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -282,7 +282,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "8b8p/boZJEqocxUC1G22sv" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -320,10 +320,13 @@ }, { "__id__": 16 + }, + { + "__id__": 18 } ], "_prefab": { - "__id__": 18 + "__id__": 20 }, "_lpos": { "__type__": "cc.Vec3", @@ -352,7 +355,7 @@ "y": 0, "z": 0 }, - "_id": "03KAkUqStGDaHp3GFOQbYR" + "_id": "" }, { "__type__": "cc.UITransform", @@ -376,7 +379,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "feVdaeICBA07uZAmLtAU07" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -421,7 +424,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "b6GW+6L8VHILl5D4tnXajn" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -457,12 +460,30 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "5dO4+lWM1I74eFWhnD+bgo" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", "fileId": "MW2xRLaeE9itOcgHj0p+dQ" }, + { + "__type__": "cc.BlockInputEvents", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 19 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "4fv3LfgR5DPIoVUfROUOzr" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -478,32 +499,39 @@ }, { "__type__": "cc.Node", - "_name": "协议内容", + "_name": "协议滚动", "_objFlags": 0, "__editorExtras__": {}, "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 22 + } + ], "_active": true, "_components": [ { - "__id__": 20 + "__id__": 28 }, { - "__id__": 22 + "__id__": 30 }, { - "__id__": 24 + "__id__": 32 + }, + { + "__id__": 34 } ], "_prefab": { - "__id__": 26 + "__id__": 36 }, "_lpos": { "__type__": "cc.Vec3", "x": -5.5, - "y": -208.5, + "y": -5, "z": 0 }, "_lrot": { @@ -527,7 +555,57 @@ "y": 0, "z": 0 }, - "_id": "fcOwSwvSpMlaZQPDjbA1f7" + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "协议内容", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 21 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 23 + }, + { + "__id__": 25 + } + ], + "_prefab": { + "__id__": 27 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -203.5, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" }, { "__type__": "cc.UITransform", @@ -535,11 +613,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 22 }, "_enabled": true, "__prefab": { - "__id__": 21 + "__id__": 24 }, "_contentSize": { "__type__": "cc.Size", @@ -551,7 +629,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "e4LJQjJ6FE6LjQy5yIP382" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -563,11 +641,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 22 }, "_enabled": true, "__prefab": { - "__id__": 23 + "__id__": 26 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -596,48 +674,12 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "bfkfehGNlIxp/fTJn+oOTB" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", "fileId": "1jOdeOrfg2vu2QJl+s+VeA" }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 19 - }, - "_enabled": true, - "__prefab": { - "__id__": 25 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": -5.5, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "32AaRKNa5IUaPeF5jIfKTl" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "D1Qfc6QTWy0vKDGsx+qLQg" - }, { "__type__": "cc.PrefabInfo", "root": { @@ -651,6 +693,146 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 21 + }, + "_enabled": true, + "__prefab": { + "__id__": 29 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 729, + "height": 590 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "0bFbdo0btAnLpeg5XLVeih" + }, + { + "__type__": "cc.Mask", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 21 + }, + "_enabled": true, + "__prefab": { + "__id__": 31 + }, + "_type": 0, + "_inverted": false, + "_segments": 64, + "_alphaThreshold": 0.1, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "8bWDB4GxZEjZGyBHlWT8Ry" + }, + { + "__type__": "cc.Graphics", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 21 + }, + "_enabled": true, + "__prefab": { + "__id__": 33 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_lineWidth": 1, + "_strokeColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_lineJoin": 2, + "_lineCap": 0, + "_fillColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 0 + }, + "_miterLimit": 10, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "d5mkJTZEZFOru7Ifoe3YX/" + }, + { + "__type__": "cc.ScrollView", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 21 + }, + "_enabled": true, + "__prefab": { + "__id__": 35 + }, + "bounceDuration": 1, + "brake": 0.5, + "elastic": true, + "inertia": true, + "horizontal": false, + "vertical": true, + "cancelInnerEvents": true, + "scrollEvents": [], + "_content": { + "__id__": 22 + }, + "_horizontalScrollBar": null, + "_verticalScrollBar": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "cfxheTMxVE45LJ6nYtvr4t" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "edhlQTi55MNo1ZWcRw9+qA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.Node", "_name": "Spirit253", @@ -663,17 +845,17 @@ "_active": true, "_components": [ { - "__id__": 28 + "__id__": 38 }, { - "__id__": 30 + "__id__": 40 }, { - "__id__": 32 + "__id__": 42 } ], "_prefab": { - "__id__": 34 + "__id__": 44 }, "_lpos": { "__type__": "cc.Vec3", @@ -702,7 +884,7 @@ "y": 0, "z": 0 }, - "_id": "770rm+dOJPVJdLQnhaoDaZ" + "_id": "" }, { "__type__": "cc.UITransform", @@ -710,11 +892,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 29 + "__id__": 39 }, "_contentSize": { "__type__": "cc.Size", @@ -726,7 +908,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "02BeJasixOrpU3LpuhrkT+" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -738,11 +920,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 31 + "__id__": 41 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -771,7 +953,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "f372YRc0pCR5LhJkRDp5mj" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -783,11 +965,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 43 }, "_alignFlags": 16, "_target": null, @@ -807,7 +989,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "46JhwhkSdK0KV8Lt3p+HOy" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -836,7 +1018,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 36 + "__id__": 46 }, "_contentSize": { "__type__": "cc.Size", @@ -848,7 +1030,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "07nq/yMyxNHq4xU2y77PJH" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -864,7 +1046,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 38 + "__id__": 48 }, "_alignFlags": 45, "_target": null, @@ -884,7 +1066,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "fdaBoOjw5KN4+Xi8tr/T8R" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -913,7 +1095,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 41 + "__id__": 51 }, "_contentSize": { "__type__": "cc.Size", @@ -925,7 +1107,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "52OXNuOopHTL574Hc5gszK" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -941,7 +1123,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 43 + "__id__": 53 }, "_alignFlags": 45, "_target": null, @@ -961,7 +1143,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "73lGoAY61JlYZxdhx3MnbQ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -977,7 +1159,6 @@ }, "fileId": "xSY2ymJa1y54ctanvQH+WQ", "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null + "targetOverrides": null } -] +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab index 6650630..0403efb 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab @@ -76,9 +76,6 @@ }, { "__id__": 29 - }, - { - "__id__": 37 } ], "_active": true, @@ -687,21 +684,25 @@ "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 30 + } + ], "_active": true, "_components": [ { - "__id__": 30 + "__id__": 38 }, { - "__id__": 32 + "__id__": 40 }, { - "__id__": 34 + "__id__": 42 } ], "_prefab": { - "__id__": 36 + "__id__": 44 }, "_lpos": { "__type__": "cc.Vec3", @@ -732,156 +733,34 @@ }, "_id": "" }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 29 - }, - "_enabled": true, - "__prefab": { - "__id__": 31 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 208, - "height": 59 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "t4s+2kLm2F//czXxT9Xo8A" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 29 - }, - "_enabled": true, - "__prefab": { - "__id__": 33 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "d313aa40-9397-4b44-8100-65ea389233f3@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 0, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "/rrApudp/UA/nOAEHDamNA" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 29 - }, - "_enabled": true, - "__prefab": { - "__id__": 35 - }, - "_alignFlags": 16, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 671.37, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "v+C5/D9J96sTVhcAhSrUng" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "QQkO56LzjjkpTd+xNv6zcA", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, { "__type__": "cc.Node", "_name": "Spirit3104", "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 29 }, "_children": [], "_active": true, "_components": [ { - "__id__": 38 + "__id__": 31 }, { - "__id__": 40 + "__id__": 33 }, { - "__id__": 42 + "__id__": 35 } ], "_prefab": { - "__id__": 44 + "__id__": 37 }, "_lpos": { "__type__": "cc.Vec3", - "x": 671.87, - "y": 311.5, + "x": 0, + "y": 0, "z": 0 }, "_lrot": { @@ -913,16 +792,16 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 37 + "__id__": 30 }, "_enabled": true, "__prefab": { - "__id__": 39 + "__id__": 32 }, "_contentSize": { "__type__": "cc.Size", - "width": 63.005419342885375, - "height": 17.64 + "width": 81.03130596899703, + "height": 22.680000000000003 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -941,11 +820,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 37 + "__id__": 30 }, "_enabled": true, "__prefab": { - "__id__": 41 + "__id__": 34 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -960,10 +839,10 @@ "_string": "Spirit3104", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 14.758333333333333, - "_fontSize": 14, + "_actualFontSize": 18.975, + "_fontSize": 18, "_fontFamily": "Arial", - "_lineHeight": 14, + "_lineHeight": 18, "_overflow": 0, "_enableWrapText": true, "_font": null, @@ -1009,19 +888,19 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 37 + "__id__": 30 }, "_enabled": true, "__prefab": { - "__id__": 43 + "__id__": 36 }, - "_alignFlags": 16, + "_alignFlags": 18, "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 671.87, + "_left": 63.484347015501484, + "_right": 63.484347015501484, + "_top": 18.159999999999997, + "_bottom": 18.159999999999997, + "_horizontalCenter": 0, "_verticalCenter": 0, "_isAbsLeft": true, "_isAbsRight": true, @@ -1029,8 +908,8 @@ "_isAbsBottom": true, "_isAbsHorizontalCenter": true, "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, + "_originalWidth": 81.03130596899703, + "_originalHeight": 22.680000000000003, "_alignMode": 2, "_lockFlags": 0, "_id": "" @@ -1052,6 +931,128 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 29 + }, + "_enabled": true, + "__prefab": { + "__id__": 39 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 208, + "height": 59 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "t4s+2kLm2F//czXxT9Xo8A" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 29 + }, + "_enabled": true, + "__prefab": { + "__id__": 41 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "d313aa40-9397-4b44-8100-65ea389233f3@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "/rrApudp/UA/nOAEHDamNA" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 29 + }, + "_enabled": true, + "__prefab": { + "__id__": 43 + }, + "_alignFlags": 16, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 671.37, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "v+C5/D9J96sTVhcAhSrUng" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "QQkO56LzjjkpTd+xNv6zcA", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer29_PhoneInfo.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer29_PhoneInfo.prefab index 02305f3..80f1cb3 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer29_PhoneInfo.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer29_PhoneInfo.prefab @@ -32,10 +32,13 @@ }, { "__id__": 320 + }, + { + "__id__": 322 } ], "_prefab": { - "__id__": 322 + "__id__": 324 }, "_lpos": { "__type__": "cc.Vec3", @@ -64,7 +67,7 @@ "y": 0, "z": 0 }, - "_id": "0dPWQwe71CaaWxFukC0dmh" + "_id": "" }, { "__type__": "cc.Node", @@ -160,7 +163,7 @@ "y": 0, "z": 0 }, - "_id": "86DrwwFWFLnpMhnKei8SJB" + "_id": "" }, { "__type__": "cc.Node", @@ -171,7 +174,7 @@ "__id__": 2 }, "_children": [], - "_active": false, + "_active": true, "_components": [ { "__id__": 4 @@ -213,7 +216,7 @@ "y": 0, "z": 0 }, - "_id": "7e/bY1uCVF5ZFp4QVhiNPw" + "_id": "" }, { "__type__": "cc.UITransform", @@ -237,7 +240,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "5989hTVRxCRo43NyQedMpF" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -282,7 +285,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "683oZQuadHQpmE0P1ziIiW" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -318,7 +321,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "8bBAPPl1dK6olXY7EYnOrM" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -388,7 +391,7 @@ "y": 0, "z": 0 }, - "_id": "9aoFkdrG9ErKTNmXj812dZ" + "_id": "" }, { "__type__": "cc.UITransform", @@ -412,7 +415,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "7ecVAjjZ5HZIyWUsSpKzv/" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -457,7 +460,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "d9oWjMnlBGXqqfkzm+P3UV" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -493,7 +496,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "73N6D7U7pMuZLW7yZuS+ef" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -563,7 +566,7 @@ "y": 0, "z": 0 }, - "_id": "53jWyGF5NOtajgp/LveP18" + "_id": "" }, { "__type__": "cc.UITransform", @@ -587,7 +590,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "43Q1lbFTZIoLlTTmYFZTsX" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -632,7 +635,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "e72EamOwRAZqckjgnGHyR2" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -668,7 +671,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "6eCEM1ytpPzqou/LABRGYO" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -738,7 +741,7 @@ "y": 0, "z": 0 }, - "_id": "1a2gKhsSZCP5hW12/voZEl" + "_id": "" }, { "__type__": "cc.UITransform", @@ -762,7 +765,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "f5WtI5yDFB3Ip1kj1fJsAc" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -807,7 +810,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "d4HpMkwZ1My7tFucG/dm5i" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -843,7 +846,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "394zAowV5HabKbqzz0HaM6" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -913,7 +916,7 @@ "y": 0, "z": 0 }, - "_id": "4ewPrUGYtAwYwMCK8/4jsb" + "_id": "" }, { "__type__": "cc.UITransform", @@ -937,7 +940,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "08AjAOPNFDPYihcKySCYB2" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -982,7 +985,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "76ujKeDbBPTIB5RxMawiCG" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1018,7 +1021,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "7dJe6eMqlPo5F07daSEMVR" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1088,7 +1091,7 @@ "y": 0, "z": 0 }, - "_id": "9fU1XNIgJDmLNB2oCfl75V" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1112,7 +1115,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "b7r6oHttdOYKCMCwOE45+R" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1157,7 +1160,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "9fQkvrVBBNdJiGqRcRXJO+" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1193,7 +1196,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "f8FfupKAVMOq5bvsEuUUql" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1263,7 +1266,7 @@ "y": 0, "z": 0 }, - "_id": "52lXeZ+oxFJZW/2aSn4pGs" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1287,7 +1290,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "4fE+ftaAdLXJoLhcezPm3w" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1332,7 +1335,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "051/PLFkZP+IGCYKxADK98" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1368,7 +1371,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "36mPfX5CZOn5dnLRg+XCgz" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1438,7 +1441,7 @@ "y": 0, "z": 0 }, - "_id": "5c+tsK7mVPT5sUHS/xPBaX" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1462,7 +1465,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "09sT0ltspO9KHQwcfi8ziC" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1507,7 +1510,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "2f2n1+Qn9B5ImRCrpNgjdm" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1543,7 +1546,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "e4tYm2UQtAIZmzCc+Xu3pw" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1613,7 +1616,7 @@ "y": 0, "z": 0 }, - "_id": "7eHwHzVzBLzpletxq+jP4c" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1637,7 +1640,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "0aLrSq37xMcIno0s1pJkT4" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1682,7 +1685,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "b01/qWg+FEfpQs9dFCwH92" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1718,7 +1721,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "9cdZn2n19B3YmHQzMZXgjm" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1788,7 +1791,7 @@ "y": 0, "z": 0 }, - "_id": "aa1/EBoUlDf7Fhqw0ZRRtk" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1812,7 +1815,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "f294EtQppC97JWm0co96ej" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1857,7 +1860,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "21wSO5r0JD7qEeA0/f1KlA" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1893,7 +1896,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "b9UOxfC0JPcZG5gMpOewdy" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -1963,7 +1966,7 @@ "y": 0, "z": 0 }, - "_id": "728OYDDFdEfL6aSJ8BUJA0" + "_id": "" }, { "__type__": "cc.UITransform", @@ -1987,7 +1990,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "659TR3AuhHcqES+wU7ZpDg" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2032,7 +2035,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "0cgHK/G+dFF7nrFqgA5q8U" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2068,7 +2071,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "e7e+1BmG9EuqCjCsK0B4fZ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2138,7 +2141,7 @@ "y": 0, "z": 0 }, - "_id": "62wF9JzidKZaQB+QgAZh3f" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2162,7 +2165,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "3fPUohOw5L8J35Iqd6TSW+" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2207,7 +2210,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "72la0KbCVDzZa9wEOrHuJN" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2243,7 +2246,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "19L36fWjVI7ZcWHi+j3S+0" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2313,7 +2316,7 @@ "y": 0, "z": 0 }, - "_id": "31NUUmgnNDXqXHJEy9e4Mp" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2337,7 +2340,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "4d2lX7kiVBeJ+CgpXKwztk" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2382,7 +2385,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "dasR3WOMJN6pwuUh4RGcuI" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2418,7 +2421,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "baO9WLid1APLlFVd/k5dG8" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2488,7 +2491,7 @@ "y": 0, "z": 0 }, - "_id": "70vHlSs4NFeqdqH15EJ0so" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2512,7 +2515,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "cbwdGyC+hPG75NBnKs/Jep" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2557,7 +2560,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "38VhFZLt9OZr5d5ZbFa6PG" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2593,7 +2596,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "a7FXh0c2dNQrhUDmXf6WvH" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2663,7 +2666,7 @@ "y": 0, "z": 0 }, - "_id": "95VStiTktL/a3m8mN0eY+J" + "_id": "" }, { "__type__": "cc.UITransform", @@ -2687,7 +2690,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "e0tCsXeYBIlqnTh3cq3TeY" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2732,7 +2735,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "90PJXf4WJAcYU6bQmK/Emj" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2768,7 +2771,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "7cG9TDPVVDmqi7cyx4pEht" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2809,7 +2812,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "e5pU3pIMlPFIpj7ZsZeeii" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2827,7 +2830,7 @@ "__prefab": { "__id__": 126 }, - "_alignFlags": 45, + "_alignFlags": 18, "_target": null, "_left": 0, "_right": 0, @@ -2843,9 +2846,9 @@ "_isAbsVerticalCenter": true, "_originalWidth": 1600, "_originalHeight": 720, - "_alignMode": 2, + "_alignMode": 1, "_lockFlags": 0, - "_id": "9bVnexdDdJeYOXtk4J1K+Q" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -2982,7 +2985,7 @@ "y": 0, "z": 0 }, - "_id": "8dgbqBz3dD3Lha2yc4c9qD" + "_id": "" }, { "__type__": "cc.Node", @@ -3035,7 +3038,7 @@ "y": 0, "z": 0 }, - "_id": "39RXBuBihLQKlCruhksgFN" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3059,7 +3062,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "4aV0pJ4XhOtKdicKV5uNN1" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3104,7 +3107,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "23dd90UtdJVKD+5SYjGCda" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3140,7 +3143,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "d77ZF1XEZCA6h9JpbBWZGn" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3210,7 +3213,7 @@ "y": 0, "z": 0 }, - "_id": "8fuVQGqBBMxqrCMcK2ZoRM" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3234,7 +3237,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "89nPGxAI5AlKpyNJUnFe3f" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3279,7 +3282,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "99OFhaoRtBzYuHM8iERlKM" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3315,7 +3318,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "2dhUBG3yVMbp689pTvfeNq" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3385,7 +3388,7 @@ "y": 0, "z": 0 }, - "_id": "16nothxgdO2IrdT2HABlbh" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3409,7 +3412,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "21SRUm5UhHx7lZgk1R4JX+" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3454,7 +3457,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "3f4j083+lOgqs5W8LnaNc7" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3490,7 +3493,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "d9B422Ly1KNJpeMROXwDBv" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3560,7 +3563,7 @@ "y": 0, "z": 0 }, - "_id": "8bW8+AD1ZDAb5C4QnHpGw0" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3584,7 +3587,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "11NbKrGv9MaILj5vMbhHHU" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3629,7 +3632,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "67MkGpQ0hF574JLahXOgBU" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3665,7 +3668,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "05XE9oJg5CeIWIr+MDJG8s" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3735,7 +3738,7 @@ "y": 0, "z": 0 }, - "_id": "84f/m9axpNALfUMiZ3IGWS" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3759,7 +3762,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "37LlmtIrlHXoxsmNr2iaSX" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3804,7 +3807,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "40G+/3joBC86mEnn2z5yxf" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3840,7 +3843,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "093oOLNHRIcpWhGSOz0vWe" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3910,7 +3913,7 @@ "y": 0, "z": 0 }, - "_id": "e9rkaNax1HD7cNqTXQDZ2i" + "_id": "" }, { "__type__": "cc.UITransform", @@ -3934,7 +3937,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "2dI383bndIYpmcieOf3S1U" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -3979,7 +3982,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "5eoE1afCJA5JPI/ISgbZBF" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4015,7 +4018,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "442t4nVztHhprHagKBPaK2" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4085,7 +4088,7 @@ "y": 0, "z": 0 }, - "_id": "97Wg7UCppAnraeyh7YB5W5" + "_id": "" }, { "__type__": "cc.UITransform", @@ -4109,7 +4112,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "34oKLLk0FFK5qpk3M8s9xD" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4154,7 +4157,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "99r4pIyJZHQZpaenz0RVLQ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4190,7 +4193,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "d9j7DamKpJHrPPRLsAQRMn" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4260,7 +4263,7 @@ "y": 0, "z": 0 }, - "_id": "29Z/fwQuhJ9Z6voO/ExPK0" + "_id": "" }, { "__type__": "cc.UITransform", @@ -4284,7 +4287,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "78kEqA0IlGCqDFMrKrOenF" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4329,7 +4332,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "621ebtLyFG95Brdbg4Q0yx" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4365,7 +4368,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "afW0gxqslL9qSFWlYZYamZ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4435,7 +4438,7 @@ "y": 0, "z": 0 }, - "_id": "c5i4asRitEfYkh77k7J6Dp" + "_id": "" }, { "__type__": "cc.UITransform", @@ -4459,7 +4462,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "77BvNPgD9OI68JL0IcyrQz" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4504,7 +4507,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "10pK+msrFIRZwcGMg2r80E" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4540,7 +4543,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "95R1f5CNZI7Y1Z1bXO1A24" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4610,7 +4613,7 @@ "y": 0, "z": 0 }, - "_id": "afAceb/oZNRbc7BDg/DeVk" + "_id": "" }, { "__type__": "cc.UITransform", @@ -4626,7 +4629,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 63.041015625, + "width": 63.005419342885375, "height": 17.64 }, "_anchorPoint": { @@ -4634,7 +4637,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "84qBz2a21C76+LwY2n2VhL" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4665,7 +4668,7 @@ "_string": "Spirit3209", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 14, + "_actualFontSize": 14.758333333333333, "_fontSize": 14, "_fontFamily": "Arial", "_lineHeight": 14, @@ -4702,7 +4705,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "acmPhlelVMTqP/8hCNh3cf" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4738,7 +4741,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "1cOcNVOs5Kd5olEnJpshPw" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4808,7 +4811,7 @@ "y": 0, "z": 0 }, - "_id": "94vSoOVH5EJIu6jxU6o2BH" + "_id": "" }, { "__type__": "cc.UITransform", @@ -4824,7 +4827,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 7, + "width": 6.996047430830039, "height": 17.64 }, "_anchorPoint": { @@ -4832,7 +4835,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "51UEIa/tpBzJaYGdGHGujH" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4863,7 +4866,7 @@ "_string": "s", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 14, + "_actualFontSize": 14.758333333333333, "_fontSize": 14, "_fontFamily": "Arial", "_lineHeight": 14, @@ -4900,7 +4903,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "08NgyiXqhIJKBCZ0KqErxl" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -4936,7 +4939,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "3b745UjWJPxY6OlRHIxdtU" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5006,7 +5009,7 @@ "y": 0, "z": 0 }, - "_id": "26CgRNnK1C2rGI6SNZoz+4" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5030,7 +5033,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "14PIh8hSpPE4Q4fejcigiL" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5075,7 +5078,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "99SRtZ48NJn4mx4B+Icki8" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5111,7 +5114,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "b1+ejEWN1MJ4ub10FjIsXt" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5181,7 +5184,7 @@ "y": 0, "z": 0 }, - "_id": "dcNTDteZNGB7PvbzGt3T1t" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5205,7 +5208,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "e278RSXK1Jf5Lv63tdyNOH" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5250,7 +5253,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "19C5kbiq9HLLyOzabILujW" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5286,7 +5289,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "d9ttEdRO5F3LdCNsiZJFeK" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5356,7 +5359,7 @@ "y": 0, "z": 0 }, - "_id": "892QkGYKBMsYXwMvT78MO+" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5372,7 +5375,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 7, + "width": 6.996047430830039, "height": 17.64 }, "_anchorPoint": { @@ -5380,7 +5383,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "b7cEkjYgpLkLv2aLCHfsUK" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5411,7 +5414,7 @@ "_string": "s", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 14, + "_actualFontSize": 14.758333333333333, "_fontSize": 14, "_fontFamily": "Arial", "_lineHeight": 14, @@ -5448,7 +5451,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "194sR/MxhD6LZqSMoFFB47" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5484,7 +5487,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "57WOIdUsRICpIXSeh8e8ZJ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5554,7 +5557,7 @@ "y": 0, "z": 0 }, - "_id": "8fnWiuky5CA4BZgFepro2L" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5570,15 +5573,15 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 100, - "height": 25.2 + "width": 99.98412603446145, + "height": 25.200000000000003 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "2dEGd+IX9CFYfweu/yHGnu" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5609,7 +5612,7 @@ "_string": "白名单管理", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 20, + "_actualFontSize": 21.083333333333336, "_fontSize": 20, "_fontFamily": "Arial", "_lineHeight": 20, @@ -5646,7 +5649,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "05A3g4cFhLzLWjAbxWIlrO" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5682,7 +5685,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "08g653mOpAAoGkGS3mTlIB" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5752,7 +5755,7 @@ "y": 0, "z": 0 }, - "_id": "ccD/pDBLVKFqA/nBVMRMa6" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5768,7 +5771,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 42, + "width": 41.976284584980235, "height": 17.64 }, "_anchorPoint": { @@ -5776,7 +5779,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "6dxhQYkXlL+6xnBkBpV4Cb" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5807,7 +5810,7 @@ "_string": "玩家ID", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 14, + "_actualFontSize": 14.758333333333333, "_fontSize": 14, "_fontFamily": "Arial", "_lineHeight": 14, @@ -5844,7 +5847,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "274PHLgUZKE4ZrBBgWak6I" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5880,7 +5883,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "d3QuwIs1lJQ4ut/HiGIv+I" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -5950,7 +5953,7 @@ "y": 0, "z": 0 }, - "_id": "a27a2oVvtI17jSXe9eGulB" + "_id": "" }, { "__type__": "cc.UITransform", @@ -5966,7 +5969,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 42, + "width": 41.976284584980235, "height": 17.64 }, "_anchorPoint": { @@ -5974,7 +5977,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "424VxEYSJHz5Kv2+kbpWeU" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6005,7 +6008,7 @@ "_string": "魅力值", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 14, + "_actualFontSize": 14.758333333333333, "_fontSize": 14, "_fontFamily": "Arial", "_lineHeight": 14, @@ -6042,7 +6045,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "04nx3Zd21KKLIz/rrl58N3" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6078,7 +6081,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "3fS2iYXtlNGra4VtnjMdDB" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6148,7 +6151,7 @@ "y": 0, "z": 0 }, - "_id": "b0UpjXiNtAzbz4EvLZ9NX8" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6172,7 +6175,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "95Fu/mZ2FOzL8ObtiGwrw6" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6217,7 +6220,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "b7pEF4K+hBs4bQZuoQzTx8" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6253,7 +6256,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "2ecQOFjp9NN4PFR5nTgze3" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6323,7 +6326,7 @@ "y": 0, "z": 0 }, - "_id": "02e8BvZj5PHZYoEQL1+d0O" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6339,7 +6342,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 7, + "width": 6.996047430830039, "height": 17.64 }, "_anchorPoint": { @@ -6347,7 +6350,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "21ImE9y7xPUoWnJ6BRJAjK" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6378,7 +6381,7 @@ "_string": "s", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 14, + "_actualFontSize": 14.758333333333333, "_fontSize": 14, "_fontFamily": "Arial", "_lineHeight": 14, @@ -6415,7 +6418,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "79BlKSnqFCgqfLFUzx0iCi" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6451,7 +6454,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "27UDfiRrVEq6x3U0Xgnejd" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6521,7 +6524,7 @@ "y": 0, "z": 0 }, - "_id": "e9eUCPLzlH55cgnH9pVFu7" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6545,7 +6548,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "77My4sdJ1LLL+VpzpW5ZPt" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6590,7 +6593,7 @@ "_isTrimmedMode": true, "_useGrayscale": false, "_atlas": null, - "_id": "2cKvhycypD9Ze4vE0e7MeI" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6626,7 +6629,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "61T8CQiGtDDpyBOA/XlBdP" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6696,7 +6699,7 @@ "y": 0, "z": 0 }, - "_id": "59N94sZO1FTaqAY8PfPFzl" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6712,7 +6715,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 56, + "width": 55.96837944664031, "height": 17.64 }, "_anchorPoint": { @@ -6720,7 +6723,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "23D+A+CzNIM7TDjONY3XsZ" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6751,7 +6754,7 @@ "_string": "搜索玩家", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 14, + "_actualFontSize": 14.758333333333333, "_fontSize": 14, "_fontFamily": "Arial", "_lineHeight": 14, @@ -6788,7 +6791,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "f3dIFarwZACad3UFoZll2n" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6824,7 +6827,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "c01kZCw5lJsJ6iHA0opZON" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6894,7 +6897,7 @@ "y": 0, "z": 0 }, - "_id": "57cb+FJA5HebHvk1S2jOnA" + "_id": "" }, { "__type__": "cc.UITransform", @@ -6910,15 +6913,15 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 51.333984375, - "height": 15.120000000000001 + "width": 51.3339583204669, + "height": 15.12 }, "_anchorPoint": { "__type__": "cc.Vec2", "x": 0.5, "y": 0.5 }, - "_id": "6aEpOIMXlCZYJ8VxAsRyw2" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -6949,7 +6952,7 @@ "_string": "添加/修改", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 12, + "_actualFontSize": 12.65, "_fontSize": 12, "_fontFamily": "Arial", "_lineHeight": 12, @@ -6986,7 +6989,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "1b3qYF4B1Nz5oQxofgqsXo" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7022,7 +7025,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "feO6lB3GtCypo5xj1Z8yUG" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7092,7 +7095,7 @@ "y": 0, "z": 0 }, - "_id": "54PFQON6VKKp4DmBXUUG44" + "_id": "" }, { "__type__": "cc.UITransform", @@ -7108,7 +7111,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 63.041015625, + "width": 63.005419342885375, "height": 17.64 }, "_anchorPoint": { @@ -7116,7 +7119,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "47Vnj6mrxCGoG1Tj0WqePA" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7147,7 +7150,7 @@ "_string": "Spirit3209", "_horizontalAlign": 1, "_verticalAlign": 1, - "_actualFontSize": 14, + "_actualFontSize": 14.758333333333333, "_fontSize": 14, "_fontFamily": "Arial", "_lineHeight": 14, @@ -7184,7 +7187,7 @@ "y": 2 }, "_shadowBlur": 2, - "_id": "09qyMH43NANo0GL+WNXlrU" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7220,7 +7223,7 @@ "_originalHeight": 0, "_alignMode": 2, "_lockFlags": 0, - "_id": "ffm/CJXWpNLrcS5tBt4vzl" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7261,7 +7264,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "c0qS436yxH4J5vT4vqSmYa" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7297,7 +7300,7 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "5c7V/F3wtMa61Sm7CVUwHb" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7338,7 +7341,7 @@ "x": 0.5, "y": 0.5 }, - "_id": "31h+R9KaVECIUc5kPB++8q" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", @@ -7358,10 +7361,10 @@ }, "_alignFlags": 45, "_target": null, - "_left": -800, - "_right": 800, - "_top": 360, - "_bottom": -360, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, "_horizontalCenter": 0, "_verticalCenter": 0, "_isAbsLeft": true, @@ -7374,12 +7377,38 @@ "_originalHeight": 720, "_alignMode": 2, "_lockFlags": 0, - "_id": "13F6VuzFNFIp0QsSsuMSm/" + "_id": "" }, { "__type__": "cc.CompPrefabInfo", "fileId": "HluBcjZ7/jMY07j0jdhNkw" }, + { + "__type__": "1f765tAwURK4rGwe2upn+F6", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 323 + }, + "loginTitle": { + "__uuid__": "2896a7a8-b0d7-43a2-a6fb-a98624d63ea7@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "codeWaiting": { + "__uuid__": "1bf097b6-9c86-4afd-8740-94ae426fa0f8@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "7dTa1uKnlGk5UDlFQf8K0X" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -7390,7 +7419,6 @@ }, "fileId": "HUPiVAu02owbfAK2LmJ9yg", "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null + "targetOverrides": null } -] +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/resources.meta b/cocoscreator_projects/YouleNexus/assets/resources.meta new file mode 100644 index 0000000..49f6aa2 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/resources.meta @@ -0,0 +1,14 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "1830368b-b039-44a1-8023-ede4c94e9dd3", + "files": [], + "subMetas": {}, + "userData": { + "isBundle": true, + "bundleConfigID": "default", + "bundleName": "resources", + "priority": 8 + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/resources/dev-config.meta b/cocoscreator_projects/YouleNexus/assets/resources/dev-config.meta new file mode 100644 index 0000000..0bc269c --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/resources/dev-config.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "2d251f16-3748-411c-88b6-6ef547bc63a5", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt b/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt new file mode 100644 index 0000000..df73461 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt @@ -0,0 +1,1500 @@ +{ + "showmessage": "", + "visitor_server_tcp": "115.159.146.131:51088", + "visitor_server_http": "115.159.146.131:51089", + "isclose": 1, + "scrollmsg": "官方郑重声明:近期有不法之徒以片面的游戏得分数据欺骗玩家购买外挂,已有玩家上当受骗,在此官方郑重声明,本游戏绝无外挂,凡自称有外挂的都是骗子,切勿上当受骗!", + "noticemsg": "", + "menunotice": { + "advert0": { + "type": 0, + "imgUrl": "http://tsgames.daoqi88.cn/image/adver0_youle2.png", + "imgAdUrl": "", + "height": 470, + "msg": "", + "url": "" + }, + "advert1": { + "msg": "", + "url": "" + }, + "advert2": { + "msg": "", + "url": "" + } + }, + "Ads": "本游戏仅为娱乐休闲使用,房卡所有#游戏通用", + "roomCardName": "房卡", + "nameImgFrame_1": 1, + "starName": "金币", + "nameImgFrame_2": 2, + "shieldShowRebate": 1, + "WechatPublicNo_ewm": "", + "behaviorList": [ + [ + 3233, + "rankList" + ], + [ + 604, + "rankList" + ], + [ + 76, + "ewm" + ], + [ + 3232, + "vipRank" + ], + [ + 3231, + "vipRank" + ], + [ + 679, + "match" + ], + [ + 1719, + "match" + ] + ], + "managerUrl": "http://dlweb.daoqi88.cn/white_list.html", + "service_qq": "", + "rankList": 0, + "wareHouse": 0, + "betaText": "Beta", + "ramSeat": 1, + "payCSS": "", + "payJS": "", + "cdKey": 2, + "agentlist": [ + { + "agentid": "veRa0qrBf0df2K1G4de2tgfmVxB2jxpv", + "agentname": "进贤", + "scrollmsg": "聚友棋牌仅限娱乐,禁止赌博!一经发现立即封号。客服号改为:jyqp181999", + "showmessage": "", + "xxroomdeduct": 1, + "audio_domain":"gameaudio.daoqi88.cn", + "audio_bucket":"gameaudio", + "html_buyroomcard": 2, + "html_applysales": 2, + "sale_wechat": "jyqp181999", + "agentmode": 2, + "WechatPublicNo_ewm": "http://tsgames.daoqi88.cn/image_ewm/ewm_jinxian2.png", + "service_wechat": "jyqp181999", + "service_tel": "", + "url_topup": "http://dlwebv3/wan_purchase_new.html", + "url_download": "http://sharegame/gameshare.html", + "logimage2": "http://op0g0jeqa.bkt.clouddn.com/log_jinxian.png", + "h5shareurl": "http://h5game1/gameLobby920/index2.html", + "game_share_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", + "shareh5title": "进贤聚友棋牌", + "shareh5desc": "一起来嗨~", + "gameNoticeShow": 0, + "shareType":1, + "shareImgUrl":"https://tsgames.daoqi88.cn/image/match.png", + "hallNoticeShow": 0, + "gameHallConfig": { + "topImgUrl": "http://tsgames.daoqi88.cn/image_xgamehall/gamehalllogo.png" + }, + "menunotice": { + "advert0": { + "type": 0, + "imgUrl": "http://tsgames.daoqi88.cn/image/adver0_jinxian2.png", + "imgAdUrl": "", + "height": 470, + "msg": "进贤本地玩法8年老平台##绿色平台,仅供娱乐!绝无外挂!##客服微信:jyqp181999", + "url": "" + } + }, + "rebateConfig": { + "from": 3, + "value": 0, + "rebateCharm": 1 + }, + "gamelist": [{ + "gameid": "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO", + "gamename": "进贤聚友", + "game_server_tcp": "8.139.255.236:56072", + "game_server_http": "8.139.255.236:56073", + "game_version": 261, + "game_zip": "http://tsgames.daoqi88.cn/zip2/gamehall.zip", + "game_size": "4.61M", + "game_hall_dir": "gamehall", + "game_down_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", + "game_down_memo": "进贤聚友,让欢乐无限分享", + "hideSpid": [ + 478 + ], + + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 1, + "marketname": "App Store", + "app_version": 1, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 44, + "app_download": "itms-services://?action=download-manifest&url=https://daoqi.daoqi888.cn/plist/gamehall_jinxianv2.plist", + "app_size": "2.61M", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 50, + "app_download": "https://daoqi.daoqi888.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 2, + "app_download": "https://daoqi.daoqi888.cn/harmonyos/gamehall_jinxian_harmonyosv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "a52y0ajd106pKU1KIvF1zRii8h1gRem5", + "gamename": "三个老K", + "sharename": "【三个老K】", + "category": 1, + "game_server_tcp": "8.139.255.236:56084", + "game_server_http": "8.139.255.236:56085", + "game_version": 155, + "game_zip": "http://tsgames.daoqi88.cn/zip2/sangelaok.zip", + "game_size": "9.07M", + "game_hall_dir": "sangelaok", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/3glaoK.png", + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "rZDp0wfvC0ZaPT5iLqh2itGFsnJaDvzc", + "gamename": "进贤麻将", + "category": 2, + "game_server_tcp": "8.139.255.236:56056", + "game_server_http": "8.139.255.236:56057", + "sharename": "【进贤麻将】", + "game_version": 41, + "game_zip": "http://tsgames.daoqi88.cn/zip2/majiang_jx.zip", + "game_size": "10.59M", + "game_hall_dir": "majiang_jx", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/mj_jx.png", + "haverobot": 1, + "game_config": { + "test": 1, + "asetcount": [ + 8, + 16 + ], + "needroomcard": [ + [ + 1, + 3, + 3, + 3 + ], + [ + 2, + 6, + 6, + 6 + ] + ], + "needroomcard_joinroom": [ + [ + 1, + 3, + 3, + 3 + ], + [ + 2, + 6, + 6, + 6 + ] + ], + "Time_Quick": 70, + "Time_playcard": 60, + "Time_initial": 160 + }, + "system_room": [{ + "type": 2, + "title": "", + "roomtype": [ + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + 0, + 2, + 1, [ + 1, + 2, + 400 + ], + 0, + 2 + ], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_1.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [ + 1, + 1, + 1, + 0, + 1, + 0, + 3, + 1, + 0, + 0, + 0, + 0, [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + 0, + 2, + 1, [ + 1, + 2, + 400 + ], + 0, + 2 + ], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_2.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [ + 1, + 1, + 1, + 0, + 1, + 0, + 3, + 1, + 0, + 0, + 0, + 0, [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + 0, + 2, + 1, [ + 2, + 100, + 20000 + ], + 0, + 2 + ], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_3.png", + "removeRobot": 1 + }, + { + "type": 2, + "title": "", + "roomtype": [ + 1, + 1, + 1, + 0, + 1, + 0, + 3, + 1, + 0, + 0, + 0, + 0, [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + 0, + 2, + 1, [ + 3, + 1000, + 200000 + ], + 0, + 2 + ], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_4.png", + "removeRobot": 1 + } + ], + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "sJgp0FArn0olVc3yitp3xThV036fG7n2", + "gamename": "跑得快", + "sharename": "【跑得快】", + "category": 1, + "game_server_tcp": "8.149.128.168:56024", + "game_server_http": "8.149.128.168:56025", + "game_version": 54, + "game_zip": "http://tsgames.daoqi88.cn/zip2/pdk_card_client-jinxian.zip", + "game_size": "9.8M", + "game_hall_dir": "pdk_card_client-jinxian", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/paodk-jx.png", + "haverobot": 1, + "game_config": { + "test": 1, + "star_beishu": [ + 1, + 10 + ], + "star_xianzhi": [ + 100, + 1000 + ], + "star_chashuifei": 300, + "needroomcard": [ + [ + 1, + 4, + 3, + 2 + ], + [ + 2, + 8, + 6, + 4 + ] + ], + "needroomcard_joinroom": [ + [ + 1, + 4, + 3, + 2 + ], + [ + 2, + 8, + 6, + 4 + ] + ], + "asetcount": [ + 8, + 16 + ], + "Time_Quick": 70, + "Time_playcard": 60, + "Time_initial": 160, + "paytype": "房卡", + "horse": "买马", + "horse_mun": [ + 1, + 2, + 3, + 4 + ], + "move_x": 20 + }, + "system_room": [{ + "type": 2, + "title": "4人初级场 10张", + "roomtype": [ + 1, + 1, + 1, + 2, + 1, -1, + 1, -1, -1, -1, -1, + 0, [ + [ + [ + [ + 2, + 3 + ], + [ + 1, + 5 + ] + ], + [ + [ + 3, + 4 + ], + [ + 3, + 4 + ] + ], + [ + [ + 1, + 2 + ], + [ + 1, + 4 + ] + ] + ], + [ + 8, + 1, + 1, + 0 + ], + 1, + 1, [ + 2, + 3 + ] + ], + 0, + 2, + 1, [ + 1, + 2, + 400 + ], + 0, + 2 + ], + "removeRobot": 1 + }, + { + "type": 2, + "title": "4人中级场 10张", + "roomtype": [ + 1, + 1, + 1, + 2, + 1, -1, + 1, -1, -1, -1, -1, + 0, [ + [ + [ + [ + 2, + 3 + ], + [ + 1, + 5 + ] + ], + [ + [ + 3, + 4 + ], + [ + 3, + 4 + ] + ], + [ + [ + 1, + 2 + ], + [ + 1, + 4 + ] + ] + ], + [ + 8, + 1, + 1, + 0 + ], + 1, + 1, [ + 2, + 3 + ] + ], + 0, + 2, + 1, [ + 2, + 100, + 20000 + ], + 0, + 2 + ], + "removeRobot": 1 + }, + { + "type": 2, + "title": "4人高级场 10张", + "roomtype": [ + 1, + 1, + 1, + 2, + 1, -1, + 1, -1, -1, -1, -1, + 0, [ + [ + [ + [ + 2, + 3 + ], + [ + 1, + 5 + ] + ], + [ + [ + 3, + 4 + ], + [ + 3, + 4 + ] + ], + [ + [ + 1, + 2 + ], + [ + 1, + 4 + ] + ] + ], + [ + 8, + 1, + 1, + 0 + ], + 1, + 1, [ + 2, + 3 + ] + ], + 0, + 2, + 1, [ + 3, + 1000, + 200000 + ], + 0, + 2 + ], + "removeRobot": 1 + } + ], + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "6ujc0IfoL0t1zS7J2dZ1eFh2u2t9682b", + "gamename": "找朋友", + "isdebugger": 0, + "category": 1, + "sharename": "【找朋友】", + "game_server_tcp": "8.149.128.168:56010", + "game_server_http": "8.149.128.168:56011", + "game_version": 7, + "game_zip": "http://tsgames.daoqi88.cn/zip2/zpy.zip", + "game_size": "11.16M", + "game_hall_dir": "zpy", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/zhaopy.png", + "game_config": { + "buckle": [ + 2, + 4 + ], + "personal": [ + 1, + 2 + ], + "numbergames": [ + 4, + 8 + ], + "mode":1, + "direction": 0, + "jiesuan": 1, + "touxiang": 0 + }, + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "sJgp0FArn0olVc8yitp0xThV036fG7n0", + "gamename": "关牌", + "sharename": "【关牌】", + "category": 1, + "game_server_tcp": "8.149.128.168:56016", + "game_server_http": "8.149.128.168:56017", + "isdebugger": 0, + "game_version": 54, + "game_zip": "http://tsgames.daoqi88.cn/zip2/guanpai-jx.zip", + "game_size": "11.0M", + "game_hall_dir": "guanpai-jx", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/guanpai.png", + "game_config": { + "buckle": [ + 1, + 2 + ], + "personal": [ + 1, + 2 + ], + "numbergames": [ + 6, + 12 + ], + "tiaoshi": 0, + "people": [ + 2, + 3 + ], + "difen": 10, + "zha": [ + 5, + 10, + 20 + ], + "zidongchu": 0, + "beishu": [ + 1, + 100 + ], + "xianzhi": [ + 1000, + 50000 + ], + "chxz": [ + 0, + 100 + ] + }, + "system_room": [{ + "type": 2, + "title": "练习场", + "roomtype": [ + 1, + 2, + 1, + 2, + 1, + 2, [ + 1, + 200 + ] + ] + }, + { + "type": 2, + "title": "初级场", + "roomtype": [ + 1, + 2, + 1, + 2, + 1, + 2, [ + 10, + 2000 + ] + ] + }, + { + "type": 2, + "title": "中级场", + "roomtype": [ + 1, + 2, + 1, + 2, + 1, + 2, [ + 20, + 4000 + ] + ] + }, + { + "type": 2, + "title": "高级场", + "roomtype": [ + 1, + 2, + 1, + 2, + 1, + 2, [ + 50, + 10000 + ] + ] + } + ], + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "FDtj0pGhT0Evjd3efAS5wkIlamiTh2oa", + "gamename": "斗地主", + "sharename": "【斗地主】", + "category": 1, + "game_server_tcp": "8.149.128.168:56008", + "game_server_http": "8.149.128.168:56009", + "isdebugger": 0, + "game_version": 30, + "game_zip": "http://tsgames.daoqi88.cn/zip2/doudizhu.zip", + "game_size": "13.3M", + "game_hall_dir": "doudizhu", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/doudizhu.png", + "game_config": { + "jushu": [ + 6, + 12 + ], + "kouka": [ + [ + 1, + 2 + ], + [ + 2, + 4 + ] + ], + "dizhu": [ + 1, + 5, + 10 + ], + "gold": [ + 1, + 50, + 1, + 1000 + ], + "tg_time": [ + 700, + 700, + 700, + 2500 + ], + "process": [ + [ + 1, + 50, + 0 + ], + [ + 2, + 15000, + 0 + ], + [ + 3, + 5000, -1 + ] + ], + "moshi": [ + 0, + 1, + 0 + ], + "renshu": [ + 2, + 3, + 4 + ], + "paixing": [ + [ + 1, [ + 1, + 1 + ] + ], + [ + 1, [ + 2, + 1 + ] + ], + [ + 1, [ + 1, + 5 + ] + ], + [ + 1, [ + 3, + 1 + ] + ], + [ + 1, [ + 3, + 1 + ] + ], + [ + 1, [ + 3, + 1 + ] + ], + [ + 0, [ + 3, + 1 + ] + ], + [ + 1, [ + 4, + 1 + ] + ], + [ + 1, [ + 4, + 1 + ] + ], + [ + 1, [ + 2, + 3 + ] + ], + [ + 1, [ + 3, + 2 + ] + ], + [ + 2, [ + 4, + 1 + ] + ], + [ + 3, [ + 1, + 2 + ] + ], + [ + 0, [ + 5, + 1 + ] + ] + ], + "playtime": [ + 15, + 5, + 15 + ], + "card_len": [ + [ + 17, + 13 + ], + [ + 20, + 14 + ] + ], + "del_cards": [ + [ + 3, [ + 1, + 2, + 3, + 4 + ] + ], + [ + 4, [ + 1, + 2, + 3, + 4 + ] + ], + [ + 5, [ + 1, + 2, + 3, + 4 + ] + ] + ], + "dipai": [ + 1, + 3, + 14 + ] + }, + "channellist": [{ + "channelid": "FtJf073aa0d6rI1xD8J1Y42fINTm0ziK", + "channelname": "进贤", + "game_version": 0, + "game_zip": "", + "game_size": "", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":true, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + } + ] + }, + { + "agentid": "1B2h0ccl205c390Y28m1Ajdplkuu4wgy", + "agentname": "测试", + "scrollmsg": "聚友棋牌仅限娱乐,禁止赌博!一经发现立即封号。客服号改为:jyqp181999", + "showmessage": "", + "xxroomdeduct": 1, + "audio_domain":"gameaudio.daoqi88.cn", + "audio_bucket":"gameaudio", + "html_buyroomcard": 2, + "html_applysales": 2, + "sale_wechat": "jyqp181999", + "agentmode": 2, + "WechatPublicNo_ewm": "http://tsgames.daoqi88.cn/image_ewm/ewm_jinxian2.png", + "service_wechat": "jyqp181999", + "service_tel": "", + "url_topup": "http://dlwebv3/wan_purchase_new.html", + "url_download": "http://sharegame/gameshare.html", + "logimage2": "http://op0g0jeqa.bkt.clouddn.com/log_jinxian.png", + "h5shareurl": "http://h5game1/gameLobby920/index2.html", + "game_share_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", + "shareh5title": "进贤聚友棋牌", + "shareh5desc": "一起来嗨~", + "gameNoticeShow": 0, + "shareType":1, + "showVisitor":1, + "shareImgUrl":"https://tsgames.daoqi88.cn/image/match.png", + "hallNoticeShow": 0, + "gameHallConfig": { + "topImgUrl": "http://tsgames.daoqi88.cn/image_xgamehall/gamehalllogo.png" + }, + "menunotice": { + "advert0": { + "type": 0, + "imgUrl": "http://tsgames.daoqi88.cn/image/adver0_jinxian2.png", + "imgAdUrl": "", + "height": 470, + "msg": " 【测试专用】 ##进贤本地玩法8年老平台##绿色平台,仅供娱乐!绝无外挂!##客服微信:jyqp181999", + "url": "" + } + }, + "rebateConfig": { + "from": 3, + "value": 0, + "rebateCharm": 1 + }, + "gamelist": [ + { + "gameid": "G2hw0ubng0zcoI0r4mx3H2yr4GejidwO", + "gamename": "进贤聚友", + "game_server_tcp": "8.139.255.236:56072", + "game_server_http": "8.139.255.236:56073", + "game_version": 261, + "game_zip": "http://tsgames.daoqi88.cn/zip2/gamehall.zip", + "game_size": "4.61M", + "game_hall_dir": "gamehall", + "game_down_image": "http://tsgames.daoqi88.cn/image_download/download_jinxian.jpg", + "game_down_memo": "进贤聚友,让欢乐无限分享", + "hideSpid": [ + 478 + ], + + "channellist": [{ + "channelid": "aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 1, + "marketname": "App Store", + "app_version": 1, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 45, + "app_download": "itms-services://?action=download-manifest&url=https://daoqi.daoqi888.cn/plist/gamehall_jinxianv2_test.plist", + "app_size": "2.61M", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 50, + "app_download": "https://daoqi.daoqi888.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":false, + "app_version": 2, + "app_download": "https://daoqi.daoqi888.cn/harmonyos/gamehall_jinxian_harmonyosv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "jzWe0kzmN0tAcZ5MLks3rFWcxNecogyu", + "gamename": "新进贤麻将", + "category": 2, + "game_server_tcp": "8.139.255.236:56030", + "game_server_http": "8.139.255.236:56031", + "sharename": "【新进贤麻将】", + "game_version": 7, + "game_zip": "http://tsgames.daoqi88.cn/zip2/jinxianmahjong.zip", + "game_size": "10.59M", + "game_hall_dir": "jinxianmahjong", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/mj_jx.png", + "haverobot": 1, + "game_config": { }, + "system_room": [{ + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_1.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_2.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_3.png", + "removeRobot": 1 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_4.png", + "removeRobot": 1 + } + ], + "channellist": [{ + "channelid": "aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":false, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + }, + { + "gameid": "J9r4022t80pxlb01zOK76DN1WD4QewFl", + "gamename": "二七王", + "category": 2, + "game_server_tcp": "8.139.255.236:56030", + "game_server_http": "8.139.255.236:56031", + "sharename": "【二七王】", + "game_version": 1, + "game_zip": "http://tsgames.daoqi88.cn/zip2/jinxianmahjong.zip", + "game_size": "10.59M", + "game_hall_dir": "jinxianmahjong", + "game_hall_image": "http://tsgames.daoqi88.cn/image_xxgamehall/mj_jx.png", + "haverobot": 1, + "game_config": { }, + "system_room": [{ + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_1.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_2.png", + "roomLeave": 1, + "removeRobot": 0 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_3.png", + "removeRobot": 1 + }, + { + "type": 2, + "title": "", + "roomtype": [], + "image": "http://tsgames.daoqi88.cn/image_system/mj_jx_4.png", + "removeRobot": 1 + } + ], + "channellist": [{ + "channelid": "aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG", + "channelname": "进贤", + "ios_defdownload_marketid": 2, + "and_defdownload_marketid": 3, + "game_version": 0, + "game_zip": "", + "game_size": "", + "marketlist": [{ + "marketid": 2, + "marketname": "苹果企业签", + "app_version": 0, + "app_download": "", + "app_size": "", + "game_version": 0, + "game_zip": "", + "game_size": "" + }, + { + "marketid": 3, + "marketname": "安卓", + "app_version": 0, + "app_download": "", + "app_size": "" + }, + { + "marketid": 4, + "marketname": "鸿蒙", + "deviceLogin":false, + "app_version": 1, + "app_download": "http://daoqi.daoqikj.cn/apk/gamehall_jinxianv2.apk", + "app_size": "6.61M" + } + ] + }] + } + ] + } + ] +} \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt.meta b/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt.meta new file mode 100644 index 0000000..918ea13 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.1", + "importer": "text", + "imported": true, + "uuid": "cf18eee8-2e2c-476f-85c7-7cf6bcb17aee", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene deleted file mode 100644 index c591c27..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene +++ /dev/null @@ -1,1393 +0,0 @@ -[ - { - "__type__": "cc.SceneAsset", - "_name": "LocalPlatformLogin", - "_objFlags": 0, - "__editorExtras__": {}, - "_native": "", - "scene": { - "__id__": 1 - } - }, - { - "__type__": "cc.Scene", - "_name": "LocalPlatformLogin", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": null, - "_children": [ - { - "__id__": 2 - } - ], - "_active": true, - "_components": [], - "_prefab": { - "__id__": 36 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "autoReleaseAssets": false, - "_globals": { - "__id__": 37 - }, - "_id": "67e69aac-9982-4e78-a44c-1d9c42f016ae" - }, - { - "__type__": "cc.Node", - "_name": "Canvas", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 3 - }, - { - "__id__": 5 - }, - { - "__id__": 7 - }, - { - "__id__": 17 - }, - { - "__id__": 19 - }, - { - "__id__": 24 - }, - { - "__id__": 29 - } - ], - "_active": true, - "_components": [ - { - "__id__": 34 - }, - { - "__id__": 35 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 800, - "y": 360, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "4fmPDGIcdPtKTxMtiB6nco" - }, - { - "__type__": "cc.Node", - "_name": "Camera", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 4 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 1000 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "56FfrqrBlPOJEcpxhwztIB" - }, - { - "__type__": "cc.Camera", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": null, - "_projection": 0, - "_priority": 0, - "_fov": 45, - "_fovAxis": 0, - "_orthoHeight": 360, - "_near": 0.1, - "_far": 2000, - "_color": { - "__type__": "cc.Color", - "r": 20, - "g": 29, - "b": 45, - "a": 255 - }, - "_depth": 1, - "_stencil": 0, - "_clearFlags": 7, - "_rect": { - "__type__": "cc.Rect", - "x": 0, - "y": 0, - "width": 1, - "height": 1 - }, - "_aperture": 19, - "_shutter": 7, - "_iso": 0, - "_screenScale": 1, - "_visibility": 1107296256, - "_targetTexture": null, - "_postProcess": null, - "_usePostProcess": false, - "_cameraType": -1, - "_trackingType": 0, - "_id": "5fJx8MoDNLDruFvKpvUqV3" - }, - { - "__type__": "cc.Node", - "_name": "Host", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 6 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "273kGrcqlGnqgl7sJetcLH" - }, - { - "__type__": "25970YMrRZG8YntQr+ux0rZ", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 5 - }, - "_enabled": true, - "__prefab": null, - "loadingPrefab": { - "__uuid__": "3e5a41f3-b043-45a5-94f6-e299ea9acc61", - "__expectedType__": "cc.Prefab" - }, - "loginPrefab": { - "__uuid__": "b209cefb-b381-4c7c-b765-94a5ab75a89c", - "__expectedType__": "cc.Prefab" - }, - "loadingRoot": { - "__id__": 7 - }, - "loginRoot": { - "__id__": 17 - }, - "resultRoot": { - "__id__": 19 - }, - "statusOverlay": { - "__id__": 24 - }, - "statusLabel": { - "__id__": 27 - }, - "resultLabel": { - "__id__": 22 - }, - "stopButton": { - "__id__": 29 - }, - "_id": "94INvS4pdAnIe3wFZxabB7" - }, - { - "__type__": "cc.Node", - "_name": "LoadingRoot", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [ - { - "__id__": 8 - } - ], - "_active": true, - "_components": [ - { - "__id__": 16 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "40omEGvnFOKq2lXAwsqtP8" - }, - { - "__type__": "cc.Node", - "_objFlags": 0, - "_parent": { - "__id__": 7 - }, - "_prefab": { - "__id__": 9 - }, - "__editorExtras__": {} - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 8 - }, - "asset": { - "__uuid__": "3e5a41f3-b043-45a5-94f6-e299ea9acc61", - "__expectedType__": "cc.Prefab" - }, - "fileId": "D6NzGhQBg/6lYKr/OZsmJQ", - "instance": { - "__id__": 10 - }, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.PrefabInstance", - "fileId": "00Yyr3cfRNcZX1WLG7VLxz", - "prefabRootNode": null, - "mountedChildren": [], - "mountedComponents": [], - "propertyOverrides": [ - { - "__id__": 11 - }, - { - "__id__": 13 - }, - { - "__id__": 14 - }, - { - "__id__": 15 - } - ], - "removedComponents": [] - }, - { - "__type__": "CCPropertyOverrideInfo", - "targetInfo": { - "__id__": 12 - }, - "propertyPath": [ - "_name" - ], - "value": "Layer1_Logo" - }, - { - "__type__": "cc.TargetInfo", - "localID": [ - "D6NzGhQBg/6lYKr/OZsmJQ" - ] - }, - { - "__type__": "CCPropertyOverrideInfo", - "targetInfo": { - "__id__": 12 - }, - "propertyPath": [ - "_lpos" - ], - "value": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - } - }, - { - "__type__": "CCPropertyOverrideInfo", - "targetInfo": { - "__id__": 12 - }, - "propertyPath": [ - "_lrot" - ], - "value": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - } - }, - { - "__type__": "CCPropertyOverrideInfo", - "targetInfo": { - "__id__": 12 - }, - "propertyPath": [ - "_euler" - ], - "value": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - } - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 7 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "12ZBzo5G9KBoOxPTXRKk/Q" - }, - { - "__type__": "cc.Node", - "_name": "LoginRoot", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [], - "_active": false, - "_components": [ - { - "__id__": 18 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "d6V5u3kY9ASbSWXectzlbS" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 17 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "40Uk4k/ONMnJXSfDjQDunZ" - }, - { - "__type__": "cc.Node", - "_name": "ResultRoot", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [ - { - "__id__": 20 - } - ], - "_active": false, - "_components": [ - { - "__id__": 23 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "4e40f7pcFAipccXYiKt1LW" - }, - { - "__type__": "cc.Node", - "_name": "ResultLabel", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 19 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 21 - }, - { - "__id__": 22 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "f9tsqXSTtDdqA0mcIgv5m8" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 20 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 1200, - "height": 380 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "44DJyd4DhIlI4pbBdfv/QM" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 20 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 245, - "g": 246, - "b": 250, - "a": 255 - }, - "_string": "登录结果", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 0, - "_fontSize": 36, - "_fontFamily": "Arial", - "_lineHeight": 48, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "01Oedc9UVL64Gj9z6SWzgN" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 19 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "fcIBurW2VOWo3ZMkOtLxIu" - }, - { - "__type__": "cc.Node", - "_name": "StatusOverlay", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [ - { - "__id__": 25 - } - ], - "_active": true, - "_components": [ - { - "__id__": 28 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "a6kdhP/8ZLMLeFOXf1x03K" - }, - { - "__type__": "cc.Node", - "_name": "StatusLabel", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 24 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 26 - }, - { - "__id__": 27 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": -265, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "13eN54GNpEHIFD+205kDRk" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 25 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 363.8892470046937, - "height": 47.879999999999995 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "b2FWK2DwBNLL7XlUTSI/Nw" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 25 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 245, - "g": 246, - "b": 250, - "a": 255 - }, - "_string": "正在加载资源和连接本地服务…", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 27.408333333333335, - "_fontSize": 26, - "_fontFamily": "Arial", - "_lineHeight": 38, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "51RskzXn5Ata2xeyOVlNCY" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 24 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "a2g6t0veZNVry/oIZkFXdJ" - }, - { - "__type__": "cc.Node", - "_name": "StopButton", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 2 - }, - "_children": [ - { - "__id__": 30 - } - ], - "_active": true, - "_components": [ - { - "__id__": 33 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 665, - "y": 310, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "00faTTaCpK+6dsBhx902Te" - }, - { - "__type__": "cc.Node", - "_name": "StopLabel", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 29 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 31 - }, - { - "__id__": 32 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "e4Tfs0ZLVBiaGDxp3kpgsI" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 30 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 103.96835628705533, - "height": 47.879999999999995 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "d0s+NCQGxLt5QFk5h0CZVa" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 30 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 245, - "g": 246, - "b": 250, - "a": 255 - }, - "_string": "停止验证", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 27.408333333333335, - "_fontSize": 26, - "_fontFamily": "Arial", - "_lineHeight": 38, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "eewB3DGj9MzpyN67ez8juQ" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 29 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 220, - "height": 64 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "52C95shHpLdqYKR29izOA+" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 1600, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "ebjzNd6xRN6rCykuT0wir7" - }, - { - "__type__": "cc.Canvas", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": null, - "_cameraComponent": { - "__id__": 4 - }, - "_alignCanvasWithScreen": true, - "_id": "793QSuOMFHY6zVWL61HGbM" - }, - { - "__type__": "cc.PrefabInfo", - "root": null, - "asset": null, - "fileId": "67e69aac-9982-4e78-a44c-1d9c42f016ae", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": [ - { - "__id__": 8 - } - ] - }, - { - "__type__": "cc.SceneGlobals", - "ambient": { - "__id__": 38 - }, - "shadows": { - "__id__": 39 - }, - "_skybox": { - "__id__": 40 - }, - "fog": { - "__id__": 41 - }, - "octree": { - "__id__": 42 - }, - "skin": { - "__id__": 43 - }, - "lightProbeInfo": { - "__id__": 44 - }, - "postSettings": { - "__id__": 45 - }, - "bakedWithStationaryMainLight": false, - "bakedWithHighpLightmap": false - }, - { - "__type__": "cc.AmbientInfo", - "_skyColorHDR": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.5, - "z": 0.8, - "w": 0.520833125 - }, - "_skyColor": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.5, - "z": 0.8, - "w": 0.520833125 - }, - "_skyIllumHDR": 20000, - "_skyIllum": 20000, - "_groundAlbedoHDR": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.2, - "z": 0.2, - "w": 1 - }, - "_groundAlbedo": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.2, - "z": 0.2, - "w": 1 - }, - "_skyColorLDR": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.5, - "z": 0.8, - "w": 1 - }, - "_skyIllumLDR": 20000, - "_groundAlbedoLDR": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.2, - "z": 0.2, - "w": 1 - } - }, - { - "__type__": "cc.ShadowsInfo", - "_enabled": false, - "_type": 0, - "_normal": { - "__type__": "cc.Vec3", - "x": 0, - "y": 1, - "z": 0 - }, - "_distance": 0, - "_planeBias": 1, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 76 - }, - "_maxReceived": 4, - "_size": { - "__type__": "cc.Vec2", - "x": 1024, - "y": 1024 - } - }, - { - "__type__": "cc.SkyboxInfo", - "_envLightingType": 0, - "_envmapHDR": null, - "_envmap": null, - "_envmapLDR": null, - "_diffuseMapHDR": null, - "_diffuseMapLDR": null, - "_enabled": false, - "_useHDR": true, - "_editableMaterial": null, - "_reflectionHDR": null, - "_reflectionLDR": null, - "_rotationAngle": 0 - }, - { - "__type__": "cc.FogInfo", - "_type": 0, - "_fogColor": { - "__type__": "cc.Color", - "r": 200, - "g": 200, - "b": 200, - "a": 255 - }, - "_enabled": false, - "_fogDensity": 0.3, - "_fogStart": 0.5, - "_fogEnd": 300, - "_fogAtten": 5, - "_fogTop": 1.5, - "_fogRange": 1.2, - "_accurate": false - }, - { - "__type__": "cc.OctreeInfo", - "_enabled": false, - "_minPos": { - "__type__": "cc.Vec3", - "x": -1024, - "y": -1024, - "z": -1024 - }, - "_maxPos": { - "__type__": "cc.Vec3", - "x": 1024, - "y": 1024, - "z": 1024 - }, - "_depth": 8 - }, - { - "__type__": "cc.SkinInfo", - "_enabled": true, - "_blurRadius": 0.01, - "_sssIntensity": 3 - }, - { - "__type__": "cc.LightProbeInfo", - "_giScale": 1, - "_giSamples": 1024, - "_bounces": 2, - "_reduceRinging": 0, - "_showProbe": true, - "_showWireframe": true, - "_showConvex": false, - "_data": null, - "_lightProbeSphereVolume": 1 - }, - { - "__type__": "cc.PostSettingsInfo", - "_toneMappingType": 0 - } -] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLoading.scene b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLoading.scene new file mode 100644 index 0000000..a1a46d8 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLoading.scene @@ -0,0 +1,416 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "PlatformLoading", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "scene": { + "__id__": 1 + } + }, + { + "__type__": "cc.Scene", + "_name": "PlatformLoading", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "autoReleaseAssets": false, + "_globals": { + "__id__": 7 + }, + "_id": "262ea1ac-58e5-4144-b79c-6e854d75d693" + }, + { + "__type__": "cc.Node", + "_name": "Canvas", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + } + ], + "_active": true, + "_components": [ + { + "__id__": 5 + }, + { + "__id__": 6 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 800, + "y": 360, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "d9wH0yQuNFD6cIVq4RHwPA" + }, + { + "__type__": "cc.Node", + "_name": "Camera", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 1000 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "03SwPEvXVIrKlz1Ycix1/b" + }, + { + "__type__": "cc.Camera", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": null, + "_projection": 0, + "_priority": 0, + "_fov": 45, + "_fovAxis": 0, + "_orthoHeight": 360, + "_near": 1, + "_far": 2000, + "_color": { + "__type__": "cc.Color", + "r": 51, + "g": 51, + "b": 51, + "a": 255 + }, + "_depth": 1, + "_stencil": 0, + "_clearFlags": 7, + "_rect": { + "__type__": "cc.Rect", + "x": 0, + "y": 0, + "width": 1, + "height": 1 + }, + "_aperture": 19, + "_shutter": 7, + "_iso": 0, + "_screenScale": 1, + "_visibility": 1107296256, + "_targetTexture": null, + "_postProcess": null, + "_usePostProcess": false, + "_cameraType": -1, + "_trackingType": 0, + "_id": "67kXVpE8xNtp7r60I1VAI2" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "adybdpE3VDW4/URT3c6gX8" + }, + { + "__type__": "cc.Canvas", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_cameraComponent": { + "__id__": 4 + }, + "_alignCanvasWithScreen": true, + "_id": "c6tdlDQhRJNIBJ8DjXiFQ/" + }, + { + "__type__": "cc.SceneGlobals", + "ambient": { + "__id__": 8 + }, + "shadows": { + "__id__": 9 + }, + "_skybox": { + "__id__": 10 + }, + "fog": { + "__id__": 11 + }, + "octree": { + "__id__": 12 + }, + "skin": { + "__id__": 13 + }, + "lightProbeInfo": { + "__id__": 14 + }, + "postSettings": { + "__id__": 15 + }, + "bakedWithStationaryMainLight": false, + "bakedWithHighpLightmap": false + }, + { + "__type__": "cc.AmbientInfo", + "_skyColorHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyColor": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyIllumHDR": 20000, + "_skyIllum": 20000, + "_groundAlbedoHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_groundAlbedo": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_skyColorLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 1 + }, + "_skyIllumLDR": 20000, + "_groundAlbedoLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + } + }, + { + "__type__": "cc.ShadowsInfo", + "_enabled": false, + "_type": 0, + "_normal": { + "__type__": "cc.Vec3", + "x": 0, + "y": 1, + "z": 0 + }, + "_distance": 0, + "_planeBias": 1, + "_shadowColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 76 + }, + "_maxReceived": 4, + "_size": { + "__type__": "cc.Vec2", + "x": 1024, + "y": 1024 + } + }, + { + "__type__": "cc.SkyboxInfo", + "_envLightingType": 0, + "_envmapHDR": null, + "_envmap": null, + "_envmapLDR": null, + "_diffuseMapHDR": null, + "_diffuseMapLDR": null, + "_enabled": false, + "_useHDR": true, + "_editableMaterial": null, + "_reflectionHDR": null, + "_reflectionLDR": null, + "_rotationAngle": 0 + }, + { + "__type__": "cc.FogInfo", + "_type": 0, + "_fogColor": { + "__type__": "cc.Color", + "r": 200, + "g": 200, + "b": 200, + "a": 255 + }, + "_enabled": false, + "_fogDensity": 0.3, + "_fogStart": 0.5, + "_fogEnd": 300, + "_fogAtten": 5, + "_fogTop": 1.5, + "_fogRange": 1.2, + "_accurate": false + }, + { + "__type__": "cc.OctreeInfo", + "_enabled": false, + "_minPos": { + "__type__": "cc.Vec3", + "x": -1024, + "y": -1024, + "z": -1024 + }, + "_maxPos": { + "__type__": "cc.Vec3", + "x": 1024, + "y": 1024, + "z": 1024 + }, + "_depth": 8 + }, + { + "__type__": "cc.SkinInfo", + "_enabled": true, + "_blurRadius": 0.01, + "_sssIntensity": 3 + }, + { + "__type__": "cc.LightProbeInfo", + "_giScale": 1, + "_giSamples": 1024, + "_bounces": 2, + "_reduceRinging": 0, + "_showProbe": true, + "_showWireframe": true, + "_showConvex": false, + "_data": null, + "_lightProbeSphereVolume": 1 + }, + { + "__type__": "cc.PostSettingsInfo", + "_toneMappingType": 0 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene.meta b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLoading.scene.meta similarity index 72% rename from cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene.meta rename to cocoscreator_projects/YouleNexus/assets/scenes/PlatformLoading.scene.meta index a88364b..dd03581 100644 --- a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene.meta +++ b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLoading.scene.meta @@ -2,7 +2,7 @@ "ver": "1.1.50", "importer": "scene", "imported": true, - "uuid": "67e69aac-9982-4e78-a44c-1d9c42f016ae", + "uuid": "262ea1ac-58e5-4144-b79c-6e854d75d693", "files": [ ".json" ], diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLogin.scene b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLogin.scene new file mode 100644 index 0000000..54c01f4 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLogin.scene @@ -0,0 +1,416 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "PlatformLogin", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "scene": { + "__id__": 1 + } + }, + { + "__type__": "cc.Scene", + "_name": "PlatformLogin", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "autoReleaseAssets": false, + "_globals": { + "__id__": 7 + }, + "_id": "c6bf90e9-2e5e-4b7b-bfc0-ed8a4e243e4d" + }, + { + "__type__": "cc.Node", + "_name": "Canvas", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + } + ], + "_active": true, + "_components": [ + { + "__id__": 5 + }, + { + "__id__": 6 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 800, + "y": 360, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "faTOCTLXRFdpZeZi7lNLQq" + }, + { + "__type__": "cc.Node", + "_name": "Camera", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 1000 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "6ayrdwHr9GLKXUxRdk2hH5" + }, + { + "__type__": "cc.Camera", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": null, + "_projection": 0, + "_priority": 0, + "_fov": 45, + "_fovAxis": 0, + "_orthoHeight": 360, + "_near": 1, + "_far": 2000, + "_color": { + "__type__": "cc.Color", + "r": 51, + "g": 51, + "b": 51, + "a": 255 + }, + "_depth": 1, + "_stencil": 0, + "_clearFlags": 7, + "_rect": { + "__type__": "cc.Rect", + "x": 0, + "y": 0, + "width": 1, + "height": 1 + }, + "_aperture": 19, + "_shutter": 7, + "_iso": 0, + "_screenScale": 1, + "_visibility": 1107296256, + "_targetTexture": null, + "_postProcess": null, + "_usePostProcess": false, + "_cameraType": -1, + "_trackingType": 0, + "_id": "51tpdCan1JboRRHZOyhxuv" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "8ej3BZPwhAD6WQVYXaPCUB" + }, + { + "__type__": "cc.Canvas", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_cameraComponent": { + "__id__": 4 + }, + "_alignCanvasWithScreen": true, + "_id": "cduZE+wFRG1q4NzbTvVx8E" + }, + { + "__type__": "cc.SceneGlobals", + "ambient": { + "__id__": 8 + }, + "shadows": { + "__id__": 9 + }, + "_skybox": { + "__id__": 10 + }, + "fog": { + "__id__": 11 + }, + "octree": { + "__id__": 12 + }, + "skin": { + "__id__": 13 + }, + "lightProbeInfo": { + "__id__": 14 + }, + "postSettings": { + "__id__": 15 + }, + "bakedWithStationaryMainLight": false, + "bakedWithHighpLightmap": false + }, + { + "__type__": "cc.AmbientInfo", + "_skyColorHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyColor": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyIllumHDR": 20000, + "_skyIllum": 20000, + "_groundAlbedoHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_groundAlbedo": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_skyColorLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 1 + }, + "_skyIllumLDR": 20000, + "_groundAlbedoLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + } + }, + { + "__type__": "cc.ShadowsInfo", + "_enabled": false, + "_type": 0, + "_normal": { + "__type__": "cc.Vec3", + "x": 0, + "y": 1, + "z": 0 + }, + "_distance": 0, + "_planeBias": 1, + "_shadowColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 76 + }, + "_maxReceived": 4, + "_size": { + "__type__": "cc.Vec2", + "x": 1024, + "y": 1024 + } + }, + { + "__type__": "cc.SkyboxInfo", + "_envLightingType": 0, + "_envmapHDR": null, + "_envmap": null, + "_envmapLDR": null, + "_diffuseMapHDR": null, + "_diffuseMapLDR": null, + "_enabled": false, + "_useHDR": true, + "_editableMaterial": null, + "_reflectionHDR": null, + "_reflectionLDR": null, + "_rotationAngle": 0 + }, + { + "__type__": "cc.FogInfo", + "_type": 0, + "_fogColor": { + "__type__": "cc.Color", + "r": 200, + "g": 200, + "b": 200, + "a": 255 + }, + "_enabled": false, + "_fogDensity": 0.3, + "_fogStart": 0.5, + "_fogEnd": 300, + "_fogAtten": 5, + "_fogTop": 1.5, + "_fogRange": 1.2, + "_accurate": false + }, + { + "__type__": "cc.OctreeInfo", + "_enabled": false, + "_minPos": { + "__type__": "cc.Vec3", + "x": -1024, + "y": -1024, + "z": -1024 + }, + "_maxPos": { + "__type__": "cc.Vec3", + "x": 1024, + "y": 1024, + "z": 1024 + }, + "_depth": 8 + }, + { + "__type__": "cc.SkinInfo", + "_enabled": true, + "_blurRadius": 0.01, + "_sssIntensity": 3 + }, + { + "__type__": "cc.LightProbeInfo", + "_giScale": 1, + "_giSamples": 1024, + "_bounces": 2, + "_reduceRinging": 0, + "_showProbe": true, + "_showWireframe": true, + "_showConvex": false, + "_data": null, + "_lightProbeSphereVolume": 1 + }, + { + "__type__": "cc.PostSettingsInfo", + "_toneMappingType": 0 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLogin.scene.meta b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLogin.scene.meta new file mode 100644 index 0000000..0dd43be --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformLogin.scene.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.1.50", + "importer": "scene", + "imported": true, + "uuid": "c6bf90e9-2e5e-4b7b-bfc0-ed8a4e243e4d", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene new file mode 100644 index 0000000..46e785b --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene @@ -0,0 +1,316 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "PlatformStartup", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "scene": { + "__id__": 1 + } + }, + { + "__type__": "cc.Scene", + "_name": "PlatformStartup", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "autoReleaseAssets": false, + "_globals": { + "__id__": 4 + }, + "_id": "1c10c2ee-adc2-496f-9005-d3e0e6ebbb56" + }, + { + "__type__": "cc.Node", + "_name": "PlatformServices", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 3 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "edK1iknZ1EDKLpdkoVy5NZ" + }, + { + "__type__": "6dba8Keu9dNqL1ILyFO7MyT", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "loadingScene": { + "__uuid__": "262ea1ac-58e5-4144-b79c-6e854d75d693", + "__expectedType__": "cc.SceneAsset" + }, + "loginScene": { + "__uuid__": "c6bf90e9-2e5e-4b7b-bfc0-ed8a4e243e4d", + "__expectedType__": "cc.SceneAsset" + }, + "loadingPrefab": { + "__uuid__": "3e5a41f3-b043-45a5-94f6-e299ea9acc61", + "__expectedType__": "cc.Prefab" + }, + "loginPrefab": { + "__uuid__": "b209cefb-b381-4c7c-b765-94a5ab75a89c", + "__expectedType__": "cc.Prefab" + }, + "protocolPrefab": { + "__uuid__": "8fad42f6-6e14-4311-a344-e6ca87bc0087", + "__expectedType__": "cc.Prefab" + }, + "phonePrefab": { + "__uuid__": "b9ec1622-fe6d-4a07-8958-3a0732040e91", + "__expectedType__": "cc.Prefab" + }, + "_id": "09m0ERGqxLWKsJIBthaeXP" + }, + { + "__type__": "cc.SceneGlobals", + "ambient": { + "__id__": 5 + }, + "shadows": { + "__id__": 6 + }, + "_skybox": { + "__id__": 7 + }, + "fog": { + "__id__": 8 + }, + "octree": { + "__id__": 9 + }, + "skin": { + "__id__": 10 + }, + "lightProbeInfo": { + "__id__": 11 + }, + "postSettings": { + "__id__": 12 + }, + "bakedWithStationaryMainLight": false, + "bakedWithHighpLightmap": false + }, + { + "__type__": "cc.AmbientInfo", + "_skyColorHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyColor": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyIllumHDR": 20000, + "_skyIllum": 20000, + "_groundAlbedoHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_groundAlbedo": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_skyColorLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 1 + }, + "_skyIllumLDR": 20000, + "_groundAlbedoLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + } + }, + { + "__type__": "cc.ShadowsInfo", + "_enabled": false, + "_type": 0, + "_normal": { + "__type__": "cc.Vec3", + "x": 0, + "y": 1, + "z": 0 + }, + "_distance": 0, + "_planeBias": 1, + "_shadowColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 76 + }, + "_maxReceived": 4, + "_size": { + "__type__": "cc.Vec2", + "x": 1024, + "y": 1024 + } + }, + { + "__type__": "cc.SkyboxInfo", + "_envLightingType": 0, + "_envmapHDR": null, + "_envmap": null, + "_envmapLDR": null, + "_diffuseMapHDR": null, + "_diffuseMapLDR": null, + "_enabled": false, + "_useHDR": true, + "_editableMaterial": null, + "_reflectionHDR": null, + "_reflectionLDR": null, + "_rotationAngle": 0 + }, + { + "__type__": "cc.FogInfo", + "_type": 0, + "_fogColor": { + "__type__": "cc.Color", + "r": 200, + "g": 200, + "b": 200, + "a": 255 + }, + "_enabled": false, + "_fogDensity": 0.3, + "_fogStart": 0.5, + "_fogEnd": 300, + "_fogAtten": 5, + "_fogTop": 1.5, + "_fogRange": 1.2, + "_accurate": false + }, + { + "__type__": "cc.OctreeInfo", + "_enabled": false, + "_minPos": { + "__type__": "cc.Vec3", + "x": -1024, + "y": -1024, + "z": -1024 + }, + "_maxPos": { + "__type__": "cc.Vec3", + "x": 1024, + "y": 1024, + "z": 1024 + }, + "_depth": 8 + }, + { + "__type__": "cc.SkinInfo", + "_enabled": true, + "_blurRadius": 0.01, + "_sssIntensity": 3 + }, + { + "__type__": "cc.LightProbeInfo", + "_giScale": 1, + "_giSamples": 1024, + "_bounces": 2, + "_reduceRinging": 0, + "_showProbe": true, + "_showWireframe": true, + "_showConvex": false, + "_data": null, + "_lightProbeSphereVolume": 1 + }, + { + "__type__": "cc.PostSettingsInfo", + "_toneMappingType": 0 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene.meta b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene.meta new file mode 100644 index 0000000..9aba202 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/PlatformStartup.scene.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.1.50", + "importer": "scene", + "imported": true, + "uuid": "1c10c2ee-adc2-496f-9005-d3e0e6ebbb56", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/scene-2d.scene b/cocoscreator_projects/YouleNexus/assets/scenes/scene-2d.scene new file mode 100644 index 0000000..0479cfb --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/scene-2d.scene @@ -0,0 +1,449 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "scene-2d", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "scene": { + "__id__": 1 + } + }, + { + "__type__": "cc.Scene", + "_name": "scene-2d", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "autoReleaseAssets": false, + "_globals": { + "__id__": 8 + }, + "_id": "aa6aad6d-aef0-4b69-95a0-18ffda41f4c7" + }, + { + "__type__": "cc.Node", + "_name": "Canvas", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + } + ], + "_active": true, + "_components": [ + { + "__id__": 5 + }, + { + "__id__": 6 + }, + { + "__id__": 7 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 800, + "y": 360.00000000000006, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "beI88Z2HpFELqR4T5EMHpg" + }, + { + "__type__": "cc.Node", + "_name": "Camera", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 1000 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "ebFwiq8gBFaYpqYbdoDODe" + }, + { + "__type__": "cc.Camera", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": null, + "_projection": 0, + "_priority": 0, + "_fov": 45, + "_fovAxis": 0, + "_orthoHeight": 360, + "_near": 0, + "_far": 2000, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_depth": 1, + "_stencil": 0, + "_clearFlags": 7, + "_rect": { + "__type__": "cc.Rect", + "x": 0, + "y": 0, + "width": 1, + "height": 1 + }, + "_aperture": 19, + "_shutter": 7, + "_iso": 0, + "_screenScale": 1, + "_visibility": 1108344832, + "_targetTexture": null, + "_postProcess": null, + "_usePostProcess": false, + "_cameraType": -1, + "_trackingType": 0, + "_id": "63WIch3o5BEYRlXzTT0oWc" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 720 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "d6rUX5yfhMlKoWX2bSbawx" + }, + { + "__type__": "cc.Canvas", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_cameraComponent": { + "__id__": 4 + }, + "_alignCanvasWithScreen": true, + "_id": "12O/ljcVlEqLmVm3U2gEOQ" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 5.684341886080802e-14, + "_bottom": 5.684341886080802e-14, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "c5V1EV8IpMtrIvY1OE9t2u" + }, + { + "__type__": "cc.SceneGlobals", + "ambient": { + "__id__": 9 + }, + "shadows": { + "__id__": 10 + }, + "_skybox": { + "__id__": 11 + }, + "fog": { + "__id__": 12 + }, + "octree": { + "__id__": 13 + }, + "skin": { + "__id__": 14 + }, + "lightProbeInfo": { + "__id__": 15 + }, + "postSettings": { + "__id__": 16 + }, + "bakedWithStationaryMainLight": false, + "bakedWithHighpLightmap": false + }, + { + "__type__": "cc.AmbientInfo", + "_skyColorHDR": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0.520833125 + }, + "_skyColor": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0.520833125 + }, + "_skyIllumHDR": 20000, + "_skyIllum": 20000, + "_groundAlbedoHDR": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_groundAlbedo": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_skyColorLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 1 + }, + "_skyIllumLDR": 20000, + "_groundAlbedoLDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + } + }, + { + "__type__": "cc.ShadowsInfo", + "_enabled": false, + "_type": 0, + "_normal": { + "__type__": "cc.Vec3", + "x": 0, + "y": 1, + "z": 0 + }, + "_distance": 0, + "_planeBias": 1, + "_shadowColor": { + "__type__": "cc.Color", + "r": 76, + "g": 76, + "b": 76, + "a": 255 + }, + "_maxReceived": 4, + "_size": { + "__type__": "cc.Vec2", + "x": 512, + "y": 512 + } + }, + { + "__type__": "cc.SkyboxInfo", + "_envLightingType": 0, + "_envmapHDR": null, + "_envmap": null, + "_envmapLDR": null, + "_diffuseMapHDR": null, + "_diffuseMapLDR": null, + "_enabled": false, + "_useHDR": true, + "_editableMaterial": null, + "_reflectionHDR": null, + "_reflectionLDR": null, + "_rotationAngle": 0 + }, + { + "__type__": "cc.FogInfo", + "_type": 0, + "_fogColor": { + "__type__": "cc.Color", + "r": 200, + "g": 200, + "b": 200, + "a": 255 + }, + "_enabled": false, + "_fogDensity": 0.3, + "_fogStart": 0.5, + "_fogEnd": 300, + "_fogAtten": 5, + "_fogTop": 1.5, + "_fogRange": 1.2, + "_accurate": false + }, + { + "__type__": "cc.OctreeInfo", + "_enabled": false, + "_minPos": { + "__type__": "cc.Vec3", + "x": -1024, + "y": -1024, + "z": -1024 + }, + "_maxPos": { + "__type__": "cc.Vec3", + "x": 1024, + "y": 1024, + "z": 1024 + }, + "_depth": 8 + }, + { + "__type__": "cc.SkinInfo", + "_enabled": false, + "_blurRadius": 0.01, + "_sssIntensity": 3 + }, + { + "__type__": "cc.LightProbeInfo", + "_giScale": 1, + "_giSamples": 1024, + "_bounces": 2, + "_reduceRinging": 0, + "_showProbe": true, + "_showWireframe": true, + "_showConvex": false, + "_data": null, + "_lightProbeSphereVolume": 1 + }, + { + "__type__": "cc.PostSettingsInfo", + "_toneMappingType": 0 + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/scene-2d.scene.meta b/cocoscreator_projects/YouleNexus/assets/scenes/scene-2d.scene.meta new file mode 100644 index 0000000..15aa42c --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scenes/scene-2d.scene.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.1.50", + "importer": "scene", + "imported": true, + "uuid": "aa6aad6d-aef0-4b69-95a0-18ffda41f4c7", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts deleted file mode 100644 index ba09187..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { _decorator, assetManager, Component, Label, Node, Prefab, ProgressBar, Sprite, instantiate, isValid, sys } from 'cc'; -import { DEBUG } from 'cc/env'; -import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts'; -import { resolveLaunchWaitTime } from '../../framework/config/remote-config.ts'; -import type { NativeHost } from '../../framework/config/sources/native-settings.ts'; -import type { PageModel } from '../../framework/presentation/ui-contracts.ts'; -import { createLocalLoginHost, type LocalLoginHost } from './local-login-host.ts'; -import type { LocalLoginEvidence } from './local-login-transport.ts'; -import { CocosLocalLoginView, CocosLogoCountdownView, createLogoCountdown, createAssetLoadTask, createCocosFrameScheduler, positionDiagnosticStopButtonBelow, requireNode } from './local-login-view.ts'; - -const { ccclass, property } = _decorator; - -/** Explicit DEBUG browser scene entry. All business state belongs to its host Runtime. */ -@ccclass('LocalPlatformLogin') -export class LocalPlatformLogin extends Component { - @property(Prefab) loadingPrefab: Prefab | null = null; - @property(Prefab) loginPrefab: Prefab | null = null; - @property(Node) loadingRoot: Node | null = null; - @property(Node) loginRoot: Node | null = null; - @property(Node) resultRoot: Node | null = null; - @property(Node) statusOverlay: Node | null = null; - @property(Label) statusLabel: Label | null = null; - @property(Label) resultLabel: Label | null = null; - @property(Node) stopButton: Node | null = null; - - private host: LocalLoginHost | null = null; - private view: CocosLocalLoginView | null = null; - private resourceProgress: ProgressBar | null = null; - private loginInstance: Node | null = null; - private stopped = false; - private firstDrawAt: number | null = null; - private version: number | null = null; - private countdown: ReturnType | null = null; - private countdownView: CocosLogoCountdownView | null = null; - private readonly pending = new Set<() => void>(); - private readonly evidence: (LocalLoginEvidence & { at: number })[] = []; - private pageModel: PageModel | null = null; - private readonly stopTouch = () => this.stop(); - - async start(): Promise { - // This guard precedes reading browser storage, preparing account or creating socket. - if (!DEBUG || sys.isNative) throw new Error('LocalPlatformLogin requires DEBUG browser preview'); - try { - const loadingRoot = this.required(this.loadingRoot, 'loadingRoot'); - const loginRoot = this.required(this.loginRoot, 'loginRoot'); - const stopButton = this.required(this.stopButton, 'stopButton'); - this.view = new CocosLocalLoginView({ loadingRoot, loginRoot, - resultRoot: this.required(this.resultRoot, 'resultRoot'), - statusOverlay: this.required(this.statusOverlay, 'statusOverlay'), - statusLabel: this.required(this.statusLabel, 'statusLabel'), - resultLabel: this.required(this.resultLabel, 'resultLabel') }); - this.required(this.loadingPrefab, 'loadingPrefab'); - this.required(this.loginPrefab, 'loginPrefab'); - const logo = requireNode(loadingRoot, 'Layer1_Logo'); - this.resourceProgress = requireNode(logo, 'group-1/Spirit254').getComponent(ProgressBar); - if (this.resourceProgress === null) throw new Error('Layer1_Logo/group-1/Spirit254 requires ProgressBar'); - requireNode(logo, 'group-32').active = false; - const countdownLabels = logo.getComponentsInChildren(Label).filter(label => label.node.name === 'Spirit3104'); - if (countdownLabels.length !== 1) throw new Error('Logo requires exactly one Spirit3104 Label'); - const logoButton = requireNode(logo, 'group-1/Spirit3103'); - positionDiagnosticStopButtonBelow(stopButton, logoButton); - this.countdownView = new CocosLogoCountdownView(logoButton, countdownLabels[0].node, - () => this.countdown?.skip()); - this.countdown = createLogoCountdown({ now: () => performance.now(), - schedule: (callback, ms) => { - const timer = window.setTimeout(() => { - try { callback(); } catch (error) { this.fail(error); } - }, ms); - return () => window.clearTimeout(timer); - }, render: state => this.countdownView?.render(state) }); - this.validateSprites(loadingRoot); - stopButton.on(Node.EventType.TOUCH_END, this.stopTouch); - this.view.activate('loading'); - const firstDraw = this.nextDraw().then(() => { - this.firstDrawAt = performance.now(); - this.record({ kind: 'loading-first-draw', data: null }); - }); - // A configuration failure can cancel this frame before Runtime consumes its gate. - void firstDraw.catch(() => {}); - const view = this.view; - const browser = window as unknown as NativeHost & { returnCitySN?: unknown }; - this.host = createLocalLoginHost({ - configOptions: { hostKind: 'h5', search: window.location.search, - win: browser, fetcher: new HttpConfigFetcher() }, - storage: window.localStorage, random: Math.random, now: Date.now, - sampleHost: () => ({ location: null, - ...('returnCitySN' in browser ? { returnCitySN: browser.returnCitySN } : {}) }), - view: { - render: model => { this.pageModel = model; this.record({ kind: 'page-model', data: model }); view.render(model); }, - activate: page => { view.activate(page); this.record({ kind: 'page-active', data: page }); }, - showOverlay: model => view.showOverlay(model), - }, - scheduler: createCocosFrameScheduler(), - onConfigResolved: config => { - this.version = config.identity.version; - this.required(this.countdown, 'Logo countdown').start(resolveLaunchWaitTime(config.remoteConfig)); - }, - onResourcesReady: () => { - this.countdown?.resourcesReady(); - this.required(this.resourceProgress, 'resource ProgressBar').node.active = false; - }, - onSocketReady: ready => this.countdown?.socketReady(ready), - onStopped: () => { this.countdown?.cancel(); this.countdownView?.dispose(); }, - loadResources: () => this.loadResources(), - waitForMinimumDisplay: async () => { - await Promise.all([firstDraw, this.required(this.countdown, 'Logo countdown').promise]); - const drawnAt = this.required(this.firstDrawAt, 'loading first draw'); - this.record({ kind: 'minimum-display-ready', data: performance.now() - drawnAt }); - }, - createSocket: url => new WebSocket(url), record: event => this.record(event), - onBusy: busy => view.setBusy(busy), - onFailure: error => this.fail(error), - }); - await this.host.start(); - } catch (error) { this.fail(error); } - } - - private required(value: T | null, name: string): T { - if (value === null || value === undefined) throw new Error(`LocalPlatformLogin missing ${name}`); - return value; - } - - private record(event: LocalLoginEvidence): void { - this.evidence.push(Object.freeze({ ...event, at: performance.now() })); - } - - private validateSprites(root: Node): void { - for (const sprite of root.getComponentsInChildren(Sprite)) { - if (!sprite.node.activeInHierarchy) continue; - const frame = sprite.spriteFrame; - if (frame === null || !isValid(frame) || frame.texture === null || !isValid(frame.texture)) { - throw new Error(`Invalid SpriteFrame/texture dependency: ${sprite.node.name}`); - } - } - } - - private async loadResources(): Promise { - if (this.stopped) throw new Error('Local login resources cancelled'); - const serializedPrefab = this.required(this.loginPrefab, 'loginPrefab'); - const loading = this.required(this.loadingPrefab, 'loadingPrefab'); - if (!isValid(serializedPrefab) || !isValid(serializedPrefab.data) || !isValid(loading) || !isValid(loading.data)) { - throw new Error('Local login serialized Prefab dependency is invalid'); - } - const progress = this.required(this.resourceProgress, 'resource ProgressBar'); - const task = createAssetLoadTask((onProgress, onComplete) => { - assetManager.loadAny(serializedPrefab.uuid, onProgress, (error, asset) => onComplete(error, asset as Prefab)); - }, value => { progress.progress = value; }); - this.pending.add(task.cancel); - let prefab: Prefab; - try { prefab = await task.promise; } - finally { this.pending.delete(task.cancel); } - if (this.stopped) throw new Error('Local login resources cancelled'); - if (!isValid(prefab) || !isValid(prefab.data)) throw new Error('Loaded login Prefab dependency is invalid'); - const instance = instantiate(prefab); - this.loginInstance = instance; - instance.parent = this.required(this.loginRoot, 'loginRoot'); - const view = this.required(this.view, 'view'); - view.bindLogin(instance, () => { - const host = this.required(this.host, 'host'); - void host.login().catch(error => this.fail(error)); - }); - view.setVersion(this.required(this.version, 'resolved identity.version')); - // Validate active prefab content before the root is exposed as the login page. - for (const sprite of instance.getComponentsInChildren(Sprite)) { - if (!sprite.node.active) continue; - const frame = sprite.spriteFrame; - if (frame === null || !isValid(frame) || frame.texture === null || !isValid(frame.texture)) { - throw new Error(`Invalid login SpriteFrame/texture dependency: ${sprite.node.name}`); - } - } - await this.nextDraw(); - this.record({ kind: 'resources-ready', data: { login: prefab.uuid, loading: loading.uuid } }); - } - - private nextDraw(): Promise { - return new Promise((resolve, reject) => { - if (this.stopped) { reject(new Error('Local login frame cancelled')); return; } - const cancelFrame = createCocosFrameScheduler().request(() => { - this.pending.delete(cancel); resolve(); - }); - const cancel = () => { cancelFrame(); this.pending.delete(cancel); reject(new Error('Local login frame cancelled')); }; - this.pending.add(cancel); - }); - } - - private delay(milliseconds: number): Promise { - return new Promise((resolve, reject) => { - if (this.stopped) { reject(new Error('Local login timer cancelled')); return; } - const timer = window.setTimeout(() => { this.pending.delete(cancel); resolve(); }, milliseconds); - const cancel = () => { window.clearTimeout(timer); this.pending.delete(cancel); reject(new Error('Local login timer cancelled')); }; - this.pending.add(cancel); - }); - } - - private release(): void { - this.stopped = true; - this.countdown?.cancel(); - this.countdownView?.dispose(); - for (const cancel of [...this.pending]) cancel(); - this.stopButton?.off(Node.EventType.TOUCH_END, this.stopTouch); - this.view?.dispose(); - this.host?.stop(); - } - - private fail(error: unknown): void { - if (this.stopped) return; - this.record({ kind: 'component-failure', data: error }); - try { this.release(); } - catch (cleanup) { - this.record({ kind: 'cleanup-failure', data: cleanup }); - this.view?.showFailure({ error, cleanup }); - return; - } - this.view?.showFailure(error); - if (this.view === null && this.statusLabel !== null) this.statusLabel.string = `验证失败:${String(error)}`; - } - - stop(): void { - if (this.stopped) return; - this.view?.showStopped(); - try { this.release(); } - catch (error) { this.view?.showFailure(error); } - this.record({ kind: 'stopped', data: null }); - } - - onDestroy(): void { - this.stop(); - if (this.loginInstance !== null && isValid(this.loginInstance)) this.loginInstance.destroy(); - } - - /** Local memory only: callers must redact raw account/device IDs before sharing. */ - getDiagnostics() { - return Object.freeze({ events: Object.freeze([...this.evidence]), snapshot: this.host?.getSnapshot(), - pageModel: this.pageModel, stopped: this.stopped, pendingCount: this.pending.size, - firstDrawAt: this.firstDrawAt, resourceProgress: this.resourceProgress?.progress }); - } -} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts deleted file mode 100644 index 68cda18..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { resolveLocalStartup } from '../../framework/config/local-startup.ts'; -import type { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../framework/config/runtime-config.ts'; -import { createLocalLoginSources, type LocalHostSnapshot, type LocalLoginSources } from '../../framework/adapters/local/login-sources.ts'; -import type { LocalStoragePort } from '../../framework/adapters/local/local-storage.ts'; -import { PlatformRuntime } from '../../framework/platform/runtime.ts'; -import type { PlatformState } from '../../framework/platform/stores/platform-types.ts'; -import { PlatformUiController } from '../../framework/presentation/platform-ui-controller.ts'; -import type { PlatformViewPort, FrameScheduler } from '../../framework/presentation/ui-contracts.ts'; -import { createDiagnosticGameEntry } from './local-login-game-entry.ts'; -import { createLocalLoginWire } from './local-login-wire.ts'; -import { LocalLoginTransport, type EvidenceSink } from './local-login-transport.ts'; - -export interface LocalLoginHostOptions { - configOptions: ResolveRuntimeConfigOptions; - storage: LocalStoragePort; - random: () => number; - now: () => number; - sampleHost: () => LocalHostSnapshot; - view: PlatformViewPort; - scheduler: FrameScheduler; - loadResources: (config: RuntimeConfig) => Promise; - waitForMinimumDisplay: (config: RuntimeConfig) => Promise; - onConfigResolved?: (config: RuntimeConfig) => void; - onResourcesReady?: () => void; - onSocketReady?: (ready: boolean) => void; - onStopped?: () => void; - createSocket: (url: string) => WebSocket; - record: EvidenceSink; - onBusy: (busy: boolean) => void; - onFailure: (error: unknown) => void; -} - -export interface LocalLoginHost { - start(): Promise; - login(): Promise; - stop(): void; - getSnapshot(): { state: PlatformState | null; ready: boolean; busy: boolean }; -} - -class LocalLoginHostFault extends Error { - readonly errors: readonly unknown[]; - constructor(errors: readonly unknown[]) { - super('Local login host failed with additional observer or cleanup errors'); - this.name = 'LocalLoginHostFault'; - this.errors = Object.freeze([...errors]); - } -} - -function collected(errors: readonly unknown[]): unknown { - return errors.length === 1 ? errors[0] : new LocalLoginHostFault(errors); -} - -/** Development composition only; Runtime remains the sole business state owner. */ -export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLoginHost { - let runtime: PlatformRuntime | null = null; - let controller: PlatformUiController | null = null; - let sources: LocalLoginSources | null = null; - let startup: Promise | null = null; - let loginOperation: Promise | null = null; - let terminal = false; - let busy = false; - let cleanupErrors: unknown[] | null = null; - let serverFailure: { data: unknown } | null = null; - let unsubscribeState: (() => void) | null = null; - - function requireActive(): void { - if (terminal) throw new Error('Local login host is stopped or terminal'); - } - - function setBusy(value: boolean): void { - if (busy === value) return; - busy = value; - options.onBusy(value); - } - - function shutdown(errors: unknown[]): void { - terminal = true; - cleanupErrors = errors; - try { options.onStopped?.(); } catch (error) { errors.push(error); } - try { unsubscribeState?.(); unsubscribeState = null; } catch (error) { errors.push(error); } - // Disable input before releasing either owner; dispose preserves drawn output. - try { setBusy(false); } catch (error) { errors.push(error); } - try { controller?.dispose(); } catch (error) { errors.push(error); } - try { runtime?.stop(); } catch (error) { errors.push(error); } - cleanupErrors = null; - } - - function fail(error: unknown): void { - if (cleanupErrors !== null) { - if (!cleanupErrors.includes(error)) cleanupErrors.push(error); - return; - } - if (terminal) return; - const errors = [error]; - shutdown(errors); - options.onFailure(collected(errors)); - } - - function wireFault(error: unknown): void { - if (serverFailure === null) { fail(error); return; } - // The Wire scope marker follows the existing denial/kick handler. Keep the - // original server payload, and retain any independent Wire cleanup errors. - const extras = error instanceof Error && 'errors' in error - ? (error as Error & { errors: readonly unknown[] }).errors.slice(1) : []; - fail(collected([serverFailure.data, ...extras])); - } - - function transportFault(error: unknown): void { - fail(serverFailure === null ? error : collected([serverFailure.data, error])); - } - - async function readyTask(task: () => Promise): Promise { - try { requireActive(); await task(); requireActive(); } - catch (error) { fail(error); throw error; } - } - - function start(): Promise { - if (startup !== null) return startup; - if (terminal) return Promise.reject(new Error('Local login host is stopped or terminal')); - startup = Promise.resolve().then(async () => { - requireActive(); - const context = await resolveLocalStartup(options.configOptions); - requireActive(); - options.onConfigResolved?.(context.config); - requireActive(); - const { mode, isDebugger, identity, servers, source, gameserver } = context.config; - options.record({ kind: 'startup-config', data: { mode, isDebugger, identity, servers, source, gameserver } }); - requireActive(); - const entry = createDiagnosticGameEntry(context.config.identity.gameid, options.record); - sources = createLocalLoginSources({ context, storage: options.storage, random: options.random, - now: options.now, sampleHost: options.sampleHost, - isSessionActive: () => busy || runtime?.state.app.phase === 'logged-in' }); - controller = new PlatformUiController({ - render: model => { - if (terminal) return; - try { options.view.render(model); } - catch (error) { fail(error); throw error; } - }, - activate: page => { - if (terminal) return; - try { - options.view.activate(page); - if (!terminal && page === 'lobby') setBusy(false); - } catch (error) { fail(error); throw error; } - }, - showOverlay: model => { - if (terminal) return; - if (model.kind === 'denial') serverFailure = { data: model.outcome.data }; - if (model.kind === 'kicked') serverFailure = { data: model.data }; - try { options.view.showOverlay(model); } - catch (error) { - if (model.kind === 'fatal') fail(collected([model.error, error])); - else transportFault(error); - throw error; - } - if (model.kind === 'fatal') fail(model.error); - }, - }, { - request: callback => { - try { - const cancel = options.scheduler.request(callback); - return () => { - try { cancel(); } - catch (error) { fail(error); throw error; } - }; - } catch (error) { fail(error); throw error; } - }, - }, fail); - runtime = new PlatformRuntime({ gameEntry: entry, - resolveRuntimeConfig: async () => context.config, - createWireClient: () => createLocalLoginWire({ context, record: options.record, onFault: wireFault, - transportFactory: () => new LocalLoginTransport({ context, createSocket: options.createSocket, - record: options.record, onFault: transportFault }) }), - scene: controller, loadResources: () => readyTask(async () => { - await options.loadResources(context.config); - requireActive(); options.onResourcesReady?.(); - }), - waitForMinimumDisplay: () => readyTask(() => options.waitForMinimumDisplay(context.config)), - getLoginDeviceSnapshot: sources.getLoginDeviceSnapshot, - }); - unsubscribeState = runtime.subscribeState(state => { - if (!terminal) options.onSocketReady?.(state.app.phase === 'connected' || state.app.phase === 'logged-in'); - }, fail); - controller.connect(runtime); - requireActive(); - await runtime.start(); - requireActive(); - }).catch(error => { fail(error); throw error; }); - return startup; - } - - function login(): Promise { - if (terminal) return Promise.reject(new Error('Local login host is stopped or terminal')); - if (busy && loginOperation !== null) return loginOperation; - if (runtime === null || controller === null || sources === null || !runtime.ready) { - return Promise.reject(new Error('Local login host is not ready for login')); - } - const activeSources = sources; - const activeController = controller; - loginOperation = Promise.resolve().then(async () => { - requireActive(); - const account = await activeSources.prepareLogin(); - requireActive(); - activeController.login(account); - }).catch(error => { fail(error); throw error; }); - try { setBusy(true); } catch (error) { fail(error); } - return loginOperation; - } - - return { - start, login, - stop() { - if (terminal) return; - const errors: unknown[] = []; - shutdown(errors); - if (errors.length > 0) { - const error = collected(errors); - options.onFailure(error); - throw error; - } - }, - getSnapshot: () => ({ state: runtime === null ? null : runtime.state, - ready: !terminal && runtime !== null && runtime.ready, busy }), - }; -} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts.meta deleted file mode 100644 index 53c7288..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-host.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "2c6ce55f-b5a3-4503-bc65-ca12c5537c8f", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts deleted file mode 100644 index 444026d..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { - assertLocalConnectionTarget, - type LocalStartupContext, -} from '../../framework/config/local-startup.ts'; -import type { Transport } from '../../framework/net/transport.ts'; - -export type LocalLoginEvidence = { kind: string; data: unknown }; -export type EvidenceSink = (event: LocalLoginEvidence) => void; - -interface LocalLoginTransportOptions { - context: LocalStartupContext; - createSocket: (url: string) => WebSocket; - onFault: (error: unknown) => void; - record: EvidenceSink; -} - -/** ES2020 aggregate: retain originals before notifying a terminal observer once. */ -class LocalLoginTransportFault extends Error { - readonly errors: readonly unknown[]; - constructor(errors: readonly unknown[]) { - super('Local login transport failed with an additional cleanup error'); - this.name = 'LocalLoginTransportFault'; - this.errors = Object.freeze([...errors]); - } -} - -/** Local-only socket boundary; raw evidence precedes protocol decoding. */ -export class LocalLoginTransport implements Transport { - private socket: WebSocket | null = null; - private generation = 0; - private openCallback?: () => void; - private messageCallback?: (frame: string) => void; - private closeCallback?: () => void; - - constructor(private readonly options: LocalLoginTransportOptions) {} - - connect(url: string): void { - try { - assertLocalConnectionTarget(this.options.context, url); - } catch (error) { - this.options.onFault(error); - throw error; - } - this.close(); - let socket: WebSocket; - try { - socket = this.options.createSocket(url); - } catch (error) { - this.options.onFault(error); - throw error; - } - this.socket = socket; - const generation = this.generation; - socket.onopen = () => { - if (this.socket === socket) this.openCallback?.(); - }; - socket.onmessage = (event) => { - if (this.socket !== socket) return; - if (typeof event.data !== 'string') { - this.stopWithFault(new TypeError('Local login WebSocket requires text frames')); - return; - } - try { - this.options.record({ kind: 'transport-receive', data: event.data }); - } catch (error) { - throw this.stopWithFault(error); - } - if (this.socket === socket) this.messageCallback?.(event.data); - }; - socket.onclose = () => { - if (this.socket !== socket) return; - this.socket = null; - this.closeCallback?.(); - }; - socket.onerror = (event) => { - if (this.socket !== socket) return; - try { - this.options.record({ kind: 'transport-error', data: event }); - } catch (error) { - throw this.stopWithFault(error); - } - if (this.socket !== socket || this.generation !== generation) return; - // Invalidate before close: browsers may dispatch close after error. - this.socket = null; - try { - socket.close(); - } catch (error) { - this.options.onFault(error); - throw error; - } - if (this.generation === generation) this.closeCallback?.(); - }; - } - - send(frame: string): void { - const socket = this.socket; - if (!socket || socket.readyState !== 1) { - throw new Error('Local login WebSocket must be OPEN before send'); - } - try { - socket.send(frame); - this.options.record({ kind: 'transport-send', data: frame }); - } catch (error) { - throw this.stopWithFault(error); - } - } - - close(): void { - this.generation++; - const socket = this.socket; - this.socket = null; - if (!socket) return; - try { - socket.close(); - } catch (error) { - this.options.onFault(error); - throw error; - } - } - - onOpen(callback: () => void): void { this.openCallback = callback; } - onMessage(callback: (frame: string) => void): void { this.messageCallback = callback; } - onClose(callback: () => void): void { this.closeCallback = callback; } - - private stopWithFault(error: unknown): unknown { - const socket = this.socket; - this.socket = null; - const errors: unknown[] = [error]; - try { - socket?.close(); - } catch (cleanupError) { - errors.push(cleanupError); - } - const fault = errors.length === 1 ? error : new LocalLoginTransportFault(errors); - this.options.onFault(fault); - return fault; - } -} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts.meta deleted file mode 100644 index dbeb6af..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "2bbf94d8-6aa1-4b5f-a94c-c3f12205f46c", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts deleted file mode 100644 index 9486a02..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { Director, Label, Node, Sprite, UITransform, Vec3, Widget, director } from 'cc'; -import type { FrameScheduler, OverlayModel, PageId, PageModel, PlatformViewPort } from '../../framework/presentation/ui-contracts.ts'; - -interface ViewReferences { - loadingRoot: Node; loginRoot: Node; resultRoot: Node; - statusOverlay: Node; statusLabel: Label; resultLabel: Label; -} - -export interface LogoCountdownState { readonly visible: boolean; readonly text: string; } - -const DIAGNOSTIC_STOP_GAP = 12; - -/** Keeps the scene diagnostic control clear of the native Logo click region. */ -export function positionDiagnosticStopButtonBelow(stopButton: Node, logoButton: Node): void { - const stopTransform = stopButton.getComponent(UITransform); - const logoTransform = logoButton.getComponent(UITransform); - const stopParentTransform = stopButton.parent?.getComponent(UITransform); - if (!stopTransform || !logoTransform || !stopParentTransform) { - throw new Error('Diagnostic stop layout requires native UITransform bindings'); - } - const widget = stopButton.getComponent(Widget); - if (widget) { - widget.updateAlignment(); - widget.enabled = false; - } - const logoBottom = logoTransform.convertToWorldSpaceAR( - new Vec3((0.5 - logoTransform.anchorX) * logoTransform.width, -logoTransform.anchorY * logoTransform.height, 0), - ); - const bottomInStopParent = stopParentTransform.convertToNodeSpaceAR(logoBottom); - const current = stopButton.position; - stopButton.setPosition(new Vec3(current.x, - bottomInStopParent.y - DIAGNOSTIC_STOP_GAP - (1 - stopTransform.anchorY) * stopTransform.height, - current.z)); -} - -/** Native input and measured label layout, independent of serialized label parentage. */ -export class CocosLogoCountdownView { - private readonly label: Label; - private readonly buttonTransform: UITransform; - private readonly textTransform: UITransform; - private readonly parentTransform: UITransform; - private disposed = false; - - constructor(private readonly button: Node, private readonly text: Node, private readonly onSkip: () => void) { - const label = text.getComponent(Label), buttonTransform = button.getComponent(UITransform); - const textTransform = text.getComponent(UITransform), parentTransform = text.parent?.getComponent(UITransform); - if (!label || !buttonTransform || !textTransform || !parentTransform) { - throw new Error('Logo countdown requires Label and native UITransform bindings'); - } - this.label = label; this.buttonTransform = buttonTransform; - this.textTransform = textTransform; this.parentTransform = parentTransform; - // The label follows the actual button center, so its old absolute Widget must not fight layout. - const widget = text.getComponent(Widget); if (widget) widget.enabled = false; - this.label.overflow = Label.Overflow.NONE; - button.active = false; text.active = false; - button.on(Node.EventType.TOUCH_END, onSkip); - } - - render(state: LogoCountdownState): void { - if (this.disposed) return; - this.button.active = state.visible; this.text.active = state.visible; - this.label.string = state.text; - if (!state.visible) return; - this.button.getComponent(Widget)?.updateAlignment(); - this.label.updateRenderData(true); - const button = this.buttonTransform, text = this.textTransform; - const center = button.convertToWorldSpaceAR(new Vec3((0.5 - button.anchorX) * button.width, (0.5 - button.anchorY) * button.height, 0)); - const position = this.parentTransform.convertToNodeSpaceAR(center); - position.x += (text.anchorX - 0.5) * text.width; - position.y += (text.anchorY - 0.5) * text.height; - this.text.setPosition(position); - } - - dispose(): void { - if (this.disposed) return; - this.disposed = true; - this.button.off(Node.EventType.TOUCH_END, this.onSkip); - this.button.active = false; this.text.active = false; - } -} - -/** Display gate only. Runtime still independently owns resource/config/socket gates. */ -export function createLogoCountdown(options: { - now(): number; - schedule(callback: () => void, milliseconds: number): () => void; - render(state: LogoCountdownState): void; -}) { - let started = false, finished = false, resources = false, socket = false; - let remaining = 0, deadline = 0; - let cancelDeadline: (() => void) | null = null, cancelTick: (() => void) | null = null; - let resolve!: () => void, reject!: (error: unknown) => void; - const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); - // Cancellation may precede the host attaching the display gate. - void promise.catch(() => {}); - const render = () => options.render({ visible: started && !finished && remaining > 0, - text: `倒计时${Math.ceil(Math.max(0, remaining) / 1000)}s${resources ? ' 跳过' : ''}` }); - const finish = (error?: unknown) => { - if (finished) return; - finished = true; - const failures: unknown[] = error === undefined ? [] : [error]; - for (const cancel of [cancelDeadline, cancelTick]) { - try { cancel?.(); } catch (failure) { failures.push(failure); } - } - cancelDeadline = null; cancelTick = null; - try { render(); } catch (failure) { failures.push(failure); } - if (failures.length > 0) reject(failures[0]); else resolve(); - }; - const guard = (operation: () => void) => { - if (finished) return; - try { operation(); } catch (error) { finish(error); } - }; - const checkDeadline = () => { - cancelDeadline = null; - if (finished) return; - const wait = deadline - options.now(); - if (wait <= 0) { finish(); return; } - guard(() => { cancelDeadline = options.schedule(checkDeadline, wait); }); - }; - const tick = () => { - cancelTick = null; - if (finished) return; - guard(() => { - remaining -= 1000; - render(); - if (remaining > 1000) cancelTick = options.schedule(tick, 1000); - }); - }; - render(); - return { - promise, - start(milliseconds: number) { - if (finished) return; - if (started) throw new Error('Logo countdown already started'); - if (!Number.isFinite(milliseconds) || milliseconds < 0) throw new Error('Invalid launchWaitTime'); - started = true; remaining = milliseconds; deadline = options.now() + milliseconds; - guard(() => { - render(); checkDeadline(); - if (!finished && remaining > 1000) cancelTick = options.schedule(tick, 1000); - }); - }, - resourcesReady() { guard(() => { resources = true; render(); }); }, - socketReady(value: boolean) { if (!finished) socket = value; }, - skip() { if (!finished && started && resources && socket) finish(); }, - cancel() { finish(new Error('Logo countdown cancelled')); }, - }; -} - -export interface AssetLoadTask { - readonly promise: Promise; - cancel(): void; -} - -type AssetLoadStarter = ( - onProgress: (completed: number, total: number) => void, - onComplete: (error: unknown, asset?: T) => void, -) => void; - -/** Owns loader callback lifetime so stop/failure cannot revive the login flow. */ -export function createAssetLoadTask(start: AssetLoadStarter, setProgress: (progress: number) => void): AssetLoadTask { - let settled = false; - let resolveTask!: (asset: T) => void; - let rejectTask!: (error: unknown) => void; - const promise = new Promise((resolve, reject) => { resolveTask = resolve; rejectTask = reject; }); - const reject = (error: unknown) => { - if (settled) return; - settled = true; - rejectTask(error); - }; - try { - setProgress(0); - start((completed, total) => { - if (settled) return; - if (!Number.isInteger(completed) || !Number.isInteger(total) || completed < 0 || total <= 0 || completed > total) { - reject(new Error(`Invalid asset load progress: completed=${completed}, total=${total}`)); - return; - } - try { setProgress(completed / total); } - catch (error) { reject(error); } - }, (error, asset) => { - if (settled) return; - if (error !== null && error !== undefined) { reject(error); return; } - if (asset === null || asset === undefined) { reject(new Error('Asset loader completed without an asset')); return; } - settled = true; - resolveTask(asset); - }); - } catch (error) { reject(error); } - return { promise, cancel() { reject(new Error('Asset load cancelled')); } }; -} - -export function requireNode(root: Node, path: string): Node { - const node = root.getChildByPath(path); - if (node === null) throw new Error(`Local login binding missing node: ${path}`); - return node; -} - -function describeFailure(error: unknown): string { - if (error instanceof Error) { - const details = 'errors' in error ? (error as Error & { errors: unknown[] }).errors : []; - return [error.message, ...details.map(describeFailure)].join('\n'); - } - if (typeof error === 'string') return error; - return JSON.stringify(error, (_key, value: unknown) => value instanceof Error ? describeFailure(value) : value); -} - -/** Projection of Controller models; owns only Cocos bindings and input state. */ -export class CocosLocalLoginView implements PlatformViewPort { - private visitor: Node | null = null; - private version: Label | null = null; - private mask: Node | null = null; - private touch: (() => void) | null = null; - private canLogin = false; - private busy = false; - private terminal = false; - private page: PageId = 'loading'; - - constructor(private readonly refs: ViewReferences) { - for (const [name, value] of Object.entries(refs)) { - if (value === null || value === undefined) throw new Error(`Local login binding missing ${name}`); - } - } - - bindLogin(instance: Node, onLogin: () => void): void { - if (this.visitor !== null || this.terminal) throw new Error('Local login view cannot bind twice or after disposal'); - const visitor = requireNode(instance, 'group-2/游客登录'); - if (visitor.getComponent(Sprite) === null) throw new Error('游客登录 requires Sprite'); - if (visitor.getComponent(UITransform) === null) throw new Error('游客登录 requires UITransform'); - const version = requireNode(instance, 'group-2/版本显示').getComponent(Label); - if (version === null) throw new Error('版本显示 requires Label'); - const mask = requireNode(instance, 'group-2/登录中遮罩'); - const hidden = ['微信登录', '手机登录', 'qq登录', '快速登录', '隐藏快速登录-1', '隐藏快速登录-2', - '登录头像背景框', '登录中头像', '登录中昵称', '登录中动画', '登录中头像框'] - .map(name => requireNode(instance, `group-2/${name}`)); - hidden.forEach(node => { node.active = false; }); - this.visitor = visitor; this.version = version; this.mask = mask; - this.touch = () => { - if (!this.terminal && !this.busy && this.canLogin && this.page === 'login') onLogin(); - }; - visitor.on(Node.EventType.TOUCH_END, this.touch); - mask.active = this.busy; - } - - setVersion(version: number): void { - if (this.version === null) throw new Error('Login instance must be bound before version display'); - this.version.string = String(version); - } - - render(model: PageModel): void { - if (this.terminal) return; - switch (model.page) { - case 'loading': this.refs.statusLabel.string = '正在加载资源和连接本地服务…'; break; - case 'login': this.canLogin = model.canLogin; break; - case 'lobby': - this.refs.resultLabel.string = `登录成功\n玩家 ID:${model.self.playerid}\n昵称:${model.self.nickname}\n豆豆:${model.self.bean}`; - break; - case 'room': throw new Error('Local login view cannot present room'); - } - } - - activate(page: PageId): void { - if (this.terminal) return; - if (page === 'room') throw new Error('Local login view cannot activate room'); - this.page = page; - this.refs.loadingRoot.active = page === 'loading'; - this.refs.loginRoot.active = page === 'login'; - this.refs.resultRoot.active = page === 'lobby'; - this.refs.statusOverlay.active = page === 'loading' || this.busy; - } - - setBusy(busy: boolean): void { - if (this.terminal) return; - this.busy = busy; - if (this.mask !== null) this.mask.active = busy; - this.refs.statusOverlay.active = busy || this.page === 'loading'; - if (busy) this.refs.statusLabel.string = '正在登录,请稍候…'; - } - - showOverlay(model: OverlayModel): void { - if (this.terminal) return; - switch (model.kind) { - case 'none': this.refs.statusOverlay.active = this.busy || this.page === 'loading'; break; - case 'reconnect': - this.refs.statusOverlay.active = true; - this.refs.statusLabel.string = '连接中断,正在重连…'; break; - case 'denial': this.showFailure(model.outcome.data); break; - case 'kicked': this.showFailure(model.data); break; - case 'fatal': this.showFailure(model.error); break; - } - } - - showFailure(error: unknown): void { - this.dispose(); - this.refs.statusOverlay.active = true; - this.refs.statusLabel.string = `验证失败:${describeFailure(error)}`; - } - - showStopped(): void { - if (this.terminal) return; - this.dispose(); - this.refs.statusOverlay.active = true; - this.refs.statusLabel.string = '已停止本次验证'; - } - - dispose(): void { - this.terminal = true; this.canLogin = false; this.busy = false; - if (this.visitor !== null && this.touch !== null) this.visitor.off(Node.EventType.TOUCH_END, this.touch); - this.touch = null; - if (this.mask !== null) this.mask.active = false; - } -} - -export function createCocosFrameScheduler(): FrameScheduler { - return { request(callback) { - const onDraw = () => { director.off(Director.EVENT_AFTER_DRAW, onDraw); callback(); }; - director.on(Director.EVENT_AFTER_DRAW, onDraw); - return () => { director.off(Director.EVENT_AFTER_DRAW, onDraw); }; - } }; -} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts.meta deleted file mode 100644 index 3eb9eab..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "5af2497c-15f2-4493-be9b-a6391d25b878", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts deleted file mode 100644 index d4e1f67..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { LocalStartupContext } from '../../framework/config/local-startup.ts'; -import type { Transport } from '../../framework/net/transport.ts'; -import { WireClient, type WireEvent } from '../../framework/net/wire-client.ts'; -import type { PlatformWireClient } from '../../framework/platform/runtime.ts'; -import { parseLoginResponse } from '../../framework/protocol/contracts/index.ts'; -import type { EvidenceSink } from './local-login-transport.ts'; - -interface LocalLoginWireOptions { - context: LocalStartupContext; - transportFactory: () => Transport; - onFault: (error: unknown) => void; - record: EvidenceSink; -} - -/** ES2020-compatible aggregate retaining original errors, including non-Error values. */ -class LocalLoginWireFault extends Error { - readonly errors: readonly unknown[]; - constructor(errors: readonly unknown[]) { - super('Local login Wire failed with additional cleanup or evidence errors'); - this.name = 'LocalLoginWireFault'; - this.errors = Object.freeze([...errors]); - } -} - -export function createLocalLoginWire(options: LocalLoginWireOptions): PlatformWireClient { - const wire = new WireClient({ servers: options.context.config.servers, transportFactory: options.transportFactory }); - const listeners = new Set<(event: WireEvent) => void>(); - let stopped = false; - let faulted = false; - let terminalDelivery = false; - - function stop(): void { - if (stopped) return; - stopped = true; - unsubscribe(); - listeners.clear(); - wire.stop(); - } - - function fail(error: unknown, operation?: string, event?: WireEvent): unknown { - if (faulted) return error; - faulted = true; - const errors = [error]; - try { stop(); } catch (cleanupError) { errors.push(cleanupError); } - if (operation !== undefined) { - try { options.record({ kind: 'scope-violation', data: { operation, event } }); } - catch (recordError) { errors.push(recordError); } - } - const fault = errors.length === 1 ? error : new LocalLoginWireFault(errors); - options.onFault(fault); - return fault; - } - - function scopeError(operation: string): Error { - return new Error(`Local platform login scope forbids ${operation}`); - } - - function assertActive(): void { - if (stopped || faulted || terminalDelivery) throw new Error('Local login Wire is stopped or terminal'); - } - - function deliver(event: WireEvent): void { - for (const listener of [...listeners]) { - if (stopped || faulted) break; - listener(event); - } - } - - const unsubscribe = wire.subscribe(event => { - if (stopped || faulted || terminalDelivery) return; - try { - options.record({ kind: 'wire-event', data: event }); - if (stopped || faulted) return; - if (event.type !== 'message') { deliver(event); return; } - const { route, rpc, data } = event.message; - if (rpc === 'connect_agentserver' || rpc === 'connect_roomserver' || (route !== 'agent' && route !== 'platform')) { - fail(scopeError(`${route}/${rpc}`), `${route}/${rpc}`, event); - return; - } - let terminalOperation: string | undefined; - if (route === 'agent' && rpc === 'player_login') { - const parsed = parseLoginResponse(data); - if (parsed.state === 0 && parsed.room !== null) { - fail(scopeError('login room recovery'), 'login room recovery', event); - return; - } - if (parsed.state !== 0) terminalOperation = 'login rejected'; - } - if (route === 'agent' && rpc === 'kick_server') terminalOperation = 'kick_server'; - if (terminalOperation !== undefined) { - // Existing runtime failure display runs first, but cannot initiate more traffic. - terminalDelivery = true; - try { deliver(event); } - catch (error) { fail(error); return; } - fail(scopeError(terminalOperation), terminalOperation, event); - return; - } - deliver(event); - } catch (error) { fail(error); } - }); - - return { - subscribe(listener) { - if (stopped || faulted) return () => {}; - listeners.add(listener); - return () => { listeners.delete(listener); }; - }, - start() { assertActive(); wire.start(); }, - send(envelope) { - assertActive(); - if (envelope.route !== 'agent' || envelope.rpc !== 'player_login') { - throw fail(scopeError(`outbound ${envelope.route}/${envelope.rpc}`), `outbound ${envelope.route}/${envelope.rpc}`); - } - wire.send(envelope); - }, - switchServer() { assertActive(); throw fail(scopeError('switchServer'), 'switchServer'); }, - reconnectCurrent() { assertActive(); wire.reconnectCurrent(); }, - stop, - }; -} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts.meta deleted file mode 100644 index 5db90f4..0000000 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "96a339e3-f954-4e96-b07f-9fe124d457f0", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login.meta new file mode 100644 index 0000000..c71ae98 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "2d0af0de-962c-48db-a658-3f0396048806", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PhoneLoginAssets.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PhoneLoginAssets.ts new file mode 100644 index 0000000..16efd52 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PhoneLoginAssets.ts @@ -0,0 +1,9 @@ +import { _decorator, Component, SpriteFrame } from 'cc'; +const {ccclass,property}=_decorator; + +/** Serialized frame references keep both original phone-panel modes in the build. */ +@ccclass('PhoneLoginAssets') +export class PhoneLoginAssets extends Component { + @property(SpriteFrame) loginTitle:SpriteFrame|null=null; + @property(SpriteFrame) codeWaiting:SpriteFrame|null=null; +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PhoneLoginAssets.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PhoneLoginAssets.ts.meta new file mode 100644 index 0000000..107e83d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PhoneLoginAssets.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "1f765b40-c144-4ae2-b1b0-7b6ba99fe17a", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts new file mode 100644 index 0000000..0d6a78d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts @@ -0,0 +1,217 @@ +import { _decorator, assetManager, Component, Director, Label, Node, Prefab, ProgressBar, SceneAsset, + UITransform, Widget, director, instantiate, isValid } from 'cc'; +import { resolveRuntimeConfig, type RuntimeConfig } from '../../framework/config/runtime-config.ts'; +import { CocosLocalConfigFetcher } from './local-config-fetcher.ts'; +import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts'; +import { resolveLaunchWaitTime } from '../../framework/config/remote-config.ts'; +import { resolveLoginPolicy, type LoginPolicy } from '../../framework/presentation/login-policy.ts'; +import { LoginAccounts, type LoginNativeSettings } from '../../framework/adapters/native/login-accounts.ts'; +import { WebViewJavascriptBridgeAdapter, type WebViewJavascriptBridgeEnvironment } from '../../framework/adapters/native/webview-javascript-bridge.ts'; +import { PlatformRuntime } from '../../framework/platform/runtime.ts'; +import { WireClient } from '../../framework/net/wire-client.ts'; +import { CocosWebSocketTransport } from '../../framework/net/cocos-transport.ts'; +import type { DeviceLoginMode, LoginAccountIdentity, LoginDeviceSnapshot } from '../../framework/protocol/contracts/login-contract.ts'; +import { createLoginStageGameEntry } from './login-stage-game-entry.ts'; +import { CocosLogoCountdownView, createLogoCountdown } from './logo-countdown.ts'; +import { requireNode } from './node-bindings.ts'; +import { SceneFlow } from './scene-flow.ts'; +import { LoginPanel } from './login-panel.ts'; + +const {ccclass,property}=_decorator; +function required(value:T|null|undefined,name:string):T{if(value==null)throw Error(`PlatformStartup missing ${name}`);return value;} + +/** One persistent owner for config/socket/account; scenes own their own visual trees. */ +@ccclass('PlatformStartup') +export class PlatformStartup extends Component { + @property(SceneAsset) loadingScene:SceneAsset|null=null; + @property(SceneAsset) loginScene:SceneAsset|null=null; + @property(Prefab) loadingPrefab:Prefab|null=null; + @property(Prefab) loginPrefab:Prefab|null=null; + @property(Prefab) protocolPrefab:Prefab|null=null; + @property(Prefab) phonePrefab:Prefab|null=null; + private runtime:PlatformRuntime|null=null; + private config:RuntimeConfig|null=null; + private policy:LoginPolicy|null=null; + private accounts:LoginAccounts|null=null; + private panel:LoginPanel|null=null; + private bridge:WebViewJavascriptBridgeAdapter|null=null; + private restoreShareLogin:(()=>void)|null=null; + private restoreLocation:(()=>void)|null=null; + private startLocation:(()=>void)|null=null; + private location:unknown=null; + private loginResponse:unknown=null; + private unsubscribe:(()=>void)|null=null; + private countdown:ReturnType|null=null; + private countdownView:CocosLogoCountdownView|null=null; + private progress:ProgressBar|null=null; + private status:Label|null=null; + private device:LoginDeviceSnapshot|null=null; + private busy=false; + private terminal=false; + private readonly events:Array<{kind:string;data:unknown}>=[]; + private readonly flow=new SceneFlow(name=>new Promise((resolve,reject)=>{ + if(!director.loadScene(name,error=>error?reject(error):resolve()))reject(Error(`Cannot load scene ${name}`)); + })); + + /** 常驻服务按加载场景、配置与桥接、连接、登录场景顺序启动;场景仅拥有自己的 UI。 */ + async start():Promise{ + try{ + required(this.loadingScene,'loadingScene');required(this.loginScene,'loginScene'); + required(this.loadingPrefab,'loadingPrefab');required(this.loginPrefab,'loginPrefab'); + required(this.protocolPrefab,'protocolPrefab');required(this.phonePrefab,'phonePrefab'); + director.addPersistRootNode(this.node); + await this.flow.enter(this.loadingScene!.name,()=>this.bindLoading()); + if(this.terminal)return; + const injected=window as unknown as {settings?:LoginNativeSettings & {startlocation(type:number):void};app_version?:number|string;app_appversion?:number|string}; + const bridgeHost=injected.app_version!==undefined&&Number(injected.app_version)!==0; + const native=bridgeHost?{ + getcompareCode:()=>Number(injected.app_appversion), + // uAgent_3 passes the original argument, not its unused local tp variable. + accreditlogin:(provider:number)=>required(this.bridge,'native bridge').call('accreditlogin',provider===2?undefined:provider,()=>{}), + }:injected.settings; + this.config=await resolveRuntimeConfig({hostKind:bridgeHost?'uAgent_3':native?'native-settings':'h5',win:window as any, + search:window.location.search,fetcher:new HttpConfigFetcher(),localFetcher:new CocosLocalConfigFetcher()}); + if(this.terminal)return; + this.policy=resolveLoginPolicy(this.config.remoteConfig); + const loginConfig={source:this.config.source,gameserver:this.config.gameserver,identity:this.config.identity, + showVisitor:this.policy.showVisitor,deviceLogin:this.policy.deviceLogin,qqLogin:this.policy.qqLogin}; + this.events.push({kind:'login-config',data:loginConfig}); + if(this.config.isDebugger)console.info('[PlatformStartup] login-config',JSON.stringify(loginConfig)); + this.accounts=new LoginAccounts({storage:window.localStorage,settings:native,random:Math.random,now:Date.now, + gameid:this.config.identity.gameid,agentid:this.config.identity.agentid,policy:this.policy, + submit:(account,mode)=>this.login(account,mode),tip:text=>this.tip(text)}); + const previous=(window as any).sharelogin; + const callback=(openid:unknown,headimgurl:unknown,nickname:unknown,sex:unknown,city:unknown,Province:unknown,unionid:unknown)=>{ + if(!this.terminal)this.acceptNative({openid,headimgurl,nickname,sex,city,Province,unionid},'global'); + }; + (window as any).sharelogin=callback; + this.restoreShareLogin=()=>{if((window as any).sharelogin===callback){if(previous===undefined)delete (window as any).sharelogin;else (window as any).sharelogin=previous;}}; + const oldLocation=(window as any).getlocationinfo; + const locationCallback=(data:unknown)=>{ + if(this.terminal)return; + try{ + const value=typeof data==='string'?JSON.parse(data):data; + if(value===null||typeof value!=='object'||Array.isArray(value))throw Error('Invalid getlocationinfo payload'); + this.location=value; + }catch(error){this.tip(error instanceof Error?error.message:String(error));} + }; + (window as any).getlocationinfo=locationCallback; + this.restoreLocation=()=>{if((window as any).getlocationinfo===locationCallback){if(oldLocation===undefined)delete (window as any).getlocationinfo;else (window as any).getlocationinfo=oldLocation;}}; + if(native){ + this.bridge=new WebViewJavascriptBridgeAdapter(window as unknown as WebViewJavascriptBridgeEnvironment); + this.bridge.register('sharelogin',data=>this.acceptNative(data,'bridge')); + this.bridge.register('getlocationinfo',locationCallback); + this.startLocation=()=>{if(bridgeHost)required(this.bridge,'native bridge').call('startlocation',2,()=>{}); + else required(injected.settings,'settings').startlocation(2);}; + } + required(this.countdown,'countdown').start(resolveLaunchWaitTime(this.config.remoteConfig)); + this.runtime=new PlatformRuntime({gameEntry:createLoginStageGameEntry(this.config.identity.gameid,event=>this.events.push(event)), + resolveRuntimeConfig:async()=>required(this.config,'config'), + createWireClient:config=>{ + const wire=new WireClient({servers:config.servers,transportFactory:()=>new CocosWebSocketTransport()}); + wire.subscribe(event=>{this.events.push({kind:'wire',data:event.type}); + if(event.type==='message'&&event.message.route==='agent'&&event.message.rpc==='player_login')this.loginResponse=event.message.data; + });return wire; + }, + scene:{showLoading:()=>{},showLogin:()=>{void this.showLogin().catch(error=>this.fail(error));}, + showLobby:()=>{try{required(this.accounts,'accounts').loginSucceeded(required(this.loginResponse,'login response'),required(this.device,'prepared login device').deviceLogin); + this.busy=false;this.panel?.setBusy(false);this.panel?.close();this.tip('登录成功');}catch(error){this.fail(error);}}, + showRoom:()=>this.fail(Error('当前入口尚未接入子游戏场景')), + showReconnect:()=>this.tip('连接中断,正在重连…'), + showKicked:data=>this.fail(Error(`账号退出:${JSON.stringify(data)}`)), + showServerDenial:outcome=>{this.busy=false;this.panel?.setBusy(false);this.tip(JSON.stringify(outcome.data));}, + showFatal:error=>this.fail(error)}, + loadResources:()=>this.loadLoginResources(), + waitForMinimumDisplay:()=>required(this.countdown,'countdown').promise, + getLoginDeviceSnapshot:()=>required(this.device,'prepared login device'), + }); + this.unsubscribe=this.runtime.subscribeState(state=>this.countdown?.socketReady(state.app.phase==='connected'||state.app.phase==='logged-in'),error=>this.fail(error)); + await this.runtime.start(); + }catch(error){this.fail(error);} + } + private canvas():Node{return required(director.getScene(),'scene').getChildByName('Canvas') ?? (()=>{throw Error('Scene requires Canvas');})();} + private createStatus():void{ + const canvas=this.canvas();const node=new Node('Status');node.parent=canvas;node.layer=canvas.layer; + node.setPosition(0,315,0);node.addComponent(UITransform).setContentSize(1200,60); + this.status=node.addComponent(Label);this.status.string='';this.status.fontSize=24;this.status.lineHeight=28; + } + private async bindLoading():Promise{ + const logo=instantiate(required(this.loadingPrefab,'loadingPrefab'));logo.parent=this.canvas();logo.setPosition(0,0,0); + this.createStatus();requireNode(logo,'group-32').active=false; + this.progress=required(requireNode(logo,'group-1/Spirit254').getComponent(ProgressBar),'Logo ProgressBar'); + const labels=logo.getComponentsInChildren(Label).filter(label=>label.node.name==='Spirit3104'); + if(labels.length!==1)throw Error('Logo requires one Spirit3104'); + this.countdownView=new CocosLogoCountdownView(requireNode(logo,'group-1/Spirit3103'),labels[0].node,()=>this.countdown?.skip()); + this.countdown=createLogoCountdown({now:()=>performance.now(),schedule:(callback,ms)=>{const id=window.setTimeout(callback,ms);return ()=>window.clearTimeout(id);}, + render:state=>this.countdownView?.render(state)}); + await new Promise(resolve=>director.once(Director.EVENT_AFTER_DRAW,resolve)); + this.events.push({kind:'scene',data:'Loading'}); + } + private async loadLoginResources():Promise{ + const prefab=required(this.loginPrefab,'loginPrefab'); + await new Promise((resolve,reject)=>assetManager.loadAny(prefab.uuid,(done,total)=>{ + if(!this.terminal&&this.progress&&total>0)this.progress.progress=done/total; + },error=>error?reject(error):resolve())); + if(this.terminal)throw Error('Startup stopped'); + this.countdown?.resourcesReady();required(this.progress,'progress').node.active=false; + } + /** 完成真实场景切换后再绑定按钮和尝试缓存登录;弹窗挂在独立 PopupRoot。 */ + private async showLogin():Promise{ + this.countdownView?.dispose();this.countdownView=null; + await this.flow.enter(required(this.loginScene,'loginScene').name,()=>{ + const root=instantiate(required(this.loginPrefab,'loginPrefab'));root.parent=this.canvas();root.setPosition(0,0,0); + const popup=new Node('PopupRoot');popup.parent=this.canvas();popup.layer=root.layer;popup.addComponent(UITransform).setContentSize(1600,720); + this.createStatus();this.panel=new LoginPanel(root,popup,required(this.protocolPrefab,'protocolPrefab'),required(this.phonePrefab,'phonePrefab'), + required(this.accounts,'accounts'),required(this.policy,'policy'),message=>this.tip(message), + ()=>!this.busy&&!this.terminal&&this.runtime?.state.app.phase==='connected'); + const version=required(requireNode(root,'group-2/版本显示').getComponent(Label),'version Label'); + // Beta 展示格式使用当前渠道版本号;只改变显示,不改变登录协议中的数值版本。 + version.string=`v${required(this.config,'config').identity.version}.0.0 Beta`; + // Label 自动尺寸更新后重新计算左上角 Widget,避免长版本文字向屏幕外扩展。 + required(version.node.getComponent(UITransform),'version UITransform').setAnchorPoint(0,1); + version.updateRenderData(true); + const versionWidget=required(version.node.getComponent(Widget),'version Widget'); + versionWidget.target=this.canvas(); + versionWidget.updateAlignment(); + this.events.push({kind:'scene',data:'Login'}); + // Legacy location acquisition is independent of login. Keep the source null + // until a real callback arrives; report host failure without stopping the socket. + try{this.startLocation?.();}catch(error){this.tip(`定位接口调用失败:${error instanceof Error?error.message:String(error)}`);} + if(this.accounts!.cached())this.accounts!.autoLogin(); + }); + } + private acceptNative(data:unknown,channel:'global'|'bridge'):void{ + if(this.terminal)return; + try{required(this.accounts,'accounts').acceptNative(data,channel);}catch(error){this.tip(error instanceof Error?error.message:String(error));} + } + /** 所有登录最终收口:再次校验协议与就绪状态,保存本次模式供登录响应使用。 */ + private login(account:LoginAccountIdentity,mode:DeviceLoginMode):void{ + const runtime=required(this.runtime,'runtime'); + if(this.busy||this.terminal)return; + if(!required(this.accounts,'accounts').requireAgreement())return; + if(!runtime.ready)throw Error('登录资源或连接尚未就绪'); + const storage=window.localStorage; + let machineid=storage.getItem('tsgame_machineId_'); + if(machineid===null){machineid=`${Date.now()}-${1000+Math.floor(Math.random()*8999)}`;storage.setItem('tsgame_machineId_',machineid);} + const host=window as unknown as {returnCitySN?:{ip:string;province:string;city:string}}; + const city=host.returnCitySN; + if(city!==undefined&&(!city||typeof city.ip!=='string'||typeof city.province!=='string'||typeof city.city!=='string'))throw Error('Invalid returnCitySN'); + this.device={location:this.location,machineid,machineroom:'',deviceLogin:mode,loginPlayerId:{enabled:false},...(city?{ip:city.ip}:{})}; + this.busy=true;this.panel?.setBusy(true); + try{runtime.login(city?{...account,province:city.province,city:city.city}:account);} + catch(error){this.busy=false;this.panel?.setBusy(false);throw error;} + } + private tip(text:string):void{if(this.status&&isValid(this.status))this.status.string=text;} + private fail(error:unknown):void{ + this.tip(error instanceof Error?error.message:String(error));console.error('[PlatformStartup]',error); + this.release(); + } + /** 幂等释放计时器、订阅和宿主回调,阻止销毁后的迟到消息重新激活流程。 */ + private release():void{ + if(this.terminal)return;this.terminal=true;this.flow.dispose();this.panel?.dispose(); + this.countdown?.cancel();this.countdownView?.dispose();this.unsubscribe?.();this.bridge?.dispose(); + this.restoreShareLogin?.();this.restoreLocation?.();this.runtime?.stop(); + } + onDestroy():void{this.release();} + getDiagnostics(){return {events:[...this.events],ready:this.runtime?.ready,busy:this.busy,terminal:this.terminal};} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts.meta new file mode 100644 index 0000000..910d262 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/PlatformStartup.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "6dba829e-bbd7-4da8-bd48-2f214eeccc93", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/local-config-fetcher.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/local-config-fetcher.ts new file mode 100644 index 0000000..5cdd702 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/local-config-fetcher.ts @@ -0,0 +1,29 @@ +import { resources, TextAsset } from 'cc'; +import { DEBUG } from 'cc/env'; +import { ConfigFetchError, ConfigParseError, type ConfigFetcher } from '../../framework/config/remote-config.ts'; + +/** + * 开发专用配置读取器。使用 Cocos 资源数据库读取项目 txt,支持预览和调试构建。 + * 路径由 profiles.ts 唯一指定;读取失败绝不改用远程,以免测试了错误的数据。 + */ +export class CocosLocalConfigFetcher implements ConfigFetcher { + async fetch(resourcePath: string): Promise { + // 除启动配置的 debug 限制外,发布构建也在引擎边界禁止加载开发配置。 + if (!DEBUG) throw new ConfigFetchError('本地配置仅允许开发预览或调试构建'); + const asset = await new Promise((resolve, reject) => { + resources.load(resourcePath, TextAsset, (error, result) => { + if (error) reject(new ConfigFetchError(`本地配置读取失败: ${resourcePath}.txt: ${error.message}`)); + else if (!result) reject(new ConfigFetchError(`本地配置资源为空: ${resourcePath}.txt`)); + else resolve(result); + }); + }); + try { + return JSON.parse(asset.text) as unknown; + } catch { + throw new ConfigParseError(`本地配置 JSON 解析失败: ${resourcePath}.txt`); + } finally { + // 内容已转为普通数据;下次启动重新加载,避免沿用旧 TextAsset 缓存。 + resources.release(resourcePath, TextAsset); + } + } +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/local-config-fetcher.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/local-config-fetcher.ts.meta new file mode 100644 index 0000000..20adc89 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/local-config-fetcher.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "97325d47-c809-4a38-9e1a-520ee2a3d863", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-panel.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-panel.ts new file mode 100644 index 0000000..3b0d919 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-panel.ts @@ -0,0 +1,166 @@ +import { Animation, assetManager, BlockInputEvents, Color, EditBox, ImageAsset, Label, Node, Prefab, ScrollView, Sprite, SpriteFrame, Texture2D, UITransform, Widget, instantiate, isValid } from 'cc'; +import type { LoginAccounts } from '../../framework/adapters/native/login-accounts.ts'; +import { loginButtons, type LoginPolicy } from '../../framework/presentation/login-policy.ts'; +import { requireNode } from './node-bindings.ts'; +import { PhoneLoginAssets } from './PhoneLoginAssets.ts'; + +/** 登录场景节点与弹窗的生命周期管理;协议状态和账户行为由 LoginAccounts 唯一持有。 */ +export class LoginPanel { + private readonly releases:Array<()=>void>=[]; + private modal:Node|null=null; + private modalReleases:Array<()=>void>=[]; + private disposed=false; + private readonly loadingAnimation:Animation; + constructor(readonly root:Node,private readonly popupRoot:Node, + private readonly protocol:Prefab,private readonly phone:Prefab, + private readonly accounts:LoginAccounts,policy:LoginPolicy, + private readonly tip:(message:string)=>void,private readonly canAct:()=>boolean){ + const group=requireNode(root,'group-2'); + const animation=requireNode(group,'Logining/登录中动画').getComponent(Animation); + if(!animation||!animation.defaultClip)throw Error('登录中动画 requires Animation and default clip'); + this.loadingAnimation=animation; + const state=loginButtons(policy,accounts.versionState,accounts.cached()!==null); + const names={wechat:'微信登录',visitor:'游客登录',phone:'手机登录',quick:'快速登录',qq:'qq登录'}; + for(const key of Object.keys(names) as Array)requireNode(group,names[key]).active=state[key]; + // JumpWxAuth uses legacy 1280-wide coordinates; prefab coordinates are centered. + const left=(name:string,x:number,width?:number,height?:number)=>{ + const node=requireNode(group,name),ui=node.getComponent(UITransform);if(!ui)throw Error(`${name} requires UITransform`); + const widget=node.getComponent(Widget);if(widget)widget.enabled=false; + if(width!==undefined&&height!==undefined)ui.setContentSize(width,height); + node.setPosition(x-640+ui.width*ui.anchorX,node.position.y,node.position.z); + }; + if(policy.deviceLogin)left('手机登录',490); + if(accounts.versionState===1){requireNode(group,'渠道logo').active=false;left('微信登录',113,320,98);left('游客登录',488);} + else left('微信登录',415); + // Both switches may be enabled by the remote hierarchy. Keep both actions + // visible, with the user-requested side-by-side layout instead of overlap. + if(state.visitor&&state.phone){ + for(const [name,x] of [['游客登录',-210],['手机登录',210]] as const){ + const node=requireNode(group,name),widget=node.getComponent(Widget);if(widget)widget.enabled=false; + node.setPosition(x,node.position.y,node.position.z); + } + } + // 登录中展示节点统一位于 prefab 的 Logining 分组,路径必须与资源一致。 + const logining=requireNode(group,'Logining'); + for(const name of ['登录头像背景框','登录中头像','登录中昵称','登录中动画','登录中头像框'])requireNode(logining,name).active=state.account; + this.setBusy(state.account); + const cached=accounts.cached();if(cached){const label=requireNode(logining,'登录中昵称').getComponent(Label);if(!label)throw Error('登录中昵称 requires Label');label.string=cached.nickname;} + if(cached?.avatar){ + const sprite=requireNode(logining,'登录中头像').getComponent(Sprite);if(!sprite)throw Error('登录中头像 requires Sprite'); + assetManager.loadRemote(cached.avatar,{ext:'.png'},(error,image)=>{ + if(this.disposed||!isValid(sprite))return; + if(error){this.tip(`登录头像加载失败:${error.message}`);return;} + const texture=new Texture2D();texture.image=image;const frame=new SpriteFrame();frame.texture=texture;sprite.spriteFrame=frame; + this.releases.push(()=>{if(isValid(sprite))sprite.spriteFrame=null;frame.destroy();texture.destroy();}); + }); + } + const check=requireNode(group,'协议确认/check');check.active=accounts.agreed; + this.bind(requireNode(group,'协议确认'),()=>{accounts.setAgreement(!accounts.agreed);check.active=accounts.agreed;}); + this.bind(requireNode(group,'协议内容'),()=>this.openProtocol()); + this.bind(requireNode(group,'微信登录'),()=>accounts.authorize('wechat')); + this.bind(requireNode(group,'qq登录'),()=>accounts.authorize('qq')); + this.bind(requireNode(group,'游客登录'),()=>accounts.visitor()); + this.bind(requireNode(group,'快速登录'),()=>accounts.device()); + this.bind(requireNode(group,'手机登录'),()=>this.openPhone()); + this.bind(requireNode(group,'隐藏快速登录-1'),()=>accounts.hiddenFirst()); + this.bind(requireNode(group,'隐藏快速登录-2'),()=>accounts.hiddenSecond()); + } + private bind(node:Node,action:()=>void,modal=false){ + const listener=()=>{if(this.disposed||!this.canAct())return;try{action();}catch(error){this.tip(error instanceof Error?error.message:String(error));}}; + node.on(Node.EventType.TOUCH_END,listener); + (modal?this.modalReleases:this.releases).push(()=>node.off(Node.EventType.TOUCH_END,listener)); + } + private open(prefab:Prefab):Node|null{ + if(this.modal!==null)return null; + const modal=instantiate(prefab);modal.parent=this.popupRoot;modal.setPosition(0,0,0);modal.active=true; + this.modal=modal;return modal; + } + /** 协议弹窗独立实例化,每次打开回到顶部,关闭时解除监听并销毁实例。 */ + openProtocol():void{ + const modal=this.open(this.protocol);if(!modal)return; + const scroll=requireNode(modal,'group-37/协议滚动').getComponent(ScrollView); + if(!scroll)throw Error('协议滚动 requires ScrollView');scroll.scrollToTop(0); + for(const name of ['Spirit253','协议遮罩'])this.bind(requireNode(modal,`group-37/${name}`),()=>this.close(),true); + } + /** 沿用原工程输入与验证码倒计时规则;弹窗不另存协议勾选状态。 */ + openPhone():void{ + if(!this.accounts.requireAgreement())return; + const modal=this.open(this.phone);if(!modal)return; + const group=requireNode(modal,'group-106'); + requireNode(modal,'group-108').active=false; + const frames=modal.getComponent(PhoneLoginAssets); + if(!frames?.loginTitle||!frames.codeWaiting)throw Error('PhoneLoginAssets requires loginTitle/codeWaiting'); + const title=requireNode(group,'Spirit3252').getComponent(Sprite);if(!title)throw Error('Spirit3252 requires Sprite');title.spriteFrame=frames.loginTitle; + const saved=this.accounts.phonePanelState(); + const input=(name:string,textName:string,placeholderName:string)=>{ + const node=requireNode(group,name),labelNode=requireNode(group,textName); + const placeholder=requireNode(group,placeholderName); + const glyph=labelNode.getComponent(UITransform);if(!glyph)throw Error(`${textName} requires UITransform`); + // Existing art remains the input background; native EditBox owns text entry. + const ui=node.getComponent(UITransform);if(!ui)throw Error(`${name} requires UITransform`); + const text=new Node('InputText');text.parent=node;text.layer=node.layer; + const textUi=text.addComponent(UITransform);textUi.setAnchorPoint(0,0.5);textUi.setContentSize(ui.width-85,ui.height); + text.setPosition(-ui.width/2+65,0,0); + const inputLabel=text.addComponent(Label);inputLabel.string='';inputLabel.fontSize=glyph.height; + inputLabel.horizontalAlign=Label.HorizontalAlign.LEFT;inputLabel.overflow=Label.Overflow.CLAMP;inputLabel.color=Color.BLACK; + const hint=new Node('Placeholder');hint.parent=node;hint.layer=node.layer; + hint.addComponent(UITransform).setContentSize(ui.width-20,ui.height); + const hintLabel=hint.addComponent(Label);hintLabel.string='';hintLabel.fontSize=glyph.height; + // Configure before onLoad so EditBox does not create orphan default labels. + labelNode.active=false;node.active=false; + const edit=node.addComponent(EditBox);edit.textLabel=inputLabel;edit.placeholderLabel=hintLabel; + edit.string='';edit.placeholder=''; + edit.inputMode=EditBox.InputMode.NUMERIC;edit.maxLength=18; + const changed=()=>{edit.string=edit.string.replace(/\D/g,'');placeholder.active=edit.string.length===0;}; + node.on(EditBox.EventType.TEXT_CHANGED,changed);this.modalReleases.push(()=>node.off(EditBox.EventType.TEXT_CHANGED,changed)); + changed();node.active=true; + const alignText=()=>{ + textUi.setAnchorPoint(0,1);textUi.setContentSize(ui.width-85,ui.height); + text.setPosition(-ui.width/2+65,ui.height/2,0);inputLabel.verticalAlign=Label.VerticalAlign.CENTER; + }; + alignText();node.on(Node.EventType.SIZE_CHANGED,alignText); + this.modalReleases.push(()=>node.off(Node.EventType.SIZE_CHANGED,alignText));return edit; + }; + const phone=input('Spirit3243','Spirit3247','Spirit3249'); + const code=input('Spirit3244','Spirit3248','Spirit3250'); + if(saved?.phoneNumber){phone.string=saved.phoneNumber;requireNode(group,'Spirit3249').active=false;} + const codeButton=requireNode(group,'Spirit3245'),codeSprite=codeButton.getComponent(Sprite); + if(!codeSprite||!codeSprite.spriteFrame)throw Error('Spirit3245 requires SpriteFrame'); + const idleFrame=codeSprite.spriteFrame; + const timerNode=requireNode(group,'Spirit3246'),timerSprite=timerNode.getComponent(Sprite); + if(!timerSprite)throw Error('Spirit3246 requires Sprite');timerSprite.enabled=false; + const timerWidget=timerNode.getComponent(Widget);if(timerWidget)timerWidget.enabled=false; + timerNode.setPosition(codeButton.position);timerNode.getComponent(UITransform)!.setContentSize(148,28); + const timerText=new Node('CountdownText');timerText.parent=timerNode;timerText.layer=timerNode.layer; + timerText.addComponent(UITransform).setContentSize(148,28); + const timerLabel=timerText.addComponent(Label);timerLabel.fontSize=28;timerLabel.color=Color.BLACK; + let restTime=saved===null?0:saved.restTime; + let waiting=restTime>0; + const renderTimer=()=>{timerNode.active=waiting;timerLabel.string=`${restTime}秒`;codeSprite.spriteFrame=waiting?frames.codeWaiting:idleFrame;}; + renderTimer(); + if(waiting){const timer=window.setInterval(()=>{restTime--;if(restTime<0){waiting=false;window.clearInterval(timer);}renderTimer();},1000); + this.modalReleases.push(()=>window.clearInterval(timer));} + this.bind(codeButton,()=>{if(restTime<=0&&phone.string)this.tip('请通过相关渠道获取');},true); + this.bind(requireNode(group,'Spirit3253'),()=>{if(!this.accounts.requireAgreement())return; + if(!phone.string||!code.string){this.tip('手机号与验证码均不能为空!');return;} + this.accounts.phone(phone.string,code.string);this.close();},true); + for(const name of ['Spirit3251','Spirit3241'])this.bind(requireNode(group,name),()=>this.close(),true); + const background=requireNode(group,'Spirit3242');if(!background.getComponent(BlockInputEvents))background.addComponent(BlockInputEvents); + } + /** 帧切换由 prefab 的 AnimationClip 驱动;登录状态只负责显隐及播放生命周期。 */ + setBusy(busy:boolean):void{ + const group=requireNode(this.root,'group-2/Logining'); + group.active=busy; + for(const name of ['登录中遮罩','登录头像背景框','登录中头像','登录中昵称','登录中动画','登录中头像框'])requireNode(group,name).active=busy; + if(busy){ + const clip=this.loadingAnimation.defaultClip!; + if(!this.loadingAnimation.getState(clip.name)?.isPlaying)this.loadingAnimation.play(clip.name); + }else this.loadingAnimation.stop(); + } + /** 清理弹窗事件和验证码计时器,避免反复打开时累积回调。 */ + close():void{ + this.modalReleases.splice(0).forEach(release=>release()); + if(this.modal&&isValid(this.modal))this.modal.destroy();this.modal=null; + } + dispose():void{this.setBusy(false);this.disposed=true;this.close();this.releases.splice(0).forEach(release=>release());} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-panel.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-panel.ts.meta new file mode 100644 index 0000000..0df7bbf --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-panel.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "e79a22bd-cbb6-41d0-a2ca-6ffb7e340f38", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-stage-game-entry.ts similarity index 71% rename from cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts rename to cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-stage-game-entry.ts index 6e39db3..1351b0b 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-stage-game-entry.ts @@ -1,8 +1,12 @@ import type { GameEntry } from '../../framework/sdk/contracts/game-entry.ts'; -import type { EvidenceSink } from './local-login-transport.ts'; +/** 登录阶段的诊断事件,仅供入口记录生命周期和越界操作。 */ +export type LoginEntryEvent = { kind: string; data: unknown }; +type EvidenceSink = (event: LoginEntryEvent) => void; -/** Satisfies entry validation while making every actual game operation a scope error. */ -export function createDiagnosticGameEntry(gameId: string | number, record: EvidenceSink): GameEntry { +/** 登录阶段尚未接入子游戏:满足运行时入口契约,但任何游戏操作均显式拒绝。 + * 接入真实子游戏时应替换此入口,不能把占位实现当作大厅或房间支持。 + */ +export function createLoginStageGameEntry(gameId: string | number, record: EvidenceSink): GameEntry { let nextInstanceId = 0; function violation(operation: string, instanceId?: number): never { record({ kind: 'scope-violation', data: { operation, instanceId } }); diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-stage-game-entry.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-stage-game-entry.ts.meta new file mode 100644 index 0000000..a90a03d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/login-stage-game-entry.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "57db0201-3b1b-482e-b783-f19e5e9a47a7", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/logo-countdown.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/logo-countdown.ts new file mode 100644 index 0000000..61df5c2 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/logo-countdown.ts @@ -0,0 +1,116 @@ +import { Label, Node, UITransform, Vec3, Widget } from 'cc'; + +/** Logo 展示状态;倒计时只控制最短展示时间,资源和连接就绪由运行时独立管理。 */ +export interface LogoCountdownState { readonly visible: boolean; readonly text: string; } + +/** Native input and measured label layout, independent of serialized label parentage. */ +export class CocosLogoCountdownView { + private readonly label: Label; + private readonly buttonTransform: UITransform; + private readonly textTransform: UITransform; + private readonly parentTransform: UITransform; + private disposed = false; + + constructor(private readonly button: Node, private readonly text: Node, private readonly onSkip: () => void) { + const label = text.getComponent(Label), buttonTransform = button.getComponent(UITransform); + const textTransform = text.getComponent(UITransform), parentTransform = text.parent?.getComponent(UITransform); + if (!label || !buttonTransform || !textTransform || !parentTransform) { + throw new Error('Logo countdown requires Label and native UITransform bindings'); + } + this.label = label; this.buttonTransform = buttonTransform; + this.textTransform = textTransform; this.parentTransform = parentTransform; + // The label follows the actual button center, so its old absolute Widget must not fight layout. + const widget = text.getComponent(Widget); if (widget) widget.enabled = false; + this.label.overflow = Label.Overflow.NONE; + button.active = false; text.active = false; + button.on(Node.EventType.TOUCH_END, onSkip); + } + + render(state: LogoCountdownState): void { + if (this.disposed) return; + this.button.active = state.visible; this.text.active = state.visible; + this.label.string = state.text; + if (!state.visible) return; + this.button.getComponent(Widget)?.updateAlignment(); + this.label.updateRenderData(true); + const button = this.buttonTransform, text = this.textTransform; + const center = button.convertToWorldSpaceAR(new Vec3((0.5 - button.anchorX) * button.width, (0.5 - button.anchorY) * button.height, 0)); + const position = this.parentTransform.convertToNodeSpaceAR(center); + position.x += (text.anchorX - 0.5) * text.width; + position.y += (text.anchorY - 0.5) * text.height; + this.text.setPosition(position); + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + this.button.off(Node.EventType.TOUCH_END, this.onSkip); + this.button.active = false; this.text.active = false; + } +} + +/** Display gate only. Runtime still independently owns resource/config/socket gates. */ +export function createLogoCountdown(options: { + now(): number; + schedule(callback: () => void, milliseconds: number): () => void; + render(state: LogoCountdownState): void; +}) { + let started = false, finished = false, resources = false, socket = false; + let remaining = 0, deadline = 0; + let cancelDeadline: (() => void) | null = null, cancelTick: (() => void) | null = null; + let resolve!: () => void, reject!: (error: unknown) => void; + const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); + // Cancellation may precede the host attaching the display gate. + void promise.catch(() => {}); + const render = () => options.render({ visible: started && !finished && remaining > 0, + text: `倒计时${Math.ceil(Math.max(0, remaining) / 1000)}s${resources ? ' 跳过' : ''}` }); + const finish = (error?: unknown) => { + if (finished) return; + finished = true; + const failures: unknown[] = error === undefined ? [] : [error]; + for (const cancel of [cancelDeadline, cancelTick]) { + try { cancel?.(); } catch (failure) { failures.push(failure); } + } + cancelDeadline = null; cancelTick = null; + try { render(); } catch (failure) { failures.push(failure); } + if (failures.length > 0) reject(failures[0]); else resolve(); + }; + const guard = (operation: () => void) => { + if (finished) return; + try { operation(); } catch (error) { finish(error); } + }; + const checkDeadline = () => { + cancelDeadline = null; + if (finished) return; + const wait = deadline - options.now(); + if (wait <= 0) { finish(); return; } + guard(() => { cancelDeadline = options.schedule(checkDeadline, wait); }); + }; + const tick = () => { + cancelTick = null; + if (finished) return; + guard(() => { + remaining -= 1000; + render(); + if (remaining > 1000) cancelTick = options.schedule(tick, 1000); + }); + }; + render(); + return { + promise, + start(milliseconds: number) { + if (finished) return; + if (started) throw new Error('Logo countdown already started'); + if (!Number.isFinite(milliseconds) || milliseconds < 0) throw new Error('Invalid launchWaitTime'); + started = true; remaining = milliseconds; deadline = options.now() + milliseconds; + guard(() => { + render(); checkDeadline(); + if (!finished && remaining > 1000) cancelTick = options.schedule(tick, 1000); + }); + }, + resourcesReady() { guard(() => { resources = true; render(); }); }, + socketReady(value: boolean) { if (!finished) socket = value; }, + skip() { if (!finished && started && resources && socket) finish(); }, + cancel() { finish(new Error('Logo countdown cancelled')); }, + }; +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/logo-countdown.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/logo-countdown.ts.meta new file mode 100644 index 0000000..6d32de9 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/logo-countdown.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "1baca79a-2862-41de-b01e-616eb90baf04", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/node-bindings.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/node-bindings.ts new file mode 100644 index 0000000..df81645 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/node-bindings.ts @@ -0,0 +1,8 @@ +import { Node } from 'cc'; + +/** 按迁移资源的固定路径取节点;绑定缺失直接报错,避免把资源错误掩盖成按钮无响应。 */ +export function requireNode(root: Node, path: string): Node { + const node = root.getChildByPath(path); + if (node === null) throw new Error(`Platform login binding missing node: ${path}`); + return node; +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/node-bindings.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/node-bindings.ts.meta new file mode 100644 index 0000000..934100a --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/node-bindings.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "a7ceb35e-66c2-4ecf-ba76-3e8ee1aac9a0", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/scene-flow.ts b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/scene-flow.ts new file mode 100644 index 0000000..eb87515 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/scene-flow.ts @@ -0,0 +1,18 @@ +/** Serializes scene lifetime; services are owned by the persistent startup component. */ +export class SceneFlow { + private pending:Promise=Promise.resolve(); + private disposed=false; + constructor(private readonly load:(name:string)=>Promise){} + /** 串行加载后绑定新场景;等待期间已销毁则禁止继续绑定。 */ + enter(name:string,bind:()=>void|Promise):Promise{ + const next=this.pending.then(async()=>{ + if(this.disposed)throw Error('SceneFlow disposed'); + await this.load(name); + if(this.disposed)throw Error('SceneFlow disposed'); + await bind(); + }); + this.pending=next; + return next; + } + dispose():void{this.disposed=true;} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/scene-flow.ts.meta b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/scene-flow.ts.meta new file mode 100644 index 0000000..65b787c --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/platform-login/scene-flow.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "5fb154af-f704-44d3-9786-640db8ac3051", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/out/legacy-migration/BackHall_Layer.prefab b/cocoscreator_projects/YouleNexus/out/legacy-migration/BackHall_Layer.prefab index 5b599e6..7c1d49f 100644 --- a/cocoscreator_projects/YouleNexus/out/legacy-migration/BackHall_Layer.prefab +++ b/cocoscreator_projects/YouleNexus/out/legacy-migration/BackHall_Layer.prefab @@ -196,7 +196,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "yuLUeDXKPyDesn2Q1qhhWq" + "fileId": "b32JxFEuROt3TrHJPbNqtv" }, { "__type__": "cc.ProgressBar", @@ -234,7 +234,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "qc3XCkoNQIDpH6wTCxQi7x" + "fileId": "ObM6lyV6YYtGvMYs4ei9VA" }, { "__type__": "cc.Widget", @@ -266,7 +266,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "U5IlbCYg18o2PRDG5LmLi9" + "fileId": "pPsz4F8UHqSoNaFwhIjyrY" }, { "__type__": "cc.PrefabInfo", @@ -276,7 +276,7 @@ "asset": { "__id__": 0 }, - "fileId": "6fepRbLq0GfRc6IezdSOgw", + "fileId": "zZDIED9SXJMz2T6Q5o8Vj8", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -307,7 +307,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Ez6HTW5UovXUbxsmsQt1FE" + "fileId": "XD0CiaJTxW9sdG0s7m9lCi" }, { "__type__": "cc.Widget", @@ -343,7 +343,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "8489KtK9YDJpc9pM2rJnQ5" + "fileId": "G4b7x02HDKQduoMmoiOfAy" }, { "__type__": "cc.PrefabInfo", @@ -353,7 +353,7 @@ "asset": { "__id__": 0 }, - "fileId": "o1NkKWcu9BPc5XrLksvN6B", + "fileId": "oMLH0QNQy0BmAyi3EE6Kjt", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -384,7 +384,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "SehSZqVEb085kMOfoTcvYa" + "fileId": "n3vCuv72ApZ1uVw93PRiHd" }, { "__type__": "cc.Widget", @@ -420,7 +420,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "0ztNT7zoYHcqwxEE1gkqxB" + "fileId": "tl0l6z7daFX50685hlfgr9" }, { "__type__": "cc.PrefabInfo", @@ -430,7 +430,7 @@ "asset": { "__id__": 0 }, - "fileId": "6LFTIo7Z6sUOfqsJgVpz04", + "fileId": "RWU14cHHVlX2UDhd9X43g7", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null diff --git a/cocoscreator_projects/YouleNexus/out/legacy-migration/Protol_Layer.prefab b/cocoscreator_projects/YouleNexus/out/legacy-migration/Protol_Layer.prefab index a30ac51..ca59b1f 100644 --- a/cocoscreator_projects/YouleNexus/out/legacy-migration/Protol_Layer.prefab +++ b/cocoscreator_projects/YouleNexus/out/legacy-migration/Protol_Layer.prefab @@ -205,7 +205,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "u6iFoyVSsDCf7O8mOeAVKS" + "fileId": "ttLMdGQNfMqWrYZynHkGDH" }, { "__type__": "cc.Sprite", @@ -250,7 +250,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "RQ15R9kItMeM8jj3220l4X" + "fileId": "fRZkGOv1urgFw8Ak0fzL1F" }, { "__type__": "cc.Widget", @@ -284,7 +284,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "psTpVYM4XYJKhMfhS1ruMB" + "fileId": "LkaxLguQityt3Sivh9g3Pc" }, { "__type__": "cc.PrefabInfo", @@ -294,7 +294,7 @@ "asset": { "__id__": 0 }, - "fileId": "1puOtXKPT4nBm2e11y9lbT", + "fileId": "ue5AFKfj6nsyEkZBJCGnrM", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -378,7 +378,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "D5HWHcLEAQZsuXVmhu0yys" + "fileId": "Yd7mhmnggPie0AWlWrmMA5" }, { "__type__": "cc.Sprite", @@ -423,7 +423,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Nb5FFb8adxEmwmCefz9pot" + "fileId": "EEb1vzbh4AB9FnhcIzFso8" }, { "__type__": "cc.Widget", @@ -455,7 +455,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "4qE04DTORykivqdU4AIv5f" + "fileId": "IMZTDkHZUKD3FsYxtxtY7L" }, { "__type__": "cc.PrefabInfo", @@ -465,7 +465,7 @@ "asset": { "__id__": 0 }, - "fileId": "PZ6oONqr7RjodZhhV8RT0l", + "fileId": "mEacoNsXyscsTiWF2N751I", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -549,7 +549,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "iC90mAI0q6RTSEFGVSXgvc" + "fileId": "16xxKHWp4EIgjez58oy1eo" }, { "__type__": "cc.Sprite", @@ -594,7 +594,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "haYjXlRPtYOKIGE64TSyVl" + "fileId": "buLpw6lDAfLS5iuacaACLg" }, { "__type__": "cc.Widget", @@ -626,7 +626,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "LKL3T0bw27UdoONwSWyYGW" + "fileId": "EGkAfOvvadMRdZKHNe3MX0" }, { "__type__": "cc.PrefabInfo", @@ -636,7 +636,7 @@ "asset": { "__id__": 0 }, - "fileId": "7yknDa8VbX4Ix6OJ1i6nkq", + "fileId": "Pn8ZX71OXWrBUXXzKkISLh", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -720,7 +720,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "zzra4CkCzqFKe69JdBWdeK" + "fileId": "qM6ForTwIpD6MOJOhnZQeS" }, { "__type__": "cc.Sprite", @@ -765,7 +765,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "BK9E81FtDCVn75b59au9pF" + "fileId": "WBC4EEA2yC3sCRi5vqEMur" }, { "__type__": "cc.Widget", @@ -797,7 +797,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "ECPEIRe4c5WnccAUfGjkqY" + "fileId": "jksMjvJCiFFc6fk1oDe59n" }, { "__type__": "cc.PrefabInfo", @@ -807,7 +807,7 @@ "asset": { "__id__": 0 }, - "fileId": "uhRHwknn4SFHqdPSyMR5uD", + "fileId": "mjKGFdFaqaHZOP6vASvm2T", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -838,7 +838,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "43IfomALQcOTipLcyDwTs1" + "fileId": "NEUDScLXeOyUaRYpzzSdwS" }, { "__type__": "cc.Widget", @@ -874,7 +874,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "Pw8AVmZUrybxMOrLHKgSSP" + "fileId": "yU7fWEXjloqkyHdJTNuOO5" }, { "__type__": "cc.PrefabInfo", @@ -884,7 +884,7 @@ "asset": { "__id__": 0 }, - "fileId": "IlNaN5A8P0dQhF8E9Yx7nt", + "fileId": "NmigEGjkGY32J4dwShFP7R", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null @@ -915,7 +915,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "ZMnfGBzF5DGII4RMALE6el" + "fileId": "ERoBjCusgKp8Xnv3l66PIE" }, { "__type__": "cc.Widget", @@ -951,7 +951,7 @@ }, { "__type__": "cc.CompPrefabInfo", - "fileId": "i8q8guTJmnghedToQD3G7G" + "fileId": "Ey8iM8NbAYWmikqFZ8DdmT" }, { "__type__": "cc.PrefabInfo", @@ -961,7 +961,7 @@ "asset": { "__id__": 0 }, - "fileId": "Z8ImfsM2tYFs5phLftN067", + "fileId": "S3ZUouTGopxL6K9LuhUne2", "instance": null, "targetOverrides": null, "nestedPrefabInstanceRoots": null diff --git a/cocoscreator_projects/YouleNexus/tsconfig.json b/cocoscreator_projects/YouleNexus/tsconfig.json index 7dc649a..1547c4c 100644 --- a/cocoscreator_projects/YouleNexus/tsconfig.json +++ b/cocoscreator_projects/YouleNexus/tsconfig.json @@ -4,6 +4,7 @@ /* Add your custom configuration here. */ "compilerOptions": { - "strict": false + "strict": false, + "allowImportingTsExtensions": true } } diff --git a/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts b/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts index f6ee075..c46ac44 100644 --- a/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts +++ b/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts @@ -2,7 +2,9 @@ import { remoteFixture } from '../../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts'; -import { STARTUP_CONFIG } from '../../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import type { LocalStartupContext } from '../../../YouleNexus/assets/framework/config/local-startup.ts'; import { LOCAL_MACHINE_KEY, localAccountKey, type LocalStoragePort } from '../../../YouleNexus/assets/framework/adapters/local/local-storage.ts'; import { createLocalLoginSources, type LocalHostSnapshot } from '../../../YouleNexus/assets/framework/adapters/local/login-sources.ts'; diff --git a/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts b/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts index cda6f2f..5b6a06e 100644 --- a/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts +++ b/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts @@ -2,7 +2,9 @@ import { remoteFixture } from '../../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts'; -import { STARTUP_CONFIG } from '../../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import type { LocalStartupContext } from '../../../YouleNexus/assets/framework/config/local-startup.ts'; import { LOCAL_MACHINE_KEY, localAccountKey, type LocalStoragePort } from '../../../YouleNexus/assets/framework/adapters/local/local-storage.ts'; import { createLocalVisitorProvider } from '../../../YouleNexus/assets/framework/adapters/local/visitor-account.ts'; @@ -121,7 +123,7 @@ test('scope isolates identity types, ignores version, and serializes the server assert.notEqual(localAccountKey(base), localAccountKey(context({ marketid: '7', version: 1 }))); assert.equal(localAccountKey(base), localAccountKey(context({ marketid: 7, version: 999 }))); assert.equal(localAccountKey(base), - 'youle.local-test.account.v1:["local",["ws://127.0.0.1:3088"],"veRa0qrBf0df2K1G4de2tgfmVxB2jxpv","J9r4022t80pxlb01zOK76DN1WD4QewFl","FtJf073aa0d6rI1xD8J1Y42fINTm0ziK",7]'); + 'youle.local-test.account.v1:["local",["ws://127.0.0.1:3088"],"1B2h0ccl205c390Y28m1Ajdplkuu4wgy","J9r4022t80pxlb01zOK76DN1WD4QewFl","aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG",7]'); }); test('reset is isolated, refuses active sessions, and refuses overlap with getAccount', async () => { diff --git a/cocoscreator_projects/framework-tests/config/bootstrap.test.ts b/cocoscreator_projects/framework-tests/config/bootstrap.test.ts index 378eeda..e1dfc96 100644 --- a/cocoscreator_projects/framework-tests/config/bootstrap.test.ts +++ b/cocoscreator_projects/framework-tests/config/bootstrap.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import { test } from 'node:test'; import assert from 'node:assert/strict'; import { resolveBootstrap } from '../../YouleNexus/assets/framework/config/bootstrap.ts'; @@ -9,7 +11,7 @@ test('resolveBootstrap:显式 hostKind 透传严格 RuntimeConfig,并保留 async fetch() { return { player_server_tcp: '5.5.5.5:5000' }; }, }; const result = await resolveBootstrap({ - hostKind: 'h5', win: {}, search: '', fetcher, + startupConfig:STARTUP_CONFIG, hostKind: 'h5', win: {}, search: '', fetcher, }); assert.equal(result.source, 'remote'); assert.equal(result.profile.name, 'local'); diff --git a/cocoscreator_projects/framework-tests/config/local-config-fetcher.test.ts b/cocoscreator_projects/framework-tests/config/local-config-fetcher.test.ts new file mode 100644 index 0000000..4655aa7 --- /dev/null +++ b/cocoscreator_projects/framework-tests/config/local-config-fetcher.test.ts @@ -0,0 +1,47 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { runInNewContext } from 'node:vm'; +import ts from 'typescript'; +import { ConfigFetchError, ConfigParseError } from '../../YouleNexus/assets/framework/config/remote-config.ts'; + +// 在引擎资源边界验证错误、JSON 解析和缓存释放,不以 HTTP 模拟项目资源。 +function fixture(debug: boolean, text: string, error?: Error) { + const calls: string[] = []; + const exports: any = {}; + class TextAsset {} + const resources = { + load(path: string, type: unknown, done: Function) { + assert.equal(type, TextAsset); calls.push(`load:${path}`); done(error, { text }); + }, + release(path: string, type: unknown) { assert.equal(type, TextAsset); calls.push(`release:${path}`); }, + }; + const source = readFileSync(new URL('../../YouleNexus/assets/scripts/platform-login/local-config-fetcher.ts', import.meta.url), 'utf8'); + runInNewContext(ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText, { exports, require(name: string) { + if (name === 'cc') return { resources, TextAsset }; + if (name === 'cc/env') return { DEBUG: debug }; + if (name.endsWith('/remote-config.ts')) return { ConfigFetchError, ConfigParseError }; + throw Error(`Unexpected import: ${name}`); + } }); + return { fetcher: new exports.CocosLocalConfigFetcher(), calls }; +} + +test('project txt parses JSON and releases the asset after every load', async () => { + const f = fixture(true, '{"showVisitor":1}'); + assert.equal((await f.fetcher.fetch('dev-config/test')).showVisitor, 1); + await f.fetcher.fetch('dev-config/test'); + assert.deepEqual(f.calls, ['load:dev-config/test', 'release:dev-config/test', 'load:dev-config/test', 'release:dev-config/test']); +}); + +test('release build never loads local txt; missing or malformed assets expose source errors', async () => { + const release = fixture(false, '{}'); + await assert.rejects(release.fetcher.fetch('dev-config/test'), ConfigFetchError); + assert.deepEqual(release.calls, []); + const missing = fixture(true, '', Error('not found')); + await assert.rejects(missing.fetcher.fetch('dev-config/test'), /test.txt.*not found/); + const malformed = fixture(true, 'not json'); + await assert.rejects(malformed.fetcher.fetch('dev-config/test'), ConfigParseError); + assert.deepEqual(malformed.calls, ['load:dev-config/test', 'release:dev-config/test']); +}); diff --git a/cocoscreator_projects/framework-tests/config/local-startup.test.ts b/cocoscreator_projects/framework-tests/config/local-startup.test.ts index 3270bf3..611c2a4 100644 --- a/cocoscreator_projects/framework-tests/config/local-startup.test.ts +++ b/cocoscreator_projects/framework-tests/config/local-startup.test.ts @@ -7,7 +7,9 @@ import { resolveLocalStartup, type LocalStartupContext, } from '../../YouleNexus/assets/framework/config/local-startup.ts'; -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import type { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; import type { ConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config.ts'; import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts'; @@ -22,7 +24,7 @@ function options(search = ''): ResolveRuntimeConfigOptions & { return { player_server_tcp: 'remote.example:3088' }; }, }; - return { hostKind: 'h5', win: {}, search, fetcher }; + return { startupConfig:STARTUP_CONFIG, hostKind: 'h5', win: {}, search, fetcher }; } test('local debug fetches remote config before choosing authoritative local servers', async () => { @@ -174,3 +176,12 @@ test('local consumers validate the injected source snapshot and its exact target assert.throws(()=>assertLocalStartupContext({...context,config:{...context.config,startupConfig:{...context.config.startupConfig,...patch}}} as any),/mode|useLocalServer|localServers|gameserver/); } }); + +test('local startup validates the selected project txt source', async () => { + const localFetcher={async fetch(){return {showVisitor:1};}}; + const context=await resolveLocalStartup({startupConfig:{...STARTUP_CONFIG,configSource:'local'},hostKind:'h5',win:{},search:'', + fetcher:{async fetch(){throw Error('remote forbidden');}},localFetcher}); + assert.equal(context.config.source,'local'); + assert.equal(context.config.gameserver,STARTUP_CONFIG.localConfigResource); + assert.doesNotThrow(()=>assertLocalStartupContext(context)); +}); diff --git a/cocoscreator_projects/framework-tests/config/profiles.test.ts b/cocoscreator_projects/framework-tests/config/profiles.test.ts index 1afbea5..5a4ca7d 100644 --- a/cocoscreator_projects/framework-tests/config/profiles.test.ts +++ b/cocoscreator_projects/framework-tests/config/profiles.test.ts @@ -1,11 +1,13 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { - STARTUP_CONFIG, + STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG, validateStartupConfig, decodeGameConfig, resolveGameServer, } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); test('decodeGameConfig:按旧协议把 -/# 解码为远程 txt URL', () => { assert.equal(decodeGameConfig('host-path#8080-config'), 'http://host/path:8080/config.txt'); @@ -23,7 +25,7 @@ test('resolveGameServer:空 gameconfig 使用配置源,非空值使用解码 }); test('one frozen source defines the complete default startup configuration', () => { - assert.deepEqual(STARTUP_CONFIG, { mode: 'debug', gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt', useLocalServer: true, localServers: ['ws://127.0.0.1:3088'] }); + assert.deepEqual(STARTUP_CONFIG, { mode: 'debug', configSource: 'remote', localConfigResource: 'dev-config/update_jsonv2', gameserver: 'https://tsgames.daoqi88.cn/config_test/update_jsonv2.txt', useLocalServer: true, localServers: ['ws://127.0.0.1:3088'] }); assert.ok(Object.isFrozen(STARTUP_CONFIG)); assert.ok(Object.isFrozen(STARTUP_CONFIG.localServers)); }); test('source validation rejects malformed configuration without defaults', () => { @@ -32,3 +34,11 @@ test('source validation rejects malformed configuration without defaults', () => } for (const url of ['ws://127.4.3.2:3088','wss://localhost:3088','ws://[::1]:3088']) assert.deepEqual(validateStartupConfig({...STARTUP_CONFIG,localServers:[url]}).localServers,[url]); }); + +test('config source and local resource paths are explicit and local is debug only', () => { + for (const patch of [{configSource:undefined},{configSource:'other'},{configSource:'local',mode:'release'}, + {localConfigResource:''},{localConfigResource:'../outside'},{localConfigResource:'dev-config/a.txt'},{localConfigResource:'/absolute'}]) { + assert.throws(()=>validateStartupConfig({...STARTUP_CONFIG,...patch} as any), /startup config/); + } + assert.equal(validateStartupConfig({...STARTUP_CONFIG,configSource:'local'}).configSource,'local'); +}); diff --git a/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts b/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts index 01f2382..367a8fc 100644 --- a/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts +++ b/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import { test } from 'node:test'; import assert from 'node:assert/strict'; import { HttpConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config-fetcher.ts'; @@ -36,7 +38,7 @@ test('resolveRuntimeConfig:把 cacheBust 注入唯一的 HTTP 请求构造点' }); assert.equal(calls.length, 1); - assert.equal(calls[0]?.input, 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt?123'); + assert.equal(calls[0]?.input, `${STARTUP_CONFIG.gameserver}?123`); assert.deepEqual(calls[0]?.init, { method: 'POST', body: '' }); }); diff --git a/cocoscreator_projects/framework-tests/config/remote-config.test.ts b/cocoscreator_projects/framework-tests/config/remote-config.test.ts index 29e977e..a5c9a80 100644 --- a/cocoscreator_projects/framework-tests/config/remote-config.test.ts +++ b/cocoscreator_projects/framework-tests/config/remote-config.test.ts @@ -17,6 +17,19 @@ function legacy(raw: unknown, name: string, selected = identity) { GameData: { AgentId: selected.agentid, GameId: selected.gameid, ChannelId: selected.channelid, marketID: selected.marketid }, }); } + +test('get_paravalue is the shared legacy-compatible reader, including an absent data wrapper', () => { + assert.equal(typeof api.get_paravalue, 'function'); + const raw = {showVisitor:1,agentlist:[ + {agentid:11,showVisitor:0,gamelist:[{gameid:22,deviceLogin:true}]}, + {agentid:11,showVisitor:2}, + ]}; + for(const name of ['showVisitor','deviceLogin','missing']) { + assert.deepEqual(api.get_paravalue({data:raw},name,identity),legacy(raw,name)); + assert.deepEqual(view(raw).getValue(name),api.get_paravalue({data:raw},name,identity)); + } + assert.equal(api.get_paravalue({},'showVisitor',identity),legacy(undefined,'showVisitor')); +}); test('memory view agrees with actual legacy function across all five scopes and preserves raw objects', () => { const scopes: Record[] = [ { hall_config: { root: true }, rootOnly: 'root', falsy: 'inherited' }, diff --git a/cocoscreator_projects/framework-tests/config/runtime-config.test.ts b/cocoscreator_projects/framework-tests/config/runtime-config.test.ts index 32393d3..6f85ecf 100644 --- a/cocoscreator_projects/framework-tests/config/runtime-config.test.ts +++ b/cocoscreator_projects/framework-tests/config/runtime-config.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts'; @@ -30,9 +32,9 @@ test('h5:只读 query 身份,不访问原生宿主,并生成严格 remote assert.deepEqual(result.identity, { ...BUILD_IDENTITY, agentid: 'HA', channelid: 'HC' }); assert.deepEqual(result.servers, ['ws://5.5.5.5:5000', 'ws://5.5.5.5:5000']); assert.equal(result.source, 'remote'); - assert.equal(result.gameserver, 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt'); + assert.equal(result.gameserver, STARTUP_CONFIG.gameserver); assert.equal(result.rawConfig, REMOTE); - assert.deepEqual(fetcher.calls, ['https://tsgames.daoqi88.cn/config/update_jsonv2.txt']); + assert.deepEqual(fetcher.calls, [STARTUP_CONFIG.gameserver]); assert.equal(Object.isFrozen(result), true); assert.equal(Object.isFrozen(result.identity), true); assert.equal(Object.isFrozen(result.servers), true); @@ -201,7 +203,7 @@ for (const mode of ['debug','release'] as const) for (const useLocalServer of [t source.localServers=['ws://localhost:9999']; assert.deepEqual(result.startupConfig.localServers,STARTUP_CONFIG.localServers); }); test('default runtime starts without query configuration', async () => { - const result=await resolveRuntimeConfig({hostKind:'h5',win:{},search:'',fetcher:returning({notice:'kept'})}); + const result=await resolveRuntimeConfig({startupConfig:STARTUP_CONFIG,hostKind:'h5',win:{},search:'',fetcher:returning({notice:'kept'})}); assert.deepEqual(result.startupConfig,STARTUP_CONFIG); assert.deepEqual(result.servers,STARTUP_CONFIG.localServers); }); @@ -216,3 +218,38 @@ for (const mode of ['debug','release'] as const) for (const useLocalServer of [t const fetcher={async fetch(){throw new Error('remote unavailable');}}; await assert.rejects(resolveRuntimeConfig({startupConfig:{...STARTUP_CONFIG,mode,useLocalServer},hostKind:'h5',win:{},search:'',fetcher}),/远程配置抓取失败/); }); + +// 本地来源必须在运行时边界隔离网络读取,并复用正式配置的层级取值规则。 +test('local txt selects one source, ignores gameconfig and preserves scoped values', async () => { + const remote = returning(REMOTE); + const local = returning({showVisitor: 1, agentlist: [{agentid: BUILD_IDENTITY.agentid, deviceLogin: true}]}); + const result = await resolveRuntimeConfig({startupConfig:{...STARTUP_CONFIG, configSource:'local'}, + hostKind:'h5',win:{},search:'?gameconfig=unwanted-host',fetcher:remote,localFetcher:local}); + assert.equal(result.source,'local'); + assert.deepEqual(remote.calls,[]); + assert.deepEqual(local.calls,[STARTUP_CONFIG.localConfigResource]); + assert.equal(result.remoteConfig.getValue('showVisitor'),1); + assert.equal(result.remoteConfig.getValue('deviceLogin'),true); + assert.deepEqual(result.servers,STARTUP_CONFIG.localServers); +}); + +test('local txt errors never fall back to remote, release rejects before I/O', async () => { + const remote=returning(REMOTE); let localCalls=0; + const localFetcher={async fetch(){localCalls++;throw Error('missing txt');}}; + const options={startupConfig:{...STARTUP_CONFIG,configSource:'local' as const},hostKind:'h5' as const,win:{},search:'',fetcher:remote,localFetcher}; + await assert.rejects(resolveRuntimeConfig(options),/本地配置读取失败/); + assert.equal(localCalls,1);assert.equal(remote.calls.length,0); + await assert.rejects(resolveRuntimeConfig({...options,startupConfig:{...options.startupConfig,mode:'release'}}),/configSource/); + assert.equal(localCalls,1); + await assert.rejects(resolveRuntimeConfig({...options,localFetcher:undefined}),/localFetcher/); +}); + +test('local txt and socket target are independent; remote mode never touches local loader', async () => { + const remote=returning(REMOTE),local=returning(REMOTE); + const options={hostKind:'h5' as const,win:{},search:'',fetcher:remote,localFetcher:local}; + const result=await resolveRuntimeConfig({...options,startupConfig:{...STARTUP_CONFIG,configSource:'local',useLocalServer:false}}); + assert.deepEqual(result.servers,['ws://5.5.5.5:5000','ws://5.5.5.5:5000']); + assert.equal(remote.calls.length,0);assert.equal(local.calls.length,1); + await resolveRuntimeConfig({...options,startupConfig:{...STARTUP_CONFIG,configSource:'remote'}}); + assert.equal(remote.calls.length,1);assert.equal(local.calls.length,1); +}); diff --git a/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts b/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts index 12d3543..498d237 100644 --- a/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts +++ b/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { resolveLocalStartup } from '../../YouleNexus/assets/framework/config/local-startup.ts'; @@ -13,7 +14,7 @@ class MapStorage implements LocalStoragePort { } function resolve(search = '?profile=local') { - return resolveLocalStartup({ hostKind: 'h5', win: {}, search, + return resolveLocalStartup({ startupConfig:{...STARTUP_CONFIG,configSource:'remote'}, hostKind: 'h5', win: {}, search, fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } }); } diff --git a/cocoscreator_projects/framework-tests/integration/login-flow.test.ts b/cocoscreator_projects/framework-tests/integration/login-flow.test.ts index 174014a..29b286d 100644 --- a/cocoscreator_projects/framework-tests/integration/login-flow.test.ts +++ b/cocoscreator_projects/framework-tests/integration/login-flow.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; diff --git a/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts b/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts index db91afe..41c451a 100644 --- a/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts +++ b/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; diff --git a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts deleted file mode 100644 index 5c55540..0000000 --- a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import { test } from 'node:test'; -import type { PageModel, OverlayModel, PageId } from '../../YouleNexus/assets/framework/presentation/ui-contracts.ts'; -import type { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; - -function deferred() { - let resolve!: () => void; - let reject!: (error: unknown) => void; - const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); - return { promise, resolve, reject }; -} -async function flush() { for (let i = 0; i < 30; i++) await Promise.resolve(); } -function fixture(name = 'player-login-success.json') { - return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')); -} -class Socket { - readyState = 0; - sent: string[] = []; - closes = 0; - closeError?: Error; - onopen: (() => void) | null = null; - onclose: (() => void) | null = null; - onmessage: ((event: { data: string }) => void) | null = null; - onerror: (() => void) | null = null; - send(frame: string) { this.sent.push(frame); } - close() { this.closes++; if (this.closeError) throw this.closeError; this.readyState = 3; this.onclose?.(); } - open() { this.readyState = 1; this.onopen?.(); } - push(message: unknown) { this.onmessage?.({ data: JSON.stringify(message) }); } -} -async function setup(overrides: { config?: Partial; render?: (model: PageModel) => void; - cancelFrame?: () => void; showOverlay?: (model: OverlayModel) => void; - record?: (event: { kind: string; data: unknown }) => void; onBusy?: (busy: boolean) => void; - onConfigResolved?: (config: RuntimeConfig) => void; onResourcesReady?: () => void; - onSocketReady?: (ready: boolean) => void; onStopped?: () => void } = {}) { - const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-host.ts') - .catch(error => assert.fail(`Local login host implementation must exist: ${String(error)}`)); - const resources = deferred(), minimum = deferred(); - const sockets: Socket[] = [], pages: PageModel[] = [], overlays: OverlayModel[] = [], activations: PageId[] = []; - const failures: unknown[] = [], busy: boolean[] = [], evidence: { kind: string; data: unknown }[] = []; - const values = new Map(), frames = new Set<() => void>(); - let fetches = 0; - const host = api.createLocalLoginHost({ - onConfigResolved: overrides.onConfigResolved, onResourcesReady: overrides.onResourcesReady, - onSocketReady: overrides.onSocketReady, onStopped: overrides.onStopped, - configOptions: { hostKind: 'h5', win: {}, search: '?profile=local&version=731', - fetcher: { async fetch() { fetches++; return { player_server_tcp: 'remote.example:3088' }; } }, ...overrides.config }, - storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } }, - random: () => .5, now: () => 123456, sampleHost: () => ({ location: null }), - view: { render: model => { overrides.render?.(model); pages.push(model); }, - activate: page => { activations.push(page); }, showOverlay: model => { overlays.push(model); overrides.showOverlay?.(model); } }, - scheduler: { request: callback => { frames.add(callback); return () => { frames.delete(callback); overrides.cancelFrame?.(); }; } }, - loadResources: () => resources.promise, waitForMinimumDisplay: () => minimum.promise, - createSocket: () => { const socket = new Socket(); sockets.push(socket); return socket as unknown as WebSocket; }, - record: event => { evidence.push(event); overrides.record?.(event); }, - onBusy: value => { busy.push(value); overrides.onBusy?.(value); }, onFailure: error => { failures.push(error); }, - }); - const draw = () => { for (const frame of [...frames]) { frames.delete(frame); frame(); } }; - const ready = async () => { const starting = host.start(); resources.resolve(); minimum.resolve(); await flush(); sockets[0]!.open(); await starting; draw(); }; - return { host, resources, minimum, sockets, pages, overlays, activations, failures, busy, evidence, values, frames, draw, ready, fetches: () => fetches }; -} - -test('host exposes resolved config and independent socket/resources readiness before display, and stops late events', async () => { - const configs: RuntimeConfig[] = [], socket: boolean[] = []; let resources = 0, stopped = 0; - const h = await setup({ onConfigResolved: config => configs.push(config), onResourcesReady: () => resources++, - onSocketReady: ready => socket.push(ready), onStopped: () => stopped++ }); - const starting = h.host.start(); await flush(); - assert.equal(configs.length, 1); assert.equal(configs[0].identity.version, 731); - assert.equal(configs[0].remoteConfig.getValue('player_server_tcp'), 'remote.example:3088'); - h.sockets[0].open(); await flush(); assert.equal(socket[socket.length - 1], true); - assert.equal(h.host.getSnapshot().ready, false); - h.resources.resolve(); await flush(); assert.equal(resources, 1); - assert.equal(h.host.getSnapshot().ready, false); - h.minimum.resolve(); await starting; h.host.stop(); assert.equal(stopped, 1); - const events = socket.length; h.sockets[0].open(); await flush(); assert.equal(socket.length, events); -}); - -test('host stop cancels presentation and ignores a resource completion still pending', async () => { - let resources = 0, stopped = 0; - const h = await setup({ onResourcesReady: () => resources++, onStopped: () => stopped++ }); - const starting = h.host.start(); const rejection = assert.rejects(starting, /stop|terminal/); - await flush(); h.host.stop(); h.resources.resolve(); h.minimum.resolve(); - await rejection; assert.equal(resources, 0); assert.equal(stopped, 1); -}); - -for (const order of [ - ['resources', 'socket', 'minimum'], ['resources', 'minimum', 'socket'], - ['socket', 'resources', 'minimum'], ['socket', 'minimum', 'resources'], - ['minimum', 'resources', 'socket'], ['minimum', 'socket', 'resources'], -] as const) test(`all startup gates remain independent in ${order.join(' -> ')} order`, async t => { - const h = await setup(); t.after(() => h.host.stop()); - const starting = h.host.start(); await flush(); - for (let index = 0; index < order.length; index++) { - const gate = order[index]; - if (gate === 'socket') h.sockets[0].open(); else h[gate].resolve(); - await flush(); - assert.equal(h.host.getSnapshot().ready, index === order.length - 1); - assert.equal(h.activations.includes('login'), index === order.length - 1); - } - await starting; h.draw(); assert.equal(h.activations.includes('login'), true); -}); - -test('pending config cannot be bypassed by previously completed resource/display tasks', async t => { - const config = deferred(); let resolved = 0; - const h = await setup({ config: { fetcher: { async fetch() { await config.promise; return {}; } } }, - onConfigResolved: () => resolved++ }); - t.after(() => h.host.stop()); const starting = h.host.start(); - h.resources.resolve(); h.minimum.resolve(); await flush(); - assert.equal(resolved, 0); assert.equal(h.sockets.length, 0); assert.equal(h.host.getSnapshot().ready, false); - config.resolve(); await flush(); assert.equal(resolved, 1); assert.equal(h.host.getSnapshot().ready, false); - h.sockets[0].open(); await starting; h.draw(); assert.equal(h.host.getSnapshot().ready, true); -}); - -for (const missing of ['resources', 'minimum', 'socket'] as const) test(`login waits for ${missing}, and start composes only once`, async t => { - const h = await setup(); t.after(() => h.host.stop()); - const starting = h.host.start(); const repeated = h.host.start(); - assert.strictEqual(starting, repeated); - await flush(); - if (missing !== 'resources') h.resources.resolve(); - if (missing !== 'minimum') h.minimum.resolve(); - if (missing !== 'socket') h.sockets[0]!.open(); - await flush(); - assert.equal(h.host.getSnapshot().ready, false); - await assert.rejects(h.host.login(), /ready/i); - assert.equal(h.values.size, 0); assert.deepEqual(h.sockets[0]!.sent, []); - h.resources.resolve(); h.minimum.resolve(); if (missing === 'socket') h.sockets[0]!.open(); - await starting; - assert.equal(h.host.getSnapshot().ready, true); assert.equal(h.fetches(), 1); - assert.equal(h.sockets.length, 1); - assert.equal(h.evidence.filter(event => event.kind === 'startup-config').length, 1); - await h.host.login(); - assert.equal(JSON.parse(h.sockets[0]!.sent[0]!).data.version, 731); -}); - -test('duplicate clicks send once; response updates the sole Store and lobby projection and clears busy', async t => { - const h = await setup(); t.after(() => h.host.stop()); await h.ready(); - const first = h.host.login(), second = h.host.login(); assert.strictEqual(first, second); - assert.equal(h.host.getSnapshot().busy, true); - await first; await h.host.login(); assert.equal(h.sockets[0]!.sent.length, 1); - assert.deepEqual(h.busy, [true]); - h.sockets[0]!.push(fixture()); h.draw(); - assert.equal(h.host.getSnapshot().state?.app.phase, 'logged-in'); - assert.equal(h.host.getSnapshot().state?.room.kind, 'outside'); - assert.deepEqual(h.pages[h.pages.length - 1], { page: 'lobby', self: { playerid: 430511, nickname: '测试号', avatar: 'http://a', bean: 0 } }); - assert.deepEqual(h.busy, [true, false]); - const before = h.pages.length; h.host.stop(); h.draw(); assert.equal(h.pages.length, before); -}); - -for (const rpc of ['player_login', 'kick_server']) test(`${rpc} retains original server reason through terminal cleanup`, async t => { - const h = await setup(); t.after(() => h.host.stop()); await h.ready(); await h.host.login(); - const data = { state: 17, msg: '原始服务器原因' }; - h.sockets[0]!.push({ route: 'agent', rpc, data }); - assert.equal(h.failures.length, 1); assert.deepEqual(h.failures[0], data); - assert.equal(h.host.getSnapshot().ready, false); assert.equal(h.host.getSnapshot().busy, false); - assert.equal(h.sockets[0]!.closes, 1); - await assert.rejects(h.host.login(), /stop|terminal/i); -}); - -test('room recovery is refused before any player or room state enters Store', async t => { - const h = await setup(); t.after(() => h.host.stop()); await h.ready(); await h.host.login(); - h.sockets[0]!.push(fixture('player-login-room.json')); - assert.match(String(h.failures[0]), /room recovery/); - assert.equal(h.host.getSnapshot().state?.players.selfPlayerId, null); - assert.equal(h.host.getSnapshot().state?.room.kind, 'outside'); - assert.equal(h.pages.some(model => model.page === 'lobby'), false); -}); - -test('corrupt account cache prevents sending and preserves the cache', async t => { - const h = await setup(); t.after(() => h.host.stop()); await h.ready(); - // Discover the authority-owned key from a prior independent local login. - const prior = await setup(); t.after(() => prior.host.stop()); await prior.ready(); await prior.host.login(); - for (const [key, value] of prior.values) h.values.set(key, value); - const accountKey = [...h.values].find(([, value]) => value.includes('account'))![0]; - h.values.set(accountKey, '{broken'); - await assert.rejects(h.host.login(), /invalid JSON/); - assert.equal(h.sockets[0]!.sent.length, 0); assert.equal(h.values.get(accountKey), '{broken'); - assert.equal(h.failures.length, 1); -}); - -for (const phase of ['config', 'resources', 'prepare'] as const) test(`stop during ${phase} suppresses late connection, send and render`, async () => { - const h = await setup(); - let operation: Promise; - if (phase === 'prepare') { await h.ready(); operation = h.host.login(); } - else { operation = h.host.start(); if (phase === 'resources') await flush(); } - const settled = operation.catch(() => {}); - // Let config resolution / account preparation begin before terminal stop. - if (phase !== 'resources') await Promise.resolve(); - h.host.stop(); const pageCount = h.pages.length; - h.resources.resolve(); h.minimum.resolve(); await settled; await flush(); h.draw(); - assert.equal(h.pages.length, pageCount); assert.equal(h.frames.size, 0); - assert.equal(h.sockets.flatMap(socket => socket.sent).length, 0); - if (phase === 'config') assert.equal(h.sockets.length, 0); - assert.deepEqual(h.failures, []); -}); - -for (const config of [{ startupConfig: {...STARTUP_CONFIG,mode:'release' as const} }, { startupConfig: {...STARTUP_CONFIG,useLocalServer:false} }, { hostKind: 'native-settings' as const }]) test('invalid local input cannot create an account or connection', async () => { - const h = await setup({ config }); - await assert.rejects(h.host.start(), /local startup/); - assert.equal(h.values.size, 0); assert.equal(h.sockets.length, 0); assert.equal(h.failures.length, 1); -}); - -test('resource failure is reported with original object after shutdown', async () => { - const h = await setup(); const error = new Error('resource dependency missing'); - const starting = h.host.start(); await flush(); h.resources.reject(error); h.minimum.resolve(); - await assert.rejects(starting); - assert.strictEqual(h.failures[0], error); assert.equal(h.sockets[0]!.closes, 1); -}); - -test('observer and socket cleanup failures both survive terminal reporting', async t => { - const observer = new Error('render observer failed'), cleanup = new Error('socket close failed'); - const h = await setup({ render: model => { if (model.page === 'lobby') throw observer; } }); - t.after(() => h.host.stop()); await h.ready(); await h.host.login(); h.sockets[0]!.closeError = cleanup; - h.sockets[0]!.push(fixture()); - const contains = (value: unknown, target: unknown): boolean => value === target || - (!!value && typeof value === 'object' && 'errors' in value && (value.errors as unknown[]).some(error => contains(error, target))); - assert.equal(contains(h.failures[0], observer), true); assert.equal(contains(h.failures[0], cleanup), true); -}); - -test('resource error remains primary when closing its socket also fails', async () => { - const h = await setup(); const primary = new Error('resource failed'), cleanup = new Error('close failed'); - const starting = h.host.start(); await flush(); h.sockets[0]!.closeError = cleanup; - h.resources.reject(primary); h.minimum.resolve(); await assert.rejects(starting); - const errors = (h.failures[0] as { errors: unknown[] }).errors; - assert.strictEqual(errors[0], primary); assert.ok(errors.includes(cleanup)); -}); - -test('kick data remains primary when closing its socket also fails', async () => { - const h = await setup(); await h.ready(); await h.host.login(); - const cleanup = new Error('kick close failed'); h.sockets[0]!.closeError = cleanup; - const data = { msg: 'original kick reason' }; - h.sockets[0]!.push({ route: 'agent', rpc: 'kick_server', data }); - const errors = (h.failures[0] as { errors: unknown[] }).errors; - assert.deepEqual(errors[0], data); assert.ok(errors.includes(cleanup)); -}); - -test('pending frame cancellation retains its primary error when socket cleanup also fails', async () => { - const primary = new Error('pending frame cancel failed'), cleanup = new Error('socket close failed'); - let armed = false; - const h = await setup({ cancelFrame: () => { if (armed) throw primary; } }); - await h.ready(); await h.host.login(); - armed = true; h.sockets[0]!.closeError = cleanup; - // The real login response schedules a Store frame, then showLobby flush cancels it. - h.sockets[0]!.push(fixture()); - assert.equal(h.failures.length, 1); - const fault = h.failures[0] as { errors?: unknown[] }; - assert.ok(fault.errors, 'primary and cleanup failures must both be retained'); - assert.strictEqual(fault.errors[0], primary); - assert.ok(fault.errors.includes(cleanup)); - assert.equal(h.host.getSnapshot().ready, false); assert.equal(h.frames.size, 0); - assert.equal(h.sockets[0]!.closes, 1); -}); - -test('fatal overlay observer failure retains the original Runtime error object', async () => { - const observer = new Error('fatal overlay observer failed'); - let primary: Error | undefined; - const h = await setup({ showOverlay: model => { - if (model.kind === 'fatal') { primary = model.error; throw observer; } - } }); - await h.ready(); - h.sockets[0]!.push({ route: 'agent', rpc: 'unsupported_test_rpc', data: {} }); - assert.ok(primary); assert.match(primary.message, /Unsupported platform rpc/); - assert.equal(h.failures.length, 1); - const fault = h.failures[0] as { errors?: unknown[] }; - assert.ok(fault.errors, 'Runtime error and overlay observer error must both be retained'); - assert.strictEqual(fault.errors[0], primary); assert.strictEqual(fault.errors[1], observer); - assert.equal(h.host.getSnapshot().ready, false); assert.equal(h.sockets[0]!.closes, 1); -}); - -test('remote fetch and parse failures leave local sockets and account storage untouched', async () => { - for (const fetch of [async () => { throw new Error('network down'); }, async () => ({ agentlist: {} }), async () => null]) { - const h = await setup({ config: { fetcher: { fetch } } }); - const result = h.host.start().catch(error => error); - await flush(); - assert.equal(h.sockets.length, 0); - assert.equal(h.values.size, 0); - assert.ok(await result instanceof Error); - assert.equal(h.failures.length, 1); - } -}); - -test('stop while remote fetch is pending prevents late socket creation', async () => { - const pending = deferred(); - let fetched = false; - const h = await setup({ config: { fetcher: { async fetch() { - fetched = true; await pending.promise; return { player_server_tcp: 'remote.example:3088' }; - } } } }); - const result = h.host.start().catch(error => error); - await flush(); assert.equal(fetched, true); assert.equal(h.sockets.length, 0); - h.host.stop(); pending.resolve(); await result; - assert.equal(h.sockets.length, 0); assert.equal(h.values.size, 0); -}); - -test('startup diagnostics exclude raw remote fields', async t => { - const h = await setup({ config: { fetcher: { async fetch() { - return { player_server_tcp: 'remote.example:3088', privateConfig: 'DO_NOT_RECORD_REMOTE_SECRET' }; - } } } }); - t.after(() => h.host.stop()); await h.ready(); - assert.equal(JSON.stringify(h.evidence).includes('DO_NOT_RECORD_REMOTE_SECRET'), false); -}); - -test('default no-query host reaches login ready and ignores conflicting selectors', async t => { - for (const search of ['', '?mode=release&mode=other&profile=prod&profile=unknown']) { - const h=await setup({config:{search}}); t.after(()=>h.host.stop()); await h.ready(); - assert.equal(h.fetches(),1); assert.equal(h.host.getSnapshot().ready,true); assert.ok(h.activations.includes('login')); assert.equal(h.failures.length,0); - } -}); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts deleted file mode 100644 index 752fbab..0000000 --- a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts +++ /dev/null @@ -1,323 +0,0 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; -import { resolveLocalStartup } from '../../YouleNexus/assets/framework/config/local-startup.ts'; -import { WireClient } from '../../YouleNexus/assets/framework/net/wire-client.ts'; -import type { Clock } from '../../YouleNexus/assets/framework/net/heartbeat.ts'; - -class FakeSocket { - readyState = 0; - sent: string[] = []; - closes = 0; - sendError?: Error; - closeError?: Error; - onopen: ((event: Event) => void) | null = null; - onmessage: ((event: MessageEvent) => void) | null = null; - onclose: ((event: CloseEvent) => void) | null = null; - onerror: ((event: Event) => void) | null = null; - send(frame: string): void { - if (this.sendError) throw this.sendError; - this.sent.push(frame); - } - close(): void { - this.closes++; - if (this.closeError) throw this.closeError; - this.readyState = 3; - this.onclose?.({} as CloseEvent); - } - open(): void { this.readyState = 1; this.onopen?.({} as Event); } - message(data: unknown): void { this.onmessage?.({ data } as MessageEvent); } -} - -async function setup(overrides: { - createSocket?: (url: string) => WebSocket; - record?: (event: { kind: string; data: unknown }) => void; - onFault?: (error: unknown) => void; -} = {}) { - const module = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts') - .catch((error: unknown) => assert.fail(`LocalLoginTransport implementation must exist: ${String(error)}`)); - const context = await resolveLocalStartup({ - hostKind: 'h5', win: {}, search: '?profile=local', - fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } }, - }); - const sockets: FakeSocket[] = []; - const urls: string[] = []; - const faults: unknown[] = []; - const evidence: { kind: string; data: unknown }[] = []; - const transport = new module.LocalLoginTransport({ - context, - createSocket: overrides.createSocket ?? ((url) => { - urls.push(url); - const socket = new FakeSocket(); - sockets.push(socket); - return socket as unknown as WebSocket; - }), - record: overrides.record ?? ((event) => evidence.push(event)), - onFault: (error) => { faults.push(error); overrides.onFault?.(error); }, - }); - return { transport, context, sockets, urls, faults, evidence, url: context.config.servers[0]! }; -} - -test('requires OPEN and records only frames successfully sent', async () => { - const h = await setup(); - assert.throws(() => h.transport.send('{}'), /open/i); - h.transport.connect(h.url); - assert.throws(() => h.transport.send('{}'), /open/i); - const socket = h.sockets[0]!; - socket.open(); - h.transport.send('frame'); - assert.deepEqual(socket.sent, ['frame']); - assert.deepEqual(h.evidence, [{ kind: 'transport-send', data: 'frame' }]); - const error = new Error('send failed'); - socket.sendError = error; - assert.throws(() => h.transport.send('unsent'), (caught) => caught === error); - assert.equal(h.evidence.length, 1); - assert.equal(h.faults[h.faults.length - 1], error); -}); - -test('validates every connection target before constructing and reports the thrown error', async () => { - const h = await setup(); - h.transport.connect(h.url); - assert.throws(() => h.transport.connect('wss://remote.example/'), (error) => { - assert.equal(h.faults[h.faults.length - 1], error); - return /loopback/i.test(String(error)); - }); - assert.deepEqual(h.urls, [h.url]); -}); - -test('socket constructor failure is reported before rethrowing the same object and cause', async () => { - const cause = new Error('root'); - const error = Object.assign(new Error('constructor failed'), { cause }); - const h = await setup({ createSocket: () => { throw error; } }); - assert.throws(() => h.transport.connect(h.url), (caught) => { - assert.equal(h.faults[0], error); - return caught === error && error.cause === cause; - }); -}); - -test('records raw handshake before message delivery', async () => { - const order: unknown[] = []; - const h = await setup({ record: (event) => order.push(event) }); - h.transport.onMessage((frame) => order.push(frame)); - h.transport.connect(h.url); - h.sockets[0]!.open(); - h.sockets[0]!.message('@toconcon raw handshake'); - assert.deepEqual(order, [ - { kind: 'transport-receive', data: '@toconcon raw handshake' }, '@toconcon raw handshake', - ]); -}); - -test('non-text frames stop, report TypeError and never reach consumers or reconnect callback', async () => { - const h = await setup(); - let delivered = 0; - let closed = 0; - h.transport.onMessage(() => delivered++); - h.transport.onClose(() => closed++); - h.transport.connect(h.url); - const socket = h.sockets[0]!; - socket.open(); - socket.message(new ArrayBuffer(2)); - socket.message('late frame'); - assert.equal(delivered, 0); - assert.equal(closed, 0); - assert.equal(socket.closes, 1); - assert.equal(h.faults.length, 1); - assert.ok(h.faults[0] instanceof TypeError); - assert.deepEqual(h.evidence, []); - assert.throws(() => h.transport.send('late'), /open/i); -}); - -for (const direction of ['receive', 'send']) { - test(`${direction} evidence failure preserves original error and stops without reconnect`, async () => { - const error = new Error('evidence storage failed'); - const h = await setup({ record: () => { throw error; } }); - let delivered = 0; - let closed = 0; - h.transport.onMessage(() => delivered++); - h.transport.onClose(() => closed++); - h.transport.connect(h.url); - const socket = h.sockets[0]!; - socket.open(); - const run = () => direction === 'receive' ? socket.message('frame') : h.transport.send('frame'); - assert.throws(run, (caught) => caught === error); - assert.equal(h.faults[0], error); - assert.equal(socket.closes, 1); - assert.equal(delivered, 0); - assert.equal(closed, 0); - assert.throws(() => h.transport.send('late'), /open/i); - }); -} - -test('error followed by close notifies once and retains original error event', async () => { - const h = await setup(); - let closed = 0; - h.transport.onClose(() => closed++); - h.transport.connect(h.url); - const socket = h.sockets[0]!; - const error = {} as Event; - socket.onerror?.(error); - socket.onclose?.({} as CloseEvent); - assert.equal(closed, 1); - assert.deepEqual(h.faults, []); - assert.deepEqual(h.evidence, [{ kind: 'transport-error', data: error }]); -}); - -test('explicit close and superseded sockets invalidate every old callback', async () => { - const h = await setup(); - const calls: string[] = []; - h.transport.onOpen(() => calls.push('open')); - h.transport.onMessage(() => calls.push('message')); - h.transport.onClose(() => calls.push('close')); - h.transport.connect(h.url); - const old = h.sockets[0]!; - h.transport.connect(h.url); - old.open(); old.message('old'); old.onerror?.({} as Event); old.onclose?.({} as CloseEvent); - assert.equal(old.closes, 1); - const current = h.sockets[1]!; - current.open(); - h.transport.close(); - current.open(); current.message('late'); current.onerror?.({} as Event); - assert.deepEqual(calls, ['open']); - assert.deepEqual(h.faults, []); - assert.deepEqual(h.evidence, []); -}); - -test('close failure is reported and thrown after invalidation', async () => { - const h = await setup(); - h.transport.connect(h.url); - const socket = h.sockets[0]!; - const error = new Error('close failed'); - socket.closeError = error; - assert.throws(() => h.transport.close(), (caught) => caught === error); - assert.equal(h.faults[0], error); - assert.throws(() => h.transport.send('late'), /open/i); -}); - -test('explicit stop from error evidence observer suppresses the pending error close callback', async () => { - const h = await setup({ record: () => h.transport.close() }); - let closed = 0; - h.transport.onClose(() => closed++); - h.transport.connect(h.url); - h.sockets[0]!.onerror?.({} as Event); - assert.equal(closed, 0); -}); - -test('a replacement connection from error evidence observer cannot receive the old close callback', async () => { - const h = await setup({ record: () => h.transport.connect(h.url) }); - let closed = 0; - h.transport.onClose(() => closed++); - h.transport.connect(h.url); - h.sockets[0]!.onerror?.({} as Event); - assert.equal(h.sockets.length, 2); - assert.equal(closed, 0); -}); - -test('natural close lets real WireClient reconnect while explicit stop cancels it', async () => { - const h = await setup(); - const timers = new Map void>(); - const clock: Clock = { - setTimeout: (fn) => { const key = {}; timers.set(key, fn); return key; }, - clearTimeout: (key) => { timers.delete(key); }, - }; - const wire = new WireClient({ servers: h.context.config.servers, transportFactory: () => h.transport, clock }); - wire.start(); - h.sockets[0]!.open(); - h.sockets[0]!.close(); - assert.equal(timers.size, 1); - const retry = [...timers.values()][0]!; - timers.clear(); retry(); - assert.equal(h.sockets.length, 2); - wire.stop(); - assert.equal(timers.size, 0); -}); - -test('natural error reconnects real WireClient even when terminal onFault stops the host', async () => { - const h = await setup({ onFault: () => wire.stop() }); - const timers = new Map void>(); - const clock: Clock = { - setTimeout: (fn) => { const key = {}; timers.set(key, fn); return key; }, - clearTimeout: (key) => { timers.delete(key); }, - }; - const wire = new WireClient({ servers: h.context.config.servers, transportFactory: () => h.transport, clock }); - wire.start(); - h.sockets[0]!.open(); - const error = {} as Event; - h.sockets[0]!.onerror?.(error); - h.sockets[0]!.onclose?.({} as CloseEvent); - assert.deepEqual(h.faults, []); - assert.deepEqual(h.evidence, [{ kind: 'transport-error', data: error }]); - assert.equal(timers.size, 1); - const retry = [...timers.values()][0]!; - timers.clear(); retry(); - assert.equal(h.sockets.length, 2); - wire.stop(); - assert.equal(timers.size, 0); -}); - -for (const failure of ['record', 'close']) { - test(`socket error ${failure} failure reports terminal original and suppresses reconnect`, async () => { - const error = new Error(`${failure} failed`); - const h = await setup(failure === 'record' ? { record: () => { throw error; } } : {}); - let closed = 0; - h.transport.onClose(() => closed++); - h.transport.connect(h.url); - const socket = h.sockets[0]!; - if (failure === 'close') socket.closeError = error; - const event = {} as Event; - assert.throws(() => socket.onerror?.(event), (caught) => caught === error); - assert.deepEqual(h.faults, [error]); - assert.equal(closed, 0); - socket.onclose?.({} as CloseEvent); - assert.equal(closed, 0); - assert.throws(() => h.transport.send('late'), /open/i); - }); -} - -test('non-text and cleanup failures reach a single terminal observer together', async () => { - const observed: unknown[] = []; - const h = await setup({ onFault: (error) => { if (observed.length === 0) observed.push(error); } }); - h.transport.connect(h.url); - const socket = h.sockets[0]!; - const cleanupError = new Error('close failed'); - socket.closeError = cleanupError; - socket.message(new ArrayBuffer(1)); - assert.equal(h.faults.length, 1); - assert.equal(observed[0], h.faults[0]); - const fault = observed[0] as Error & { errors: unknown[] }; - assert.ok(fault instanceof Error); - assert.equal(fault.errors.length, 2); - assert.ok(fault.errors[0] instanceof TypeError); - assert.equal(fault.errors[1], cleanupError); - assert.throws(() => h.transport.send('late'), /open/i); -}); - -for (const operation of ['receive-record', 'error-record', 'send-record', 'send']) { - test(`${operation} and close failures are reported once and thrown as the same combined fault`, async () => { - const primary = new Error('primary failed'); - const cleanup = new Error('close failed'); - const observed: unknown[] = []; - const h = await setup({ - ...(operation === 'send' ? {} : { record: () => { throw primary; } }), - onFault: (error) => { if (observed.length === 0) observed.push(error); }, - }); - h.transport.connect(h.url); - const socket = h.sockets[0]!; - socket.open(); - socket.closeError = cleanup; - if (operation === 'send') socket.sendError = primary; - let thrown: unknown; - try { - if (operation === 'receive-record') socket.message('frame'); - else if (operation === 'error-record') socket.onerror?.({} as Event); - else h.transport.send('frame'); - } catch (error) { thrown = error; } - assert.equal(h.faults.length, 1); - assert.equal(thrown, observed[0]); - assert.equal(thrown, h.faults[0]); - assert.ok(thrown instanceof Error); - const errors = (thrown as Error & { errors: unknown[] }).errors; - assert.equal(errors.length, 2); - assert.equal(errors[0], primary); - assert.equal(errors[1], cleanup); - assert.throws(() => h.transport.send('late'), /open/i); - }); -} diff --git a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts deleted file mode 100644 index 2272c98..0000000 --- a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts +++ /dev/null @@ -1,391 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { readFileSync, existsSync } from 'node:fs'; -import { runInNewContext } from 'node:vm'; -import ts from 'typescript'; -import { EventEmitter } from 'node:events'; -import { resolveLaunchWaitTime } from '../../YouleNexus/assets/framework/config/remote-config.ts'; - -class Sprite {} -class Vec3 { constructor(public x = 0, public y = 0, public z = 0) {} } -class Widget { enabled = true; updateAlignment() {} } -class UITransform { - width = 100; height = 30; anchorX = 0.5; anchorY = 0.5; - convertToWorldSpaceAR(value: Vec3) { return new Vec3(value.x + 250, value.y + 70, value.z); } - convertToNodeSpaceAR(value: Vec3) { return new Vec3(value.x - 20, value.y - 10, value.z); } -} -class ProgressBar { progress = 0; } -class Label { - static Overflow = { NONE: 0 }; overflow = 1; - string = ''; updateRenderData() {} -} -class Node extends EventEmitter { - static EventType = { TOUCH_END: 'touch-end' }; - active = true; - children: Node[] = []; - parent: Node | null = null; - position = new Vec3(); - setPosition(position: Vec3) { this.position = position; } - components = new Map(); - constructor(public name = '') { super(); } - getComponent(type: unknown) { return this.components.get(type) ?? null; } - getComponentsInChildren(type: unknown): any[] { - const own = this.getComponent(type); - return [...(own ? [own] : []), ...this.children.flatMap(child => child.getComponentsInChildren(type))]; - } - getChildByPath(path: string): Node | null { - const [head, ...tail] = path.split('/'); - const child = this.children.find(node => node.name === head); - return child ? (tail.length ? child.getChildByPath(tail.join('/')) : child) : null; - } -} -const director = new EventEmitter(); -function countdownFixture() { - const { createLogoCountdown } = load(); - assert.equal(typeof createLogoCountdown, 'function'); - let now = 0; let id = 0; - const timers = new Map void }>(); - const states: { visible: boolean; text: string }[] = []; - const gate = createLogoCountdown({ now: () => now, - schedule: (callback: () => void, ms: number) => { - const key = ++id; timers.set(key, { at: now + ms, callback }); return () => timers.delete(key); - }, render: (state: { visible: boolean; text: string }) => states.push(state) }); - const advance = (ms: number) => { - const end = now + ms; - while (true) { - const next = [...timers].filter(([, timer]) => timer.at <= end).sort((a, b) => a[1].at - b[1].at)[0]; - if (!next) break; - now = next[1].at; timers.delete(next[0]); next[1].callback(); - } - now = end; - }; - return { gate, timers, states, advance, last: () => states[states.length - 1]! }; -} - -test('Logo countdown ticks legacy milliseconds and skip requires config, resource and socket readiness', async () => { - const f = countdownFixture(); let complete = false; void f.gate.promise.then(() => { complete = true; }); - assert.equal(f.last().visible, false); - f.gate.skip(); f.gate.start(2500); - assert.equal(f.last().text, '倒计时3s'); - f.advance(1000); assert.equal(f.last().text, '倒计时2s'); - f.gate.resourcesReady(); assert.equal(f.last().text, '倒计时2s 跳过'); - f.gate.skip(); await Promise.resolve(); assert.equal(complete, false); - f.gate.socketReady(true); await Promise.resolve(); assert.equal(complete, false, 'premature click must not queue'); - f.gate.skip(); await f.gate.promise; - assert.equal(f.last().visible, false); assert.equal(f.timers.size, 0); -}); - -test('Logo exact deadline is independent from resources and socket; terminal cancellation ignores late events', async () => { - const f = countdownFixture(); f.gate.start(10); f.advance(9); - assert.equal(f.last().visible, true); f.advance(1); await f.gate.promise; - assert.equal(f.last().visible, false); assert.equal(f.timers.size, 0); - const cancelled = countdownFixture(); cancelled.gate.start(5000); - const rejection = assert.rejects(cancelled.gate.promise, /cancelled/); - cancelled.gate.cancel(); await rejection; - const count = cancelled.states.length; - cancelled.gate.start(9999); cancelled.gate.resourcesReady(); cancelled.gate.socketReady(true); cancelled.gate.skip(); cancelled.advance(10000); - assert.equal(cancelled.states.length, count); assert.equal(cancelled.timers.size, 0); -}); - -test('Logo render failure at completion rejects the gate and clears all timers', async () => { - const { createLogoCountdown } = load(); let now = 0; let failRender = false; - const callbacks = new Set<() => void>(); - const gate = createLogoCountdown({ now: () => now, - schedule: (callback: () => void) => { callbacks.add(callback); return () => callbacks.delete(callback); }, - render: () => { if (failRender) throw new Error('label rendering failed'); } }); - gate.start(2500); failRender = true; now = 2500; - const rejection = assert.rejects(gate.promise, /label rendering failed/); - assert.doesNotThrow(() => { const callback = [...callbacks][0]; callbacks.delete(callback); callback(); }); - await rejection; assert.equal(callbacks.size, 0); -}); - -test('Logo scheduling and tick render failures settle pending display promises', async () => { - const { createLogoCountdown } = load(); - const failed = createLogoCountdown({ now: () => 0, schedule: () => { throw new Error('timer failed'); }, render() {} }); - const scheduling = assert.rejects(failed.promise, /timer failed/); - assert.doesNotThrow(() => failed.start(2500)); await scheduling; - let failRender = false; const callbacks = new Set<() => void>(); - const gate = createLogoCountdown({ now: () => 0, - schedule: (callback: () => void) => { callbacks.add(callback); return () => callbacks.delete(callback); }, - render: () => { if (failRender) throw new Error('tick rendering failed'); } }); - gate.start(2500); failRender = true; - const rendering = assert.rejects(gate.promise, /tick rendering failed/); - assert.doesNotThrow(() => { const callback = [...callbacks][1]; callbacks.delete(callback); callback(); }); - await rendering; assert.equal(callbacks.size, 0); -}); -const source = new URL('../../YouleNexus/assets/scripts/local-platform-login/local-login-view.ts', import.meta.url); -function load() { - assert.ok(existsSync(source), 'Cocos local login view implementation must exist'); - const exports = {}; - runInNewContext(ts.transpileModule(readFileSync(source, 'utf8'), { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, - }).outputText, { exports, require: (name: string) => { - assert.equal(name, 'cc'); - return { Node, Sprite, UITransform, Label, Widget, Vec3, director, Director: { EVENT_AFTER_DRAW: 'draw' } }; - }, Error }); - return exports as any; -} - -test('Logo native binding measures label around button center and releases its touch listener', () => { - const { CocosLogoCountdownView } = load(); assert.equal(typeof CocosLogoCountdownView, 'function'); - const button = new Node('Spirit3103'), text = new Node('Spirit3104'), parent = new Node('group'); - text.parent = parent; - for (const node of [button, text, parent]) node.components.set(UITransform, new UITransform()); - const label = new Label(); text.components.set(Label, label); - const widget = new Widget(); text.components.set(Widget, widget); - let clicks = 0; const view = new CocosLogoCountdownView(button, text, () => clicks++); - assert.equal(button.active, false); assert.equal(text.active, false); - view.render({ visible: true, text: '倒计时3s 跳过' }); - assert.equal(label.string, '倒计时3s 跳过'); assert.equal(label.overflow, Label.Overflow.NONE); - assert.equal(widget.enabled, false); assert.equal(text.position.x, 230); assert.equal(text.position.y, 60); - button.emit('touch-end'); assert.equal(clicks, 1); - view.dispose(); assert.equal(button.listenerCount('touch-end'), 0); assert.equal(button.active, false); -}); - -test('diagnostic stop button is placed below Logo across different parent coordinates without changing its right-side x', () => { - const { positionDiagnosticStopButtonBelow } = load(); - assert.equal(typeof positionDiagnosticStopButtonBelow, 'function'); - const logo = new Node('Spirit3103'), logoParent = new Node('logo-parent'); - const stop = new Node('StopButton'), canvas = new Node('Canvas'); - logo.parent = logoParent; stop.parent = canvas; stop.position = new Vec3(665, 310, 7); - const logoTransform = new UITransform(); - Object.assign(logoTransform, { width: 208, height: 59, anchorX: 0.5, anchorY: 0.5 }); - logoTransform.convertToWorldSpaceAR = value => new Vec3(value.x + 671.37, value.y + 311.5, value.z); - const stopTransform = new UITransform(); - Object.assign(stopTransform, { width: 220, height: 64, anchorX: 0.5, anchorY: 0.5 }); - const canvasTransform = new UITransform(); - canvasTransform.convertToNodeSpaceAR = value => new Vec3(value.x - 100, value.y - 40, value.z); - logo.components.set(UITransform, logoTransform); stop.components.set(UITransform, stopTransform); - canvas.components.set(UITransform, canvasTransform); - const widget = new Widget(); stop.components.set(Widget, widget); - - positionDiagnosticStopButtonBelow(stop, logo); - - assert.equal(widget.enabled, false, 'runtime position must not be overwritten by the native Widget'); - assert.equal(stop.position.x, 665); assert.equal(stop.position.z, 7); - assert.equal(stop.position.y, 198, 'button top is 12 px below the logo bottom in Canvas coordinates'); -}); -function fixture() { - const refs = { loadingRoot: new Node(), loginRoot: new Node(), resultRoot: new Node(), - statusOverlay: new Node(), statusLabel: new Label(), resultLabel: new Label() }; - const instance = new Node(); const group = new Node('group-2'); instance.children.push(group); - for (const name of ['游客登录', '版本显示', '微信登录', '手机登录', 'qq登录', '快速登录', - '隐藏快速登录-1', '隐藏快速登录-2', '登录中遮罩', '登录头像背景框', '登录中头像', - '登录中昵称', '登录中动画', '登录中头像框']) { - const node = new Node(name); group.children.push(node); - node.components.set(Sprite, new Sprite()); node.components.set(UITransform, new UITransform()); - node.components.set(Label, new Label()); - } - return { refs, instance, group, visitor: instance.getChildByPath('group-2/游客登录')! }; -} -test('required binding fails explicitly, including missing Sprite', () => { - const { CocosLocalLoginView } = load(); const f = fixture(); - const view = new CocosLocalLoginView(f.refs); - assert.throws(() => view.bindLogin(new Node(), () => {}), /游客登录/); - f.visitor.components.delete(Sprite); - assert.throws(() => view.bindLogin(f.instance, () => {}), /Sprite/); -}); -test('PageModel drives result labels and blocks room presentation', () => { - const { CocosLocalLoginView } = load(); const f = fixture(); const view = new CocosLocalLoginView(f.refs); - view.render({ page: 'lobby', self: { playerid: 19, nickname: '测试玩家', bean: 72, avatar: 'unused' } }); - view.activate('lobby'); - assert.equal(f.refs.resultLabel.string, '登录成功\n玩家 ID:19\n昵称:测试玩家\n豆豆:72'); - assert.equal(f.refs.resultRoot.active, true); assert.equal(f.refs.loginRoot.active, false); - assert.throws(() => view.activate('room'), /room/); -}); -test('busy mask and readiness gate touch, terminal disposal removes ownership', () => { - const { CocosLocalLoginView } = load(); const f = fixture(); const view = new CocosLocalLoginView(f.refs); - let clicks = 0; view.bindLogin(f.instance, () => clicks++); view.setVersion(123); - assert.equal((f.instance.getChildByPath('group-2/版本显示')!.getComponent(Label) as Label).string, '123'); - assert.equal(f.instance.getChildByPath('group-2/微信登录')!.active, false); - view.render({ page: 'login', canLogin: false }); view.activate('login'); f.visitor.emit('touch-end'); - assert.equal(clicks, 0); - view.render({ page: 'login', canLogin: true }); view.setBusy(true); f.visitor.emit('touch-end'); - assert.equal(clicks, 0); assert.equal(f.instance.getChildByPath('group-2/登录中遮罩')!.active, true); - view.setBusy(false); f.visitor.emit('touch-end'); assert.equal(clicks, 1); - view.showFailure({ state: 7, msg: '服务器拒绝' }); view.showOverlay({ kind: 'none' }); - assert.match(f.refs.statusLabel.string, /服务器拒绝/); - f.visitor.emit('touch-end'); assert.equal(clicks, 1); - view.dispose(); assert.equal(f.visitor.listenerCount('touch-end'), 0); -}); -test('frame request is one shot and real cancellation removes pending draw callback', () => { - const { createCocosFrameScheduler } = load(); const scheduler = createCocosFrameScheduler(); let calls = 0; - const cancel = scheduler.request(() => calls++); cancel(); director.emit('draw'); assert.equal(calls, 0); - scheduler.request(() => calls++); director.emit('draw'); director.emit('draw'); assert.equal(calls, 1); - assert.equal(director.listenerCount('draw'), 0); -}); - -test('asset load task reports validated native loader progress from zero through completion', async () => { - const { createAssetLoadTask } = load(); - const progress: number[] = []; - let onProgress!: (completed: number, total: number) => void; - let onComplete!: (error: unknown, asset?: unknown) => void; - const task = createAssetLoadTask((progressCallback: typeof onProgress, completeCallback: typeof onComplete) => { - onProgress = progressCallback; onComplete = completeCallback; - }, (value: number) => progress.push(value)); - - assert.deepEqual(progress, [0]); - onProgress(1, 2); onProgress(2, 2); onComplete(null, { uuid: 'login' }); - assert.deepEqual(progress, [0, 0.5, 1]); - assert.deepEqual(await task.promise, { uuid: 'login' }); -}); - -test('asset load task rejects invalid counts and loader failures without reporting guessed progress', async () => { - const { createAssetLoadTask } = load(); - for (const invalid of [[0, 0], [-1, 2], [3, 2], [0.5, 2], [1, Number.NaN]]) { - const progress: number[] = []; - let onProgress!: (completed: number, total: number) => void; - const task = createAssetLoadTask((progressCallback: typeof onProgress) => { onProgress = progressCallback; }, - (value: number) => progress.push(value)); - onProgress(invalid[0], invalid[1]); - await assert.rejects(task.promise, /asset load progress/i); - assert.deepEqual(progress, [0]); - } - - let complete!: (error: unknown, asset?: unknown) => void; - const failed = createAssetLoadTask((_progress: unknown, callback: typeof complete) => { complete = callback; }, () => {}); - complete(new Error('decoder failed')); - await assert.rejects(failed.promise, /decoder failed/); -}); - -test('asset load task cancellation settles pending work and ignores late loader callbacks', async () => { - const { createAssetLoadTask } = load(); - const progress: number[] = []; - let onProgress!: (completed: number, total: number) => void; - let onComplete!: (error: unknown, asset?: unknown) => void; - const task = createAssetLoadTask((progressCallback: typeof onProgress, completeCallback: typeof onComplete) => { - onProgress = progressCallback; onComplete = completeCallback; - }, (value: number) => progress.push(value)); - task.cancel(); - await assert.rejects(task.promise, /cancelled/i); - onProgress(1, 2); onComplete(null, { uuid: 'late' }); - assert.deepEqual(progress, [0]); -}); - -test('failure display preserves nested server and cleanup errors', () => { - const { CocosLocalLoginView } = load(); const f = fixture(); - const view = new CocosLocalLoginView(f.refs); - view.showFailure({ error: { state: 8, msg: '版本拒绝' }, cleanup: new Error('socket close failed') }); - assert.match(f.refs.statusLabel.string, /版本拒绝/); - assert.match(f.refs.statusLabel.string, /socket close failed/); -}); - -test('component rejects non-debug/native before creating host or writing browser storage', async () => { - const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url); - assert.ok(existsSync(path), 'LocalPlatformLogin component must exist'); - for (const [debug, native] of [[false, false], [true, true]]) { - let hosts = 0; const exports: any = {}; - class Component {} - const decorator = (..._args: unknown[]) => (target: unknown) => target; - runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, - }).outputText, { exports, require: (name: string) => { - if (name === 'cc/env') return { DEBUG: debug }; - if (name === 'cc') return { Component, Node, Label, Prefab: class {}, sys: { isNative: native }, - _decorator: { ccclass: decorator, property: () => () => {} } }; - if (name.endsWith('local-login-host.ts')) return { createLocalLoginHost: () => { hosts++; } }; - if (name.endsWith('local-login-view.ts')) return load(); - if (name.endsWith('remote-config.ts')) return { resolveLaunchWaitTime }; - if (name.endsWith('remote-config-fetcher.ts')) return { HttpConfigFetcher: class {} }; - throw new Error(`Unexpected dependency ${name}`); - }, Error }); - const component = new exports.LocalPlatformLogin(); - await assert.rejects(async () => component.start(), /DEBUG.*browser/); - assert.equal(hosts, 0); - } -}); - -test('component stop cancels pending draw and timer work', async () => { - const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url); - const exports: any = {}; const timers = new Map void>(); let timerId = 0; - runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, - }).outputText, { exports, require: (name: string) => { - if (name === 'cc/env') return { DEBUG: true }; - if (name === 'cc') return { Component: class {}, Node, Label, Prefab: class {}, ProgressBar, sys: { isNative: false }, - _decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } }; - if (name.endsWith('local-login-host.ts')) return {}; - if (name.endsWith('local-login-view.ts')) return load(); - if (name.endsWith('remote-config.ts')) return { resolveLaunchWaitTime }; - if (name.endsWith('remote-config-fetcher.ts')) return {}; - throw new Error(`Unexpected dependency ${name}`); - }, Error, performance: { now: () => 123 }, window: { - setTimeout: (callback: () => void) => { timers.set(++timerId, callback); return timerId; }, - clearTimeout: (id: number) => timers.delete(id), - } }); - const component = new exports.LocalPlatformLogin(); - const draw = component.nextDraw(); const timer = component.delay(900); - const drawRejection = assert.rejects(draw, /cancelled/); const timerRejection = assert.rejects(timer, /cancelled/); - component.stop(); await Promise.all([drawRejection, timerRejection]); - assert.equal(timers.size, 0); assert.equal(director.listenerCount('draw'), 0); - assert.equal(component.getDiagnostics().pendingCount, 0); -}); - -test('minimum display ignores an early timer wake and cancellation clears its remaining wait', async () => { - const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url); - const exports: any = {}; const timers = new Map void>(); let timerId = 0; let clock = 100; - let hostOptions: any; - runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, - }).outputText, { exports, require: (name: string) => { - if (name === 'cc/env') return { DEBUG: true }; - if (name === 'cc') return { Component: class {}, Node, Label, Sprite, UITransform, ProgressBar, Prefab: class {}, sys: { isNative: false }, - _decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } }; - if (name.endsWith('local-login-host.ts')) return { createLocalLoginHost: (options: unknown) => { - hostOptions = options; return { start: async () => {}, stop() {}, getSnapshot: () => ({}) }; - } }; - if (name.endsWith('local-login-view.ts')) return load(); - if (name.endsWith('remote-config.ts')) return { resolveLaunchWaitTime }; - if (name.endsWith('remote-config-fetcher.ts')) return { HttpConfigFetcher: class {} }; - throw new Error(`Unexpected dependency ${name}`); - }, Error, performance: { now: () => clock }, window: { - location: { search: '?profile=local' }, localStorage: {}, - setTimeout: (callback: () => void) => { timers.set(++timerId, callback); return timerId; }, - clearTimeout: (id: number) => timers.delete(id), - } }); - async function started(emitDraw = true) { - const component = new exports.LocalPlatformLogin(); const f = fixture(); - const stopButton = new Node('StopButton'), canvas = new Node('Canvas'); - stopButton.parent = canvas; stopButton.components.set(UITransform, new UITransform()); - canvas.components.set(UITransform, new UITransform()); - Object.assign(component, f.refs, { stopButton, loadingPrefab: {}, loginPrefab: {} }); - const logo = new Node('Layer1_Logo'); const group = new Node('group-1'); - const bar = new Node('Spirit254'); bar.components.set(ProgressBar, new ProgressBar()); - const button = new Node('Spirit3103'), text = new Node('Spirit3104'); - for (const node of [button, text, group]) node.components.set(UITransform, new UITransform()); - const label = new Label() as Label & { node: Node }; label.node = text; text.components.set(Label, label); text.parent = group; - group.children.push(bar, button, text); - logo.children.push(group, new Node('group-32')); component.loadingRoot.children.push(logo); - component.loadingRoot.getComponentsInChildren = () => []; - await component.start(); if (emitDraw) director.emit('draw'); await Promise.resolve(); - return component; - } - const drain = async () => { for (let i = 0; i < 5; i++) await Promise.resolve(); }; - function wake(at: number) { - clock = at; const [id, callback] = timers.entries().next().value!; - timers.delete(id); callback(); - } - const component = await started(); let completed = false; - hostOptions.onConfigResolved({ identity: { version: 731 }, remoteConfig: { getValue: (name: string) => name === 'launchWaitTime' ? 500 : null } }); - const minimum = hostOptions.waitForMinimumDisplay().then(() => { completed = true; }); - await drain(); wake(599.8); await drain(); - assert.equal(completed, false, '499.8 ms elapsed must not open minimum display gate'); - assert.equal(component.getDiagnostics().events.some((event: any) => event.kind === 'minimum-display-ready'), false); - assert.equal(timers.size, 1); - wake(600); await minimum; assert.equal(completed, true); assert.equal(timers.size, 0); - component.stop(); - - clock = 1000; const cancelled = await started(); - hostOptions.onConfigResolved({ identity: { version: 731 }, remoteConfig: { getValue: (name: string) => name === 'launchWaitTime' ? 500 : null } }); - const cancelledMinimum = hostOptions.waitForMinimumDisplay(); - const rejection = assert.rejects(cancelledMinimum, /cancelled/); - await drain(); wake(1499.8); await drain(); - assert.equal(timers.size, 1); cancelled.stop(); await rejection; - assert.equal(timers.size, 0); assert.equal(cancelled.getDiagnostics().pendingCount, 0); - - const undrawn = await started(false); let zeroComplete = false; - hostOptions.onConfigResolved({ identity: { version: 731 }, remoteConfig: { getValue: (name: string) => name === 'hall_config' ? { launchWaitTime: 0 } : null } }); - const zeroMinimum = hostOptions.waitForMinimumDisplay().then(() => { zeroComplete = true; }); - await drain(); assert.equal(zeroComplete, false, 'zero display deadline cannot bypass first native frame'); - director.emit('draw'); await zeroMinimum; assert.equal(zeroComplete, true); undrawn.stop(); -}); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts deleted file mode 100644 index 339488a..0000000 --- a/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import { test } from 'node:test'; -import { resolveLocalStartup } from '../../YouleNexus/assets/framework/config/local-startup.ts'; -import { RECONNECT_INTERVAL_MS } from '../../YouleNexus/assets/framework/core/constants.ts'; -import type { WireEvent } from '../../YouleNexus/assets/framework/net/wire-client.ts'; -import type { OutboundEnvelope } from '../../YouleNexus/assets/framework/core/types/envelope.ts'; -import { FakeTransport } from '../helpers/fake-transport.ts'; - -const login: OutboundEnvelope = { app: 'youle', route: 'agent', rpc: 'player_login', data: { gameid: 101 } }; -function fixture(name: string) { - return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')); -} -async function setup(recordHook?: (event: { kind: string; data: unknown }) => void) { - const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts') - .catch(error => assert.fail(`Local login Wire implementation must exist: ${String(error)}`)); - const context = await resolveLocalStartup({ hostKind: 'h5', win: {}, search: '?profile=local', - fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } }); - const transports: FakeTransport[] = []; - const events: WireEvent[] = []; - const evidence: { kind: string; data: unknown }[] = []; - const faults: unknown[] = []; - const order: string[] = []; - const wire = api.createLocalLoginWire({ context, - transportFactory: () => { const transport = new FakeTransport(); transports.push(transport); return transport; }, - record: event => { evidence.push(event); recordHook?.(event); }, - onFault: error => { order.push('fault'); faults.push(error); assert.equal(transports[0]?.closeCalls, 1); }, - }); - wire.subscribe(event => { events.push(event); order.push(event.type === 'message' ? event.message.rpc : event.type); }); - const push = async (message: unknown) => { transports[0]!.serverPush(JSON.stringify(message)); await transports[0]!.flush(); }; - return { wire, context, transports, events, evidence, faults, order, push }; -} - -test('start is not open; only original lobby and ordinary agent/platform events are forwarded', async t => { - const h = await setup(); t.after(() => h.wire.stop()); - h.wire.start(); - assert.equal(h.events.length, 0); - await h.transports[0]!.flush(); - let unsubscribed = 0; - const unsubscribe = h.wire.subscribe(() => unsubscribed++); unsubscribe(); unsubscribe(); - await h.push(fixture('player-login-success.json')); - await h.push({ route: 'agent', rpc: 'show_message', data: { msg: 'hello' } }); - await h.push({ route: 'platform', rpc: 'notice', data: {} }); - assert.equal(unsubscribed, 0); - assert.equal(h.events.filter(event => event.type === 'message').length, 3); - for (const event of h.events) assert.ok(h.evidence.some(item => item.kind === 'wire-event' && item.data === event)); - assert.deepEqual(h.faults, []); -}); - -for (const [name, message, error] of [ - ['room recovery', fixture('player-login-room.json'), /login room recovery/], - ['malformed login', { route: 'agent', rpc: 'player_login', data: { state: 0 } }, /\$\.data\.playerid/], - ['room route', { route: 'room', rpc: 'notice', data: {} }, /room/], - ['game route', { route: 'mahjong', rpc: 'notice', data: {} }, /mahjong/], - ['agent switch', { route: 'agent', rpc: 'connect_agentserver', data: {} }, /connect_agentserver/], - ['platform switch', { route: 'platform', rpc: 'connect_roomserver', data: {} }, /connect_roomserver/], -] as const) test(`${name} stops before downstream delivery and remains terminal`, async t => { - t.mock.timers.enable({ apis: ['setTimeout'] }); - const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); - await h.push(message); - assert.equal(h.faults.length, 1); assert.match(String(h.faults[0]), error); - assert.equal(h.events.length, 1); - await h.push(fixture('player-login-success.json')); - assert.throws(() => h.wire.reconnectCurrent(), /stopped|terminal/); - assert.throws(() => h.wire.start(), /stopped|terminal/); - h.wire.stop(); h.wire.stop(); t.mock.timers.tick(60000); - assert.equal(h.transports.length, 1); assert.equal(h.events.length, 1); assert.equal(h.faults.length, 1); -}); - -for (const message of [ - { route: 'agent', rpc: 'player_login', data: { state: 1 } }, - { route: 'agent', rpc: 'kick_server', data: { msg: 'bye' } }, -]) test(`${message.rpc} failure reaches existing handlers before host notification`, async t => { - const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); - h.wire.subscribe(event => { if (event.type === 'message') h.wire.stop(); }); - await h.push(message); - assert.deepEqual(h.order, ['open', message.rpc, 'fault']); - assert.equal(h.faults.length, 1); -}); - -test('outbound scope and explicit switch fail without sending or reconnecting', async t => { - for (const operation of ['send', 'switch'] as const) { - const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); - assert.throws(() => operation === 'send' - ? h.wire.send({ ...login, rpc: 'create_room' }) : h.wire.switchServer(h.context.config.servers[0]!), /scope|switchServer/); - assert.deepEqual(h.transports[0]!.sent, []); assert.equal(h.faults.length, 1); - } -}); - -test('normal network close reconnects with the same single login envelope', async t => { - t.mock.timers.enable({ apis: ['setTimeout'] }); - const h = await setup(); t.after(() => h.wire.stop()); - h.wire.subscribe(event => { if (event.type === 'open') h.wire.send(login); }); - h.wire.start(); await h.transports[0]!.flush(); - h.transports[0]!.serverError(); await h.transports[0]!.flush(); t.mock.timers.tick(RECONNECT_INTERVAL_MS); - assert.equal(h.transports.length, 2); await h.transports[1]!.flush(); - assert.equal(h.transports[0]!.sent[0], JSON.stringify(login)); - assert.deepEqual(h.transports[1]!.sent, h.transports[0]!.sent); assert.deepEqual(h.faults, []); -}); - -test('scope fault retains its primary error and independent cleanup error', async t => { - const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); - const cleanup = new Error('cleanup failed'); - h.transports[0]!.close = () => { h.transports[0]!.closeCalls++; throw cleanup; }; - await h.push({ route: 'room', rpc: 'notice', data: {} }); - assert.equal(h.faults.length, 1); - const errors = (h.faults[0] as { errors: unknown[] }).errors; - assert.match(String(errors[0]), /room/); assert.equal(errors[1], cleanup); - h.wire.stop(); assert.equal(h.transports[0]!.closeCalls, 1); -}); - -test('evidence failure preserves the exact thrown value plus cleanup error and prevents delivery', async t => { - const primary = { reason: 'evidence unavailable' }; - const h = await setup(event => { - if (event.kind === 'wire-event' && (event.data as WireEvent).type === 'message') throw primary; - }); - t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); - const cleanup = new Error('close failed'); - h.transports[0]!.close = () => { h.transports[0]!.closeCalls++; throw cleanup; }; - await h.push(fixture('player-login-success.json')); - assert.deepEqual((h.faults[0] as { errors: unknown[] }).errors, [primary, cleanup]); - assert.equal(h.events.length, 1); assert.equal(h.faults.length, 1); -}); - -test('listener error is reported unchanged, stops remaining delivery, and stops only once', async t => { - const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); - const primary = new Error('downstream failed'); - h.wire.subscribe(() => { throw primary; }); - let laterDeliveries = 0; h.wire.subscribe(() => laterDeliveries++); - await h.push(fixture('player-login-success.json')); - assert.equal(h.faults[0], primary); assert.equal(laterDeliveries, 0); - h.wire.stop(); assert.equal(h.transports[0]!.closeCalls, 1); -}); - -test('terminal callback cannot send or reconnect before runtime stop; host notification is single', async t => { - const h = await setup(); t.after(() => h.wire.stop()); h.wire.start(); await h.transports[0]!.flush(); - h.wire.subscribe(event => { - if (event.type !== 'message') return; - assert.throws(() => h.wire.send(login), /terminal/); - assert.throws(() => h.wire.reconnectCurrent(), /terminal/); - }); - await h.push({ route: 'agent', rpc: 'kick_server', data: { msg: 'bye' } }); - assert.equal(h.faults.length, 1); assert.deepEqual(h.transports[0]!.sent, []); -}); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/game-entry.test.ts b/cocoscreator_projects/framework-tests/platform-login/game-entry.test.ts similarity index 93% rename from cocoscreator_projects/framework-tests/local-platform-login/game-entry.test.ts rename to cocoscreator_projects/framework-tests/platform-login/game-entry.test.ts index 68aa08b..e2fca00 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/game-entry.test.ts +++ b/cocoscreator_projects/framework-tests/platform-login/game-entry.test.ts @@ -4,10 +4,10 @@ import { GameSessionHost } from '../../YouleNexus/assets/framework/sdk/runtime/g import type { GameHost, PlatformToGameEvent, GameServerMessage } from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; async function setup() { - const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-game-entry.ts') + const api = await import('../../YouleNexus/assets/scripts/platform-login/login-stage-game-entry.ts') .catch(error => assert.fail(`Diagnostic GameEntry implementation must exist: ${String(error)}`)); const evidence: { kind: string; data: unknown }[] = []; - return { entry: api.createDiagnosticGameEntry(101, event => evidence.push(event)), evidence }; + return { entry: api.createLoginStageGameEntry(101, event => evidence.push(event)), evidence }; } test('real GameSessionHost validates and releases one diagnostic instance without entering a game', async () => { diff --git a/cocoscreator_projects/framework-tests/platform-login/loading-animation.test.ts b/cocoscreator_projects/framework-tests/platform-login/loading-animation.test.ts new file mode 100644 index 0000000..2861aec --- /dev/null +++ b/cocoscreator_projects/framework-tests/platform-login/loading-animation.test.ts @@ -0,0 +1,32 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { runInNewContext } from 'node:vm'; +import ts from 'typescript'; + +test('login busy state starts Animation once, hides on completion and restarts on retry', () => { + const exports: any = {}; + const source = readFileSync(new URL('../../YouleNexus/assets/scripts/platform-login/login-panel.ts', import.meta.url), 'utf8'); + runInNewContext(ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText, { exports, require(name: string) { + if (name === './node-bindings.ts') return { requireNode(root: any, path: string) { + const result = root.paths[path]; if (!result) throw Error(`Missing ${path}`); return result; + } }; + return {}; + } }); + const children = Object.fromEntries(['登录中遮罩','登录头像背景框','登录中头像','登录中昵称','登录中动画','登录中头像框'] + .map(name => [name, {active:false}])); + const group = {active:false, paths:children}; + let playing = false, starts = 0, stops = 0; + const panel = Object.create(exports.LoginPanel.prototype); + panel.root = {paths:{'group-2/Logining':group}}; + panel.loadingAnimation = {defaultClip:{name:'LoginLoading'}, getState:()=>({isPlaying:playing}), + play(name: string){assert.equal(name,'LoginLoading');playing=true;starts++;}, stop(){playing=false;stops++;}}; + panel.setBusy(true); panel.setBusy(true); + assert.equal(starts,1); assert.equal(group.active,true); + assert.ok(Object.values(children).every(node=>node.active)); + panel.setBusy(false); + assert.equal(stops,1); assert.equal(group.active,false); assert.equal(playing,false); + panel.setBusy(true); assert.equal(starts,2); +}); diff --git a/cocoscreator_projects/framework-tests/platform-login/login-accounts.test.ts b/cocoscreator_projects/framework-tests/platform-login/login-accounts.test.ts new file mode 100644 index 0000000..7133d51 --- /dev/null +++ b/cocoscreator_projects/framework-tests/platform-login/login-accounts.test.ts @@ -0,0 +1,76 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { LoginAccounts, nicknameHash } from '../../YouleNexus/assets/framework/adapters/native/login-accounts.ts'; +import { resolveLoginPolicy } from '../../YouleNexus/assets/framework/presentation/login-policy.ts'; +import { buildLoginRequest } from '../../YouleNexus/assets/framework/protocol/contracts/login-contract.ts'; +test('native authorization preserves provider numbers, agreement guard, callback and cache', () => { + const values=new Map();const calls:number[]=[];const submitted:any[]=[];const tips:string[]=[]; + const service=new LoginAccounts({storage:{getItem:k=>values.get(k)??null,setItem:(k,v)=>{values.set(k,v);}}, + settings:{accreditlogin:p=>calls.push(p),getcompareCode:()=>0},random:()=>0.2,now:()=>100, + gameid:'g',agentid:'a',policy:resolveLoginPolicy({raw:{},getValue:()=>null}),submit:(a,d)=>submitted.push([a,d]),tip:t=>tips.push(t)}); + service.setAgreement(false);service.authorize('wechat');assert.deepEqual(calls,[]);assert.deepEqual(tips,['未同意协议']); + service.setAgreement(true);service.authorize('wechat');service.authorize('qq');assert.deepEqual(calls,[2,1]); + service.acceptNative({openid:'o',headimgurl:'a',nickname:'n',sex:1,city:'c',Province:'p',unionid:'u'},'bridge'); + assert.equal(submitted[0][0].avatar,'a');assert.equal(submitted[0][0].province,'p'); + assert.ok(values.has('tsgame_wxinfo'));assert.equal(service.cached()?.nickname,'n'); +}); +test('all login paths require the current agreement and rejected paths have no side effects',()=>{ + const values=new Map(),tips:string[]=[],submitted:unknown[]=[],native:number[]=[]; + const service=new LoginAccounts({storage:{getItem:k=>values.get(k)??null,setItem:(k,v)=>{values.set(k,v);}}, + settings:{getcompareCode:()=>0,accreditlogin:p=>native.push(p)},random:()=>0.2,now:()=>100, + gameid:'g',agentid:'a',policy:resolveLoginPolicy({raw:{},getValue:()=>null}),submit:a=>submitted.push(a),tip:t=>tips.push(t)}); + assert.equal(service.agreed,true);service.setAgreement(false); + service.authorize('wechat');service.authorize('qq');service.visitor();service.device();service.phone('00123','000456');service.autoLogin(); + service.acceptNative({},'global');service.acceptNative({},'bridge'); + for(let i=0;i<5;i++)service.hiddenFirst();service.hiddenSecond(); + assert.equal(tips.length,9);assert.ok(tips.every(t=>t==='未同意协议')); + assert.equal(values.size,0);assert.equal(submitted.length,0);assert.equal(native.length,0); + service.setAgreement(true);service.visitor();assert.equal(submitted.length,1); +}); +test('successful device login persists server identity for the next automatic login', () => { + const values=new Map();const submitted:any[]=[]; + const service=new LoginAccounts({storage:{getItem:k=>values.get(k)??null,setItem:(k,v)=>{values.set(k,v);}}, + random:()=>0.2,now:()=>100,gameid:'g',agentid:'a',policy:{...resolveLoginPolicy({raw:{},getValue:()=>null}),deviceLogin:true}, + submit:(a,d)=>submitted.push([a,d]),tip:()=>{}}); + service.loginSucceeded({state:0,playerid:42,openid:'server-o',avatar:'server-a',nickname:'server-n',sex:2,city:'c',province:'p',unionid:'u',tel:'13800138000'}, {enabled:true,telphone:'13800138000'}); + service.autoLogin();assert.equal(submitted[0][0].openid,'server-o'); + assert.deepEqual(submitted[0][1],{enabled:true,telphone:'13800138000'}); + assert.equal(values.get('tsgame_playerid_ga'),'42');assert.equal(values.get('tsgame_dLogin'),'1'); +}); +test('visitor login and cached visitor stay phone-free when both login buttons are enabled', () => { + const values=new Map();const submitted:any[]=[]; + const options={storage:{getItem:(k:string)=>values.get(k)??null,setItem:(k:string,v:string)=>{values.set(k,v);}}, + random:()=>0.2,now:()=>100,gameid:'g',agentid:'a',policy:{...resolveLoginPolicy({raw:{},getValue:()=>null}),showVisitor:true,deviceLogin:true}, + submit:(a:unknown,d:unknown)=>submitted.push([a,d]),tip:()=>{}}; + const service=new LoginAccounts(options); + service.visitor();assert.deepEqual(submitted[0][1],{enabled:false}); + assert.doesNotThrow(()=>service.loginSucceeded({state:0,playerid:42},submitted[0][1])); + assert.equal(values.get('tsgame_playerid_ga'),'42'); + assert.equal(values.has('tsgame_devicePlayer'),false);assert.equal(values.has('tsgame_dLogin'),false); + const reloaded=new LoginAccounts(options);reloaded.autoLogin(); + assert.deepEqual(submitted[1],[submitted[0][0],{enabled:false}]); + assert.doesNotThrow(()=>reloaded.loginSucceeded({state:0,playerid:42},submitted[1][1])); +}); +test('global and WVJB authorization retain their distinct legacy cache destinations', () => { + const values=new Map(); + const service=new LoginAccounts({storage:{getItem:k=>values.get(k)??null,setItem:(k,v)=>{values.set(k,v);}}, + random:()=>0.2,now:()=>100,gameid:'g',agentid:'a',policy:{...resolveLoginPolicy({raw:{},getValue:()=>null}),showVisitor:true},submit:()=>{},tip:()=>{}}); + const data={openid:'o',headimgurl:'a',nickname:'n',sex:1,city:'c',Province:'p',unionid:'u'}; + service.acceptNative(data,'global');assert.ok(values.has('tsgame_visitorinfo'));assert.equal(values.has('tsgame_wxinfo'),false); + service.acceptNative(data,'bridge');assert.ok(values.has('tsgame_wxinfo')); +}); +test('legacy nickname MD5 matches standard vectors',()=>{ + assert.equal(nicknameHash(''),'d41d8cd98f00b204e9800998ecf8427e'); + assert.equal(nicknameHash('abc'),'900150983cd24fb0d6963f7d28e17f72'); +}); +test('phone login emits original sms fields and does not masquerade as cached auto login', () => { + const values=new Map();const submitted:any[]=[]; + const service=new LoginAccounts({storage:{getItem:k=>values.get(k)??null,setItem:(k,v)=>{values.set(k,v);}}, + random:()=>0.2,now:()=>100,gameid:'g',agentid:'a',policy:resolveLoginPolicy({raw:{},getValue:()=>null}), + submit:(a,d)=>submitted.push([a,d]),tip:()=>{}}); + service.phone('13800138000','123456'); + const [account,mode]=submitted[0];assert.equal(account.unionid,'phone_13800138000'); + const result=buildLoginRequest({identity:{agentid:'a',gameid:'g',channelid:'c',marketid:4,version:1}},account, + {location:null,machineid:'m',machineroom:'',deviceLogin:mode,loginPlayerId:{enabled:false}}); + assert.equal(result.data.telphone,'13800138000');assert.equal(result.data.smmcode,'123456');assert.equal(result.data.telphoneAuto,false); +}); diff --git a/cocoscreator_projects/framework-tests/platform-login/login-policy.test.ts b/cocoscreator_projects/framework-tests/platform-login/login-policy.test.ts new file mode 100644 index 0000000..e487598 --- /dev/null +++ b/cocoscreator_projects/framework-tests/platform-login/login-policy.test.ts @@ -0,0 +1,40 @@ +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { resolveLoginPolicy, loginButtons } from '../../YouleNexus/assets/framework/presentation/login-policy.ts'; +import { resolveRuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; + +const remote = (values: Record) => ({raw:values,getValue:(key:string)=>values[key] ?? null}); +test('source defaults and channel overrides retain legacy button order', () => { + const policy = resolveLoginPolicy(remote({})); + assert.deepEqual(loginButtons(policy, 0, false), {wechat:true,visitor:false,phone:false,quick:false,qq:false,account:false}); + assert.deepEqual(loginButtons(policy, 1, false), {wechat:true,visitor:true,phone:false,quick:false,qq:false,account:false}); + const configured = resolveLoginPolicy(remote({deviceLogin:1,hall_config:{showVisitor:1,qqLogin:1}})); + assert.deepEqual(loginButtons(configured, 0, false), {wechat:false,visitor:true,phone:true,quick:false,qq:true,account:false}); + assert.deepEqual(loginButtons(configured, 0, true), {wechat:false,visitor:true,phone:false,quick:false,qq:true,account:true}); +}); +test('invalid upstream login options fail at the config source', () => { + assert.throws(()=>resolveLoginPolicy(remote({deviceLogin:'yes'})), /deviceLogin/); + assert.throws(()=>resolveLoginPolicy(remote({hall_config:{qqLogin:[]}})), /qqLogin/); +}); + +test('build identity selects its own remote agent showVisitor through to login buttons', async () => { + // Relevant hierarchy captured from config_test/update_jsonv2.txt on 2026-09-06. + const config = {agentlist:[ + {agentid:'veRa0qrBf0df2K1G4de2tgfmVxB2jxpv',gamelist:[{gameid:'G2hw0ubng0zcoI0r4mx3H2yr4GejidwO'}]}, + {agentid:'1B2h0ccl205c390Y28m1Ajdplkuu4wgy',showVisitor:1,gamelist:[ + {gameid:'J9r4022t80pxlb01zOK76DN1WD4QewFl',channellist:[ + {channelid:'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG',marketlist:[{marketid:4,deviceLogin:true}]}]}]}, + ]}; + const options={startupConfig:{...STARTUP_CONFIG,configSource:'remote' as const},hostKind:'h5' as const,win:{},search:'',fetcher:{fetch:async()=>config}}; + const resolved=await resolveRuntimeConfig(options); + assert.equal(resolved.remoteConfig.getValue('showVisitor'),1); + assert.equal(resolved.identity.channelid,'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG'); + const buttons=loginButtons(resolveLoginPolicy(resolved.remoteConfig),0,false); + assert.equal(buttons.visitor,true);assert.equal(buttons.wechat,false); + assert.equal(buttons.phone,true,'market deviceLogin and agent showVisitor both take effect'); + const other=await resolveRuntimeConfig({...options,search:'?agentid=veRa0qrBf0df2K1G4de2tgfmVxB2jxpv'}); + assert.equal(resolveLoginPolicy(other.remoteConfig).showVisitor,false,'must not read another agent configuration'); +}); diff --git a/cocoscreator_projects/framework-tests/platform-login/logo-countdown.test.ts b/cocoscreator_projects/framework-tests/platform-login/logo-countdown.test.ts new file mode 100644 index 0000000..9b896be --- /dev/null +++ b/cocoscreator_projects/framework-tests/platform-login/logo-countdown.test.ts @@ -0,0 +1,144 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync, existsSync } from 'node:fs'; +import { runInNewContext } from 'node:vm'; +import ts from 'typescript'; +import { EventEmitter } from 'node:events'; +import { resolveLaunchWaitTime } from '../../YouleNexus/assets/framework/config/remote-config.ts'; + +class Sprite {} +class ScrollView { scrollToTop() {} stopAutoScroll() {} } +class Vec3 { constructor(public x = 0, public y = 0, public z = 0) {} } +class Widget { enabled = true; updateAlignment() {} } +class UITransform { + width = 100; height = 30; anchorX = 0.5; anchorY = 0.5; + convertToWorldSpaceAR(value: Vec3) { return new Vec3(value.x + 250, value.y + 70, value.z); } + convertToNodeSpaceAR(value: Vec3) { return new Vec3(value.x - 20, value.y - 10, value.z); } +} +class ProgressBar { progress = 0; } +class Label { + static Overflow = { NONE: 0 }; overflow = 1; + string = ''; updateRenderData() {} +} +class Node extends EventEmitter { + static EventType = { TOUCH_END: 'touch-end' }; + active = true; + children: Node[] = []; + parent: Node | null = null; + position = new Vec3(); + setPosition(position: Vec3) { this.position = position; } + components = new Map(); + constructor(public name = '') { super(); } + getComponent(type: unknown) { return this.components.get(type) ?? null; } + getComponentsInChildren(type: unknown): any[] { + const own = this.getComponent(type); + return [...(own ? [own] : []), ...this.children.flatMap(child => child.getComponentsInChildren(type))]; + } + getChildByPath(path: string): Node | null { + const [head, ...tail] = path.split('/'); + const child = this.children.find(node => node.name === head); + return child ? (tail.length ? child.getChildByPath(tail.join('/')) : child) : null; + } +} +const director = new EventEmitter(); +function countdownFixture() { + const { createLogoCountdown } = load(); + assert.equal(typeof createLogoCountdown, 'function'); + let now = 0; let id = 0; + const timers = new Map void }>(); + const states: { visible: boolean; text: string }[] = []; + const gate = createLogoCountdown({ now: () => now, + schedule: (callback: () => void, ms: number) => { + const key = ++id; timers.set(key, { at: now + ms, callback }); return () => timers.delete(key); + }, render: (state: { visible: boolean; text: string }) => states.push(state) }); + const advance = (ms: number) => { + const end = now + ms; + while (true) { + const next = [...timers].filter(([, timer]) => timer.at <= end).sort((a, b) => a[1].at - b[1].at)[0]; + if (!next) break; + now = next[1].at; timers.delete(next[0]); next[1].callback(); + } + now = end; + }; + return { gate, timers, states, advance, last: () => states[states.length - 1]! }; +} + +test('Logo countdown ticks legacy milliseconds and skip requires config, resource and socket readiness', async () => { + const f = countdownFixture(); let complete = false; void f.gate.promise.then(() => { complete = true; }); + assert.equal(f.last().visible, false); + f.gate.skip(); f.gate.start(2500); + assert.equal(f.last().text, '倒计时3s'); + f.advance(1000); assert.equal(f.last().text, '倒计时2s'); + f.gate.resourcesReady(); assert.equal(f.last().text, '倒计时2s 跳过'); + f.gate.skip(); await Promise.resolve(); assert.equal(complete, false); + f.gate.socketReady(true); await Promise.resolve(); assert.equal(complete, false, 'premature click must not queue'); + f.gate.skip(); await f.gate.promise; + assert.equal(f.last().visible, false); assert.equal(f.timers.size, 0); +}); + +test('Logo exact deadline is independent from resources and socket; terminal cancellation ignores late events', async () => { + const f = countdownFixture(); f.gate.start(10); f.advance(9); + assert.equal(f.last().visible, true); f.advance(1); await f.gate.promise; + assert.equal(f.last().visible, false); assert.equal(f.timers.size, 0); + const cancelled = countdownFixture(); cancelled.gate.start(5000); + const rejection = assert.rejects(cancelled.gate.promise, /cancelled/); + cancelled.gate.cancel(); await rejection; + const count = cancelled.states.length; + cancelled.gate.start(9999); cancelled.gate.resourcesReady(); cancelled.gate.socketReady(true); cancelled.gate.skip(); cancelled.advance(10000); + assert.equal(cancelled.states.length, count); assert.equal(cancelled.timers.size, 0); +}); + +test('Logo render failure at completion rejects the gate and clears all timers', async () => { + const { createLogoCountdown } = load(); let now = 0; let failRender = false; + const callbacks = new Set<() => void>(); + const gate = createLogoCountdown({ now: () => now, + schedule: (callback: () => void) => { callbacks.add(callback); return () => callbacks.delete(callback); }, + render: () => { if (failRender) throw new Error('label rendering failed'); } }); + gate.start(2500); failRender = true; now = 2500; + const rejection = assert.rejects(gate.promise, /label rendering failed/); + assert.doesNotThrow(() => { const callback = [...callbacks][0]; callbacks.delete(callback); callback(); }); + await rejection; assert.equal(callbacks.size, 0); +}); + +test('Logo scheduling and tick render failures settle pending display promises', async () => { + const { createLogoCountdown } = load(); + const failed = createLogoCountdown({ now: () => 0, schedule: () => { throw new Error('timer failed'); }, render() {} }); + const scheduling = assert.rejects(failed.promise, /timer failed/); + assert.doesNotThrow(() => failed.start(2500)); await scheduling; + let failRender = false; const callbacks = new Set<() => void>(); + const gate = createLogoCountdown({ now: () => 0, + schedule: (callback: () => void) => { callbacks.add(callback); return () => callbacks.delete(callback); }, + render: () => { if (failRender) throw new Error('tick rendering failed'); } }); + gate.start(2500); failRender = true; + const rendering = assert.rejects(gate.promise, /tick rendering failed/); + assert.doesNotThrow(() => { const callback = [...callbacks][1]; callbacks.delete(callback); callback(); }); + await rendering; assert.equal(callbacks.size, 0); +}); +const source = new URL('../../YouleNexus/assets/scripts/platform-login/logo-countdown.ts', import.meta.url); +function load() { + assert.ok(existsSync(source), 'Cocos local login view implementation must exist'); + const exports = {}; + runInNewContext(ts.transpileModule(readFileSync(source, 'utf8'), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText, { exports, require: (name: string) => { + assert.equal(name, 'cc'); + return { Node, Sprite, ScrollView, UITransform, Label, Widget, Vec3, director, Director: { EVENT_AFTER_DRAW: 'draw' } }; + }, Error }); + return exports as any; +} + +test('Logo native binding measures label around button center and releases its touch listener', () => { + const { CocosLogoCountdownView } = load(); assert.equal(typeof CocosLogoCountdownView, 'function'); + const button = new Node('Spirit3103'), text = new Node('Spirit3104'), parent = new Node('group'); + text.parent = parent; + for (const node of [button, text, parent]) node.components.set(UITransform, new UITransform()); + const label = new Label(); text.components.set(Label, label); + const widget = new Widget(); text.components.set(Widget, widget); + let clicks = 0; const view = new CocosLogoCountdownView(button, text, () => clicks++); + assert.equal(button.active, false); assert.equal(text.active, false); + view.render({ visible: true, text: '倒计时3s 跳过' }); + assert.equal(label.string, '倒计时3s 跳过'); assert.equal(label.overflow, Label.Overflow.NONE); + assert.equal(widget.enabled, false); assert.equal(text.position.x, 230); assert.equal(text.position.y, 60); + button.emit('touch-end'); assert.equal(clicks, 1); + view.dispose(); assert.equal(button.listenerCount('touch-end'), 0); assert.equal(button.active, false); +}); diff --git a/cocoscreator_projects/framework-tests/platform-login/phone-code.test.ts b/cocoscreator_projects/framework-tests/platform-login/phone-code.test.ts new file mode 100644 index 0000000..9a8c0fa --- /dev/null +++ b/cocoscreator_projects/framework-tests/platform-login/phone-code.test.ts @@ -0,0 +1,15 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readPhoneCode } from '../../YouleNexus/assets/framework/adapters/native/phone-code.ts'; + +test('original phone cache restores the number and 30-second remainder without auto-filling the code',()=>{ + const raw=JSON.stringify({phoneNumber:'0013800138000',code:'1234',codeTime:1000}); + assert.deepEqual(readPhoneCode(raw,2500),{phoneNumber:'0013800138000',restTime:28}); + assert.deepEqual(readPhoneCode(raw,31000),{phoneNumber:'0013800138000',restTime:0}); + assert.deepEqual(readPhoneCode(raw,32000),{phoneNumber:'0013800138000',restTime:0}); + assert.equal(readPhoneCode(null,32000),null); +}); +test('malformed phone cache exposes the source error',()=>{ + assert.throws(()=>readPhoneCode('{',100),SyntaxError); + assert.throws(()=>readPhoneCode(JSON.stringify({phoneNumber:'123',codeTime:'bad'}),100),/codeTime/); +}); diff --git a/cocoscreator_projects/framework-tests/platform-login/scene-flow.test.ts b/cocoscreator_projects/framework-tests/platform-login/scene-flow.test.ts new file mode 100644 index 0000000..654d48b --- /dev/null +++ b/cocoscreator_projects/framework-tests/platform-login/scene-flow.test.ts @@ -0,0 +1,12 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { SceneFlow } from '../../YouleNexus/assets/scripts/platform-login/scene-flow.ts'; +test('scene transitions are sequential and disposal prevents late scene binding',async()=>{ + const calls:string[]=[];let finish!:()=>void; + const flow=new SceneFlow(async name=>{calls.push(name);if(name==='Loading')await new Promise(r=>finish=r);}); + const loading=flow.enter('Loading',()=>calls.push('bindLoading')); + const login=flow.enter('Login',()=>calls.push('bindLogin')); + await Promise.resolve();assert.deepEqual(calls,['Loading']);finish();await loading;await login; + assert.deepEqual(calls,['Loading','bindLoading','Login','bindLogin']); + flow.dispose();await assert.rejects(flow.enter('Login',()=>{}),/disposed/); +}); diff --git a/cocoscreator_projects/framework-tests/platform/commands.test.ts b/cocoscreator_projects/framework-tests/platform/commands.test.ts index 1748b2f..84899da 100644 --- a/cocoscreator_projects/framework-tests/platform/commands.test.ts +++ b/cocoscreator_projects/framework-tests/platform/commands.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; diff --git a/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts b/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts index e3e83c8..36bb3a1 100644 --- a/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts +++ b/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; diff --git a/cocoscreator_projects/framework-tests/platform/runtime.test.ts b/cocoscreator_projects/framework-tests/platform/runtime.test.ts index a826318..fdef004 100644 --- a/cocoscreator_projects/framework-tests/platform/runtime.test.ts +++ b/cocoscreator_projects/framework-tests/platform/runtime.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import { remoteFixture } from '../fixtures/remote-config.ts'; import { readFileSync } from 'node:fs'; import assert from 'node:assert/strict'; diff --git a/cocoscreator_projects/framework-tests/platform/startup.test.ts b/cocoscreator_projects/framework-tests/platform/startup.test.ts index 8630652..cdde8b3 100644 --- a/cocoscreator_projects/framework-tests/platform/startup.test.ts +++ b/cocoscreator_projects/framework-tests/platform/startup.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import { remoteFixture } from '../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; @@ -131,4 +133,4 @@ test('StartupOrchestrator: bootstrap 可独立调用(不依赖 onLogin)', async ready.then(() => { resolved = true; }); await new Promise((r) => setImmediate(r)); assert.equal(resolved, false, '仅 bootstrap 不足以触发 ready'); -}); \ No newline at end of file +}); diff --git a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts index 11f20be..9655b32 100644 --- a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts +++ b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts @@ -1,4 +1,6 @@ -import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG as EDITABLE_STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; +// 固定测试来源,允许开发者独立切换项目的 local/remote 开关。 +const STARTUP_CONFIG = Object.freeze({...EDITABLE_STARTUP_CONFIG, configSource:'remote' as const}); import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; diff --git a/docs/development-local-config.md b/docs/development-local-config.md new file mode 100644 index 0000000..c96acb1 --- /dev/null +++ b/docs/development-local-config.md @@ -0,0 +1,33 @@ +# 开发测试配置来源 + +唯一开关在 `cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts` 的 `STARTUP_CONFIG`: + +```ts +mode: 'debug', +configSource: 'local', // local:项目 txt;remote:gameserver 地址 +localConfigResource: 'dev-config/update_jsonv2', // resources 相对路径,不带 .txt +``` + +本地文件为 `cocoscreator_projects/YouleNexus/assets/resources/dev-config/update_jsonv2.txt`。 +初始内容取自 2026-09-06 当前 `gameserver` 的完整配置快照,不会自动与远程同步。 +修改并保存 profiles.ts 或 txt 后,切回 Creator,等待资源导入和脚本编译完成,再启动预览。 +仅刷新浏览器不保证 Creator 已完成文件扫描;如日志仍显示旧 source,需在 Creator +资源管理器中刷新对应 profiles.ts 和 txt,再重新预览。 +文件扩展名是 `.txt`,内容必须是合法 JSON,不能添加注释或尾逗号。 + +默认仍为 `configSource: 'remote'`。本地模式跳过远程请求及 `gameconfig` 地址覆盖; +渠道身份仍按原规则解析。两种来源共用原工程 `get_paravalue` 的层级取值规则, +修改某个键时需留意 agent/game/channel/market 中的对应值。 +控制台 `[PlatformStartup] login-config` 的 `source` 和 `gameserver` 显示实际读取来源。 + +`configSource` 只决定配置内容从哪里读取;`useLocalServer` 独立决定 WebSocket +是否使用 `localServers`,切换配置来源不会隐式切换游戏服务器。 + +本地模式同时要求 `mode: 'debug'` 和 Cocos 开发预览/调试构建。 +发布模式或非调试构建会明确拒绝本地读取;文件缺失、JSON 错误也直接报错,绝不回退远程。 + +## 已验证的预览缓存问题(2026-09-06) + +曾复现磁盘 profiles.ts 已为 local,但新浏览器预览仍输出 remote。通过 Creator +asset-db 刷新 profiles.ts 和本地 txt 后,日志恢复为 local,且读取到 marketid=4 +下的 deviceLogin=true,手机登录按钮正常显示。无需修改配置内容或 get_paravalue。 diff --git a/docs/protocol/07-RPC全量清单.md b/docs/protocol/07-RPC全量清单.md new file mode 100644 index 0000000..5da4f8d --- /dev/null +++ b/docs/protocol/07-RPC全量清单.md @@ -0,0 +1,833 @@ +# 平台模板 RPC 全量清单 + +> 核对日期:2026-09-05;基线:codex/local-platform-login @ 556c47a。本文是后续联调的索引与覆盖快照,不替代 01–05 章的字段契约,也不表示清单内接口已经验收。 + +## 范围与计数 + +- 共 **86 个唯一名称**:旧 RpcList 的 **82 个**(重复赋值按名称去重),加表外 submit_error、show_message、refresh_task_state,以及 h5RpcList.joinRoom。 +- 这是 Game_Surface_3 平台模板的全量:包含大厅、房间、纯推送、历史/保留名、HTTP 特例和外部桥接。具体子游戏自定义 route/rpc 没有在模板中穷举定义,留到子游戏阶段;本轮不选择或接入真实子游戏。 +- 86 是索引名称数,不是 86 个可直接调用的 WebSocket 接口。submit_log 不另计;握手 @toconcon、心跳 @serverheartbeat 和 webserve-服务器未工作 是传输控制数据,不是业务 RPC。 +- platform 是合法路由名称,但本次模板发送扫描未发现独占 route=platform 的命名业务请求;不复制 agent/room 名称来凑第三份清单。纯推送的路由归档不是当前本地服务器抓包证明。 + +## 接入与验收状态口径 + +- **L**:本地真实 Cocos 启动、加载、游客登录已验收;仅 player_login。依据[验收记录](../superpowers/reports/2026-09-05-local-platform-login-acceptance.md)。 +- **R**:新 PlatformRuntime 的严格平台分发表已接入(共 11 个,含 L),仅表示源码处理链存在;除 L 外,当前批次没有真实 RPC 成功验收证据。 +- **N**:不在新 PlatformRuntime 平台分发表中;不代表旧 H5 或旧 PlatformSession 完全没有实现。协议常量存在、旧 room-handlers 存在,均不能算新 Runtime 已接入。 +- **X**:保留、HTTP 特例或本地桥接,不按普通 WS 平台命令判定。具体子游戏生命周期支持也不等于其某个对局 rpc 已实现。 +- 当前 LocalPlatformLogin 联调入口只允许发 agent/player_login;R 中的房间/切服条目仍受测试边界阻断。本文不放宽测试入口。 +- 条目中“旧发送/接收函数”只表示静态定义,不能推断调用点活跃或服务器确实响应。请求/响应摘要取自对应协议章节;未明确之处保留待核实,不补字段。 + +## 发现的差异与注意事项 + +1. refresh_task_state:当前旧源码引用未定义常量;02 章称硬编码字符串,已不符。新 routes.ts 也未收录。 +2. submit_error、show_message:旧路径确有硬编码发送/命名接收器,但不在旧 RpcList,也不在新 routes.ts;不能只扫常量表。 +3. submit_log:实际 rpc 仍为 submit_error;change_room 的响应为 change_seat;quick_enter_share_room 没有同名接收器。 +4. README 仍写 WS 双层包装,而 01 章已更正为单层 {app,route,rpc,data};MessageEvent.data 是浏览器事件壳。以 01 章与已验收传输实现为准。 +5. 02 章登录 version:string 与本地已验收 number 值有差异;openid/unionid 不得依据历史“游客可空”注释省略。 +6. 新 routes.ts 的注释分组不是权威 rpc→route 映射,例如 create_room/self_join_room/get_player_grade1/get_player_grade2 在 room 注释区域,但旧发送实际为 agent;本清单按协议与发送源码归档。 +7. can_award 的旧接收器调用未定义 C_Player.can_award;五个社交保留名没有旧 Net 接收器;不能按完整可用协议验收。 +8. update_roomcard/update_bean:05_Func.js 的原生 yPaytype 与 WVJB yPaytype 回调直接上行,02 章“仅接收、无主动请求”不完整;已在条目中补齐,不能划为只读接口。 +9. 本轮仅整理清单,未修改上述历史文档、业务代码、服务端或任何 Cocos 序列化资源。 + +## 配置来源与批次边界 + +- agentid/gameid/channelid/marketid/version 继续由现有配置来源经 runtime-config/local-startup 解析;各 RPC 只携带它所要求的字段,不能为统一外观给所有请求塞上整套身份。规范字段名为 channelid,不能写 chanelid。 +- openid/unionid 与游客资料由现有 visitor-account 来源读取缓存或生成;登录后的 playerid 来自服务器与 Store。查询目标 playerid 与当前登录 playerid 的语义要逐项区分。 +- roomcode/seat/shortcode/roomtype/deskinfo 等属于后续房间或子游戏范围;短信、原生数据、支付渠道等有各自前置条件,不能生成假值补齐。 +- 后续优先梳理非房间平台 RPC 的只读查询,再逐项处理写操作和推送;房间相关及子游戏最后进行。列入本清单不等于授权执行。 + +## 按功能快速浏览 + +- **大厅 · 登录 / 账号(6)**:`player_login`、`query_player2`、`binding_phone`、`send_phone_checkcode`、`send_phone_code_wechat`、`setSign`。 +- **大厅 · 房间创建 / 进入(大厅侧)(8)**:`create_room`、`self_join_room`、`quick_enter_share_room`、`advanced_roomlist`、`advanced_createroom`、`get_share_room`、`getInfoByShortCode`、`switchRoomList`。 +- **大厅 · 战绩 / 排行 / 财富(5)**:`get_player_grade1`、`get_player_grade2`、`get_treasurelist`、`getShortCodeRankList`、`getVipRankList`。 +- **大厅 · 任务系统(5)**:`get_player_task`、`player_finish_task`、`get_task_award`、`refresh_task_state`、`can_award`。 +- **大厅 · 支付 / 充值 / 资产(4)**:`get_paylist`、`pay_succ`、`topup_card`、`giveCoin`。 +- **大厅 · 仓库 / 星星 / 魅力(4)**:`set_bankpwd`、`change_star`、`update_charm`、`setAllCharm`。 +- **大厅 · 邀请码 / 绑定(2)**:`binding_invitecode`、`get_player_invitecode`。 +- **大厅 · VIP 管理 / 黑白名单(4)**:`optBanList`、`getPlayerWhiteList`、`optWhiteList`、`setVipForbidSelect`。 +- **大厅 · 其它 agent 协议(8)**:`submit_opinion`、`submit_location`、`submit_phoneinfo`、`submit_error`、`kick_server`、`broadcast`、`connect_agentserver`、`playerBehavior`。 +- **房间 · 房间生命周期(8)**:`self_break_room`、`other_break_room`、`self_exit_room`、`other_exit_room`、`player_prepare`、`change_room`、`share_room`、`change_seat`。 +- **房间 · 进房推送(他人视角)(3)**:`other_join_room`、`other_offline`、`other_online`。 +- **房间 · 解散投票(开局后)(8)**:`self_apply_free_room`、`other_apply_free_room`、`self_agree_free_room`、`other_agree_free_room`、`self_refuse_free_room`、`other_refuse_free_room`、`free_room`、`beanroom_surrender`。 +- **房间 · 开局(2)**:`self_makewar`、`other_makewar`。 +- **房间 · 房间内社交(6)**:`send_text`、`send_voice`、`send_gift`、`send_phiz`、`call_phone`、`hangup_phone`。 +- **保留名称 · 房间社交(5)**:`receive_chat`、`play_voice`、`other_send_gift`、`other_callphone`、`other_hangup`。 +- **大厅 · 资产更新与系统推送(4)**:`update_bean`、`update_roomcard`、`kick_offline`、`show_message`。 +- **房间 · 服务器切换(room 侧)(1)**:`connect_roomserver`。 +- **子游戏 · 框架保留名称(2)**:`over_game`、`agentserver_game`。 +- **外部桥接 · 非网络 RPC(1)**:`joinRoom`。 + +## 全量条目 + +### 大厅 · 登录 / 账号(6) + +#### 01. player_login + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**L / R**;新 Rpc 常量:有。 +- 用途:连接建立后 `onopen` 自动发送;断线重连、切服后重发(12_Logic.js:35、:61 构造,09_Net.js:122 `Send_login` 注入)。 +- 请求:构建身份 agentid/gameid/channelid/marketid/version;账号 openid/unionid/nickname/avatar/sex/province/city;设备与环境 machineid/machineroom/location/ip;按来源契约携带设备登录和缓存 playerid 等可选字段。完整结构见登录契约与 02/04 章。 +- 响应/推送:state(0 成功);账号/资产字段,以及可能存在的房间恢复快照。完整结构见 04 章与当前 login-contract;本批只验收不在房的成功登录。 +- 注意:真实本地启动→加载→游客登录已验收;version=1(number)已被本地服务器接受。02 章 version:string、游客 openid 可空的描述不能作为本地联调输入依据;本地服务要求 openid 与 unionid。房间恢复不在已验收范围。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:14);旧函数:[Send_login](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:122)、[player_login](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:240)。 + +#### 02. query_player2 + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:仓库转账前查询目标玩家信息。 +- 请求:`agentid`、`playerid`(目标玩家ID)。 +- 响应/推送:`avatar`(string)、`nickname`(string)、`playerid`(int);昵称/头像均空时提示"未找到对应玩家"。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:44);旧函数:[Send_query_player2](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:869)、[query_player2](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:872)。 + +#### 03. binding_phone + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:绑定手机号。 +- 请求:`agentid`、`playerid`、`phonenum`、`smmcode`(短信验证码)。 +- 响应/推送:`phonenum`(string,回写 `C_Player.tel`)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:51);旧函数:[Send_binding_phone](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:829)、[binding_phone](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:833)。 + +#### 04. send_phone_checkcode + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:发送手机短信验证码。 +- 请求:`agentid`、`phonenum`(构造点未集中定位,至少含手机号)。 +- 响应/推送:无业务字段(`Desk.send_phone_checkcode` 为空实现,07_Desk.js:1342)。 +- 注意:⚠️ 请求字段以后端实现为准。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:58);旧函数:[Send_send_phone_checkcode](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:837)、[send_phone_checkcode](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:841)。 + +#### 05. send_phone_code_wechat + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:微信渠道发送手机验证码(绑定手机流程的另一入口)。 +- 请求:`agentid`、`phonenum`(唯一调用点 11_GameUI.js:2620-2623 当前被注释,按注释代码为 agentid+phonenum)。 +- 响应/推送:无业务字段(`Desk.send_phone_code_wechat` 为空实现,07_Desk.js:1345)。 +- 注意:⚠️ 框架完整定义并接线,但**当前前端唯一调用点(11_GameUI.js:2623)被注释,实际不发送**;字段以后端实现为准。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:65);旧函数:[Send_send_phone_code_wechat](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:845)、[send_phone_code_wechat](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:849)。 + +#### 06. setSign + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:设置个性签名。 +- 请求:`agentid`、`playerid`、`sign`。 +- 响应/推送:`sign`(string,回显写入 `C_Player.sign`)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:72);旧函数:[Send_setSign](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:735)、[setSign](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:739)。 + +### 大厅 · 房间创建 / 进入(大厅侧)(8) + +#### 07. create_room + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:玩家创建房间。 +- 请求:`agentid`、`playerid`、`gameid`、`roomtype`(房间类型配置数组,见 doc04);`Send_create_room` 自动注入 `ip`=`C_Player.ip`、`location`=`C_Player.addr`(09_Net.js:106-107)。 +- 响应/推送:`state`(0成功)、`roomcode`、`seat`、`roomtype`、`makewar`、`asetcount`、`shortcode`、`infinite`;失败 `showerror`/`error`。详见 [04 → 房间响应公共字段](./04-数据结构.md#房间创建--进入响应公共字段)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:83);旧函数:[Send_create_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:104)、[create_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:111)。 + +#### 08. self_join_room + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**R**;新 Rpc 常量:有。 +- 用途:输入房号 / 快速加入 / H5 唤起 / 进 VIP 配置房。 +- 请求:`agentid`、`playerid`、`gameid`、`roomcode`(房号);`Send_self_join_room` 自动注入 `location`=`C_Player.addr`、`ip`=`C_Player.ip`(09_Net.js:254-255);进 VIP 配置房入口额外带 `vipMatch:1`(12_Logic.js:2171);比赛进房入口可带 `match_id`。 +- 响应/推送:`state`、`roomcode`、`seat`、`isowner`、`players[]`、`roomtype`、`makewar`、`asetcount`、`deskwar`、`deskinfo`(重连快照)。详见 [04 → 房间响应公共字段](./04-数据结构.md#房间创建--进入响应公共字段)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:90);旧函数:[Send_self_join_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:251)、[self_join_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:259)。 + +#### 09. quick_enter_share_room + +- 路由/通道:agent;方向:C→S;结果经其它 rpc 返回;状态:**N**;新 Rpc 常量:有。 +- 用途:快速进入分享/星星场房间。 +- 请求:`agentid`、`playerid`、`gameid`、`type`(房间类型)、`roomtype`(可选)。 +- 响应/推送:走 self_join_room 进房流程(无独立 `Net.quick_enter_share_room` 接收函数,结果通过 self_join_room/show_message 等回包)。 +- 注意:无 Net.quick_enter_share_room;关联 self_join_room/show_message,不能按同名请求响应配对。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:97);旧函数:[Send_quick_enter_share_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:651)。 + +#### 10. advanced_roomlist + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:拉取 VIP/高级房间列表。 +- 请求:`agentid`、`playerid`、`gameid`。 +- 响应/推送:房间列表对象,整包写入 `GameData.snrRoomList` 并渲染(07_Desk.js:1176)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:104);旧函数:[Send_advanced_roomlist](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:659)、[advanced_roomlist](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:664)。 + +#### 11. advanced_createroom + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:创建 VIP/高级房间。 +- 请求:`agentid`、`gameid`、`playerid`、`tea`(茶水费)、`infinite`(0/1无限局)、`roomtype`、`videoConfig`(可选)、`rebateLimit`(可选)、`rebateType`(可选)。 +- 响应/推送:`tea`、`rebateLimit` 及房间配置(整包写入 `GameData.snrRoomList`,并回拉 advanced_roomlist,07_Desk.js:1180)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:111);旧函数:[Send_advanced_createroom](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:668)、[advanced_createroom](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:673)。 + +#### 12. get_share_room + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:获取分享/星星场房间列表(仅非大厅环境发送)。 +- 请求:`agentid`、`playerid`、`gameid`。 +- 响应/推送:房间数组(`Desk.get_share_room` / `Game_Modify.getShareRoom` 处理)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:118);旧函数:[Send_get_share_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:640)、[get_share_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:647)。 + +#### 13. getInfoByShortCode + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:按短码批量查询房间信息(VIP 房列表)。 +- 请求:`agentid`、`gameid`、`shortcodeList`(短码列表)。 +- 响应/推送:`roomInfo`(短号房间信息) → `GameUI.setVipRoomListData`(07_Desk.js:1284)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:125);旧函数:[Send_getInfoByShortCode](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:753)、[getInfoByShortCode](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:757)。 + +#### 14. switchRoomList + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:开关房间在列表中的可见/可进入状态。 +- 请求:`agentid`、`playerid`、`gameid`、`isClose`(0开/1关)。 +- 响应/推送:`state`(0成功)、`isClose`;失败 `error`(07_Desk.js:1267)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:132);旧函数:[Send_switchRoomList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:743)、[switchRoomList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:747)。 + +### 大厅 · 战绩 / 排行 / 财富(5) + +#### 15. get_player_grade1 + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:拉取战绩(类型1)。 +- 请求:`agentid`、`playerid`、`gameid`、`type`(可选)、`direction`(可选,翻页)、`gradeidx`(可选,分页索引)。 +- 响应/推送:战绩数据(`gameCombat.get_player_grade1` 渲染)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:143);旧函数:[Send_get_player_grade1](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:359)、[get_player_grade1](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:364)。 + +#### 16. get_player_grade2 + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:拉取战绩(类型2)。 +- 请求:透传调用方 `_data`,构造点未集中定位;至少含 `agentid`/`playerid`/`gameid`。 +- 响应/推送:战绩数据(`gameCombat.get_player_grade2` 渲染)。 +- 注意:⚠️ 请求字段构造点未定位,待核对。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:150);旧函数:[Send_get_player_grade2](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:371)、[get_player_grade2](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:376)。 + +#### 17. get_treasurelist + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:财富榜。 +- 请求:`agentid`、`gameid`。 +- 响应/推送:`list`(排行数组) → `Desk.get_treasurelist`。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:157);旧函数:[Send_get_treasurelist](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:685)、[get_treasurelist](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:690)。 + +#### 18. getShortCodeRankList + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:短号场排行榜。 +- 请求:`agentid`、`playerid`、`shortcode`。 +- 响应/推送:成功为排行数据(整包写入 `GameData.vipRank.data`);失败 `error:true` + `message`(07_Desk.js:1311)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:164);旧函数:[Send_getShortCodeRankList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:769)、[getShortCodeRankList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:773)。 + +#### 19. getVipRankList + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:VIP 排行榜。 +- 请求:`agentid`、`limit`(条数)。 +- 响应/推送:`list`(VIP排行数组) → `GameData.rankList`(07_Desk.js:1328)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:171);旧函数:[Send_getVipRankList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:787)、[getVipRankList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:791)。 + +### 大厅 · 任务系统(5) + +#### 20. get_player_task + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:获取任务列表。 +- 请求:`agentid`、`playerid`、`gameid`。 +- 响应/推送:`tasks`(任务数组) → `Desk.get_player_task`。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:182);旧函数:[Send_get_player_task](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:408)、[get_player_task](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:413)。 + +#### 21. player_finish_task + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:上报任务完成(如分享成功触发,06_Player.js:414)。 +- 请求:`agentid`、`playerid`、`taskid`。 +- 响应/推送:`state`(int)——`state==1` 且当前 `taskstate==0` 时把 `C_Player.taskstate` 置 1(06_Player.js:476)。 +- 注意:原文档"响应含 `taskstate`"无源码依据,已删除——接收函数仅读 `_msg.data.state`。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:189);旧函数:[Send_player_finish_task](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:420)、[player_finish_task](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:424)。 + +#### 22. get_task_award + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:领取任务奖励。 +- 请求:`agentid`、`playerid`、`gameid`、`taskid`。 +- 响应/推送:`taskid`(对应任务 state 置 2=已领取)、`taskstate`(写入 `C_Player.taskstate`)(06_Player.js:482)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:196);旧函数:[Send_get_task_award](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:428)、[get_task_award](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:434)。 + +#### 23. refresh_task_state + +- 路由/通道:agent;方向:C→S 意图;当前旧代码缺常量,不能认定可发送;状态:**N**;新 Rpc 常量:无。 +- 用途:刷新任务可领取状态。 +- 请求:透传 `_data`(构造点未集中定位,至少含 `agentid`/`playerid`)。 +- 响应/推送:服务器推送 `can_award`(见下条)。 +- 注意:本次核实:09_Net.js:440 用 RpcList.refresh_task_state,但 02_Const.js 无此定义;全模板搜索未找到赋值。未被外部注入时该值为 undefined,JSON.stringify 会省略 rpc。02 章“硬编码字符串”的记载与当前源码不符,待后续专项核对;本轮不修代码。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:203);旧函数:[Send_can_award](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:439)。 + +#### 24. can_award + +- 路由/通道:agent;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。 +- 用途:服务器通知有任务可领取。 +- 请求:无(纯推送)。 +- 响应/推送:任务可领取标记(载荷字段待后端确认)。 +- 注意:🐛 `06_Player.js` **未定义** `Player.prototype.can_award`(全文无此方法)。推送一旦到达,`C_Player.can_award` 为 undefined,调用即抛 TypeError。当前为 bug,不可当正常协议使用。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:210);旧函数:[Send_can_award](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:439)、[can_award](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:443)。 + +### 大厅 · 支付 / 充值 / 资产(4) + +#### 25. get_paylist + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:拉取支付项列表。 +- 请求:`agentid`。 +- 响应/推送:`paylist`(支付项数组) → `GameData.payList`,并打开支付界面(09_Net.js:567)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:221);旧函数:[Send_get_paylist](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:561)、[get_paylist](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:566)。 + +#### 26. pay_succ + +- 路由/通道:HTTP(保留 agent 信封);方向:C→HTTP;未见同名 Net 接收函数;状态:**X**;新 Rpc 常量:有。 +- 用途:支付成功后通知服务器入账。 +- 请求:`agentid`、`playerid`、`channelid`、`productid`、`payid`、`amount`、`money`、`paytype`(05_Func.js:2240-2254 构造)。 +- 响应/推送:无显式 WS 回包;资产变化通过 `update_bean`/`update_roomcard` 推送(充房卡场景前端还会本地 `UpdateRoomcard`,05_Func.js:2263)。 +- 注意:原工程支付流程使用 HTTP;存在 Send_pay_succ 定义,但调用点被注释。不得仅凭常量/发送函数判为活跃 WS 请求。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:228);旧函数:[Send_pay_succ](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:572)。 + +#### 27. topup_card + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:充值卡兑换。 +- 请求:`agentid`、`playerid`、`cardno`(卡号)。 +- 响应/推送:`Desk.topup_card` 为空实现(07_Desk.js:1365),资产变化经 update_bean/update_roomcard 推送。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:235);旧函数:[Send_topup_card](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:862)、[topup_card](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:866)。 + +#### 28. giveCoin + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:仓库面板向他人转账豆豆/金币。 +- 请求:`agentid`、`playerid`(转出)、`toPlayerid`(转入目标ID)、`gameid`、`count`(数量)、`password`(仓库密码)(11_GameUI.js:2132-2139)。 +- 响应/推送:`state`(0成功)、`star2`(转出后**仓库星星**数 → `setWareHouseStarCOunt`,**非豆豆**);失败 `showerror`/`error`(07_Desk.js:1381)。 +- 注意:响应 `star2` 为仓库星星数,注意区别于豆豆余额。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:242);旧函数:[Send_giveCoin](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:875)、[giveCoin](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:878)。 + +### 大厅 · 仓库 / 星星 / 魅力(4) + +#### 29. set_bankpwd + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:设置仓库密码。 +- 请求:`agentid`、`playerid`、`unionid`、`password`。 +- 响应/推送:`state`(0成功)、`password`;失败 `showerror`/`error`(07_Desk.js:1228)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:253);旧函数:[Send_set_bankpwd](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:696)、[set_bankpwd](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:700)。 + +#### 30. change_star + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:仓库存/取(豆豆 ↔ 仓库星星)。 +- 请求:`agentid`、`playerid`、`mode`(0存入/1取出,源码 `safeInputType-1`)、`password`(仓库密码,字符串)、`count`(数量)(11_GameUI.js:2065-2069 调用点齐全携带此 5 字段)。 +- 响应/推送:`state`(0成功)、`star1`(更新后豆豆余额 → `update_bean2`)、`star2`(更新后仓库星星数 → `setWareHouseStarCOunt`)、`msg`(可选提示)、`count`(可选,回填安全输入);失败 `showerror`/`error`(07_Desk.js:1238)。 +- 注意:原审计疑虑"`mode`/`password` 未见"——经核对仓库存/取调用点(11_GameUI.js:2068-2069)**确含** `mode` 与 `password`,文档正确,疑虑解除。审计提到的"agentid/playerid/toPlayerid/gameid/count"实为相邻的 `giveCoin` 转账包(11_GameUI.js:2132-2139),并非本 rpc。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:260);旧函数:[Send_change_star](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:706)、[change_star](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:710)。 + +#### 31. update_charm + +- 路由/通道:agent;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。 +- 用途:座位魅力值更新。 +- 请求:`Net.Send_update_charm`(`09_Net.js:727`) 已定义但**无任何调用点**;实际只作服务器→客户端推送。 +- 响应/推送:`seatlist`:[ {`seat`(int), `charm`(number)} ](07_Desk.js:1260 遍历 setCharm)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:267);旧函数:[Send_update_charm](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:726)、[update_charm](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:730)。 + +#### 32. setAllCharm + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:批量设置总魅力。 +- 请求:`agentid`、`playerid`、`gameid`、`value`(11_GameUI.js:1269-1275)。 +- 响应/推送:无显式业务字段,`Desk.setAllCharm` 仅本地存储并提示成功(07_Desk.js:1321)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:274);旧函数:[Send_setAllCharm](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:778)、[setAllCharm](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:782)。 + +### 大厅 · 邀请码 / 绑定(2) + +#### 33. binding_invitecode + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:绑定邀请码。 +- 请求:`agentid`、`playerid`、`invitecode`(11_GameUI.js:1367-1369)。 +- 响应/推送:`state`(0成功时写入 `invitecode`)、`invitecode`、`error`(提示文案)(06_Player.js:251)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:285);旧函数:[Send_binding_invitecode](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:586)、[binding_invitecode](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:591)。 + +#### 34. get_player_invitecode + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:获取自己的邀请码(打开绑定界面)。 +- 请求:`agentid`、`playerid`、`unionid`、`openid`(11_GameUI.js:1378-1382)。 +- 响应/推送:`invitecode`(string) → `C_Player.setInvitecod` 并打开绑定界面(09_Net.js:607)。 +- 注意:修正原文档——请求字段为 `agentid/playerid/unionid/openid`,**无 `gameid`**,新增 `unionid`/`openid`(11_GameUI.js:1378)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:292);旧函数:[Send_get_player_invitecode](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:602)、[get_player_invitecode](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:606)。 + +### 大厅 · VIP 管理 / 黑白名单(4) + +#### 35. optBanList + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:黑名单查看/添加/移除(多入口)。 +- 请求:随入口不同,公共字段 `agentid`、`playerid`、`type`: +- 响应/推送:`type`(1/3/4/5/6)、`banList`(黑名单数组)、`message`(可选提示)(07_Desk.js:1289)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:303);旧函数:[Send_optBanList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:761)、[optBanList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:765)。 + +#### 36. getPlayerWhiteList + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:获取白名单。 +- 请求:`agentid`、`playerid`、`gameid`。 +- 响应/推送:`whiteList`(数组) → `GameData.whiteList.data`(07_Desk.js:1412)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:316);旧函数:[Send_getPlayerWhiteList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:881)、[getPlayerWhiteList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:885)。 + +#### 37. optWhiteList + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:白名单添加/修改/删除。 +- 请求:`agentid`、`playerid`、`gameid`、`shortcode`、`mode`、`userid`(目标ID): +- 响应/推送:`whiteList`(更新后数组)、`mode`(可选)、`message`(可选提示)(07_Desk.js:1398)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:323);旧函数:[Send_optWhiteList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:889)、[optWhiteList](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:893)。 + +#### 38. setVipForbidSelect + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:VIP 房禁止玩家选桌开关。 +- 请求:`agentid`、`playerid`、`gameid`、`forbidSelect`(1开/0关,11_GameUI.js:2631-2639)。 +- 响应/推送:`state`(0成功)、`forbidSelect`(1开/0关);失败 `error`(07_Desk.js:1348)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:332);旧函数:[Send_setVipForbidSelect](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:854)、[setVipForbidSelect](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:858)。 + +### 大厅 · 其它 agent 协议(8) + +#### 39. submit_opinion + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:提交反馈/意见。 +- 请求:`agentid`、`playerid`、`gameid`、`content`。 +- 响应/推送:`state`(0成功)(07_Desk.js:1100)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:343);旧函数:[Send_submit_opinion](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:546)、[submit_opinion](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:550)。 + +#### 40. submit_location + +- 路由/通道:agent;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:提交定位信息(仅非大厅环境发送)。 +- 请求:`agentid`、`playerid`、`info`(定位对象)。 +- 响应/推送:经 `Game.submit_location` 处理(09_Net.js:583)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:350);旧函数:[Send_submit_location](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:576)、[submit_location](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:582)。 + +#### 41. submit_phoneinfo + +- 路由/通道:agent;方向:C→S 请求(无响应处理);状态:**N**;新 Rpc 常量:有。 +- 用途:提交手机/通讯录信息(仅非大厅环境发送)。 +- 请求:`agentid`、`playerid`、`info`{ `phoneInfo`(手机信息), `addrBook`(通讯录) }。 +- 响应/推送:`Net.submit_phoneinfo` 接收函数为空实现(09_Net.js:722)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:357);旧函数:[Send_submit_phoneinfo](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:716)、[submit_phoneinfo](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:721)。 + +#### 42. submit_error + +- 路由/通道:agent;方向:C→S;同名回包被旧接收器忽略;状态:**N**;新 Rpc 常量:无。 +- 用途:异常/日志上报(Net.submit_error 与 Net.submit_log 共用同一个实际 rpc)。 +- 请求:`packet`(出错数据包字符串)、`msg`(错误/堆栈信息)、`playerid`、`agentid`、`gameid`(09_Net.js:66-72)。 +- 响应/推送:服务器若回 rpc `"submit_error"`,前端在分发处直接 `return` 忽略(12_Logic.js:235)。 +- 注意:submit_log 是本地函数别名,不是第二个线上 rpc;不能重复计数。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:364);旧函数:[submit_error](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:55)、[submit_log](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:81)。 + +#### 43. kick_server + +- 路由/通道:agent;方向:C→S 请求 + S→C 推送;状态:**R**;新 Rpc 常量:有。 +- 用途:管理端踢出玩家 / 被踢下线弹窗。 +- 请求:`agentid` 等(构造点未集中定位)。 +- 响应/推送:`msg`(踢出提示) → `GameUI.OpenKick`(07_Desk.js:1108)。 +- 注意:⚠️ 请求字段待补。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:371);旧函数:[Send_kick_server](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:554)、[kick_server](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:557)。 + +#### 44. broadcast + +- 路由/通道:agent;方向:C→S 请求 + S→C 推送;状态:**N**;新 Rpc 常量:有。 +- 用途:广播消息/滚动公告(主要为服务器推送)。 +- 请求:`agentid` 等(`Send_broadcast` 存在,09_Net.js:530)。 +- 响应/推送:`msgtype`(0消息框/1滚动公告,可选,缺省 0)、`msgcontent`(内容)(07_Desk.js:1112)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:378);旧函数:[Send_broadcast](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:529)、[broadcast](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:533)。 + +#### 45. connect_agentserver + +- 路由/通道:agent;方向:双向(切服);状态:**R**;新 Rpc 常量:有。 +- 用途:切换到大厅服务器。 +- 请求:`Send_connect_agentserver`(`09_Net.js:515`) 透传 `_data`(切服时使用)。 +- 响应/推送:`agentserver`(新大厅服地址)、`opt`(切换原因,如 `other_break_room`/`free_room`)(09_Net.js:518)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:385);旧函数:[Send_connect_agentserver](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:514)、[connect_agentserver](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:517)。 + +#### 46. playerBehavior + +- 路由/通道:HTTP GET(历史 agent 名称);方向:C→HTTP;状态:**X**;新 Rpc 常量:有。 +- 用途:玩家行为埋点。 +- 请求:原 WS 路径(agentid/gameid/playerid/tag)**被注释**(09_Net.js:799-806),实际改走独立 HTTP GET 上报 `http://test3.1888day.com/api/gamedo/gamedo?agentid=&gameid=&playerid=&tag=`(09_Net.js:807-815)。 +- 响应/推送:HTTP 回调 `playerBehavior_Succ`/`_Fail`(09_Net.js:818/822);WS 接收函数 `Net.playerBehavior`(`09_Net.js:826`) 当前无触发。 +- 注意:当前调用走独立 HTTP 埋点地址,WS 段被注释;不归入本地 WebSocket 验收。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:392);旧函数:[Send_playerBehavior](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:796)、[playerBehavior](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:825)。 + +### 房间 · 房间生命周期(8) + +#### 47. self_break_room + +- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:房主在**未开局**前主动解散房间。 +- 请求:仅通用四字段。 +- 响应/推送:`roomcode`(可选,用于从本地"我的房间"列表移除)。处理:清空牌桌、回大厅。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:19);旧函数:[Send_self_break_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:266)、[self_break_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:271)。 + +#### 48. other_break_room + +- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。 +- 用途:他人(房主)解散房间,推送给房内其余玩家。 +- 请求:无(纯推送)。 +- 响应/推送:无业务字段;触发 `Func.exitRoom()`、清桌、回大厅、提示"房主已解散房间!"。 +- 注意:常伴随 **connect_agentserver** 切服包(`data.opt == other_break_room`,见下文)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:26);旧函数:[other_break_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:277)。 + +#### 49. self_exit_room + +- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**R**;新 Rpc 常量:有。 +- 用途:自己在**未开局**前退出房间。 +- 请求:仅通用四字段(发送处见 `08_Utl_Output.js:993`)。 +- 响应/推送:`isowner`(可选,==1 且非无限局时把房间加回本地列表), `seat`(可选,传给 `Game_Modify.myExitRoom`), `roomcode`(可选)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:33);旧函数:[Send_self_exit_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:289)、[self_exit_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:294)。 + +#### 50. other_exit_room + +- 路由/通道:room;方向:S→C 推送;状态:**R**;新 Rpc 常量:有。 +- 用途:其他玩家(未开局)退出房间。 +- 请求:无(纯推送)。 +- 响应/推送:`seat`(离开者座位)。处理:清该座位、`playercnt--`;若 `seat==0` 且非无限局,提示房主已离开。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:40);旧函数:[other_exit_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:301)。 + +#### 51. player_prepare + +- 路由/通道:room;方向:C→S 请求 + S→C 推送;状态:**R**;新 Rpc 常量:有。 +- 用途:玩家点击准备(`needprepare==1` 的房间)。 +- 请求:仅通用四字段。 +- 响应/推送:`seat`(准备者座位), `deskwar`(可选;为真表示满足开战条件 → `HideStartScene` + `Game_Modify.StartWar`)。推送给房内所有人。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:47);旧函数:[Send_player_prepare](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:628)、[player_prepare](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:631)。 + +#### 52. change_room + +- 路由/通道:room;方向:C→S;响应 rpc=change_seat;状态:**N**;新 Rpc 常量:有。 +- 用途:玩家请求换座 / 换桌。 +- 请求:仅通用四字段(**不带目标座位**,由服务器决定换到哪个空位)。发送处 `11_GameUI.js:1945`、`08_Utl_Output.js:1006`。 +- 响应/推送:rpc 名改为 **`change_seat`**,`data`:`seat1`, `seat2`(两个互换的座位号)。处理:交换两座 Desk 信息,若自己在其中则 `C_Player.SetSeat` 更新。 +- 注意:上行 rpc=`change_room`,下行 rpc=`change_seat`,二者成对。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:54);旧函数:[Send_change_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:677)。 + +#### 53. share_room + +- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:把房间分享到世界房列表。 +- 请求:通用四字段;可选 `roomlist`、`roomtype`、`shareType`(高级房分享时携带,见 `11_GameUI.js:1734`/`11_GameUI.js:1891`)。 +- 响应/推送:无业务字段;提示"已成功分享至平台!"。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:61);旧函数:[Send_share_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:634)、[share_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:637)。 + +#### 54. change_seat + +- 路由/通道:room;方向:S→C(change_room 的响应);状态:**N**;新 Rpc 常量:有。 +- 用途:服务器指定两座互换。 +- 请求:无同名上行;上行是 change_room。 +- 响应/推送:seat1、seat2。 +- 注意:独立列名,不能把 change_room 与 change_seat 合成一条而漏计。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:54);旧函数:[change_seat](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:681)。 + +### 房间 · 进房推送(他人视角)(3) + +#### 55. other_join_room + +- 路由/通道:room;方向:S→C 推送;状态:**R**;新 Rpc 常量:有。 +- 用途:其他玩家加入当前房间。 +- 请求:无(纯推送;自己进房用 `self_join_room`,走 agent 路由,见 02 章)。 +- 响应/推送:`seat`(新玩家座位) + 该玩家完整对象(直接传给 `Player.SetDeskInfo`,见 [04 · Player 座位对象](./04-数据结构.md#player-座位对象)): +- 注意:⚠️ `charm`/`sign` 是否每次必含、其余字段是否完整以 `Player.SetDeskInfo` 实际读取为准,详见 [04 章](./04-数据结构.md#player-座位对象)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:72);旧函数:[other_join_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:281)。 + +#### 56. other_offline + +- 路由/通道:room;方向:S→C 推送;状态:**R**;新 Rpc 常量:有。 +- 用途:其他玩家离线。 +- 请求:无(纯推送)。 +- 响应/推送:`seat`。处理:该座 `onstate=1`,刷新 UI。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:80);旧函数:[other_offline](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:389)。 + +#### 57. other_online + +- 路由/通道:room;方向:S→C 推送;状态:**R**;新 Rpc 常量:有。 +- 用途:其他玩家重新上线。 +- 请求:无(纯推送)。 +- 响应/推送:`seat`, `ip`。处理:该座 `onstate=0`、更新 ip。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:87);旧函数:[other_online](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:394)。 + +### 房间 · 解散投票(开局后)(8) + +#### 58. self_apply_free_room + +- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:自己申请解散房间。 +- 请求:仅通用四字段。 +- 响应/推送:`agreefree` { `state`:[各座同意状态数组], `countdown`:倒计时 }。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:101);旧函数:[Send_self_apply_free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:308)、[self_apply_free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:313)。 + +#### 59. other_apply_free_room + +- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。 +- 用途:他人申请解散。 +- 请求:无(纯推送)。 +- 响应/推送:`seat`(申请者), `agreefree` { `state`[], `countdown` }。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:108);旧函数:[other_apply_free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:319)。 + +#### 60. self_agree_free_room + +- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:自己同意解散。 +- 请求:仅通用四字段。 +- 响应/推送:无业务字段;本地把自己加入同意列表、刷新投票 UI。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:115);旧函数:[Send_self_agree_free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:323)、[self_agree_free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:328)。 + +#### 61. other_agree_free_room + +- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。 +- 用途:他人同意解散。 +- 请求:无(纯推送)。 +- 响应/推送:`seat`(同意者)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:122);旧函数:[other_agree_free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:333)。 + +#### 62. self_refuse_free_room + +- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:自己拒绝解散。 +- 请求:仅通用四字段。 +- 响应/推送:无业务字段;清空同意列表、投票结果置不通过、弹"已拒绝"结果。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:129);旧函数:[Send_self_refuse_free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:338)、[self_refuse_free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:343)。 + +#### 63. other_refuse_free_room + +- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。 +- 用途:他人拒绝解散(任一人拒绝即否决本轮)。 +- 请求:无(纯推送)。 +- 响应/推送:`seat`(拒绝者)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:136);旧函数:[other_refuse_free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:348)。 + +#### 64. free_room + +- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。 +- 用途:解散投票最终结果。 +- 请求:无(纯推送)。 +- 响应/推送:freeNow;deskfree、roomcard、tips、time、seats 按具体分支读取,字段语义见 03 章。 +- 注意:可伴随 **connect_agentserver** 切服包(`data.opt == free_room`,见下文)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:143);旧函数:[free_room](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:353)。 + +#### 65. beanroom_surrender + +- 路由/通道:room;方向:C→S 请求 + S→C 响应;状态:**N**;新 Rpc 常量:有。 +- 用途:豆豆房(金币房)投降。 +- 请求:通用四字段 + `count`(投降数量 = `GameData.surrendCount`,发送处 `11_GameUI.js:1245`)。 +- 响应/推送:`state`(0=成功 → `Game_Modify.onSurrender(_msg)`); 失败时 `showerror`(==1 则弹) / `error`(错误文本)。 +- 注意:⚠️ 未见成对的 `other_xxx` 推送,是否向房内其他玩家广播他人投降待后端确认。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:160);旧函数:[Send_beanroom_surrender](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:612)、[beanroom_surrender](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:616)。 + +### 房间 · 开局(2) + +#### 66. self_makewar + +- 路由/通道:room;方向:C→S 请求 → S→C 响应(self_makewar);状态:**N**;新 Rpc 常量:有。 +- 用途:房主主动开局。 +- 请求:仅通用四字段。 +- 响应/推送:rpc=`self_makewar`,无业务字段;触发 `Desk.self_makewar` → `Game_Modify.StartWar(_msg)`。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:171);旧函数:[Send_self_makewar](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:469)、[self_makewar](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:474)。 + +#### 67. other_makewar + +- 路由/通道:room;方向:S→C 推送;状态:**N**;新 Rpc 常量:有。 +- 用途:开局广播(房主开局或满员自动开局)给房内其他玩家。 +- 请求:无(纯推送)。 +- 响应/推送:开局信息对象 → `Desk.makewar` → `Game_Modify.StartWar(msg)`。 +- 注意:实际发牌等对局数据在此之后通过**子游戏内协议**下发(见 05 章)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:178);旧函数:[other_makewar](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:479)。 + +### 房间 · 房间内社交(6) + +#### 68. send_text + +- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 文字聊天;状态:**N**;新 Rpc 常量:有。 +- 用途:文字聊天 / 全服公告 / 预定义常用语。 +- 请求:通用四字段 + `text`(内容;点常用语时为 `Game_Config.Info.TextContent[spid-206]` 文本) + `type`(0=普通 / 1=全服公告;按是否勾选公告设 0/1) + `info`(可选;**有 info 时 `type` 改为 2**)。发送处 `11_GameUI.js:1170`(输入框)、`11_GameUI.js:2717`(常用语)。 +- 响应/推送:`type`(0=普通 / 1=全服公告 / 2=机器人 / 3=预定义文字), `text`(内容;**type==3 时为 `Game_Config.Info.TextContent` 的 1 基索引**,客户端按 `(idx-1)%len` 取文本), `seat`(发送者座位,type≠1 时使用), `info`(type==2 时附加)。 +- 注意:百人场(`vipInfinite`)只处理 type==1 公告分支。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:189);旧函数:[Send_send_text](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:399)、[send_text](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:403)。 + +#### 69. send_voice + +- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 语音;状态:**N**;新 Rpc 常量:有。 +- 用途:发送语音消息。 +- 请求:通用四字段 + `voiceurl`(已上传音频地址) + `time`(时长) + `info`(可选) + `type`(可选,有 info 时为 2)。发送处 `05_Func.js:1714`、`05_Func.js:2984`。 +- 响应/推送:`type`(0=普通 / 2=机器人), `seat`(发送座位), `voiceurl`, `time`, `info`(type==2 时)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:203);旧函数:[Send_send_voice](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:491)、[send_voice](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:494)。 + +#### 70. send_gift + +- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 互动/送礼;状态:**N**;新 Rpc 常量:有。 +- 用途:向指定座位送互动礼物。 +- 请求:通用四字段 + `giftid`(=`spid_up - 255`,按钮精灵号算出,发送处 `11_GameUI.js:2725`) + `receiveseat`(=`GameData.InteractPlayer`) + `info`(可选) + `type`(有 info 时为 2)。 +- 响应/推送:`type`(0=普通 / 2=机器人), `giftid`(客户端做 `(giftid-1)%4+1` 归一为 1~4 动画), `sendseat`(发送座位), `receiveseat`(接收座位), `info`(type==2 时)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:218);旧函数:[Send_send_gift](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:483)、[send_gift](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:486)。 + +#### 71. send_phiz + +- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 表情;状态:**N**;新 Rpc 常量:有。 +- 用途:发送表情动画。 +- 请求:通用四字段 + `text`(=`up_id`,表情按钮序号 1~`ConstVal.Emotion.count`,发送处 `11_GameUI.js:815`) + `info`(可选) + `type`(有 info 时为 2)。 +- 响应/推送:`type`(0=普通 / 2=机器人), `text`(表情 ID,客户端按 `(text-1)%ConstVal.Emotion.src_list.length+1` 归一), `seat`(发送座位), `info`(type==2 时)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:232);旧函数:[Send_send_phiz](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:538)、[send_phiz](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:542)。 + +#### 72. call_phone + +- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 拨打电话;状态:**N**;新 Rpc 常量:有。 +- 用途:拨打/接听电话,置玩家 `onstate=2`(通话中)。 +- 请求:仅通用四字段(发送处 `06_Player.js:380` / `:388` / `:396`,对应接起/电话进来/去电三种触发)。 +- 响应/推送:rpc=`call_phone`,`seat`(拨打者座位 → 该玩家 `onstate=2`)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:239);旧函数:[Send_call_phone](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:451)、[call_phone](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:455)。 + +#### 73. hangup_phone + +- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 挂断电话;状态:**N**;新 Rpc 常量:有。 +- 用途:挂断电话,置玩家 `onstate=0`。 +- 请求:仅通用四字段(发送处 `06_Player.js:372`)。 +- 响应/推送:rpc=`hangup_phone`,`seat`(挂断者座位 → 该玩家 `onstate=0`)。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:253);旧函数:[Send_hangup_phone](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:460)、[hangup_phone](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:464)。 + +### 保留名称 · 房间社交(5) + +#### 74. receive_chat + +- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。 +- 用途:聊天推送的另一可能 rpc 名(与 `send_text` 成对)。 +- 请求:未发现同名发送链。 +- 响应/推送:未定义,不能套用其它 rpc 字段。 +- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:196);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。 + +#### 75. play_voice + +- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。 +- 用途:与 `send_voice` 成对,疑为语音**播放**推送。 +- 请求:未发现同名发送链。 +- 响应/推送:未定义,不能套用其它 rpc 字段。 +- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:211);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。 + +#### 76. other_send_gift + +- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。 +- 用途:与 `send_gift` 成对,疑为他人送礼推送。 +- 请求:未发现同名发送链。 +- 响应/推送:未定义,不能套用其它 rpc 字段。 +- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:225);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。 + +#### 77. other_callphone + +- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。 +- 用途:与 `call_phone` 成对,疑为他人拨打电话推送。 +- 请求:未发现同名发送链。 +- 响应/推送:未定义,不能套用其它 rpc 字段。 +- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:246);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。 + +#### 78. other_hangup + +- 路由/通道:room(文档归类,线上未确认);方向:未确认(文档推测为下行);状态:**X**;新 Rpc 常量:有。 +- 用途:与 `hangup_phone` 成对,疑为他人挂断电话推送。 +- 请求:未发现同名发送链。 +- 响应/推送:未定义,不能套用其它 rpc 字段。 +- 注意:常量存在,旧 Net 无同名接收器。与实际已实现的 send_voice/send_text/call_phone/hangup_phone/send_gift 区分;不据此断言服务器绝不会发送。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:260);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。 + +### 大厅 · 资产更新与系统推送(4) + +#### 79. update_bean + +- 路由/通道:上行 agent;下行按 agent 归档(实际推送路由待逐项抓包);方向:C→S + S→C;状态:**N**;新 Rpc 常量:有。 +- 用途:豆豆变化(本人或房内座位) +- 请求:原生 yPaytype / WVJB yPaytype 回调直接发送 agentid、playerid、change:num、type:9;这是资产写入路径,不属于只读查询。 +- 响应/推送:bean、change/seat/type(按场景可选)、text;不同分支见原处理器。 +- 注意:02 章“无对应主动请求”与当前 05_Func.js 不符。存在发送代码不代表支付来源可信性、服务端行为或新框架已经验收。 +- 直接上行来源:[原生 yPaytype](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/05_Func.js:2191)、[WVJB yPaytype](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/05_Func.js:3053)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:406);旧函数:[update_bean](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:597)。 + +#### 80. update_roomcard + +- 路由/通道:上行 agent;下行按 agent 归档(实际推送路由待逐项抓包);方向:C→S + S→C;状态:**N**;新 Rpc 常量:有。 +- 用途:房卡变化 +- 请求:原生 yPaytype / WVJB yPaytype 回调直接发送 agentid、playerid、change:num、type:9;这是资产写入路径,不属于只读查询。 +- 响应/推送:roomcard、text(可选);change 分支条件见原处理器。 +- 注意:02 章“无对应主动请求”与当前 05_Func.js 不符。存在发送代码不代表支付来源可信性、服务端行为或新框架已经验收。 +- 直接上行来源:[原生 yPaytype](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/05_Func.js:2188)、[WVJB yPaytype](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/05_Func.js:3050)。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:405);旧函数:[update_roomcard](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:382)。 + +#### 81. kick_offline + +- 路由/通道:agent(协议归档;实际推送路由待逐项抓包);方向:S→C;状态:**N**;新 Rpc 常量:有。 +- 用途:被踢下线 +- 请求:无对应主动请求。 +- 响应/推送:fromOther、gameid(可选);旧流程同时上报 submit_error。 +- 注意:仅有旧处理器不能证明新 Runtime 接入或真实服务已覆盖。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:408);旧函数:[kick_offline](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:447)。 + +#### 82. show_message + +- 路由/通道:agent(协议归档;实际推送路由待逐项抓包);方向:S→C;状态:**N**;新 Rpc 常量:无。 +- 用途:通用提示 +- 请求:无对应主动请求。 +- 响应/推送:msg、time。 +- 注意:仅有旧处理器不能证明新 Runtime 接入或真实服务已覆盖。 +- 依据:[02-协议-agent路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/02-协议-agent路由.md:409);旧函数:[show_message](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:655)。 + +### 房间 · 服务器切换(room 侧)(1) + +#### 83. connect_roomserver + +- 路由/通道:room;方向:C→S 请求 + S→C 推送 · 切到房间服;状态:**R**;新 Rpc 常量:有。 +- 用途:从大厅服切换到房间服务器。 +- 请求:由 `Send_connect_roomserver` 发起(走 room 路由)。 +- 响应/推送:`data.roomserver`(新房间服地址)。处理:置 `GameData.ConnectType=true`、`GameData.ConnectRpc=connect_roomserver`、`GameData.Server=data.roomserver`,关闭当前连接并用新地址重连。 +- 依据:[03-协议-room路由.md](G:/Works/YouleGamesCocosCreator/docs/protocol/03-协议-room路由.md:295);旧函数:[Send_connect_roomserver](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:499)、[connect_roomserver](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:502)。 + +### 子游戏 · 框架保留名称(2) + +#### 84. over_game + +- 路由/通道:由具体子游戏契约确定;方向:文档描述 S→C;实际由子游戏确认;状态:**X**;新 Rpc 常量:有。 +- 用途:结算广播保留名;具体载荷由子游戏定义。 +- 请求:模板未定义。 +- 响应/推送:模板未定义。 +- 注意:Game_Surface_3 的 Game_Modify 为模板,不能据名字推导具体子游戏协议。 +- 依据:[05-游戏内协议与桥接.md](G:/Works/YouleGamesCocosCreator/docs/protocol/05-游戏内协议与桥接.md:144);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。 + +#### 85. agentserver_game + +- 路由/通道:由具体子游戏契约确定;方向:未定义;状态:**X**;新 Rpc 常量:有。 +- 用途:对局数据经大厅服中转的保留名。 +- 请求:模板未定义。 +- 响应/推送:模板未定义。 +- 注意:Game_Surface_3 的 Game_Modify 为模板,不能据名字推导具体子游戏协议。 +- 依据:[05-游戏内协议与桥接.md](G:/Works/YouleGamesCocosCreator/docs/protocol/05-游戏内协议与桥接.md:145);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。 + +### 外部桥接 · 非网络 RPC(1) + +#### 86. joinRoom + +- 路由/通道:H5/小程序本地桥接;方向:外部入口→前端确认流程;状态:**X**;新 Rpc 常量:有。 +- 用途:唤起进房确认;不是直接向服务器发送 joinRoom。 +- 请求:{rpc:"joinRoom",data:{roomcode}};H5 来自 URL gameData,小程序来自缓存 openminigamedata。 +- 响应/推送:确认后走 self_join_room;不产生 joinRoom 网络回包。 +- 注意:h5RpcList 唯一名称;旧小程序 checkType=8 与白名单删除撞号,不能沿用。WVJB handler 名不计为服务器 RPC。 +- 依据:[05-游戏内协议与桥接.md](G:/Works/YouleGamesCocosCreator/docs/protocol/05-游戏内协议与桥接.md:229);旧函数:无同名 Net 定义(保留/桥接按所引章节核对)。 + +## 覆盖核对方法 + +- 逐一取 02_Const.js 的 RpcList 字符串赋值去重;核对 09_Net.js 中 RpcList 引用、Net 命名函数以及硬编码 rpc;扫描全模板的直接 _SendData 调用与 rpc 赋值(包含 05_Func.js 支付桥接),补入 h5RpcList。 +- 与 02/03 章标题、02 章末尾推送表以及 05 章保留/桥接名称取并集;按实际名称一条一项,跨章节引用不重复计数。 +- 结果:82 个旧常量名称全部命中;补充 4 个名称后 86 条,无遗漏或重复;11 个现代严格分发名称全部命中。 +- 新框架状态依据 platform/runtime.ts、protocol/platform-handlers.ts、protocol/first-slice-routes.ts;旧 protocol/routes.ts 大常量表仅用于“名称存在”对照。 +- 本轮没有发送网络请求、触发 Cocos 编辑器操作或运行资源生成器;这份静态索引不新增真实验收结论。 diff --git a/docs/superpowers/plans/2026-09-06-login-scenes-and-contract.md b/docs/superpowers/plans/2026-09-06-login-scenes-and-contract.md new file mode 100644 index 0000000..5ee3dec --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-login-scenes-and-contract.md @@ -0,0 +1,67 @@ +# 登录场景与原工程接线 + +用户已确认:启动、加载、登录分别组织;协议由独立 PopupRoot 动态实例化;按钮显示和调用按 Game_Surface_3。 + +最新协议要求覆盖本文早期验收记录:默认已勾选;所有登录方式都要求当前勾选,未勾选统一提示“未同意协议”。LoginAccounts 持有唯一协议状态,授权、回调、游客、手机、快速/隐藏入口和缓存自动登录统一拦截;LocalPlatformLogin 诊断入口同步。175 项相关测试通过。 + +## 实施顺序 + +1. 在 `assets/framework/presentation/login-policy.ts` 集中实现 `JumpWxAuth` 的显示顺序,配置缺省仅在来源定义。测试覆盖审核、设备、游客、QQ、缓存账号组合,并保留 QQ 最后覆盖的源行为。 +2. 在登录适配器实现 `accreditlogin(2/1)`、`sharelogin` 回调、游客/设备/手机账号、缓存与 `player_login`。手机号使用原字段 `telphone/smmcode/telphoneAuto`,不增加服务器接口。验证码按钮沿用“请通过相关渠道获取”。针对接口参数、协议勾选及缓存编写测试。 +3. 创建常驻启动组件与 Loading/Login 场景宿主,保留单个 Runtime/连接;使用 `director.loadScene`,等待场景绑定后展示数据。Logo 继续复用既有倒计时和进度行为。 +4. 使用 Cocos MCP 移除 Login prefab 内协议实例,场景中设置 PopupRoot。协议和手机窗口独立加载、重复点击防重入、场景销毁取消加载与清理监听。 +5. 用原生 Cocos 节点和预览核对启动场景顺序、按钮显示、协议开关和滑动;执行相关测试与类型检查,记录既有失败。对比改动前资源哈希,确认只修改任务资源。 + +## 验收边界 + +- 初始 check=false;点击两次回到 false;微信未同意时不调用授权;QQ/游客遵循原分支。 +- 审核、正式、showVisitor、deviceLogin、qqLogin 和缓存组合符合源码。 +- 授权参数 2/1,原回调字段映射与手机请求字段精确一致。 +- Login prefab 无 Layer10_Protol 子节点;弹层在 PopupRoot 下,关闭后释放。 +- Startup → Loading → Login 使用真实场景切换;Runtime 和网络连接不重复创建。 +- 不修改服务器,不覆盖既有 scene/meta 工作,不提交无关文件。 + +## 本次验证结果 + +- 新入口为 `assets/scenes/PlatformStartup.scene`,实际浏览器日志依次显示 `LoadScene PlatformLoading`、`LoadScene PlatformLogin`。 +- Login prefab 已无协议实例;Startup 引用独立协议/手机 prefab,弹窗归属 PopupRoot。 +- 浏览器实际点击验证 false → true → false、协议内容上下拖动、Spirit253 关闭、遮罩关闭;最终预览 error/warn 为空。 +- Cocos 原生节点验证手机窗口生成两个数字 EditBox,字体取原资源高度 28;协议含原生 ScrollView。 +- 补齐成功设备登录缓存、玩家 ID 写回、global/WVJB 的不同缓存目的地、startlocation(2)/getlocationinfo。正式/审核按钮位置沿用源坐标,关闭对应 Widget 避免静态布局覆盖运行时位置。 +- `node --experimental-transform-types --test framework-tests/local-platform-login/*.test.ts`:97/97 通过。 +- 全工程类型检查仍有既有 5 条错误:login-sources.ts 的 unknown 字段 3 条、room-contracts.ts 的 0/1 类型 1 条、game-contract-harness.ts 的 node:assert 类型 1 条;本次新增代码没有类型错误。 +- 对比本次资源哈希清单,仅既有 Login prefab 因移除嵌套协议而变化;保留其他已存在的 scene/meta 修改。没有修改服务器或原工程,也没有提交无关文件。 +- 真机微信/QQ 授权、手机号登录成功和原生定位尚未进行端到端联调;当前验证涵盖接口参数、缓存/协议契约及 Cocos UI,不代表真实账号已登录。 + +## 远程 showVisitor 排查与修正 + +- 实际 POST `config_test/update_jsonv2.txt` 返回 200。`showVisitor=1` 属于代理 `1B2h0ccl205c390Y28m1Ajdplkuu4wgy`;原构建默认代理错误地选择另一分支,且其下没有当前 gameid。 +- 在 BUILD_IDENTITY 单一来源修正为当前 gameid 所属的代理及渠道 `aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG`,未修改远程文件、服务端或分层配置读取规则。 +- 实际 marketid=4 还配置了 deviceLogin=true。用户确认保留两个按钮同时显示,调整为游客在左、手机在右,解决原坐标重叠。 +- 预览日志确认 showVisitor=true/deviceLogin=true/qqLogin=false;实际画面确认游客与手机并排、微信隐藏,error/warn 为空。 +- 新增远程层级到 UI policy 的回归测试,并同步已有测试中失效的配置 URL 断言;登录、配置和本地适配器合计 170/170 通过。 + +## 游客成功回包 tel 校验修复 + +- 新增的设备缓存校验错误地根据全局 deviceLogin 开关强制要求游客回包包含 tel。改为使用本次请求的 DeviceLoginMode,游客成功只记录 playerid,不写设备账号缓存或伪造手机号。 +- 缓存选择为 visitorinfo 时,自动登录也按游客方式提交;同时启用手机按钮不改变这个请求分支。 +- 回归覆盖“两个按钮开启 → 游客提交 → 无 tel 成功回包 → 刷新缓存自动登录”,并保留手机账号缓存测试。172 项相关测试全部通过。 +- 实际浏览器点击游客显示“登录成功”;刷新后缓存游客自动登录也显示“登录成功”,error/warn 均为空。没有修改服务器。 + +## 手机登录弹窗对齐 + +- 来源:11_GameUI.js 的 3243/3244/3245/3251/3253/3254 分支、openPhoneBind、数字输入面板;12_Logic.js 的 readPhoneCode。 +- 登录入口使用标题第 2 帧“手机登录”;恢复图标和原提示精灵,输入后隐藏对应提示。继续使用原生 EditBox,保留前导零,数字最多 18 位,与原数字面板一致。 +- 使用原 sgame_phoneCode 缓存键恢复手机号和 30 秒剩余时间,验证码不自动回填。倒计时中切等待帧;验证码获取仍仅提示“请通过相关渠道获取”,不发送原工程已注释掉的短信 RPC。 +- 两个输入非空才调用既有 phoneLogin 对应适配器并关闭;Spirit3251 和 Spirit3241 关闭,释放输入监听和定时器。 +- PhoneLoginAssets 通过 MCP 挂载并保存两个帧引用,保证构建资源依赖;没有手改 prefab/meta。 +- 浏览器验证标题、图标、输入位置、前导零、获取提示、空验证码拦截、关闭/重开及遮罩关闭。原生 Cocos 节点验证缓存恢复、倒计时拦截/结束、非空提交一次并关闭、定时器清理。174 项相关测试通过;未发送真实手机号或验证码登录。 + +## 2026-09-06 旧诊断入口退役 + +- 新流程所需倒计时、节点绑定、登录阶段 GameEntry 已迁入 assets/scripts/platform-login,并补充职责、协议状态、缓存优先级及生命周期注释。 +- 使用 Cocos asset-db 删除 LocalPlatformLogin.scene、scripts/local-platform-login 及其 meta。迁移保留行为测试至 framework-tests/platform-login,删除旧诊断入口专属测试。 +- GameEntry 仍是显式拒绝游戏操作的占位实现,保留诊断 key/route;不代表接入大厅或房间。 +- 按现有 Login_Layer 的 Logining 分组修正头像、昵称、遮罩绑定路径,未修改 prefab。 +- 删除后 93 项相关测试通过;浏览器预览正常进入登录界面,无 error/warn。此前已有的 5 项全项目类型错误仍存在。 +- 对比操作前哈希,删除范围之外的已有序列化资源无变化。 diff --git a/docs/superpowers/reports/2026-09-05-platform-ui-function-comparison.md b/docs/superpowers/reports/2026-09-05-platform-ui-function-comparison.md new file mode 100644 index 0000000..0289c99 --- /dev/null +++ b/docs/superpowers/reports/2026-09-05-platform-ui-function-comparison.md @@ -0,0 +1,348 @@ +# 原项目与 Cocos 非房间平台界面功能对照 + +> 2026-09-05;主目录分支 codex/local-platform-login,代码基线 556c47a。目的:在继续非房间 RPC 前明确原入口、交互/状态、资源归属与 Cocos 缺口。本轮仅检查和整理文档。 + +## 结论 + +- 原项目本轮已整理 **22 个功能条目**,覆盖主要非房间平台入口、相关回包与关闭分支;其中黑白名单/VIP 管理只登记边界,房间与真实子游戏后置。文末列出尚未展开的专项项,不把本轮称为全项目每条鼠标分支的完整验收。 +- **Cocos 新项目界面逻辑尚不完整**。已通过真实验收的是 LocalPlatformLogin 诊断场景的本地启动、加载、游客登录。完整大厅、资料/设置/任务/仓库等业务面板仍缺实际接线。 +- 通过 funplay-cocos-mcp 对固定主工程 assets 递归只读检查:**55 个 prefab、3 个 scene**。55 个 prefab 均没有序列化业务组件、cc.Button、clickEvents、cc.EditBox、cc.ScrollView 或 cc.Animation。 +- 上述零计数不等于所有节点都不可点击:诊断入口运行时通过 Node.EventType.TOUCH_END 绑定游客与停止,通过代码驱动加载旋转。已同时搜索 assets/**/*.ts 的组件、实例化和事件绑定;未找到其他非房间业务面板的现代运行时接线。 +- asset-db 仅登记 Login.scene、LocalPlatformLogin.scene、scene-2d.scene;**没有 MainMenu.scene / Loading.scene**。旧 LoginFlow 仍调用 loadScene('MainMenu'),正常生产启动到大厅的链路尚未闭合。 + +## 核对边界与证据 + +- MCP 主工程:G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/,Creator 3.8.8。资源扫描 callId=fp_f377daed9f88526b,时间=2026-09-05T12:09:46.270Z;asset-db 场景查询 callId=fp_88f8733e86ee90e4;group/节点核对 callId=fp_826c54b22021a9fc、fp_f5d7b70d00007965。 +- 编辑器读取前后 query-dirty=false;未打开其它场景、未实例化资源、未触发 UI 点击或网络请求。MCP 执行器初次把路径分隔符误判为外部路径,复核脚本只有固定工程内的目录/JSON 读取后执行只读扫描。 +- 原项目路径为 projects/Game_Surface_3;gameabc_face.mouseup 分别转交 GameUI、Game_Modify、gameCombat。只查 11_GameUI.js 会漏掉模板战绩交互,目录名 01_SubGame 不能替代实际职责判断。 +- 本文中的旧行为依据当前源码;资源结构依据本次 MCP;真实登录结论引用[已保存验收报告](./2026-09-05-local-platform-login-acceptance.md),本轮没有重跑登录验收或测试。 +- RPC 字段索引见 [07-RPC全量清单](../../protocol/07-RPC全量清单.md)。本文记录界面流程,不另建一份运行时配置常量。 + +## 当前 Cocos 入口和核心的实际能力 + +- [LoginFlow.ts](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts:68):旧 PlatformSession/NetClient,硬编码 A/C/G、mock_openid 与连接兜底,成功后跳 MainMenu;这不是新 Runtime 的正式入口。 +- [LaunchFlow.ts](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/scripts/LaunchFlow.ts:17):1 秒 scheduleOnce 模拟加载后跳 Login;脚本存在,但三份现有场景未挂它。 +- [SceneStart.ts](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/scripts/SceneStart.ts:17):mock playerState 演示,3 秒后写测试资料;现有场景未挂载,不能当服务器状态回显。 +- [两个 PlayerInfoView](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/scripts/PlayerInfoView.ts:18) 与 [views/PlayerInfoView.ts](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/scripts/views/PlayerInfoView.ts:52):相同 ccclass 名。前者只有 applyPlayer,无注释声称的 bindTo;后者有旧 session binder,但头像加载仍未实现。均未挂到本轮扫描的 prefab/scene;重复类名是后续接管风险,不据静态扫描断言当前已发生运行时冲突。 +- [UI 契约](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/presentation/ui-contracts.ts:1):PageId 只有 loading/login/lobby/room;PlayerModel 为 playerid/nickname/avatar/bean;UiRuntime 命令只有 login/joinRoom/prepare/exitRoom。没有任务、仓库、排行、反馈等页面状态和命令。 +- [诊断 View](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts:39):游客触摸绑定、版本显示、busy、错误/停止;lobby 分支只是结果 Label。它隐藏其它登录方式,禁止展示 room。 +- 3A 完成的是无引擎接线核心,不包含完整平台各功能面板;上述缺口不等于要重做 3A,而是后续逐项扩展业务契约与 Cocos View。 + +## 资源名不能代替功能名 + +- **Layer7_ewm** 实际包含 group-104 黑名单和 group-105 短号排行榜;当前二维码 group-42 在 **Layer9_Feedback** 中。已有迁移参照手册的名字/功能说明需结合当前 group 核对。 +- **Layer9_Feedback** 同时有 group-74 反馈/兑换码、group-12 通知、group-42 二维码、group-98 房间列表。 +- **Layer29_PhoneInfo** 有 group-106 手机登录/绑定和 group-108 白名单;分享选项 group-107 实际在 **Layer603**。 +- **Layer601** 是 group-102 文本输入;数字输入为 Layer602_inputPannel 的 group-87;通用确认是 Layer420_CheckFree 的 group-36,虽然文件名带 Free,也被管理动作复用。 +- 对后续资源授权与绑定必须使用精确 prefab + group + 节点;不能把旧 spid 数字直接当成 Cocos 节点 ID,也不能只按 prefab 英文名分派业务。 + +## 功能逐项对照 + +下列除第 1 项的诊断边界外,均未完成新 Runtime + 实际 Cocos UI 的闭环验收。资源项标记“存在”仅指本次结构读取成功,不表示视觉、动画或业务已验收。 + +### 01. 启动、加载与游客登录 + +- 原入口与流程:旧 GameUI.utlmouseup case 177 → Func.visitorLogin;读取游客缓存或 simulatePlayerInfo → 登录数据准备 → Net.Send_login。GameUI.StartLoad / JumpWxAuth 管理显示。 +- 数据/配置:统一配置身份、游客账号、设备与环境来源;openid/unionid 必须齐全。 +- RPC/外部能力:player_login +- 关闭、错误及边界:原项目游客入口不等于微信授权入口;诊断场景仅验证本地 H5 游客,不覆盖生产/原生/其它登录。 +- Cocos 资源(存在):[Layer1_Logo](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab)、[Layer614_Loading](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab)、[Login_Layer](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab)。 +- Cocos 缺口/状态:诊断闭环已验收;普通 Login.scene 仍挂旧 LoginFlow,尚未切换为新 Runtime 正式入口。 +- 原代码依据:[11_GameUI.js:791](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:791)、[05_Func.js:1233](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/05_Func.js:1233)、[09_Net.js:122](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:122)。 + +### 02. 协议同意、微信/手机/快速登录 + +- 原入口与流程:case 248 切换 ProtocolIndex,249 打开协议,250/253 关闭;case 3 在同意协议后走 accreditlogin,否则提示。手机登录 case 3253 按 phoneType=1 调 Func.phoneLogin,3255 调 deviceLogin。 +- 数据/配置:ProtocolIndex、phoneType、设备登录缓存与原生授权返回值。 +- RPC/外部能力:认证来源最终进入 player_login;不能把所有入口当成相同游客按钮。 +- 关闭、错误及边界:本轮只追到来源/调用点;微信、QQ、原生授权全部分支与真实设备回调仍待专项验收。 +- Cocos 资源(存在):[Layer10_Protol](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protol.prefab)、[Layer18_Auth](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer18_Auth.prefab)、[Login_Layer](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab)、[Layer29_PhoneInfo](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer29_PhoneInfo.prefab)。 +- Cocos 缺口/状态:诊断 View 主动隐藏微信、手机、QQ、快速登录等节点;协议勾选与授权 UI 尚无现代接线。 +- 原代码依据:[11_GameUI.js:612](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:612)、[11_GameUI.js:1309](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:1309)、[11_GameUI.js:2560](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2560)。 + +### 03. 大厅导航、入口开关与玩家头部 + +- 原入口与流程:旧菜单按 versionState、LaunchMode、agentmode、hallConfig/sysConfig 控制入口显隐;设置 7、任务 17、分享 14、个人信息 9 等经统一 spid 事件分发;菜单进入后更新公告和按钮。 +- 数据/配置:C_Player 资料和资产;功能开关、审核版本、入口配置、公告来源。 +- RPC/外部能力:导航本身不发统一 RPC,进入具体功能后才发对应请求。 +- 关闭、错误及边界:按钮有美术节点,不代表在所有模式下都应显示或可点。 +- Cocos 资源(存在):[Layer4_MainMenu](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab)、[Layer6](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer6.prefab)、[Layer8_Notice](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer8_Notice.prefab)。 +- Cocos 缺口/状态:MainMenu.scene 不存在;Layer4 未挂业务组件。新 lobby PageModel 只有 self 的 playerid/nickname/avatar/bean,诊断只显示文字结果,不是完整大厅。 +- 原代码依据:[11_GameUI.js:3781](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:3781)、[11_GameUI.js:3915](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:3915)、[11_GameUI.js:4832](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:4832)。 + +### 04. 个人资料与个性签名 + +- 原入口与流程:case 9 → OpenInfo_hall 显示昵称、ID、IP、地区、积分、豆豆与签名;116 关闭。3156 → openTextInput → setSignCallBack → Send_setSign;回包 C_Player.setSign 刷新签名。 +- 数据/配置:资料来自 C_Player;输入回调来自 GameData.textCallback;完整资料不等于现有四字段 PlayerModel。 +- RPC/外部能力:setSign;打开本人资料本身复用已登录数据。 +- 关闭、错误及边界:签名编辑取消、长度限制、回包前后状态;旧空签名占位语义需在来源/显示契约明确,不能复制兜底。 +- Cocos 资源(存在):[Layer17_PlayerInfo](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer17_PlayerInfo.prefab)、[Layer6](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer6.prefab)、[Layer601](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer601.prefab)。 +- Cocos 缺口/状态:资料弹窗、文本输入、签名命令与响应均未接入;两个旧同名 PlayerInfoView 都不是该弹窗完整实现。 +- 原代码依据:[11_GameUI.js:4545](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:4545)、[11_GameUI.js:2350](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2350)、[12_Logic.js:2224](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/12_Logic.js:2224)、[09_Net.js:739](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:739)。 + +### 05. 设置与退出登录 + +- 原入口与流程:case 7 → OpenSetting(0),43/44/357/388 调音乐、音效、摇动、语音开关;Logic.TurnOn/Off 写设置并驱动能力。58 清登录/设备缓存并回授权页,来自大厅时提示去大厅退出。 +- 数据/配置:Utl.Config.bgm/sound/shake/voice、LaunchMode、hallConfig.logoffCallback。 +- RPC/外部能力:本地设置无对应业务 RPC;退出含原生回调条件。 +- 关闭、错误及边界:OpenSetting(mode=1) 含退房/解散行为,单列房间后置。case 47 在同一 switch 重复出现,不能按第二处标签判断必走 CloseSetting。 +- Cocos 资源(存在):[Layer609_Setting](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer609_Setting.prefab)。 +- Cocos 缺口/状态:无设置 Presenter、持久化接线、开关事件及现代退出登录生命周期。 +- 原代码依据:[11_GameUI.js:595](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:595)、[11_GameUI.js:701](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:701)、[11_GameUI.js:4201](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:4201)、[12_Logic.js:1094](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/12_Logic.js:1094)。 + +### 06. 更多、公告和通知 + +- 原入口与流程:case 5 → OpenBoard;openPallet/closePallet 管理更多托盘;updateNoticeBoard 更新菜单公告。77/80 关闭通知,但配置允许时转而拉世界房间列表。broadcast 按 msgtype 分配即时消息或滚动公告。 +- 数据/配置:配置公告、通知选择状态与服务器 msgtype/msgcontent。 +- RPC/外部能力:broadcast;关闭通知可能触发 get_share_room(房间后置)。 +- 关闭、错误及边界:配置公告、推送公告、系统通知不是同一个数据源;通知关闭存在条件分支。 +- Cocos 资源(存在):[Layer8_Notice](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer8_Notice.prefab)、[Layer9_Feedback](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab)、[Layer402_GameNotice](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer402_GameNotice.prefab)。 +- Cocos 缺口/状态:无公告队列、滚动驱动、通知选择/翻页与更多入口接线。 +- 原代码依据:[11_GameUI.js:624](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:624)、[11_GameUI.js:666](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:666)、[11_GameUI.js:3356](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:3356)、[11_GameUI.js:3578](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:3578)、[11_GameUI.js:4357](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:4357)、[07_Desk.js:1114](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1114)。 + +### 07. 反馈意见 + +- 原入口与流程:15 设置 feedMode=1 → OpenFeedback;550 取输入,非空时先 CloseFeedback 再 submit_opinion;回包 state=0 成功提示,否则失败提示。548 关闭,CloseFeedback 失焦、清输入、隐藏 group-74。 +- 数据/配置:身份来自 GameData/C_Player,content 来自输入551。 +- RPC/外部能力:submit_opinion +- 关闭、错误及边界:原逻辑发送前关闭并清空输入;失败不会自动恢复草稿。不能宣称原实现已有完整忙碌态或重试保稿。 +- Cocos 资源(存在):[Layer9_Feedback](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab)。 +- Cocos 缺口/状态:没有输入控件、反馈命令、成功/失败回显、草稿和重复提交策略。 +- 原代码依据:[11_GameUI.js:630](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:630)、[11_GameUI.js:652](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:652)、[11_GameUI.js:6211](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:6211)、[07_Desk.js:1102](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1102)。 + +### 08. 邀请码查询与绑定 + +- 原入口与流程:235 → get_player_invitecode → 回包 setInvitecod 并 OpenBind;366 粘贴过滤数字;367 读取输入365,关闭后 binding_invitecode;362 关闭。OpenBind 按已绑定状态隐藏输入或显示结果。 +- 数据/配置:agentid/playerid/openid/unionid 用于查询;绑定请求只带其协议字段与 invitecode。 +- RPC/外部能力:get_player_invitecode、binding_invitecode +- 关闭、错误及边界:已绑定/未绑定、剪贴板异步路径、空输入与服务器失败需独立核对;原长度检查曾被注释。 +- Cocos 资源(存在):[Layer16_Bind](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer16_Bind.prefab)。 +- Cocos 缺口/状态:无输入/粘贴/已绑定状态呈现,也无现代命令和回包处理。 +- 原代码依据:[11_GameUI.js:1342](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:1342)、[11_GameUI.js:1359](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:1359)、[11_GameUI.js:1375](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:1375)、[11_GameUI.js:6591](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:6591)、[09_Net.js:602](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:602)。 + +### 09. 任务列表、分享任务与领奖 + +- 原入口与流程:17 → get_player_task → Desk 写 TaskInfo → OpenTask 动态复制任务卡,按 state=0/1/其它显示进度、领取、已领取;滑动时抑制点击。去做任务关闭任务并打开分享;sharesuccess 条件满足发送 player_finish_task;get_task_award 回包改匹配任务状态并关闭重建列表。 +- 数据/配置:TaskInfo 的 taskid/title/award/finish/total/state,C_Player.taskstate 与 ConstVal.ShareTaskId。 +- RPC/外部能力:get_player_task、player_finish_task、get_task_award;refresh_task_state/can_award 有旧缺陷。 +- 关闭、错误及边界:CloseTask 清动态项;空列表、total=0、重复领奖均不能凭静态卡片推定正确。旧 UI 领奖构造未带 gameid,与 02 章字段描述存在差异,联调前核对服务器,勿消费端补值。 +- Cocos 资源(存在):[Layer26_Task](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer26_Task.prefab)、[Layer22_Share](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer22_Share.prefab)。 +- Cocos 缺口/状态:无任务列表模型、滚动/动态项、领取状态机、分享成功回调接线。 +- 原代码依据:[11_GameUI.js:727](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:727)、[11_GameUI.js:748](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:748)、[11_GameUI.js:4637](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:4637)、[07_Desk.js:942](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:942)、[06_Player.js:424](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/06_Player.js:424)、[06_Player.js:499](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/06_Player.js:499)。 + +### 10. 财富榜与 VIP 排行榜 + +- 原入口与流程:604/3233 按 hallConfig.rankList=1 打开已有榜单,=2 请求 get_treasurelist;Desk 写 GameData.rankList 后 openRankList。3231/3232 请求 getVipRankList(limit),回包同一资源以 true 模式显示;605 关闭。 +- 数据/配置:rankList 开关、sysConfig.vipRankLimit、服务器 list。 +- RPC/外部能力:get_treasurelist、getVipRankList +- 关闭、错误及边界:模式、条目数量、排序/展示字段和空数据需按服务端实际列表验证;短号场榜单另列房间管理。 +- Cocos 资源(存在):[Layer21_RankList](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer21_RankList.prefab)。 +- Cocos 缺口/状态:无列表请求、数据模型、动态行、模式切换和关闭生命周期。 +- 原代码依据:[11_GameUI.js:1853](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:1853)、[11_GameUI.js:2540](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2540)、[11_GameUI.js:7628](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:7628)、[07_Desk.js:1225](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1225)、[07_Desk.js:1330](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1330)。 + +### 11. 战绩列表、分页与详情 + +- 原入口与流程:原模板的 gameCombat 位于 01_SubGame_modify.js;84/85 切查询类型,104/105 上下页,55 分级返回、61 关闭。Send_get_player_grade1 构造分页条件;回包解析 gradeinfo/gameinfo1,记录首末 idx,打开战绩。 +- 数据/配置:CombatInfo、CombatCount、combatData 类型与分页游标。 +- RPC/外部能力:get_player_grade1;get_player_grade2 仅有透传发送和空接收体。 +- 关闭、错误及边界:目录叫 SubGame 不等于要选择真实子游戏;本项仅核对原模板自带战绩。具体查牌/回放内容与真实游戏耦合的部分后置。 +- Cocos 资源(存在):[Layer12_CombatLayer](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer12_CombatLayer.prefab)、[Layer13](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer13.prefab)。 +- Cocos 缺口/状态:无现代战绩命令、分页、列表/详情 View;不能把 grade2 算成旧前端完整功能。 +- 原代码依据:[01_SubGame_modify.js:310](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/01_SubGame/01_SubGame_modify.js:310)、[01_SubGame_modify.js:737](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/01_SubGame/01_SubGame_modify.js:737)、[01_SubGame_modify.js:777](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/01_SubGame/01_SubGame_modify.js:777)。 + +### 12. 充值入口、商品金额与确认支付 + +- 原入口与流程:10/286/508 等入口按版本、agentmode、OS、androidPay、TopUpUrl/channelKey 分流外部页、原生或 get_paylist;回包 payList → OpenPay;选择商品与渠道 → OpenCheckPay;金额面板和确认面板分别关闭。 +- 数据/配置:渠道/市场身份、支付配置、payList、payData、原生回调,不能使用测试占位数据。 +- RPC/外部能力:get_paylist;pay_succ 为 HTTP;原生 yPaytype 还可能上行 update_roomcard/update_bean。 +- 关闭、错误及边界:商品查询成功不等于支付成功;下单、取消、回调与资产推送分开验收。 +- Cocos 资源(存在):[Layer11_ChargeInfo](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer11_ChargeInfo.prefab)、[Layer14_Pay](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer14_Pay.prefab)。 +- Cocos 缺口/状态:只有资源,无商品/支付状态、渠道分流与原生回调的现代界面接线。 +- 原代码依据:[11_GameUI.js:919](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:919)、[11_GameUI.js:6362](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:6362)、[09_Net.js:566](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/09_Net.js:566)、[05_Func.js:2182](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/05_Func.js:2182)、[05_Func.js:2236](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/05_Func.js:2236)。 + +### 13. 兑换码(复用反馈面板) + +- 原入口与流程:3264 设置 feedMode=2 → OpenFeedback(true);3265 粘贴;3266 非空后关闭并发 topup_card。Desk.topup_card 为空体。 +- 数据/配置:cardno 来自输入,不是反馈 content;资产结果依赖后续推送。 +- RPC/外部能力:topup_card、update_bean/update_roomcard +- 关闭、错误及边界:不能仅因面板关闭就判兑换成功;反馈/兑换共享资源,按钮模式不能互相串用。 +- Cocos 资源(存在):[Layer9_Feedback](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab)。 +- Cocos 缺口/状态:无兑换模式、事件绑定或兑换结果与资产更新接线。 +- 原代码依据:[11_GameUI.js:2648](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2648)、[11_GameUI.js:2656](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2656)、[07_Desk.js:1366](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1366)。 + +### 14. 仓库入口、页签与初始密码 + +- 原入口与流程:3057 → openWareHouse 初始化输入,按 bankpwd 与 hallConfig.wareHouse 切密码/存取/赠送页;3059/3060 切页。3043 在初始化分支验证两次密码一致后 set_bankpwd;成功 setBankPwd(1) 并关仓库,showerror 时提示。 +- 数据/配置:bankpwd 表示是否初始化;wareHouse 的 page/selectType 和输入状态。 +- RPC/外部能力:set_bankpwd +- 关闭、错误及边界:3029/97 关闭全部 group-91/92/93/94;密码与敏感字段不得放诊断输出。 +- Cocos 资源(存在):[Layer25_wareHouse](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer25_wareHouse.prefab)。 +- Cocos 缺口/状态:无页签状态机、输入、密码设置及失败显示。 +- 原代码依据:[11_GameUI.js:1959](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:1959)、[11_GameUI.js:2019](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2019)、[11_GameUI.js:8117](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:8117)、[07_Desk.js:1230](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1230)。 + +### 15. 仓库存入与取出 + +- 原入口与流程:3034/3035 选存取,3038/3039 选数量/密码输入,3043 校验后以 mode=safeInputType-1 发 change_star;成功 star1 更新豆豆、star2 更新仓库星星,可回填 count/msg;失败按 showerror 提示。 +- 数据/配置:C_Player.bean、仓库余额、initBean、safeInputCount/password;下游不猜资产语义。 +- RPC/外部能力:change_star +- 关闭、错误及边界:旧代码包含保留赠送金币等数量约束;最终迁移须厘清权威来源,不直接搬运静默修补分支。 +- Cocos 资源(存在):[Layer25_wareHouse](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer25_wareHouse.prefab)。 +- Cocos 缺口/状态:无余额投影、输入限制、提交状态或双资产一致刷新。 +- 原代码依据:[11_GameUI.js:2045](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2045)、[07_Desk.js:1240](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1240)、[11_GameUI.js:8117](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:8117)。 + +### 16. 仓库查询目标玩家与赠送 + +- 原入口与流程:3087/3088/3089 录入目标ID/数量/密码;3096 校验目标ID长度后清旧头像/昵称并 query_player2。回包写 pImg/pNickName/pId,昵称或头像有值时刷新,否则提示未找到。3043 赠送分支发 giveCoin;成功用 star2 更新仓库星星并关闭。 +- 数据/配置:目标玩家ID与当前登录 playerid 区分;query_player2 是嵌在仓库中的查询,不是原项目独立玩家搜索页。 +- RPC/外部能力:query_player2、giveCoin +- 关闭、错误及边界:查询与赠送是不同操作;需防旧查询回包覆盖新目标,查询成功不授权自动赠送。原代码是否处理并发未证明。 +- Cocos 资源(存在):[Layer25_wareHouse](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer25_wareHouse.prefab)。 +- Cocos 缺口/状态:无目标资料 View、查询命令、过期响应处理或赠送流程。 +- 原代码依据:[11_GameUI.js:2000](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2000)、[11_GameUI.js:2119](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2119)、[07_Desk.js:1370](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1370)、[07_Desk.js:1383](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1383)、[11_GameUI.js:8789](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:8789)。 + +### 17. 手机号绑定与验证码面板 + +- 原入口与流程:3256 未绑定时 phoneType=2 → openPhoneBind;3243/3244 打开数字输入,3253 非空后 binding_phone;回包关闭并 setTel。3245 当前只提示通过相关渠道获取,发送微信验证码的代码被注释;3251/3241 关闭。 +- 数据/配置:phoneNumber/phoneCode、缓存验证码剩余时间、C_Player.tel、phoneType。 +- RPC/外部能力:binding_phone;send_phone_checkcode/send_phone_code_wechat 不能据函数存在认定有活跃界面发送。 +- 关闭、错误及边界:同一面板还承担手机登录;倒计时恢复与关闭后的定时器清理需专项验证。 +- Cocos 资源(存在):[Layer29_PhoneInfo](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer29_PhoneInfo.prefab)。 +- Cocos 缺口/状态:无 EditBox/数字输入绑定、倒计时、登录/绑定分流和回包关闭逻辑。 +- 原代码依据:[11_GameUI.js:2550](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2550)、[11_GameUI.js:2616](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2616)、[11_GameUI.js:9654](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:9654)、[07_Desk.js:1339](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1339)。 + +### 18. 分享、二维码与邀请外部能力 + +- 原入口与流程:14 打开分享,74/75 调好友/朋友圈后关闭,背景/链接来自配置;二维码 Open_ewm/Close_ewm;另一 sharePannel 选择方式后调用 Func.sharefriend。分享成功回调再驱动任务,不等于点击即完成。 +- 数据/配置:Game_Config.Share、hallConfig.shareBgUrl/shareImgUrl、shareFriendInfo 与 WVJB 回调。 +- RPC/外部能力:分享动作本身为原生/外部接口;任务完成再发 player_finish_task。 +- 关闭、错误及边界:二维码内容来源、图片加载失败、取消分享与成功回调须分别验收;不输出渠道密钥。 +- Cocos 资源(存在):[Layer22_Share](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer22_Share.prefab)、[Layer9_Feedback](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab)、[Layer603](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer603.prefab)。 +- Cocos 缺口/状态:无现代分享/二维码 Presenter、动态图加载、原生结果到任务状态的 UI 接线。 +- 原代码依据:[11_GameUI.js:876](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:876)、[11_GameUI.js:4340](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:4340)、[11_GameUI.js:6199](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:6199)、[11_GameUI.js:2669](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2669)、[06_Player.js:424](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/06_Player.js:424)。 + +### 19. 通用提示、断线、踢下线与资产通知 + +- 原入口与流程:show_message → OpenTips,按时间关闭;broadcast 分即时与滚动;kick_server/kick_offline → OpenKick;资产回包/推送经 C_Player 与 UI 通知。OpenDisConnect/CloseDisConnect 管理断线面板。 +- 数据/配置:服务器提示/资产数据、连接状态与显式计时来源。 +- RPC/外部能力:show_message、broadcast、kick_server、kick_offline、update_roomcard、update_bean +- 关闭、错误及边界:弹层优先级、重复推送、关闭计时器、停止后不重连均是生命周期验收点。 +- Cocos 资源(存在):[Layer612_Tips](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer612_Tips.prefab)、[Layer613_RoomCardUpdate](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer613_RoomCardUpdate.prefab)、[Layer615_Reconnect](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer615_Reconnect.prefab)、[Layer616_Kick](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer616_Kick.prefab)、[Layer617_iMessage](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer617_iMessage.prefab)、[Layer402_GameNotice](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer402_GameNotice.prefab)。 +- Cocos 缺口/状态:核心 OverlayModel 已有 reconnect/denial/kicked/fatal;诊断使用自己的文字遮罩。原弹窗资源尚未接管,broadcast/show_message/资产推送等无现代处理链。 +- 原代码依据:[11_GameUI.js:5838](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:5838)、[11_GameUI.js:5920](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:5920)、[11_GameUI.js:5967](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:5967)、[11_GameUI.js:6165](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:6165)、[07_Desk.js:1109](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1109)。 + +### 20. 数字/文本输入、确认框与计算器 + +- 原入口与流程:openInputPanel/openTextInput 保存输入及回调;3155 执行 textCallback 后置空关闭,3159 取消;openCheck 依 checkType 执行对应动作,247/242/246 取消。showCal/hideCal 为本地工具。 +- 数据/配置:inputPanelData/textCallback/checkType 和各业务输入值。 +- RPC/外部能力:组件自身无统一 RPC;确认动作按所属业务发包。 +- 关闭、错误及边界:旧 checkType=8 被小程序进房和白名单删除复用;不能机械照搬。回调、输入模式、键盘事件必须在关闭/销毁时处理。 +- Cocos 资源(存在):[Layer602_inputPannel](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer602_inputPannel.prefab)、[Layer607_Cal](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer607_Cal.prefab)、[Layer601](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer601.prefab)、[Layer420_CheckFree](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer420_CheckFree.prefab)。 +- Cocos 缺口/状态:没有通用输入、确认框组件和业务回调所有权。资源图标不能替代实际输入控件。 +- 原代码依据:[11_GameUI.js:1217](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:1217)、[11_GameUI.js:1304](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:1304)、[11_GameUI.js:2335](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2335)、[11_GameUI.js:7945](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:7945)、[11_GameUI.js:9142](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:9142)。 + +### 21. 商城、背包、活动、帮助与邻居入口 + +- 原入口与流程:3016/3017/3018 跳 Store/Bag/Act 外部地址;帮助 OpenHelp 调 Game_Modify.onOpenHelp(290),关闭隐藏;showNeighbor 为配置相关入口。 +- 数据/配置:外部 URL/原生打开方式、邻居数据与具体游戏帮助内容。 +- RPC/外部能力:不等于本地平台 RPC 面板;无 RPC 的入口也需要界面迁移。 +- 关闭、错误及边界:帮助正文依赖子游戏钩子,留后;外部页服务不修改、不能伪造链接。邻居只定位到 group-88 的显示/隐藏,具体点击去向尚未展开。 +- Cocos 资源(存在):[Layer8_Notice](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer8_Notice.prefab)、[Layer24_Neighbor](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer24_Neighbor.prefab)、[Layer608_Help](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer608_Help.prefab)。 +- Cocos 缺口/状态:资源存在,但无对应入口、外部能力接线或帮助内容加载。 +- 原代码依据:[11_GameUI.js:1947](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:1947)、[11_GameUI.js:4728](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:4728)、[11_GameUI.js:8027](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:8027)、[12_Logic.js:2054](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/12_Logic.js:2054)。 + +### 22. 黑白名单与 VIP 管理(边界登记,后置) + +- 原入口与流程:3140/3235 按 whiteListManager 分原生管理页或 getPlayerWhiteList;白名单增改删先确认,回包写列表并重开;3214 查询黑名单,3194/3195 增删。短号榜、全体魅力、禁止选桌与房间/VIP 状态相连。 MCP group 对照确认:黑名单 group-104、短号榜 group-105 实际在 Layer7_ewm,不能按文件名把它当二维码页。 +- 数据/配置:whiteList/blackList、shortcode、snrRoomList 与管理配置。 +- RPC/外部能力:getPlayerWhiteList、optWhiteList、optBanList、getShortCodeRankList、setAllCharm、setVipForbidSelect;相关 room 列表 RPC 后置。 +- 关闭、错误及边界:虽然上行 route=agent,仍不自动列入非房间首批;不因位置在大厅就提前执行管理写操作。 +- Cocos 资源(存在):[Layer29_PhoneInfo](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer29_PhoneInfo.prefab)、[Layer28_SeniorOptions](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer28_SeniorOptions.prefab)、[Layer7_ewm](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer7_ewm.prefab)。 +- Cocos 缺口/状态:无现代管理面板接线;这里只登记资源和调用链,不验收房间管理。 +- 原代码依据:[11_GameUI.js:2305](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2305)、[11_GameUI.js:2370](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2370)、[11_GameUI.js:2480](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/11_GameUI.js:2480)、[07_Desk.js:1291](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1291)、[07_Desk.js:1400](G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/js/00_Surface/07_Desk.js:1400)。 + +## 后续实施需要共用的验收条件 + +- 入口与可见性:按同一份模式/配置来源决定入口;资源可见、可点击与权限/前置状态一致。 +- 请求:参数来自权威配置、登录 Store 或明确用户输入;单次操作发出预期包;查询目标与本人身份不混淆。 +- 响应:先校验契约,再更新权威状态、PageModel、实际控件;列表空、拒绝、网络失败、加载失败都可识别。 +- 生命周期:打开/关闭/再次打开不重复监听;未完成请求关闭后不误开弹窗;异步旧响应不覆盖新输入;销毁清理触摸、订阅和定时器。 +- 实际边界:纯 TS 测试与资源结构检查通过后,仍需真实 Creator 预览、真实服务器包和实际界面一致的证据;支付/原生外部流程单独验收。 + +## 尚未展开的专项范围 + +- 每个动态 spid、全部模式组合、拖动裁剪、原工程视觉/声音/动画细节,尚未逐事件运行验证;本轮为入口和数据链审计。 +- 微信/QQ/原生授权、客服电话/剪贴板/分享/支付、邻居去向与外部商城页的全分支;代码来源已定位的部分列在条目中,实际设备效果待对应阶段。 +- 战绩详情里的子游戏解释、回放与帮助正文;创建/加入/房间列表、房内设置、聊天语音、投票、对局与重连快照,留到房间/子游戏阶段。 +- 已发现的旧代码差异(领奖 gameid、refresh_task_state 缺常量、can_award 空缺接收方法、重复 case/确认类型、历史默认值)须逐项核对契约后处理;本轮未修复或替服务器补约定。 + +## 编辑器结构快照 + +以下为本次完整平台 prefab 清点,包含后置的房间资源。节点数含 prefab 根和 group。每项业务组件/按钮事件/输入/滚动/动画组件均为 0;不包含运行时动态绑定。 + +- [Login_Layer.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab):20 个节点;group-2(登录)。 +- [Layer10_Protol.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protol.prefab):6 个节点;group-37。 +- [Layer11_ChargeInfo.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer11_ChargeInfo.prefab):6 个节点;group-20。 +- [Layer12_CombatLayer.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer12_CombatLayer.prefab):31 个节点;group-7、group-8、group-9。 +- [Layer13.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer13.prefab):9 个节点;group-7、group-8。 +- [Layer14_Pay.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer14_Pay.prefab):46 个节点;group-53、group-54。 +- [Layer15_JoinRoom.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab):28 个节点;group-5。 +- [Layer16_Bind.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer16_Bind.prefab):9 个节点;group-80。 +- [Layer17_PlayerInfo.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer17_PlayerInfo.prefab):17 个节点;group-18。 +- [Layer18_Auth.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer18_Auth.prefab):11 个节点;group-57。 +- [Layer19_WorldRoomList.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer19_WorldRoomList.prefab):38 个节点;group-82、group-96、group-97。 +- [Layer1_Logo.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab):8 个节点;group-1、group-32。 +- [Layer202_PlayerHeadScore.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer202_PlayerHeadScore.prefab):72 个节点;group-43、group-44、group-45、group-46、group-47、group-48、group-49、group-50、group-51、group-52、group-73。 +- [Layer21_RankList.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer21_RankList.prefab):22 个节点;group-84。 +- [Layer22_Share.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer22_Share.prefab):8 个节点;group-11。 +- [Layer24_Neighbor.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer24_Neighbor.prefab):9 个节点;group-88。 +- [Layer25_wareHouse.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer25_wareHouse.prefab):73 个节点;group-91、group-92、group-93、group-94。 +- [Layer26_Task.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer26_Task.prefab):48 个节点;group-10。 +- [Layer27_CreateRoom.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer27_CreateRoom.prefab):21 个节点;group-4。 +- [Layer28_SeniorOptions.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer28_SeniorOptions.prefab):58 个节点;group-83。 +- [Layer29_PhoneInfo.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer29_PhoneInfo.prefab):41 个节点;group-106、group-108。 +- [Layer402_GameNotice.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer402_GameNotice.prefab):4 个节点;group-56。 +- [Layer403.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer403.prefab):3 个节点;group-81。 +- [Layer404_MainSceneButton.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer404_MainSceneButton.prefab):8 个节点;group-28。 +- [Layer405_MainButton.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer405_MainButton.prefab):8 个节点;group-21。 +- [Layer408_Interact.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer408_Interact.prefab):24 个节点;group-21、group-39。 +- [Layer409_ChatVoice.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer409_ChatVoice.prefab):22 个节点;group-76。 +- [Layer410_ChatText.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer410_ChatText.prefab):22 个节点;group-75。 +- [Layer411.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer411.prefab):8 个节点;group-81。 +- [Layer413_BtnPanel.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer413_BtnPanel.prefab):23 个节点;group-21、group-81、group-99、group-100。 +- [Layer416_PlayerInfo.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer416_PlayerInfo.prefab):19 个节点;group-77、group-38。 +- [Layer418_ChatPannel.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer418_ChatPannel.prefab):56 个节点;group-30。 +- [Layer420_CheckFree.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer420_CheckFree.prefab):9 个节点;group-36。 +- [Layer4_MainMenu.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab):34 个节点;group-3。 +- [Layer50_MainScene.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer50_MainScene.prefab):13 个节点;group-21。 +- [Layer6.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer6.prefab):56 个节点;group-103、group-3。 +- [Layer601.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer601.prefab):7 个节点;group-102。 +- [Layer602_inputPannel.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer602_inputPannel.prefab):21 个节点;group-87。 +- [Layer603.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer603.prefab):5 个节点;group-107。 +- [Layer606_Apply.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer606_Apply.prefab):34 个节点;group-29、group-32。 +- [Layer607_Cal.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer607_Cal.prefab):23 个节点;group-78、group-79。 +- [Layer608_Help.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer608_Help.prefab):9 个节点;group-19。 +- [Layer609_Setting.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer609_Setting.prefab):17 个节点;group-6。 +- [Layer612_Tips.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer612_Tips.prefab):8 个节点;group-32、group-89。 +- [Layer613_RoomCardUpdate.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer613_RoomCardUpdate.prefab):5 个节点;group-34。 +- [Layer614_Loading.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab):4 个节点;group-40。 +- [Layer615_Reconnect.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer615_Reconnect.prefab):6 个节点;group-35。 +- [Layer616_Kick.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer616_Kick.prefab):5 个节点;group-33。 +- [Layer617_iMessage.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer617_iMessage.prefab):6 个节点;group-41。 +- [Layer618.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer618.prefab):5 个节点;group-85、group-86。 +- [Layer619_BackHall.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer619_BackHall.prefab):3 个节点;group-55。 +- [Layer620_Record.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer620_Record.prefab):5 个节点;group-101。 +- [Layer7_ewm.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer7_ewm.prefab):33 个节点;group-104、group-105。 +- [Layer8_Notice.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer8_Notice.prefab):12 个节点;group-3、group-95。 +- [Layer9_Feedback.prefab](G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer9_Feedback.prefab):39 个节点;group-74、group-12、group-42、group-98。 + +场景组件: + +- Login.scene:LoginFlow_Node 上的旧 LoginFlow;未挂现代完整大厅控制器。 +- LocalPlatformLogin.scene:Host 上的 LocalPlatformLogin;游客/停止监听为运行时绑定。 +- scene-2d.scene:2 个节点,无业务组件;这是用户既有未提交资源,本轮未修改。 + +## 本轮文件保护与交付 + +- 全程主目录 Git 分支,未创建 worktree;未合并、提交或推送。 +- 本轮开始有 51 项未提交文件(原 50 项加上上一轮 RPC 清单);结束时核对其状态与 SHA256,保持不变。 +- 本轮只新增本文;无业务代码、测试、配置、scene/prefab/anim/meta 改动。未运行资源生成器或网络请求。