子游戏 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
@@ -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
}
}