Commit Graph
25 Commits
Author SHA1 Message Date
joywayerandClaude Opus 4.7 ce3fbb7106 getWebdata 反向调用:大厅 observer 改永久挂钩,对齐 msext 双发行为
之前大厅 / 子游戏 observer 都 lifecycle-tied(viewWillAppear setup /
viewWillDisappear teardown),sub-game 内 overlay backgameData pop 时
只有 sub-game H5 收到 getWebdata(大厅 observer 已 teardown)。这与 msext
"NewRootVC observer 永久 + gameController observer 永久" 的双发行为不一致。

大厅 H5 可能依赖"sub-game 内 overlay 操作触发自身刷新",破坏契约。

修复:
- WebContainerViewController.subGameReturnObserver 改"永久挂钩"
  - 加 subGameReturnObserverInstalled 标志,首次 setupExternalSubscriptions
    时 install 一次,后续永不解绑(msext NewRootVC.m:593 等价)
  - teardownExternalSubscriptions 不再触碰它
- SubGameViewController.subGameReturnObserver 保持 lifecycle-tied
  - 子游戏自身 backgameData pop:sub-game 已 teardown → 仅大厅触发(单发,msext 等价)
  - 子游戏内 overlay backgameData pop:sub-game.viewWillAppear 重新 setup
    → 大厅 + sub-game 双触发(双发,msext 等价)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 23:17:39 +08:00
joywayerandClaude Opus 4.7 7be7a2abbc 子游戏 zip 升级链路完整对齐 msext(持久化目录 + advance + 版本比对)
之前新外壳的 SubGameDownloader 只看 index.html 是否存在,缺这两个关键行为:
- gamefilepath 持久化(msext NSUserDefaults["game_name"] → gamefilepath)
- 远端 game_version > 本地 version.xml 时升级 + 目录末尾追加 "1"
  (msext gameController.m:1414 等价 [NSString stringWithFormat:@"%@1", gamefilepath])

本次完整补齐:

1. 新增 SubGameDirectoryStore:UserDefaults 持久化 directory → actualDir 映射
   - current(forKey:) / advance(forKey:) / saveInitialIfNeeded(forKey:)
   - nonisolated 让 actor / MainActor 都能直接调

2. SubGameDownloader 三分支决策(msext gameController.m:1239-1255 + uplevel: 等价):
   - 目录不存在(首装)→ 下载到 currentDir,不 advance
   - 目录存在 + 远端 > 本地 → advance + 下载到新目录
   - 远端 ≤ 本地 → 复用 currentDir
   API 改为按入参(directoryKey / gameStart / remoteVersion / remoteZipURL)调用,
   返回 Outcome.actualDirectory 暴露实际生效目录名。

3. LocalAudioHandler.register 的 assetsRoot 改 @MainActor closure
   srcIsloop 每次调用时取最新值,让子游戏升级(effectiveGameDir 末尾追加 "1")
   后下一次 srcIsloop 自动用新目录路径。

4. SubGameViewController 引入 effectiveGameDir:
   - init 时同步从 SubGameDirectoryStore 读初始值(首次=request.gameDir)
   - boot pipeline 内调 ensureReady,从 outcome 同步升级后的新目录名
   - loadFileURL / AppDataWriter / srcIsloop closure 全部走 effectiveGameDir
   - resolveBoot 返回 (lobbyResolved, subGameResolved) 完整 ResolvedVersion,
     给 ensureReady 用 gameVersion 做版本比对

5. WebContainerViewController 大厅侧 LocalAudioHandler 改 closure 形式(路径不变)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 23:04:57 +08:00
joywayerandClaude Opus 4.7 57d4d93781 SplashOverlay 抽公共组件 + 进度条配色统一
大厅 / 子游戏之前各有一份 private SplashOverlay:lobby 版含错误态 UI(拉远端
配置失败 splash 上提示重试 / 前往设置);sub-game 版只有 image + label +
progressView,配色规则不同(width 0.45 vs 0.7,progressTintColor 不设)。

抽到 Source/WebView/SplashOverlay.swift 共用组件:
- 含完整加载 UI + 错误态 UI(sub-game 不调 showError,但保留兜底)
- 进度条 progressTintColor = .black,trackTintColor = .systemGray5(实色浅灰)
  之前 lobby 用 .systemBlue + label.withAlphaComponent(0.15) 半透明轨道,
  黑色填充推过半透明轨道时视觉上像"颜色逐渐加深";改实色轨道后是干脆的
  "黑滑过灰",无加深错觉
- progressView.setProgress(animated: true) 保留补间,让快下载也能看到平滑增长

WebContainer / SubGame 删除各自的 private SplashOverlay 类定义,引用同一份。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 21:34:25 +08:00
joywayerandClaude Opus 4.7 e50dfb75af 子游戏跳转链路修复 + H5 音频统一可用
修复后子游戏 SwitchOverGameData 跳转完全可用,行为对齐 daoqi msext:

