diff --git a/ylgamehall/RootViewController.swift b/ylgamehall/RootViewController.swift index 2383d98..9ea5a8d 100644 --- a/ylgamehall/RootViewController.swift +++ b/ylgamehall/RootViewController.swift @@ -95,6 +95,24 @@ final class RootViewController: UIViewController { agent[0].gamelist = \(first.gamelist?.count ?? 0) 个 """) } + + // Phase 1.11 烟雾测试:chulishengji 双子树合并 + let bc = BundleConfig.shared + let resolved = VersionResolver.resolve( + config: cfg, + agentId: bc.agent, + channelId: bc.channel, + marketId: bc.market, + gameId: bc.gameId + ) + print(""" + [VersionResolver] chulishengji 双子树合并结果: + appVersion = \(resolved.appVersion) + appDownload = \(resolved.appDownload ?? "") + gameVersion = \(resolved.gameVersion) + gameZip = \(resolved.gameZip ?? "") + showmessage = "\(resolved.showmessage ?? "")" + """) case .shortText(let msg): print(""" [RemoteConfigClient] 拉到短文本响应(耗时 \(String(format: "%.3f", elapsed))s) diff --git a/ylgamehall/Source/Network/VersionResolver.swift b/ylgamehall/Source/Network/VersionResolver.swift new file mode 100644 index 0000000..ac50d06 --- /dev/null +++ b/ylgamehall/Source/Network/VersionResolver.swift @@ -0,0 +1,249 @@ +// +// VersionResolver.swift +// ylgamehall +// +// 把远端 RemoteConfig 化简为 5 个目标字段(appVersion/appDownload/gameVersion/ +// gameZip/showmessage)的纯函数实现,照搬 msext chulishengji 双子树合并算法。 +// 详见 docs/Development-Plan.md ADR-008-D / Design §6.3.2 / msext NewRootVC.m:1372-1538。 +// + +import Foundation + +// MARK: - 决策输出 + +/// chulishengji 双子树合并后的 5 个目标字段。 +public struct ResolvedVersion: Sendable, Equatable { + /// 远端期望的原生 IPA 版本(与 BundleConfig.appVersion 比较)。0 表示远端未指定。 + public let appVersion: Int + /// IPA 升级跳转的 Safari URL。nil 表示远端未指定。 + public let appDownload: String? + /// 远端期望的 H5 zip 版本(与本地 version.xml /game/version@value 比较)。 + public let gameVersion: Int + /// H5 zip 下载 URL。nil 表示远端未指定。 + public let gameZip: String? + /// 运营公告。非空时阻塞后续升级判断(msext 杀手锏 #2)。 + public let showmessage: String? +} + +// MARK: - 内部:单条子树的提取结果 + +/// 单条子树(agent 子树 或 game 子树)4 层逐层覆盖后的 5 字段。 +private struct SubtreeExtract { + var appVersion: Int = 0 + var appDownload: String? + var gameVersion: Int = 0 + /// 注意字段名:agent 子树原项目用 `game_download`,game 子树用 `game_zip`, + /// 两者本质同一字段,新外壳两 key 都查(择一非空)。 + var gameZip: String? + var showmessage: String? +} + +// MARK: - VersionResolver + +public enum VersionResolver { + + /// chulishengji 双子树合并(msext `NewRootVC.m:1372-1538`)。 + /// + /// 1. 从 agent 子树提取(`agent → channel → market → game`,game 嵌在 market 命中后才遍历) + /// 2. 从 game 子树提取(`game → game-self → channel → market`) + /// 3. **IPA 合并**:默认 agent 赢;`gameConfig.appVersion > agentConfig.appVersion` 时 + /// game 赢(NewRootVC.m:1416) + /// 4. **zip 合并**:默认 game 赢;`agentConfig.gameVersion > gameConfig.gameVersion` 时 + /// agent 赢(NewRootVC.m:1456) + /// 5. **showmessage**:两条子树多层多次覆盖,最后写赢;若都为 nil,回退到顶层 `config.showmessage` + /// + /// - Parameters: + /// - config: 远端 RemoteConfig(Codable 解析后) + /// - agentId: 当前 BundleConfig.agent + /// - channelId: 当前 BundleConfig.channel + /// - marketId: 当前 BundleConfig.market + /// - gameId: 当前 BundleConfig.gameId + /// - Returns: 5 字段决策结果。任一字段缺失返回 0 / nil。 + public static func resolve( + config: RemoteConfig, + agentId: String, + channelId: String, + marketId: String, + gameId: String + ) -> ResolvedVersion { + // 找当前 agent + guard let agent = config.agentlist?.first(where: { $0.agentid == agentId }) else { + // agent 找不到匹配(极端情况):返回 0 / nil,让 WebContainer 走"无升级"路径 + return ResolvedVersion( + appVersion: 0, appDownload: nil, + gameVersion: 0, gameZip: nil, + showmessage: config.showmessage + ) + } + + // 找当前 agent 下的 game + let game = agent.gamelist?.first(where: { $0.gameid == gameId }) + + let agentExtract = extractAgentSubtree( + agent: agent, + channelId: channelId, + marketId: marketId, + gameId: gameId + ) + + let gameExtract: SubtreeExtract = game.map { g in + extractGameSubtree(game: g, channelId: channelId, marketId: marketId) + } ?? SubtreeExtract() + + let (appVer, appDL) = mergeIPA(agent: agentExtract, game: gameExtract) + let (gameVer, gameZip) = mergeZip(agent: agentExtract, game: gameExtract) + + // showmessage:game 子树 > agent 子树 > 顶层(任意非空就用,最后写赢) + let msg = gameExtract.showmessage + ?? agentExtract.showmessage + ?? config.showmessage + + return ResolvedVersion( + appVersion: appVer, + appDownload: appDL, + gameVersion: gameVer, + gameZip: gameZip, + showmessage: msg + ) + } + + // MARK: - agent 子树(getagentversion,NewRootVC.m:885-1013) + + /// agent 子树 4 层:agent → channel → market → game + /// (注意:game 嵌在 market 命中后才遍历 market.gamelist;与 game 子树的结构不同) + private static func extractAgentSubtree( + agent: Agent, + channelId: String, + marketId: String, + gameId: String + ) -> SubtreeExtract { + var r = SubtreeExtract() + + // 层 1:agent 自身 + applyOnto(&r, from: agent) + + // 层 2:channel + guard let channel = agent.channellist?.first(where: { $0.channelid == channelId }) + else { return r } + applyOnto(&r, from: channel) + + // 层 3:market + guard let market = channel.marketlist?.first(where: { $0.marketid == marketId }) + else { return r } + applyOnto(&r, from: market) + + // 层 4:game(嵌在 market 命中后才遍历,msext 历史结构) + // 注:Market 模型当前不带 gamelist 字段,原项目这层在 channel 节点的 marketlist + // 内部又嵌了一份 gamelist。如服务端有这层数据需要扩展 Market.gamelist。 + // 当前 RemoteConfig 模型未声明该字段,暂跳过——若上线时发现该层有版本数据需补。 + + return r + } + + // MARK: - game 子树(getgameversion,NewRootVC.m:1015-1180) + + /// game 子树 4 层:game → game-self → channel → market + /// (第 2 层是把 game 自身当作 infotwo 再嵌一遍 channellist;msext 那段曾经的 + /// agentid 判断被注释掉了,等价于直接走 game.channellist) + private static func extractGameSubtree( + game: Game, + channelId: String, + marketId: String + ) -> SubtreeExtract { + var r = SubtreeExtract() + + // 层 1:game 自身 + applyOnto(&r, from: game) + + // 层 2:game-self(理论上同 game.channellist,等价于层 3 入口,故略) + + // 层 3:channel + guard let channel = game.channellist?.first(where: { $0.channelid == channelId }) + else { return r } + applyOnto(&r, from: channel) + + // 层 4:market + guard let market = channel.marketlist?.first(where: { $0.marketid == marketId }) + else { return r } + applyOnto(&r, from: market) + + return r + } + + // MARK: - 字段级合并 + + /// IPA 合并:默认 agent 赢;game 版本号更大时 game 赢(NewRootVC.m:1408-1447) + private static func mergeIPA( + agent: SubtreeExtract, + game: SubtreeExtract + ) -> (Int, String?) { + // 都有:取较大版本对应的下载链 + if agent.appDownload != nil && game.appDownload != nil { + if game.appVersion > agent.appVersion { + return (game.appVersion, game.appDownload) + } + return (agent.appVersion, agent.appDownload) + } + // 仅一边:取那边 + if let dl = agent.appDownload { + return (agent.appVersion, dl) + } + if let dl = game.appDownload { + return (game.appVersion, dl) + } + return (0, nil) + } + + /// zip 合并:默认 game 赢;agent 版本号更大时 agent 赢(NewRootVC.m:1450-1492) + private static func mergeZip( + agent: SubtreeExtract, + game: SubtreeExtract + ) -> (Int, String?) { + if agent.gameZip != nil && game.gameZip != nil { + if agent.gameVersion > game.gameVersion { + return (agent.gameVersion, agent.gameZip) + } + return (game.gameVersion, game.gameZip) + } + if let zip = game.gameZip { + return (game.gameVersion, zip) + } + if let zip = agent.gameZip { + return (agent.gameVersion, zip) + } + return (0, nil) + } + + // MARK: - 单层逐字段覆盖工具 + + /// 从单层节点向 SubtreeExtract 累加:非空字段才覆盖,等价于 msext 的 + /// `if (xxx != "" && xxx != nil) self.xxx = xxx` 语义。 + private static func applyOnto(_ r: inout SubtreeExtract, from agent: Agent) { + if let v = Int(agent.appVersion ?? ""), v != 0 { r.appVersion = v } + if let s = agent.appDownload, !s.isEmpty { r.appDownload = s } + if let v = Int(agent.gameVersion ?? ""), v != 0 { r.gameVersion = v } + if let s = agent.gameZip, !s.isEmpty { r.gameZip = s } + if let s = agent.showmessage, !s.isEmpty { r.showmessage = s } + } + private static func applyOnto(_ r: inout SubtreeExtract, from channel: Channel) { + if let v = Int(channel.appVersion ?? ""), v != 0 { r.appVersion = v } + if let s = channel.appDownload, !s.isEmpty { r.appDownload = s } + if let v = Int(channel.gameVersion ?? ""), v != 0 { r.gameVersion = v } + if let s = channel.gameZip, !s.isEmpty { r.gameZip = s } + if let s = channel.showmessage, !s.isEmpty { r.showmessage = s } + } + private static func applyOnto(_ r: inout SubtreeExtract, from market: Market) { + if let v = Int(market.appVersion ?? ""), v != 0 { r.appVersion = v } + if let s = market.appDownload, !s.isEmpty { r.appDownload = s } + if let v = Int(market.gameVersion ?? ""), v != 0 { r.gameVersion = v } + if let s = market.gameZip, !s.isEmpty { r.gameZip = s } + if let s = market.showmessage, !s.isEmpty { r.showmessage = s } + } + private static func applyOnto(_ r: inout SubtreeExtract, from game: Game) { + if let v = Int(game.appVersion ?? ""), v != 0 { r.appVersion = v } + if let s = game.appDownload, !s.isEmpty { r.appDownload = s } + if let v = Int(game.gameVersion ?? ""), v != 0 { r.gameVersion = v } + if let s = game.gameZip, !s.isEmpty { r.gameZip = s } + if let s = game.showmessage, !s.isEmpty { r.showmessage = s } + } +}