实现子游戏按需下载,修复点子游戏白屏(§11.1 路径A)

根因:整包 gamehall.zip 顶层只含 gamehall,子游戏需按需单独下载;原 switchGame
直接 loadUrl 不存在的目录 → ArkWeb invalidFileUrl → 白屏。

对齐 Android NewwebviewActivity 的 updategamezip:
- ConfigManager.resolveGame(remote, gameid):以 gamedownloadurl 为 gameid 经
  agent→game→channel→market 分层解析子游戏 game_zip/game_version(复用 VersionResolver)。
- ResourceManager.updateSubgame:下载→暂存解压→校验 <dir>/index.html→原子换入 urlpath/<dir>。
  另加 subgameInstalled/subgameLocalVersion/subgameLocalGameId。
- BridgeGameContainer.switchGame 改 async + prepareSubgame:目录缺失先下载、已装比对远程
  版本更高才更新;下载期复用启动页同款金色进度条;远程配置 10 分钟缓存(对齐 gameconfigtime)。
- SwitchGamePayload 增 gameId(gamedownloadurl)。

真机验证:点三个老K → 下载 sangelaok.zip(9.4M) → 解压加载,子游戏正常渲染
(V1.155 与配置 game_version=155 一致);日志不再 invalidFileUrl。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-27 02:33:55 +08:00
co-authored by Claude Opus 4.8
parent e75f567948
commit de5b950d9b
5 changed files with 192 additions and 4 deletions
@@ -88,6 +88,21 @@ export class ConfigManager {
return VersionResolver.resolve(remote, this.matchKeys(fallbackGameId));
}
/**
* 子游戏版本决策:以**指定 gameid**SwitchOverGameData 的 gamedownloadurl / 子游戏 version.xml 的 gameid
* 经 agent→game→channel→market 分层解析 game_zip/game_versionagent/channel/market 取本地)。
* 复刻 Android downloadgame2/downloadgame3 的层级遍历。
*/
resolveGame(remote: RemoteConfig, gameid: string): VersionDecision {
const keys: MatchKeys = {
agentid: this.local.agent,
channelid: this.local.channel,
marketid: this.local.market,
gameid,
};
return VersionResolver.resolve(remote, keys);
}
/** 便捷:拉取 + 解析一步到位(gameid 回退值由调用方提供)。 */
async resolve(fallbackGameId: string = ''): Promise<VersionDecision> {
const remote: RemoteConfig = await this.fetchRemote();
@@ -102,6 +102,63 @@ export class ResourceManager {
return this.readVersionXml().gameId;
}
// ── 子游戏(§11.1 路径 A 按需下载)─────────────────────────────────────────
/** 子游戏入口路径 `<urlpath>/<dir>/index.html`。 */
subgameIndexPath(dir: string): string {
return `${this.cachedPaths.urlpath}/${dir}/index.html`;
}
/** 子游戏资源是否就绪(index.html 存在)。 */
subgameInstalled(dir: string): boolean {
return FileSystem.exists(this.subgameIndexPath(dir));
}
/** 子游戏本地 version.xml 信息(缺失 → 空信息)。 */
private subgameVersionXml(dir: string): VersionXmlInfo {
const p: string = `${this.cachedPaths.urlpath}/${dir}/version.xml`;
return FileSystem.exists(p) ? VersionXml.parse(FileSystem.readText(p)) : { version: 0, gameName: '', gameId: '' };
}
/** 子游戏本地版本号(缺失 0)。 */
subgameLocalVersion(dir: string): number {
return this.subgameVersionXml(dir).version;
}
/** 子游戏本地 version.xml 的 game id(查更新时优先用它解析远程版本;缺失 '')。 */
subgameLocalGameId(dir: string): string {
return this.subgameVersionXml(dir).gameId;
}
/**
* 下载并安装子游戏 zip 到 `urlpath`(复刻 Android runzip:删旧目录 → 解压到 urlpath)。
* zip 顶层即 `<dir>/`,解压后得 `urlpath/<dir>/...`。**原子化**:下载→解压暂存→校验
* `<dir>/index.html`→删旧换入,任一步失败旧资源保持不动。
*/
async updateSubgame(dir: string, downloadUrl: string, onProgress?: ProgressCallback): Promise<void> {
const url: string = downloadUrl.includes('?') ? downloadUrl : `${downloadUrl}?a=${Date.now()}`;
const zipPath: string = `${this.cachedPaths.upurlpath}/dowlod_zip/${Date.now()}_${dir}.zip`;
const staging: string = `${this.cachedPaths.urlpath}/.staging_${dir}_${Date.now()}`;
const target: string = `${this.cachedPaths.urlpath}/${dir}`;
try {
await Downloader.download(url, zipPath, onProgress);
FileSystem.rmrf(staging);
FileSystem.ensureDir(staging);
await Unzipper.unzip(zipPath, staging);
const staged: string = `${staging}/${dir}`;
if (!FileSystem.exists(`${staged}/index.html`)) {
throw new Error(`subgame package missing ${dir}/index.html`);
}
FileSystem.rmrf(target);
FileSystem.ensureDir(this.cachedPaths.urlpath);
FileSystem.rename(staged, target);
ResourceManager.log.i(`subgame ${dir} updated from ${url}`);
} finally {
FileSystem.rmrf(zipPath);
FileSystem.rmrf(staging);
}
}
/** 持久化路径到 KvStore(供容器/重启读取)。 */
async persistPaths(): Promise<void> {
this.kv.putString(KEY_URLPATH, this.cachedPaths.urlpath);