Files
youle_app_ios_v2/ylgamehall/Source/WebView/AppDataWriter.swift
T
joywayerandClaude Opus 4.7 7c856d0d66 AppDataWriter:app_data.js 内 5 个关键字段提到声明顺序最前
按用户要求调整 app_data.js 内 var 声明顺序,把 H5 调试时最关注的 5 个
核心字段提到最前:

  1. app_gameconfig     远端配置接口
  2. app_market         渠道市场标识
  3. app_agent          登录态 agent
  4. app_channel        渠道 ID
  5. app_Launchtype     大厅(0) / 子游戏(1) 标识

之后按原 msext 顺序:app_version / app_gamedir / app_gamestart /
app_appversion / app_getwifisignalLevel / app_gamename / app_invitationcode。

JS 内 var 声明的顺序对最终读取值无影响(var 提升 + 整段 script 同步
执行完才退出),契约不破,与 msext 行为等价。仅便于 H5 console 一眼
看到核心字段。

Logger.debug 输出顺序同步调整保持与文件内容顺序一致,便于联调对账。

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 18:14:10 +08:00

176 lines
7.6 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 区分
//
// 声明顺序:5 个关键字段(gameconfig / market / agent / channel / Launchtype
// 提到最前,便于 H5 console 调试时一眼看到核心值;JS var 提升机制下顺序对最终读取
// 值无影响,与 msext 行为等价。
let lines =
"var app_gameconfig='\(escape(bc.gameConfig))';" +
"var app_market='\(escape(bc.market))';" +
"var app_agent='\(escape(bc.agent))';" +
"var app_channel='\(escape(bc.channel))';" +
"var app_Launchtype=\(launchtype);" +
"var app_version=1;" +
"var app_gamedir='\(escape(gameDir))';" +
"var app_gamestart='\(escape(gameStart))';" +
"var app_appversion='\(escape(appVersion))';" +
"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_gameconfig = '\(bc.gameConfig, privacy: .public)'
app_market = '\(bc.market, privacy: .public)'
app_agent = '\(bc.agent, privacy: .public)'
app_channel = '\(bc.channel, privacy: .public)'
app_Launchtype = \(launchtype, privacy: .public)
app_version = 1
app_gamedir = '\(gameDir, privacy: .public)'
app_gamestart = '\(gameStart, privacy: .public)'
app_appversion = '\(appVersion, 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")
}
}