Files
youle_app_ios_v2/ylgamehall/Source/WebView/AppDataWriter.swift
T
joywayerandClaude Opus 4.7 829cc8f825 代码层落地 AppDataWriter + NetworkMonitor + WebContainer 接入
按 Design §7.5 蓝图落地,让 H5 启动时能从沙盒读到实际渠道/启动/设备
值,闭合 Phase 1.17 联调"app_gameconfig 不正确"现象。

新增 Source/WebView/AppDataWriter.swift:
  - public struct AppDataWriter(@MainActor)
  - ContainerRole enum:.lobby / .subGame(name:, dir:)
  - writeInitial():写 app_data.js(12 项)+ app_gamesname.js(暂只写当前
    gameStart 一项,Phase 6 子游戏完整时扩为扫描已装列表)
  - writeBattery(level:):写 app_battery.js(var app_getbattery=N;)
  - writeNetwork(code:):写 app_network.js(var app_getnetwork=N;)
  - 字面严格对齐 msext:
    * 字符串单引号包裹 'value'
    * 数值无引号(version=1 / Launchtype=0 / getwifisignalLevel=1)
    * app_gamesname 用 "var  app_gamesname=new Array(...)"(var 后两空格)
    * 大小写硬约束:Launchtype L 大、getwifisignalLevel wifi 小 + signal/Level 区分
  - escape 转义反斜杠 / 单引号 / 换行(避免渠道字段含单引号导致 H5 JS 解析错)
  - Logger debug 输出文件路径 + 每个 key=value 一行,便于 §7.5.8 联调对账

新增 Source/Resource/NetworkMonitor.swift:
  - @MainActor public final class NetworkMonitor
  - NWPathMonitor 最简 wrap:currentCode 同步快照(默认 2 WiFi,启动后被
    首次 path 更新)+ onChange MainActor 回调
  - shared 单例 + 幂等 start()

WebContainer.runBootPipelineSteps 接入:
  - switch outcome 前提取 let resolved: ResolvedVersion,避免变量作用域
    限制 case 内
  - 新增 writeAppDataFiles(resolved:):启动 NetworkMonitor + 开 battery
    监控 + 写 4 个文件首次值 + 挂 addObserver(battery) + onChange(network)
  - 调用位置:step 5 LobbyZipUpgrader 之后、step 6 loadFileURL 之前
    (与 msext NewRootVC.initJSdata 等价时序)

BuildProject 通过

Plan §6.4 里程碑加一行。

Phase 1.17 联调验证:启动后 H5 console 应读到实际 app_channel /
app_gameconfig 等值,不再是 H5 zip 包内默认占位。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 12:44:45 +08:00