【zip URL 解析】H5 字段 gamedownloadurl 实为 gameid(msext gameController.m:544
注释"游戏ID")。RemoteConfigClient 新增 lastParsed 缓存 + current() getter;
SubGameViewController.resolveBoot 做两次 VersionResolver.resolve:
  - lobby 视角(gameId=bc.gameId)→ ResolvedVersion 传 AppDataWriter 算
    app_appversion 审核标志(msext result_state 等价)
  - sub-game 视角(gameId=H5 传的 gameid)→ 真实 game_zip 下载 URL

之前 SubGameDownloader 直接把 token 当 URL 用,URLSession 报 -1002
unsupported URL;AppDataWriter resolvedVersion 始终 nil 导致子游戏 app_appversion
永远 0、丢失审核切换能力。

【push 前停大厅背景音】AppCoordinator.showSubGame 节流通过后、push 前调
AudioPlayer.stopAllBackground(msext NewRootVC.m:549-552 等价)。

【H5 音频】
- AVAudioSession.setCategory(.playback) 在 AppDelegate 启动期设置,让原生
  + WKWebView 内 <audio> 都不受静音键影响(msext RootVC.m:1302 等多处等价)
- WKWebViewConfiguration.mediaTypesRequiringUserActionForPlayback = [],子游戏
  push 后立即 loadFileURL 自播背景音不再被 WebKit 静默拦截
- LocalAudioHandler.register 增 assetsRoot 参数,srcIsloop 按容器各自的 H5
  根目录拼 assets/wav/{src}(msext gameController.m:364 等价,大厅 / 子游戏
  音频文件在各自 zip 内不重叠)。修前所有路径都指向大厅根,子游戏读不到自己的
  音频文件,AVAudioPlayer 报 OSStatus 2003334207 (wht?
  kAudioFileUnsupportedFileTypeError)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 21:33:46 +08:00
joywayerandClaude Opus 4.7 be09e6e697 splash 进度条宽度从屏幕 45% 加长到 70%
原 0.45 倍宽度在横屏中央显得偏短,下载进度反馈不够明显。加长到 0.7 倍
更接近常见 app 启动进度条视觉。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 07:29:57 +08:00
joywayerandClaude Opus 4.7 1247c14351 splash 启动图改 scaleAspectFill 全屏覆盖(与 WebView 同款撤销黑边)
接上一 commit f4307cc(WebView 全屏铺满):splash 启动图也应保持一致策略,
否则启动瞬间(iOS 系统 LaunchScreen)和过渡期(SplashOverlay)仍会显示
左右大黑边,与 WebView 全屏的视觉不连续。

3 处 contentMode 从 scaleAspectFit 改为 scaleAspectFill:
- LaunchScreen.storyboard(iOS 启动瞬间)
- WebContainerViewController 内 SplashOverlay.imageView
- SubGameViewController 内 SplashOverlay.imageView

scaleAspectFill 行为:素材 1136×640(16:9)在现代 iPhone 横屏(19.5:9)
上高度撑满,宽度按比例放大→左右轻微裁切。启动图左右是空白底色,logo
+ 文字主体居中可见,裁切无影响。

等价 daoqi msext LaunchImage Asset Catalog 时代「启动图覆盖整屏」体感。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 07:26:15 +08:00
joywayerandClaude Opus 4.7 f4307ccc80 WebView 改全屏铺满(与 daoqi msext 同款),撤销 16:9 letterbox
用户反馈:现代 iPhone 横屏(19.5:9 比例)下 16:9 letterbox 出现明显
左右大黑边,与原项目视觉不一致。

调研 daoqi msext NewRootVC.m:256:实际用 CGRectMake(0, 0, DEVW, DEVH)
直接全屏 WKWebView,没有任何 letterbox。H5 viewport 自身处理设计稿
(1280×720)→ 设备实际宽高的伸缩。msext 上线多年用户已习惯此视觉。

改 WebContainerViewController + SubGameViewController 的 setupBridgedWebView:
- 去掉 16:9 aspect 约束 + 居中 + width/height max/fill 优先级方案
- 改为 top/bottom/leading/trailing 四 edge 直接撑满 view

文件头注释同步:从「16:9 letterbox」改为「全屏铺满」。
原 Design §6.3.5 的 letterbox 决策已被现网实测推翻,未来如某些 H5
页面需要恢复 letterbox 再单独抽 LetterboxLayout(YAGNI)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 07:23:34 +08:00
joywayerandClaude Opus 4.7 4ca4459ad5 启动期错误态精确分类:NWPath 区分真断网/权限拒,按钮语义动态切换
用户反馈:之前的文案统一映射 NSURLError code,没区分"真没网"和"权限
被拒"——两类 iOS 14+ 上都报 -1009,但 NWPath.unsatisfiedReason 里有
wifiDenied / cellularDenied 标记可精确区分。点击重试时也没有针对权限
被拒场景做特殊处理(重发请求被同样的权限决定拒绝,原地循环)。

改动:

1. **NWPath snapshot 精确分类**
   - 新增 BootErrorKind: noNetworkAccess / wifiDenied / cellularDenied /
     networkTimeout / networkCannotReach / localResourceMissing / unknown
   - 新增 currentPathSnapshot() async:临时 NWPathMonitor 等首次 update
     拿当前 path
   - classifyBootError 接 path 参数:先看 NWPath.unsatisfiedReason
     (iOS 14.2+)区分权限拒/真断网;fallback 才按 NSURLError code
   - runBootPipeline 的 catch 内 await snapshot,传入精确 kind

