子游戏 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:
co-authored by
Claude Opus 4.7
parent
18fd782156
commit
7be7a2abbc
@@ -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
|
||||
) {
|
||||
// 【3】srcIsloop — 本地音频播放
|
||||
// 入参 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.xml):actualDir 末尾追加 "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: 0…1 进度,URLSession delegate 后台线程派发;UI 层负责 hop MainActor
|
||||
/// - Returns: `.alreadyExists` 命中缓存;`.downloaded(zipBytes:)` 完成下载
|
||||
/// - directoryKey: H5 传的 Gamedirectory 字面值,作为 UserDefaults 持久化 key
|
||||
/// - gameStart: H5 子游戏入口(msext 的 self.game_name,与 directoryKey 同名)
|
||||
/// - remoteVersion: 远端 ResolvedVersion.gameVersion(VersionResolver 算出的)
|
||||
/// - remoteZipURL: 远端 ResolvedVersion.gameZip 真实下载 URL
|
||||
/// - onProgress: 0…1 进度回调,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 自身写入 UserDefaults(msext 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,不 advance(advance 是升级路径专有)。
|
||||
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. 下载到 tmp。delegate 必须在 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)
|
||||
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:
|
||||
// - lobbyResolved(gameId = bc.gameId)→ 给 AppDataWriter 算 app_appversion 审核标志
|
||||
// 与 msext NewRootVC 算 result 后 initinfo 传给 gameController 等价
|
||||
// - subGameResolved(gameId = H5 传的 gameid)→ 拿真实 game_zip URL
|
||||
// - subGameResolved(gameId = 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. 准备子游戏 zip(msext 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_*.js(containerRole = .subGame)。BatteryMonitor / 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. 加载子游戏 H5(allowingReadAccessTo 给 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 下载 URL(msext gameController.uplevel: 等价)
|
||||
private func resolveBoot() async throws -> (lobbyResolved: ResolvedVersion, gameZipURL: String) {
|
||||
/// - sub-game 视角(gameId = H5 传的 gameid)给 game_version + game_zip URL(msext 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 [20]Phase 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 [3]srcIsloop 完整实现
|
||||
RemoteAudioHandler.register(on: bridge) // §3.1 [4][5]Phase 3.B/3.C/3.D stub
|
||||
|
||||
|
||||
Reference in New Issue
Block a user