子游戏 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>
This commit is contained in:
joywayer
2026-06-23 23:04:57 +08:00
co-authored by Claude Opus 4.7
parent 18fd782156
commit 7be7a2abbc
5 changed files with 212 additions and 60 deletions
@@ -21,6 +21,16 @@ public final class SubGameViewController: UIViewController {
private let request: SubGameRequest
// MARK: -
//
// init SubGameDirectoryStore = request.gameDir =
// "1" msext gameController.m:1414 advance
// boot pipeline SubGameDownloader.ensureReady game_version >
// version.xml actualDirectory loadFileURL /
// AppDataWriter / srcIsloop assets/wav request.gameDir
private var effectiveGameDir: String
// MARK: - UI
private let bridgedWebView = BridgedWebView()
@@ -38,6 +48,8 @@ public final class SubGameViewController: UIViewController {
public init(request: SubGameRequest) {
self.request = request
// registerBridgeHandlers closure self
self.effectiveGameDir = SubGameDirectoryStore.current(forKey: request.gameDir)
super.init(nibName: nil, bundle: nil)
}
@@ -78,11 +90,18 @@ public final class SubGameViewController: UIViewController {
OpenSaomaHandler.register(on: bridge)
StartLocationHandler.register(on: bridge)
// assetsRoot = H5 msext gameController.m:364
// assetsRoot = H5 msext gameController.m:364
// closure effectiveGameDir boot pipeline
// srcIsloop
LocalAudioHandler.register(
on: bridge,
assetsRoot: SandboxPaths.subGameIndex(request.gameDir, request.gameStart)
.deletingLastPathComponent()
assetsRoot: { [weak self] in
guard let self else {
return SandboxPaths.caches
}
return SandboxPaths.subGameIndex(self.effectiveGameDir, self.request.gameStart)
.deletingLastPathComponent()
}
)
RemoteAudioHandler.register(on: bridge)
@@ -176,20 +195,21 @@ public final class SubGameViewController: UIViewController {
// resolve
// - lobbyResolvedgameId = bc.gameId AppDataWriter app_appversion
// msext NewRootVC result initinfo gameController
// - subGameResolvedgameId = H5 gameid game_zip URL
// - subGameResolvedgameId = H5 gameid game_version game_zip URL
// msext gameController.uplevel:
let (lobbyResolved, subGameZipURL) = try await resolveBoot()
let resolvedRequest = SubGameRequest(
gameDir: request.gameDir,
gameStart: request.gameStart,
downloadURL: subGameZipURL,
webData: request.webData
)
let (lobbyResolved, subGameResolved) = try await resolveBoot()
let subGameZipURL = subGameResolved.gameZip ?? ""
// 1. zip + + rename
// 1. zipmsext gameController.m:1239-1255
// - currentDir
// - + > advance +
// - currentDir
splash.update(text: "准备子游戏...", progress: nil)
_ = try await SubGameDownloader.shared.ensureReady(
request: resolvedRequest,
let outcome = try await SubGameDownloader.shared.ensureReady(
directoryKey: request.gameDir,
gameStart: request.gameStart,
remoteVersion: subGameResolved.gameVersion,
remoteZipURL: subGameZipURL,
onProgress: { [weak self] p in
Task { @MainActor in
self?.splash.update(
@@ -200,14 +220,20 @@ public final class SubGameViewController: UIViewController {
}
)
// ensureReady advance
// loadFileURL / AppDataWriter / srcIsloop closure
effectiveGameDir = outcome.actualDirectory
// 2. 4 app_*.jscontainerRole = .subGameBatteryMonitor / NetworkMonitor
// currentXxx
// resolvedVersion app_appversion
// msext result_state
// containerRole.dir effectiveGameDir app_gamedir = msext gamefilepath
// "xxx1" msext app_data.js:2160
let writer = AppDataWriter(
bundleConfig: .shared,
resolvedVersion: lobbyResolved,
containerRole: .subGame(name: request.gameStart, dir: request.gameDir)
containerRole: .subGame(name: request.gameStart, dir: effectiveGameDir)
)
try writer.writeInitial()
try writer.writeBattery(BatteryMonitor.shared.currentLevel)
@@ -215,17 +241,17 @@ public final class SubGameViewController: UIViewController {
// 3. H5allowingReadAccessTo subGameRoot
splash.update(text: "进入子游戏...", progress: nil)
let indexURL = SandboxPaths.subGameIndex(request.gameDir, request.gameStart)
let indexURL = SandboxPaths.subGameIndex(effectiveGameDir, request.gameStart)
bridgedWebView.webView.loadFileURL(
indexURL,
allowingReadAccessTo: SandboxPaths.subGameRoot(request.gameDir)
allowingReadAccessTo: SandboxPaths.subGameRoot(effectiveGameDir)
)
}
/// resolve
/// - lobby gameId = bc.gameId app_appversion msext result_state
/// - sub-game gameId = H5 gameid game_zip URLmsext gameController.uplevel:
private func resolveBoot() async throws -> (lobbyResolved: ResolvedVersion, gameZipURL: String) {
/// - sub-game gameId = H5 gameid game_version + game_zip URLmsext gameController.uplevel:
private func resolveBoot() async throws -> (lobbyResolved: ResolvedVersion, subGameResolved: ResolvedVersion) {
guard let cfg = await RemoteConfigClient.shared.current() else {
throw SubGameBootError.remoteConfigUnavailable
}
@@ -244,10 +270,17 @@ public final class SubGameViewController: UIViewController {
marketId: bc.market,
gameId: request.downloadURL // H5 url gameid
)
guard let zipURL = subGameResolved.gameZip, !zipURL.isEmpty else {
// game_zip URL SubGameDownloader
// URL ensureReady
let zip = subGameResolved.gameZip ?? ""
let localVer = LocalVersionReader.subGameVersion(
dir: SubGameDirectoryStore.current(forKey: request.gameDir),
start: request.gameStart
)
if zip.isEmpty && subGameResolved.gameVersion > localVer {
throw SubGameBootError.zipURLNotFound(gameId: request.downloadURL)
}
return (lobbyResolved, zipURL)
return (lobbyResolved, subGameResolved)
}
enum SubGameBootError: Error, CustomStringConvertible {