2. **按钮语义按 kind 切换**(同一个按钮承担双语义,UX 一致)
   - 权限拒(wifiDenied / cellularDenied)→ 按钮显示"前往设置",
     点击跳 UIApplication.openSettingsURLString
   - 其他(noNetworkAccess / timeout / unreachable / localResource)→
     按钮显示"重试",点击重跑 runBootPipeline
   - SplashOverlay.showError 加 actionTitle 参数,通过 UIButton.Configuration
     动态改 title

3. **didBecomeActive 自动重试**(行业最佳做法)
   - 权限拒分支跳设置时挂 UIApplication.didBecomeActiveNotification 监听
   - 用户在设置改完权限切回 app → 自动 runBootPipeline → 进入大厅
     无需用户再点任何按钮
   - 一次性 observer:触发后自动取消,避免重复
   - runBootPipeline 入口同时 stop NWPathMonitor + stop settingsReturnObserver
     保证多次重试干净

文案精确化:
- noNetworkAccess:"当前未联网 / 请检查 Wi-Fi 或蜂窝数据后重试"
- wifiDenied:"未授权使用无线局域网 / 请前往 iOS 设置 → 本应用 → 打开
  「无线数据」,授权后将自动重试"
- cellularDenied:"未授权使用蜂窝数据 / 请前往 iOS 设置 → 本应用 → 打开
  「无线数据」,或连接 Wi-Fi 后重试"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 07:19:50 +08:00
joywayerandClaude Opus 4.7 0e446df97a splash 进度条配色:systemBlue 填充 + .label 15% alpha 轨道
跟错误态"重试"主按钮 systemBlue 视觉一致;轨道用 .label 系统语义色
的 15% alpha,浅深模式自适应(浅模式淡灰、深模式淡白),在浅色启动图
上比默认 UIProgressView 灰条更显眼。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 07:13:13 +08:00
joywayerandClaude Opus 4.7 34f76e8824 启动期错误态 UX 调整:浅背景深色字,仅保留"重试"按钮
按用户反馈精简错误态 UI:
- splash 启动图是浅色背景(米白底 + logo + 蓝色文字),白色文字在上面
  看不清。错误态显示时改为 backgroundColor = .systemBackground(浅模式
  白、深模式黑),同时隐藏启动图 imageView,文字用 .label / .secondaryLabel
  系统语义色确保两种 appearance 下都对比清晰
- 去掉"前往系统设置"次按钮:仅保留蓝色填充主按钮"重试"
- "无法连接服务器"文案删去"或前往系统设置"尾句

"权限弹窗"诉求的实际实现:
重试按钮直接重跑 runBootPipeline → RemoteConfigClient.fetch →
URLSession.dataTask 重新发起请求。这正是 Apple 推荐的"让网络请求
自然触发权限弹窗"模式:
- 首次启动若系统网络权限弹窗未响应/被拒,重新发请求会再次触发系统
  对权限状态的评估,可能复弹
- 用户在 iOS 隐私管理里改了权限后回 app,点重试就能成功
- NWPathMonitor 也在静默监听,切到正常网络后无需点击即自动重试

不需要额外的"手动申请权限" API 代码(iOS 没有公开的 API 强制弹起
被拒绝的网络权限弹窗);现有「重新发请求」即是教科书做法。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 07:10:23 +08:00
joywayerandClaude Opus 4.7 5731588d33 启动期错误处理 v2:撤回降级,splash 错误态 + NWPathMonitor 自动重试
撤回上一 commit 74f6925 的"远端 config 失败就降级进大厅"路径——错过升级
判定 = 用户用旧 IPA 不被强制升级 = 业务风险。新策略:

**远端 config 失败 → 停留 splash 显示错误态**(不弹 modal alert / 不暴露原始 NSError)

行业主流方案:
1. SplashOverlay 扩展错误态 UI(隐藏 label / progress,显示 title +
   message + "重试" 主按钮 + "前往系统设置" 次按钮),通过 onRetry /
   onOpenSettings closure 与 controller 解耦
2. WebContainerViewController.presentBootError 按 BootErrorKind 分类给出
   友好文案(networkOffline / networkTimeout / networkCannotReach /
   localResourceMissing / unknown)
3. 启动期独立 NWPathMonitor,仅在"从无网变有网"时静默自动重试一次
   (初始 nil 状态不触发,避免死循环;用户切到 Wi-Fi 后无需手动操作)
4. runBootPipeline 重新进入时 stopBootRetryWaiting + splash.hideError
   保证多次重试干净(防止 monitor 泄漏)

进大厅的必要条件(恢复严格):
- ResourceUnzipper.ensureReady 成功(本地 H5 zip 就位)
- RemoteConfigClient.fetch 拿到正确 config(升级判定可执行)
- 本地 appVersion >= 远端(否则走 IPA 升级 modal alert)
- showmessage 为空(否则走运营 modal alert)
- LobbyZipUpgrader 完成(含跳过升级 / 真升级两路径)

业务流程信号(operationalMessage / ipaUpgradeRequired)继续用 modal alert
(这两类不是错误,是产品决策的弹窗永停);技术错误(网络/解压/写文件等)
统一进 splash 错误态 + 自动重试。