172 lines
7.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// AppDataWriter.swift
// ylgamehall
//
// H5 app_*.js 预注入文件机制 — 在 loadFileURL 之前写 4 个 .js 文件到沙盒,
// H5 业务通过 <script src="app_*.js"> 同步引入即可读到实际渠道/启动/设备值。
// 与原 msext NewRootVC.initJSdata / changebattery / changenetstate 1:1 等价。
//
// 详见 docs/H5-Native-Implementation-Design.md §7.5。
//
import Foundation
import os.log
@MainActor
public struct AppDataWriter {
public enum ContainerRole: CustomStringConvertible, Sendable {
case lobby
case subGame(name: String, dir: String)
public var description: String {
switch self {
case .lobby: return "lobby"
case .subGame(let n, _): return "subGame(\(n))"
}
}
}
let bundleConfig: BundleConfig
let resolvedVersion: ResolvedVersion?
let containerRole: ContainerRole
private static let log = Logger(subsystem: "ylgamehall", category: "AppDataWriter")
public init(
bundleConfig: BundleConfig,
resolvedVersion: ResolvedVersion?,
containerRole: ContainerRole
) {
self.bundleConfig = bundleConfig
self.resolvedVersion = resolvedVersion
self.containerRole = containerRole
}
// MARK: - Public API
/// 一次性写 app_data.js(含 #1-#12 共 12 个 var+ app_gamesname.js#13)。
/// loadFileURL 前调一次。
public func writeInitial() throws {
try writeAppData()
try writeGamesName()
}
/// 写 #14 app_battery.js。loadFileURL 前补一次 + battery 变化重写。
public func writeBattery(_ level: Float) throws {
// 模拟器 batteryLevel 返回 -1(无电池硬件),用 0.0 占位避免负数
let normalized = max(level, 0)
let value = String(format: "%.2f", normalized)
let line = "var app_getbattery=\(value);\n"
let url = filePath("app_battery.js")
try line.write(to: url, atomically: true, encoding: .utf8)
Self.log.debug("[\(containerRole.description, privacy: .public)] write app_battery.js: app_getbattery=\(value, privacy: .public)\(url.path, privacy: .public)")
}
/// 写 #15 app_network.js。loadFileURL 前补一次 + NWPathMonitor 变化重写。
public func writeNetwork(_ code: Int) throws {
let line = "var app_getnetwork=\(code);\n"
let url = filePath("app_network.js")
try line.write(to: url, atomically: true, encoding: .utf8)
Self.log.debug("[\(containerRole.description, privacy: .public)] write app_network.js: app_getnetwork=\(code, privacy: .public)\(url.path, privacy: .public)")
}
// MARK: - Private writers
private func writeAppData() throws {
let bc = bundleConfig
let launchtype: Int
let gameName: String
let gameDir: String
let gameStart: String
switch containerRole {
case .lobby:
launchtype = 0
gameName = bc.gameStart
gameDir = bc.gameDir
gameStart = bc.gameStart
case .subGame(let name, let dir):
launchtype = 1
gameName = name
gameDir = dir
gameStart = name
}
let appVersion = resolvedVersion?.appVersion.description ?? bc.appVersion
// ⚠️ 严格保持字面(参 docs/H5-Native-Contract.md §4.2 + msext NewRootVC.m:1204):
// - 字符串字段单引号包裹(msext "var x='%@';"
// - 数值字段无引号(app_version=1 / app_Launchtype=0 / app_getwifisignalLevel=1
// - app_appversion msext 是 %d 形式但传入是 NSString,实际行为同字符串—— 这里转字面串与 msext 等价
// - 大小写不能改:app_Launchtype L 大写、app_getwifisignalLevel wifi 小写 + signal/Level 区分
let lines =
"var app_version=1;" +
"var app_gameconfig='\(escape(bc.gameConfig))';" +
"var app_gamedir='\(escape(gameDir))';" +
"var app_gamestart='\(escape(gameStart))';" +
"var app_agent='\(escape(bc.agent))';" +
"var app_appversion='\(escape(appVersion))';" +
"var app_market='\(escape(bc.market))';" +
"var app_channel='\(escape(bc.channel))';" +
"var app_Launchtype=\(launchtype);" +
"var app_getwifisignalLevel=1;" +
"var app_gamename='\(escape(gameName))';" +
"var app_invitationcode='\(escape(bc.other))';"
let url = filePath("app_data.js")
try lines.write(to: url, atomically: true, encoding: .utf8)
Self.log.debug("""
[\(containerRole.description, privacy: .public)] write app_data.js → \(url.path, privacy: .public)
app_version = 1
app_gameconfig = '\(bc.gameConfig, privacy: .public)'
app_gamedir = '\(gameDir, privacy: .public)'
app_gamestart = '\(gameStart, privacy: .public)'
app_agent = '\(bc.agent, privacy: .public)'
app_appversion = '\(appVersion, privacy: .public)'
app_market = '\(bc.market, privacy: .public)'
app_channel = '\(bc.channel, privacy: .public)'
app_Launchtype = \(launchtype, privacy: .public)
app_getwifisignalLevel = 1
app_gamename = '\(gameName, privacy: .public)'
app_invitationcode = '\(bc.other, privacy: .public)'
""")
}
private func writeGamesName() throws {
// Phase 1: 暂时只写当前 gameStart 一项(与 msext AppDelegate.m:259 首次启动行为一致)。
// Phase 6 子游戏完整时改为扫描 SandboxPaths.installedSubGames + 增量追加。
let names = [bundleConfig.gameStart]
let escaped = names.map { "'\(escape($0))'" }.joined(separator: ",")
// ⚠️ msext 字面:var 后两个空格 + new Array(...),沿用
let line = "var app_gamesname=new Array(\(escaped));\n"
let url = filePath("app_gamesname.js")
try line.write(to: url, atomically: true, encoding: .utf8)
Self.log.debug("[\(containerRole.description, privacy: .public)] write app_gamesname.js: \(names.count, privacy: .public) games → \(url.path, privacy: .public)\n games = \(names, privacy: .public)")
}
private func filePath(_ name: String) -> URL {
// 与 msext NewRootVC 一致:{gamedir}/{gamestart}/app_*.js
let base: URL
switch containerRole {
case .lobby:
// {Caches}/{gameDir}/{gameStart}/ = SandboxPaths.lobbyIndex 的父目录
base = SandboxPaths.lobbyIndex.deletingLastPathComponent()
case .subGame(let name, let dir):
// {Caches}/{dir}/{name}/ = SandboxPaths.subGameIndex(dir,name) 的父目录
base = SandboxPaths.subGameIndex(dir, name).deletingLastPathComponent()
}
return base.appendingPathComponent(name)
}
/// JS string 转义:反斜杠 / 单引号 / 换行(msext 用单引号包裹字符串,
/// 转义对象是单引号)。
private func escape(_ s: String) -> String {
s.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
.replacingOccurrences(of: "\n", with: "\\n")
.replacingOccurrences(of: "\r", with: "\\r")
}
}