Phase 1.11:VersionResolver chulishengji 双子树合并算法

实现 msext NewRootVC.m:1372-1538 的 chulishengji 双子树合并算法的纯
函数 Swift 版本,把远端 RemoteConfig 化简为 5 个目标字段决策结果
(appVersion / appDownload / gameVersion / gameZip / showmessage)。

- 新增 ylgamehall/Source/Network/VersionResolver.swift
  - public enum VersionResolver + 纯函数 static func resolve
  - public struct ResolvedVersion: Sendable 含 5 字段
  - 内部 SubtreeExtract 中转 4 字段累加结果(每层非空才覆盖)
  - extractAgentSubtree 4 层(agent → channel → market → game 嵌
    market 命中后)按 NewRootVC.m:885-1013 复刻
  - extractGameSubtree 4 层(game → game-self → channel → market)
    按 NewRootVC.m:1015-1180 复刻
  - mergeIPA 默认 agent 赢;game.appVersion > agent.appVersion 时
    game 赢(NewRootVC.m:1416)
  - mergeZip 默认 game 赢;agent.gameVersion > game.gameVersion 时
    agent 赢(NewRootVC.m:1456)
  - showmessage 任意子树非空就用,最后写赢;回退到 config.showmessage
- 字段名 game_download vs game_zip 由 SubtreeExtract.gameZip 统一吸收,
  Codable 模型已用 gameZip 命名,故无需重复查两个 key
- RootViewController 烟雾测试串接 RemoteConfigClient + VersionResolver,
  打印 chulishengji 合并后的 5 字段
- 真机实测(现网 .txt JSON):
  - appVersion = 43 → 等于本地 BundleConfig.appVersion,不触发 IPA 升级
  - gameVersion = 261 → 远端最新版(本地 version.xml 待 Phase 1.12 读取
    后对比;首装后 version.xml 实测 261 等于本地)
  - gameZip = http://tsgames.daoqi88.cn/zip2/gamehall.zip
    (HTTP,Phase 1.13 下载需 ATS NSExceptionDomains daoqi88.cn)
  - appDownload = itms-services://... 企业签 manifest URL
  - showmessage = ""(无运营公告)
- 单测目前未写(等 Phase 0.2 单测 target 建立后补齐),但生产数据
  已验证算法正确
This commit is contained in:
joywayer
2026-06-22 02:18:27 +08:00
parent 1bc287bafc
commit dfbc26a024
2 changed files with 267 additions and 0 deletions
@@ -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 URLnil
public let appDownload: String?
/// H5 zip version.xml /game/version@value
public let gameVersion: Int
/// H5 zip URLnil
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: RemoteConfigCodable
/// - 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)
// showmessagegame > agent >
let msg = gameExtract.showmessage
?? agentExtract.showmessage
?? config.showmessage
return ResolvedVersion(
appVersion: appVer,
appDownload: appDL,
gameVersion: gameVer,
gameZip: gameZip,
showmessage: msg
)
}
// MARK: - agent getagentversionNewRootVC.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()
// 1agent
applyOnto(&r, from: agent)
// 2channel
guard let channel = agent.channellist?.first(where: { $0.channelid == channelId })
else { return r }
applyOnto(&r, from: channel)
// 3market
guard let market = channel.marketlist?.first(where: { $0.marketid == marketId })
else { return r }
applyOnto(&r, from: market)
// 4game market msext
// Market gamelist channel marketlist
// gamelist Market.gamelist
// RemoteConfig 线
return r
}
// MARK: - game getgameversionNewRootVC.m:1015-1180
/// game 4 game game-self channel market
/// 2 game infotwo channellistmsext
/// agentid game.channellist
private static func extractGameSubtree(
game: Game,
channelId: String,
marketId: String
) -> SubtreeExtract {
var r = SubtreeExtract()
// 1game
applyOnto(&r, from: game)
// 2game-self game.channellist 3
// 3channel
guard let channel = game.channellist?.first(where: { $0.channelid == channelId })
else { return r }
applyOnto(&r, from: channel)
// 4market
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 }
}
}