diff --git a/ylgamehall/Info.plist b/ylgamehall/Info.plist
index 9246d9d..57ae4e7 100644
--- a/ylgamehall/Info.plist
+++ b/ylgamehall/Info.plist
@@ -19,6 +19,15 @@
+ NSAppTransportSecurity
+
+
+ NSAllowsArbitraryLoads
+
+
UIRequiresFullScreen
UIStatusBarHidden
diff --git a/ylgamehall/RootViewController.swift b/ylgamehall/RootViewController.swift
index 9ea5a8d..6f76861 100644
--- a/ylgamehall/RootViewController.swift
+++ b/ylgamehall/RootViewController.swift
@@ -113,6 +113,21 @@ final class RootViewController: UIViewController {
gameZip = \(resolved.gameZip ?? "")
showmessage = "\(resolved.showmessage ?? "")"
""")
+
+ // Phase 1.12 烟雾测试:读本地版本号 + 与远端对比
+ let localApp = LocalVersionReader.localAppVersion
+ let localGame = LocalVersionReader.localGameVersion
+ let needIPAUpgrade = resolved.appVersion > localApp
+ let needZipUpgrade = resolved.gameVersion > localGame
+ print("""
+ [LocalVersionReader] 本地 vs 远端对比:
+ 本地 appVersion = \(localApp)(来自 ChannelConfig.plist)
+ 远端 appVersion = \(resolved.appVersion)
+ → IPA 升级 = \(needIPAUpgrade ? "需要" : "不需要")
+ 本地 gameVersion = \(localGame)(来自 version.xml /game/version@value)
+ 远端 gameVersion = \(resolved.gameVersion)
+ → H5 zip 升级 = \(needZipUpgrade ? "需要" : "不需要")
+ """)
case .shortText(let msg):
print("""
[RemoteConfigClient] 拉到短文本响应(耗时 \(String(format: "%.3f", elapsed))s)
diff --git a/ylgamehall/Source/Resource/LocalVersionReader.swift b/ylgamehall/Source/Resource/LocalVersionReader.swift
new file mode 100644
index 0000000..2be5f05
--- /dev/null
+++ b/ylgamehall/Source/Resource/LocalVersionReader.swift
@@ -0,0 +1,75 @@
+//
+// LocalVersionReader.swift
+// ylgamehall
+//
+// 读本地两个版本号:
+// - 原生 IPA 版本(来自 ChannelConfig.plist 的 appversion 字段)
+// - H5 包版本(来自 Library/Caches/{gamedir}/{gamestart}/version.xml 的 /game/version@value)
+//
+// 详见 docs/Development-Plan.md ADR-008-F / msext NewRootVC.m:664-682。
+//
+
+import Foundation
+
+public enum LocalVersionReader {
+
+ /// 原生 IPA 版本。等价 msext `[FuncPublic filename:@"appversion"] integerValue`
+ /// 来源:ChannelConfig.plist 的 `appversion` 字段(沿用渠道注入约定,
+ /// IPA 后处理改 plist 可动态调整)。
+ /// 解析失败返回 0(msext `[nil intValue] = 0` 兜底语义)。
+ nonisolated public static var localAppVersion: Int {
+ Int(BundleConfig.shared.appVersion) ?? 0
+ }
+
+ /// 大厅 H5 包版本。等价 msext `NewRootVC.m:664-682` 的 GDataXML 路径,
+ /// 新外壳用 Foundation `XMLParser`(无外部依赖)解析。
+ ///
+ /// 文件路径:`SandboxPaths.lobbyVersionXML` =
+ /// `Library/Caches/{gamedir}/{gamestart}/version.xml`
+ /// 解析 `/game/version` 节点的 `value` 属性,转 Int。
+ ///
+ /// **缺失 / 损坏返回 0**(必须保留,首装后首次升级的兜底机制:
+ /// `[nil intValue] = 0`,任何远端 gameVersion > 0 触发下载)。
+ nonisolated public static var localGameVersion: Int {
+ readGameVersion(from: SandboxPaths.lobbyVersionXML) ?? 0
+ }
+
+ /// 子游戏 H5 包版本。等价 msext `gameController.m:1249-1254`。
+ nonisolated public static func subGameVersion(dir: String, start: String) -> Int {
+ let url = SandboxPaths.caches
+ .appendingPathComponent(dir)
+ .appendingPathComponent(start)
+ .appendingPathComponent("version.xml")
+ return readGameVersion(from: url) ?? 0
+ }
+
+ // MARK: - 内部 XML 解析
+
+ nonisolated private static func readGameVersion(from url: URL) -> Int? {
+ guard let data = try? Data(contentsOf: url) else { return nil }
+ let parser = XMLParser(data: data)
+ let handler = VersionXMLHandler()
+ parser.delegate = handler
+ guard parser.parse(), let v = handler.gameVersion else { return nil }
+ return v
+ }
+}
+
+// MARK: - XMLParser delegate
+
+/// 仅提取 ``(在 `/game/version` 路径下)的 value 转 Int。
+/// msext 的 xpath 是 `/game/version` 取属性 `value`;新外壳用 SAX 风格 XMLParser
+/// 也只需要在 `version` 元素出现时取属性即可(嵌套层级宽容化处理)。
+nonisolated private final class VersionXMLHandler: NSObject, XMLParserDelegate {
+ var gameVersion: Int?
+
+ func parser(_ parser: XMLParser,
+ didStartElement elementName: String,
+ namespaceURI: String?,
+ qualifiedName qName: String?,
+ attributes attributeDict: [String: String] = [:]) {
+ if elementName == "version", let raw = attributeDict["value"], let v = Int(raw) {
+ gameVersion = v
+ }
+ }
+}