子游戏 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
@@ -22,9 +22,13 @@ public enum LocalAudioHandler {
/// srcIsloop handler
/// - Parameters:
/// - bridge:
/// - assetsRoot: lobby / subGame H5
/// `assets/wav/{src}`
public static func register(on bridge: any BridgeProtocol, assetsRoot: URL) {
/// - assetsRoot: lobby / subGame H5
/// srcIsloop zip effectiveGameDir "1"
/// `assets/wav/{src}`
public static func register(
on bridge: any BridgeProtocol,
assetsRoot: @escaping @MainActor @Sendable () -> URL
) {
// 3srcIsloop
// src : string {assetsRoot}/assets/wav/{src}
// isloop : int 0 / 1 / -1
@@ -39,7 +43,7 @@ public enum LocalAudioHandler {
return
}
let url = assetsRoot
let url = await assetsRoot()
.appendingPathComponent("assets")
.appendingPathComponent("wav")
.appendingPathComponent(src)
@@ -0,0 +1,44 @@
//
// SubGameDirectoryStore.swift
// ylgamehall
//
// H5 Gamedirectory gameStart
// daoqi msext gameController.m:1414 / 2183-2188
// - actualDir = directory NSUserDefaults
// - zip game_version > version.xmlactualDir "1"
// msext ""
//
// UserDefaults key = "ylgh.subGameDir.<directory>"value =
//
import Foundation
/// UserDefaults 线 nonisolated actor / MainActor / 线
public enum SubGameDirectoryStore {
private static let keyPrefix = "ylgh.subGameDir."
/// directory
/// msext NSUserDefaults key gamefilepath = gamename
nonisolated public static func current(forKey directory: String) -> String {
UserDefaults.standard.string(forKey: keyPrefix + directory) ?? directory
}
/// "1" UserDefaults
/// msext gameController.m:1414 `gamefilepath = [NSString stringWithFormat:@"%@1", gamefilepath]`
@discardableResult
nonisolated public static func advance(forKey directory: String) -> String {
let next = current(forKey: directory) + "1"
UserDefaults.standard.set(next, forKey: keyPrefix + directory)
return next
}
/// 访 directory msext gameController.m:2187
/// SaveDefaultInfo advance
nonisolated public static func saveInitialIfNeeded(forKey directory: String) {
let k = keyPrefix + directory
if UserDefaults.standard.string(forKey: k) == nil {
UserDefaults.standard.set(directory, forKey: k)
}
}
}
@@ -2,15 +2,21 @@
// SubGameDownloader.swift
// ylgamehall
//
// zip + msext gameController.m:1340-1401
// - `{Caches}/{gameDir}/{gameStart}/index.html`
// - URLSession staging rename
// zip + + daoqi msext gameController.m
//
// LobbyZipUpgrader //
// msext uplevel
// sub-game
// 1)
// rename `{Caches}/{currentDir}/`currentDir
// msext: line 1243-1245 + uplevel:YES downFileFromServer
//
// docs/Development-Plan.md §5.6 / §6.7
// 2) + game_version > version.xml
// advance directory "1" newDir UserDefaults
// rename `{Caches}/{newDir}/`
// msext: gameController.m:1410-1428 uplevel:download:
//
// 3) + currentDir
// msext: initView
//
// LobbyZipUpgrader + advance sub-game
//
import Foundation
@@ -21,10 +27,18 @@ public actor SubGameDownloader {
public static let shared = SubGameDownloader()
public enum Outcome: Sendable {
///
case alreadyExists
/// + zipBytes 便 /
case downloaded(zipBytes: Int64)
/// currentDir
case alreadyExists(actualDirectory: String)
/// + `fromVersion = 0`
case upgraded(actualDirectory: String, from: Int, to: Int, zipBytes: Int64)
/// H5 directory
public var actualDirectory: String {
switch self {
case .alreadyExists(let dir): return dir
case .upgraded(let dir, _, _, _): return dir
}
}
}
public enum DownloadError: Error, Sendable {
@@ -39,9 +53,8 @@ public actor SubGameDownloader {
public init() {}
/// URLSession 60s / 300s zip 1-10 MB
/// ensureReady config session download delegate
/// didWriteData ad-hoc delegate`session.download(from:delegate:)`
/// URLSessionDownloadDelegate download-specific session
/// config session download delegate didWriteData
/// ad-hoc delegate download-specific session
nonisolated private static var defaultConfig: URLSessionConfiguration {
let cfg = URLSessionConfiguration.default
cfg.timeoutIntervalForRequest = 60
@@ -49,27 +62,83 @@ public actor SubGameDownloader {
return cfg
}
/// `{Caches}/{request.gameDir}/{request.gameStart}/index.html`
/// H5
///
/// - Parameters:
/// - request: SwitchOverGameData
/// - onProgress: 01 URLSession delegate 线UI hop MainActor
/// - Returns: `.alreadyExists` `.downloaded(zipBytes:)`
/// - directoryKey: H5 Gamedirectory UserDefaults key
/// - gameStart: H5 msext self.game_name directoryKey
/// - remoteVersion: ResolvedVersion.gameVersionVersionResolver
/// - remoteZipURL: ResolvedVersion.gameZip URL
/// - onProgress: 01 URLSession delegate 线
/// - Returns: Outcome `actualDirectory` loadFileURL
public func ensureReady(
request: SubGameRequest,
directoryKey: String,
gameStart: String,
remoteVersion: Int,
remoteZipURL: String,
onProgress: @escaping @Sendable (Double) -> Void = { _ in }
) async throws -> Outcome {
// 访 directory UserDefaultsmsext gameController.m:2187
SubGameDirectoryStore.saveInitialIfNeeded(forKey: directoryKey)
let currentDir = SubGameDirectoryStore.current(forKey: directoryKey)
let fm = FileManager.default
let indexPath = SandboxPaths.subGameIndex(request.gameDir, request.gameStart).path
if fm.fileExists(atPath: indexPath) {
return .alreadyExists
let indexPath = SandboxPaths.subGameIndex(currentDir, gameStart).path
let indexExists = fm.fileExists(atPath: indexPath)
// 1 msext line 1242-1246
// currentDir advanceadvance
if !indexExists {
let zipBytes = try await download(
to: currentDir,
zipURLString: remoteZipURL,
onProgress: onProgress
)
return .upgraded(
actualDirectory: currentDir,
from: 0,
to: remoteVersion,
zipBytes: zipBytes
)
}
guard !request.downloadURL.isEmpty else {
// 2 / 3 version.xml
let localVer = LocalVersionReader.subGameVersion(dir: currentDir, start: gameStart)
guard remoteVersion > localVer else {
// 3 msext: initView
return .alreadyExists(actualDirectory: currentDir)
}
// 2 advance
// msext gameController.m:1414-1422
let newDir = SubGameDirectoryStore.advance(forKey: directoryKey)
let zipBytes = try await download(
to: newDir,
zipURLString: remoteZipURL,
onProgress: onProgress
)
return .upgraded(
actualDirectory: newDir,
from: localVer,
to: remoteVersion,
zipBytes: zipBytes
)
}
// MARK: - + + rename
/// zip staging rename `{Caches}/{actualDirectory}/`
///
private func download(
to actualDirectory: String,
zipURLString: String,
onProgress: @escaping @Sendable (Double) -> Void
) async throws -> Int64 {
guard !zipURLString.isEmpty else {
throw DownloadError.missingDownloadURL
}
guard let zipURL = URL(string: request.downloadURL) else {
throw DownloadError.badDownloadURL(request.downloadURL)
guard let zipURL = URL(string: zipURLString) else {
throw DownloadError.badDownloadURL(zipURLString)
}
// 1. tmpdelegate URLSession init
@@ -98,6 +167,7 @@ public actor SubGameDownloader {
}
onProgress(1.0)
let fm = FileManager.default
let zipBytes = (try? fm.attributesOfItem(atPath: downloadedURL.path)[.size] as? NSNumber)?
.int64Value ?? 0
@@ -113,21 +183,21 @@ public actor SubGameDownloader {
}
try? fm.removeItem(at: downloadedURL)
// 3. rename subGameRoot staging
// msext zip `{gameStart}/` staging
// move `{Caches}/{gameDir}/` `{gameDir}/{gameStart}/index.html`
let subGameRoot = SandboxPaths.subGameRoot(request.gameDir)
// 3. rename staging
// zip `{gameStart}/` staging move
// `{Caches}/{actualDirectory}/` `{actualDirectory}/{gameStart}/index.html`
let target = SandboxPaths.subGameRoot(actualDirectory)
do {
if fm.fileExists(atPath: subGameRoot.path) {
try fm.removeItem(at: subGameRoot)
if fm.fileExists(atPath: target.path) {
try fm.removeItem(at: target)
}
try fm.moveItem(at: staging, to: subGameRoot)
try fm.moveItem(at: staging, to: target)
} catch {
try? fm.removeItem(at: staging)
throw DownloadError.stagingMoveFailed(error)
}
return .downloaded(zipBytes: zipBytes)
return zipBytes
}
}
@@ -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 {
@@ -74,10 +74,11 @@ public final class WebContainerViewController: UIViewController {
StartLocationHandler.register(on: bridge) // §3.1 20Phase 5 Phase 2 stub
// Phase 3.A + 3.C/3.D stub
// assetsRoot = H5 srcIsloop assets/wav/{src}
// assetsRoot = H5 lobbyIndex lobby
// closure
LocalAudioHandler.register(
on: bridge,
assetsRoot: SandboxPaths.lobbyIndex.deletingLastPathComponent()
assetsRoot: { SandboxPaths.lobbyIndex.deletingLastPathComponent() }
) // §3.1 3srcIsloop
RemoteAudioHandler.register(on: bridge) // §3.1 45Phase 3.B/3.C/3.D stub