raw error 仍 print 到 Xcode console(含 NSURLErrorDomain code 等开发期
排查信息),用户视角永不暴露技术细节。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 06:47:04 +08:00
joywayerandClaude Opus 4.7 74f6925be3 启动期错误处理 UX 改造:优雅降级 + 友好文案,不暴露技术细节
修复用户反馈的体验问题:网络权限未同意 / 长期不响应权限弹窗时,
启动会弹出"启动失败" + 原始 NSError("Error Domain=NSURLErrorDomain
Code=-1009 ...大量技术细节..."),用户被吓懵。

按行业主流做法(参考微信/支付宝/网易云音乐启动期错误处理)改两件事:

1. **优雅降级:远端 config 失败 ≠ 启动失败**
   runBootPipelineSteps 把"拉远端 config + 升级判定"整段包 do/catch:
   - 业务流程信号(operationalMessage / ipaUpgradeRequired)正常抛出弹窗
   - 网络 / 解析 / H5 zip 下载任何失败 → 用 LocalVersionReader 构造
     fallback ResolvedVersion,跳过升级判定,直接加载本地 H5 大厅
   - splash 文案区分在线 "加载大厅..." / 离线 "离线加载..."
   - raw error 仍 print 到 Xcode console 便于排查

   理论依据:ResourceUnzipper.ensureReady 之后本地 H5 zip 已就绪,
   远端 config 仅用于升级判定,失败时本地大厅完全可用。

2. **友好文案 + 多按钮 alert(showRetryableAlert)**
   - 新增 BootErrorKind 分类:剥洋葱看 NSURLError code 映射到
     networkOffline (-1009) / networkTimeout (-1001) / networkCannotReach
     (-1003/-1004/-1005/-1009/-1018/-1019/-1020) / localResourceMissing /
     unknown
   - 每类配友好标题 + 正文("网络未连接"/"网络较慢"/"暂时无法连接服务器"
     而非 raw "Error Domain=...")
   - 按钮组合按类别:网络类含"去设置"(跳 UIApplication.openSettingsURLString)
     + 重试 + 稍后再说;本地资源类只重试 + 稍后再说
   - "重试" 不强制("稍后再说" 让用户自由退出,避免单按钮卡死)

结果:用户在断网 / 未授权 Wi-Fi / 测试域名不可达等场景下,splash 多停
约 7s(3 次重试 + 退避)后自动降级进入大厅,**不弹任何 alert**。仅当
本地资源也失效(极罕见)时才弹友好提示,永不暴露原始 NSError。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 03:12:33 +08:00
joywayerandClaude Opus 4.7 5f3a435180 Phase 6 commit C:backgameData pop + getWebdata 反向 callback
- 新增 Source/Bridge/Handlers/BackGameDataHandler.swift(仅 SubGameViewController 注册):
  - 调 AudioPlayer.stopAllBackground 停背景音
  - AppCoordinator.popSubGame(returningData:) → popViewController + 发 .subGameDidReturn
  - data 兼容 string / object(非字符串走 JSONSerialization 序列化透传)
  - 字面 cb "backgameData"(msext gameController.m:691-709 等价,
    WXApi 清理跳过 — 微信 SDK 待 Phase 4.E)
- AudioPlayer.stopAllBackground:无条件停背景音(msext 不看 type 直接 nil 行为)
- SubGameViewController.registerBridgeHandlers:挂上 BackGameDataHandler,
  注释更新 exitRoom/getVideoinfo/createRoom 推迟到 Phase 8
- WebContainerViewController:在 setupExternalSubscriptions 挂 .subGameDidReturn
  观察者 → bridge.call("getWebdata", .string(data)),teardown 时 removeObserver
  (生命周期与 battery/network/appservice 同一对,子游戏栈顶时大厅已 teardown
  避免双发)
- Plan §5.6.3 / 6.5 / 6.6 / §8 进度已勾选

至此 SwitchOverGameData → push 子游戏 → backgameData → pop + getWebdata
完整链路接通;子游戏视频房间(exitRoom/getVideoinfo/createRoom)留待 Phase 8。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 21:05:23 +08:00
joywayerandClaude Opus 4.7 68cf7140d7 Phase 6/7 stub:SwitchOverGameData + OpenurlTitleData
补齐 H5 桥 §3.1 [15][17]两个 stub handler,让 H5 调子游戏切换 / 弹层
入口时不再报 "no handler registered",避免业务流程卡住。完整流程留
Phase 6 / Phase 7 实施。

新增:
  - Source/Bridge/Handlers/SwitchOverGameHandler.swift §3.1 [17]stub
    cb "SwitchOverGameData"(驼峰首字母大写沿用 msext 字面)
    完整实现需 AppCoordinator + SubGameViewController + SubGameDownloader
    (Phase 6.4 落地)
  - Source/Bridge/Handlers/OpenurlTitleDataHandler.swift §3.1 [15]stub
    cb "OpenurlTitleData"
    完整实现需 OverlayViewController + window.settings polyfill + 3s 节流
    (Phase 7 落地);注意契约硬约束 "title " 末尾空格已在注释提示

WebContainer.registerBridgeHandlers 接入。

至此 H5 桥 §3.1 大厅可调的全部 16 项 handler(除 backgameData / createRoom
等子游戏专属 §H 4 项)都已注册(含完整实现 / stub 两种状态),
H5 启动 / 业务期任何 handler 调用都不会报 "no handler"。

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 20:14:31 +08:00
joywayerandClaude Opus 4.7 aa80aec18c Phase 4.A:登录+分享 stub + QQ 改走 URL Scheme 不依 SDK
按用户约定调整 Phase 4 方向:微信继续走 SDK(等项目方提供),QQ 分享
改用 URL Scheme 直接调起 QQ app,**不依赖 SDK**。

调研 daoqi/msext/Class/Utils/QQShareManager.m 发现已有完整的 URL Scheme
fallback 路径:
  - line 14: __has_include(<TencentOpenAPI/QQApiInterface.h>) 条件编译
  - line 231 注释: "Fallback: 使用 URL Scheme (mqqapi://) - 无需 SDK,
    直接调起 (Legacy)"
  - mqqapi://share/to_fri / mqqapi://share/to_qzone / mqqopensdkfriend://share
    等 schema 实现见 line 733/786/789

新增 handler stub(让 H5 启动 / 业务期调登录 / 分享不报 "no handler"):
  - Source/Bridge/Handlers/AccreditLoginHandler.swift
    accreditlogin cb "Response from accreditlogin"
    完整实现需 Phase 4.B 微信 SDK + WeChatManager.authorize() + WeChatAuth.exchangeForUser
    + bridge.call("sharelogin", 7 字段含大写 Province) 反向 callback
  - Source/Bridge/Handlers/FriendsShareHandler.swift
    friendsSharetypeUrlToptitleDescript cb "sharefriend"
    完整实现需 ShareCenter 策略分发 + sharesuccess 反向 callback

WebContainer.registerBridgeHandlers 接入。

文档调整:
  - Plan §2.3 阻塞表:QQ OpenSDK 标"不需要",改 URL Scheme 路径
  - Plan §5 Phase 4 前置:去掉 QQ OpenSDK 一行
  - Plan §5 Phase 4.2:完整改写为 URL Scheme 方案,含 LSApplicationQueriesSchemes
    清单 + URL 构造样例 + "调起即 success" 乐观策略说明
  - Plan §8 进度追踪:4.2 QQ SDK Vendor 标"不需要"
  - Plan §6.4 里程碑加 3 行(前两轮 commit hash 补 + 本次)
  - CLAUDE.md 依赖管理段加 "不接入 QQ SDK" 约定(与"不引入 CocoaPods"
    并列的负面清单)
  - Verification-Checklist Phase 4 章节:E.19/E.20 stub + K/L 待 4.B/4.C
    完整后填充验证项

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:19:23 +08:00
joywayerandClaude Opus 4.7 0602b9d4b6 Phase 3.A:srcIsloop 本地音频 + 3.B/3.C/3.D handler stub
3.A 本地音频完整落地(与 msext NewRootVC initJSdata 等价):

  Source/Audio/AudioPlayer.swift(@MainActor 状态机):
    - playOnce(url) 单次按钮音;buttonPlayers 数组保活池避免多次点击
      互相打断(duration + 0.5s 后清理)
    - loopBackground(url, type:) 循环背景音;numberOfLoops = -1 +
      backgroundType 记录用于同名停止
    - stopBackground(type:) 同名校验后才停止(msext 同款语义)

  Source/Bridge/Handlers/LocalAudioHandler.swift:
    - 注册 srcIsloop handler(@MainActor enum)
    - 入参 src + isloop,按 0/1/-1 分支调 AudioPlayer.shared 对应方法
    - audioFileURL 路径:{Caches}/{gamedir}/{gamestart}/assets/wav/{src}
      与 msext NewRootVC 路径约定 1:1(H5 zip 内 assets/wav 子目录)
    - audioFileURL 标 nonisolated 因 SandboxPaths 是 nonisolated 静态
    - cb 字面 "Response from srcIsloop" 严格保持

  Source/Bridge/Handlers/RemoteAudioHandler.swift(3.B/3.C/3.D stub):
    - prepareaudio + mediaTypeAudio 仅 cb 维持契约,无真实业务
    - 待 Phase 3.B(opencore-amr .a 接入)+ 3.C(七牛 SPM + token)+
      3.D(远端播放)完整后升级
    - 按 CLAUDE.md 原则 A:H5 调啥就得有人接,即使是 stub

WebContainerViewController.registerBridgeHandlers 接入:
  - LocalAudioHandler.register(完整实现)
  - RemoteAudioHandler.register(stub)

Plan §5 Phase 3.1 勾选;3.2 真机验证留待 H5 提供音频文件后做。

Verification-Checklist.md 新增 Phase 3 章节:
  - H. 本地音频(E.14-E.16 srcIsloop 三个 isloop 值)
  - I. prepareaudio / mediaTypeAudio stub(E.17 E.18)

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:11:34 +08:00
joywayerandClaude Opus 4.7 09f071e38f Phase 2.C:ExternalSubscriptions 生命周期管理(防双发预备)
按 Design §2.4.2 把 battery / network / appservice 三组外部事件源的订阅
挂钩与 VC 生命周期配对:viewWillAppear → setup,viewWillDisappear →
teardown。配套约束:未来 Phase 6 子游戏 push 上来时大厅自动 teardown,
避免桥事件双发(栈深 ≤ 2,§2.4.3)。

WebContainerViewController 改造:
  - 新增 setupExternalSubscriptions():
    * 幂等 start 3 个 monitor(NetworkMonitor / BatteryMonitor /
      AppLifecycleObserver)
    * 设 BatteryMonitor.onChange → evaluateJavaScript("window.app_getbattery=N")
      + bridge.call("getBattery", "%.2f")
    * 设 NetworkMonitor.onChange → evaluateJavaScript("window.app_getnetwork=N")
      + bridge.call("getnetwork", "1"/"2"/"3")
    * 设 AppLifecycleObserver.onBackground/Foreground → bridge.call("appservice",
      "1"/"2")
  - 新增 teardownExternalSubscriptions():
    * 解绑 onChange / on{Background,Foreground} = nil
    * 释放对 bridge/webView 的 closure 引用避免子游戏 push 后双发
    * monitor 不停(保持后台 currentXxx 状态新鲜),下次 setup 直接 resume
  - viewWillAppear / viewWillDisappear override 调 setup / teardown 配对
  - writeAppDataFiles 精简:只写首次值 + 启动 NetworkMonitor/BatteryMonitor
    (需要 currentXxx 读首次值);AppLifecycleObserver 启动挪到 setup

至此 Phase 2 全部完成(2.A 8 个 handler + 2.B 5 项反向 callback + 2.C
生命周期管理)。

BuildProject 通过

Plan §5 Phase 2.13 勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:02:35 +08:00
joywayerandClaude Opus 4.7 4986f36967 业务期 app_getbattery / app_getnetwork 改为 evaluateJavaScript 重新赋值
按用户指示调整变化时机制:

  启动期(保持):loadFileURL 之前写 4 个 app_*.js 文件,H5 同步
    <script src> 引入读到实际值

  业务期(调整):变化时**不重写文件**,改为
    webView.evaluateJavaScript("window.app_getbattery=N;") 直接重新赋值
    全局变量。原因:H5 已加载完后 <script src> 不会再 fetch,重写文件
    对当前页面无影响(仅影响下次 reload),改 evaluateJavaScript 即时
    生效 + 无文件 IO 开销。

  与 §3.2 反向 callback 双轨同时执行:
    - evaluateJavaScript 让 H5 业务期同步读 app_xxx 永远拿到最新值
    - bridge.call 让 H5 注册的 callback handler 收到事件主动响应

WebContainerViewController.writeAppDataFiles 改动:
  - BatteryMonitor.onChange:Task @MainActor await
    webView.evaluateJavaScript("window.app_getbattery=N;") + bridge.call
  - NetworkMonitor.onChange:同款 evaluateJavaScript + bridge.call
  - AppLifecycleObserver:appservice 无对应 app_* 全局变量,只走 bridge.call

文档同步:
  - Design §7.5.2 改为「两阶段」表述:启动期写文件 + 业务期 evaluateJavaScript
  - Design §7.5.4 接入点代码示例同步
  - Contract §4.2 app_battery.js / app_network.js 子节更新时机段
  - Contract §4.2 修订记录加一行
  - Plan §6.4 里程碑加一行 + 前两轮 commit hash 补充

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 18:50:53 +08:00
joywayerandClaude Opus 4.7 23b3a1fdad Phase 2.B:3 项反向 callback 落地(getBattery / getnetwork / appservice)
完成大厅业务期实时状态推送给 H5 的事件驱动链路。每次变化时同时(1)
重写对应 app_*.js 文件供下次 reload,(2)bridge.call 反向推送给当前
页 H5 — 双轨与原 msext 行为等价。

新增 Source/Resource/BatteryMonitor.swift:
  - @MainActor public final class BatteryMonitor,shared 单例 + 幂等 start()
  - 监听 UIDevice.batteryLevelDidChangeNotification + 启动期读首次值
  - currentLevel 同步快照(max(level, 0) 兜底模拟器 -1)+ onChange 回调
  - notification block 内 MainActor.assumeIsolated 安全跨到 main actor

新增 Source/Resource/AppLifecycleObserver.swift:
  - @MainActor public final class AppLifecycleObserver,shared 单例 + 幂等 start()
  - 监听 didEnterBackgroundNotification + willEnterForegroundNotification
  - onBackground / onForeground 双钩子;assumeIsolated 同上

WebContainerViewController.writeAppDataFiles 接入(替换 inline addObserver):
  - 3 个 monitor 启动统一聚合(NetworkMonitor + BatteryMonitor + AppLifecycle)
  - BatteryMonitor.onChange:writeBattery + bridge.call("getBattery", "%.2f")
  - NetworkMonitor.onChange:writeNetwork + bridge.call("getnetwork", "1"/"2"/"3")
  - AppLifecycle.onBackground/Foreground:bridge.call("appservice", "1"/"2")
  - 首次值改读 BatteryMonitor.currentLevel 而非 UIDevice 原始值

至此 Phase 2.B 全部完成(5 项反向 callback:getphoneinfo / getBattery /
getnetwork / appservice / shakeEnd 全部接好)。

剩 Phase 2.C ExternalSubscriptions 生命周期管理(防双发,栈深 ≥ 2 时
下层不发桥事件)。

BuildProject 通过

Plan §5 Phase 2.B 全部勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 18:45:03 +08:00
joywayerandClaude Opus 4.7 d38ff0f8c4 Phase 2.A:大厅 8 个简单 handler 落地 + BridgeData 加 nonisolated
闭合 Phase 1.17 联调最后一公里(解决 Xcode 日志 4 个 "no handler registered"
+ 提前落地 4 个无外部依赖的简单 handler)。

新增 7 个 handler 模块(Source/Bridge/Handlers/):
  - ClipboardHandler.swift           §3.1 [13][14]剪贴板 UIPasteboard.general
  - ShakeHandler.swift               §3.1 [7][8][9]摇一摇 + §3.2 [5]shakeEnd 反向
                                       canShake / canVoice 状态 + motionEnded 转发钩子
  - VoicePlayingHandler.swift        §3.1 [6]actor VoiceCenter.shared 开关位
  - DeviceInfoHandler.swift          §3.1 [21]+ §3.2 [1]getphoneInfo 大写 I 入、
                                       getphoneinfo 小写 i 出反向 callback
                                       DeviceInfoSnapshot 6 字段(IDFA 用 idfv 兜底,
                                       CLAUDE.md 不接 ATT/IDFA)
  - BrowserHandler.swift             §3.1 [16]UIApplication.open async(Task @MainActor
                                       异步打开,闭包先 cb 不等 open 完成)
  - OpenSaomaHandler.swift           §3.1 [22]空 stub(契约要求注册)
  - StartLocationHandler.swift       §3.1 [20]Phase 5 完整实现,Phase 2 stub

扩展 VibratorHandler.swift:
  - §3.1 [10]vibrator(原有)+ [11]repeatvibrator(msext RootVC.m:1820 沿用:
    实际单次 vibrate 不真循环)+ [12]canclevibrator

BridgeProtocol.swift BridgeData 加 nonisolated:
  - 项目默认 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 让 BridgeData 跨 actor
    访问被拦;asString / asDouble / asInt / asBool / asArray / asObject /
    subscript 全部标 nonisolated(纯值类型本就该是 isolation-free)
  - 影响所有 @Sendable async handler 闭包内的 data?.asXxx 访问

WebContainerViewController 接入:
  - 加 private let shakeHandler = ShakeHandler() 持有
  - registerBridgeHandlers() 注册 8 个 handler 模块
  - 加 canBecomeFirstResponder = true + viewDidAppear becomeFirstResponder
  - 加 motionEnded(_:with:) 转发到 shakeHandler.handleMotionEnded
    (motion 检测必需 first responder,否则不触发)

BuildProject 通过。重装 app 后 H5 启动应不再报 "no handler registered" 错误。

Plan §5 Phase 2.A 全部勾选(含 startlocation bonus stub + BridgeData
nonisolated 重构)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 18:38:00 +08:00
joywayerandClaude Opus 4.7 829cc8f825 代码层落地 AppDataWriter + NetworkMonitor + WebContainer 接入
按 Design §7.5 蓝图落地,让 H5 启动时能从沙盒读到实际渠道/启动/设备
值,闭合 Phase 1.17 联调"app_gameconfig 不正确"现象。

新增 Source/WebView/AppDataWriter.swift:
  - public struct AppDataWriter(@MainActor)
  - ContainerRole enum:.lobby / .subGame(name:, dir:)
  - writeInitial():写 app_data.js(12 项)+ app_gamesname.js(暂只写当前
    gameStart 一项,Phase 6 子游戏完整时扩为扫描已装列表)
  - writeBattery(level:):写 app_battery.js(var app_getbattery=N;)
  - writeNetwork(code:):写 app_network.js(var app_getnetwork=N;)
  - 字面严格对齐 msext:
    * 字符串单引号包裹 'value'
    * 数值无引号(version=1 / Launchtype=0 / getwifisignalLevel=1)
    * app_gamesname 用 "var  app_gamesname=new Array(...)"(var 后两空格)
    * 大小写硬约束:Launchtype L 大、getwifisignalLevel wifi 小 + signal/Level 区分
  - escape 转义反斜杠 / 单引号 / 换行(避免渠道字段含单引号导致 H5 JS 解析错)
  - Logger debug 输出文件路径 + 每个 key=value 一行,便于 §7.5.8 联调对账

新增 Source/Resource/NetworkMonitor.swift:
  - @MainActor public final class NetworkMonitor
  - NWPathMonitor 最简 wrap:currentCode 同步快照(默认 2 WiFi,启动后被
    首次 path 更新)+ onChange MainActor 回调
  - shared 单例 + 幂等 start()

WebContainer.runBootPipelineSteps 接入:
  - switch outcome 前提取 let resolved: ResolvedVersion,避免变量作用域
    限制 case 内
  - 新增 writeAppDataFiles(resolved:):启动 NetworkMonitor + 开 battery
    监控 + 写 4 个文件首次值 + 挂 addObserver(battery) + onChange(network)
  - 调用位置:step 5 LobbyZipUpgrader 之后、step 6 loadFileURL 之前
    (与 msext NewRootVC.initJSdata 等价时序)

BuildProject 通过

Plan §6.4 里程碑加一行。

Phase 1.17 联调验证:启动后 H5 console 应读到实际 app_channel /
app_gameconfig 等值,不再是 H5 zip 包内默认占位。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 12:44:45 +08:00
joywayerandClaude Opus 4.7 6c943e2924 Phase 1.15:VibratorHandler 单次振动 handler
新增 ylgamehall/Source/Bridge/Handlers/VibratorHandler.swift:
- 无状态 public enum VibratorHandler,static func register(on:)
- 入参忽略(参考原 msext RootVC.m:1816-1818:time 参数实际不读取)
- 副作用:AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
- responseCallback:.string("vibrator"),与契约 §3.1 [10]一致
- 模拟器无振动硬件、调用静默忽略;真机才能感知

WebContainerViewController.viewDidLoad 新增 registerBridgeHandlers()
单一聚合点,Phase 2+ 在此追加更多 handler,便于发现 / 调试。

repeatvibrator / canclevibrator 严格守住 Plan §5 Phase 2.2 范围,不
提前实现。

BuildProject 通过

Plan 进度已勾选(§5 Phase 1.15 + §8)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 04:02:33 +08:00
joywayerandClaude Opus 4.7 0ecfa4fb69 修复 SplashImage 方向:素材逆时针旋转 90° 改为真正横屏
源素材 docs/res/Res/Default-568h@2x~iphone.png 是 CgBI PNG,物理 640×1136
但画面内容是"横躺"在竖向容器里 —— msext 旧 LaunchImage 系统依赖 device
orientation 自动旋转,现代 LaunchScreen.storyboard 没有这个魔法,直接显示
会看到躺倒的画面(用户反馈:启动图的横竖方向错误)。

处理:
- 用 sips -r -90 -s format png 一次性把素材逆时针旋转 90° 输出为标准
  PNG(顺便去掉 CgBI 优化标志),物理像素 1136×640,覆盖到
  ylgamehall/Assets.xcassets/SplashImage.imageset/SplashImage@2x.png
- LaunchScreen.storyboard 中 <image> 的 width/height 从 640/1136 改为
  1136/640;UIImageView 的 contentMode 从 scaleAspectFill 改为
  scaleAspectFit。aspectFit 在比 16:9 更宽的现代横屏 iPhone(如 16 Pro
  ≈2.17:1)上左右补黑边(黑底无视觉违和),保画面完整不裁切 logo
- WebContainer 的 SplashOverlay 用同款 scaleAspectFit,避免启动 → 容器
  视觉过渡时出现裁切方式跳变
- BuildProject 通过

Plan 进度已勾选(§5 Phase 1.14.b + §8 同步修订)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 03:10:53 +08:00
joywayerandClaude Opus 4.7 f795957eb1 Phase 1.14.e:webView didFinish 后 0.3s 淡出 splash
把 WKNavigationDelegate.didFinish 里的 splash.isHidden = true 换成
UIView.animate(withDuration: 0.3) 渐变 alpha=0 + completion 内
removeFromSuperview,避免渲染瞬间硬切,也让 view 层级在大厅起来后
完全干净(无空持有的 splash 占位)。

BuildProject 通过

Plan 进度已勾选(§5 Phase 1.14.e + §8)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 02:51:25 +08:00
joywayerandClaude Opus 4.7 f247b81d6d Phase 1.14.d:WebContainerViewController(16:9 letterbox + Splash + 启动流水线)
新增 ylgamehall/Source/WebView/WebContainerViewController.swift:

- 持有 BridgedWebView(间接持 BridgeBus)+ 文件内私有 SplashOverlay
- 16:9 letterbox 布局:required(aspect 16:9 + width/height ≤ superview)
  + low(width/height = superview) + center{X,Y} 居中。屏幕比 < 16:9 → 上下
  黑边,> 16:9 → 左右黑边。Auto Layout 自动选短边贴边、长边居中
- SplashOverlay:SplashImage(scaleAspectFill) + UIProgressView + UILabel;
  update(text:progress:) 切换状态机
- runBootPipeline 串:ensureReady → "拉取配置中..." → RemoteConfigClient.fetch
  → 分支(.shortText / .parsed → showmessage / IPA 升级 / LobbyZipUpgrader
  with onProgress hop 到 MainActor) → "加载大厅..." → loadFileURL(lobbyIndex,
  allowingReadAccessTo: lobbyRoot)
- 三种 alert:showBlockingAlert(短文本/showmessage,永停)/
  showIPAUpgradeAlert(确定→openURL,永停)/ showFatalAlert(重试→重跑 pipeline)
- WKNavigationDelegate.didFinish 暂用 splash.isHidden = true(1.14.e 改为
  0.3s 淡出动画)
- 横屏锁定 + statusBar 显示(与 RootViewController 一致)

不接 SceneDelegate,1.16 才换 rootViewController;本次仅落地容器代码。
BuildProject 通过

Plan 进度已勾选(§5 Phase 1.14.d + §8)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 02:50:16 +08:00