实现子游戏按需下载,修复点子游戏白屏(§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:
co-authored by
Claude Opus 4.8
parent
e75f567948
commit
de5b950d9b
@@ -2,7 +2,7 @@ import { webview } from '@kit.ArkWeb';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { window } from '@kit.ArkUI';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { ConfigManager, ResourceManager, AppDataInjector, AppDataValues } from 'domain_resource';
|
||||
import { ConfigManager, ResourceManager, AppDataInjector, AppDataValues, RemoteConfig, VersionDecision } from 'domain_resource';
|
||||
import { KvStore, LocalUploadServer } from 'platform';
|
||||
import { OutboundHandlers } from 'contracts';
|
||||
import { AppEnv, EventBus, EventPayload, NavEvents, OpenGenericWebPayload, SwitchGamePayload,
|
||||
@@ -35,6 +35,14 @@ export struct BridgeGameContainer {
|
||||
private cancels: Array<() => void> = [];
|
||||
/** 非空 → 子游戏 Web 显示(路径 A 切换)。 */
|
||||
@State private subgameUrl: string = '';
|
||||
/** 子游戏资源下载中(按需下载期间显示启动页同款进度条)。 */
|
||||
@State private subgameDownloading: boolean = false;
|
||||
/** 子游戏下载进度(0~100)。 */
|
||||
@State private subgamePercent: number = 0;
|
||||
/** 远程配置 10 分钟缓存(对齐 Android:避免每次进子游戏都重拉 config)。 */
|
||||
private cachedRemote: RemoteConfig | undefined = undefined;
|
||||
private cachedRemoteAt: number = 0;
|
||||
private static readonly REMOTE_TTL: number = 10 * 60 * 1000;
|
||||
/** 分享面板显隐(叠在 Stack 顶层,对齐 Android SharePanelHelper)。 */
|
||||
@State private sharePanelVisible: boolean = false;
|
||||
/** 当前分享面板的一次性回投事件名(用户选定平台后 emit 回 ShareProvider)。 */
|
||||
@@ -145,11 +153,14 @@ export struct BridgeGameContainer {
|
||||
}, false);
|
||||
}
|
||||
|
||||
/** 路径 A:大厅 → 子游戏(大厅去激活并保活,创建子游戏 Web)。 */
|
||||
private switchGame(p?: EventPayload): void {
|
||||
/** 路径 A:大厅 → 子游戏(必要时先按需下载,再大厅去激活并保活、创建子游戏 Web)。 */
|
||||
private async switchGame(p?: EventPayload): Promise<void> {
|
||||
if (p === undefined) {
|
||||
return;
|
||||
}
|
||||
if (this.subgameDownloading) {
|
||||
return; // 正在下载某子游戏,忽略重复触发
|
||||
}
|
||||
// 拓扑约束:仅 大厅↔子游戏,无 子游戏→子游戏。已在子游戏中时忽略——
|
||||
// 游戏逻辑须先 backgameData 回大厅、再由大厅发 SwitchOverGameData 进新子游戏。
|
||||
if (this.subgameUrl !== '') {
|
||||
@@ -162,6 +173,11 @@ export struct BridgeGameContainer {
|
||||
if (d.dir === '' || res === undefined || cfg === undefined) {
|
||||
return;
|
||||
}
|
||||
// §11.1:子游戏 H5 按需下载——目录缺失先下载解压;已装则比对远程版本,更高才更新。
|
||||
const ready: boolean = await this.prepareSubgame(d, res, cfg);
|
||||
if (!ready) {
|
||||
return; // 下载失败/无地址:留在大厅(已记录日志/弹提示)
|
||||
}
|
||||
const subDir: string = `${res.paths().urlpath}/${d.dir}`;
|
||||
// 子游戏目录自带 app_data.js(H5 同步读),launchtype='1'
|
||||
const values: AppDataValues = AppDataInjector.buildValues(cfg.getLocal(), `${res.localVersion()}`, '1', res.localGameName());
|
||||
@@ -172,6 +188,85 @@ export struct BridgeGameContainer {
|
||||
this.subgameUrl = `file://${subDir}/index.html?Launchtype=1`; // 触发子游戏 Web 渲染 → setupSubgame
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保子游戏资源就绪(复刻 Android NewwebviewActivity 进入时的 updategamezip):
|
||||
* - 目录缺失:经分层配置解析 game_zip → 下载解压(首装)。
|
||||
* - 目录已装:比对远程 game_version 与本地 version.xml,更高才更新;否则直接用。
|
||||
* - 取配置/下载失败时:已装则容忍离线用旧版,未装则提示并放弃(留在大厅)。
|
||||
* 返回 true 表示可加载子游戏。
|
||||
*/
|
||||
private async prepareSubgame(d: SwitchGamePayload, res: ResourceManager, cfg: ConfigManager): Promise<boolean> {
|
||||
const installed: boolean = res.subgameInstalled(d.dir);
|
||||
let remote: RemoteConfig;
|
||||
try {
|
||||
remote = await this.remoteConfig(cfg);
|
||||
} catch (e) {
|
||||
Logger.tag('BridgeGameContainer').w(`subgame config fetch failed: ${(e as Error).message}`);
|
||||
if (installed) {
|
||||
return true; // 离线容忍:已装直接用
|
||||
}
|
||||
this.toast('网络异常,无法下载游戏');
|
||||
return false;
|
||||
}
|
||||
// gameid:已装优先用本地 version.xml 的 gameid(对齐 downloadgame3),否则用 H5 传来的 gamedownloadurl
|
||||
const localGameId: string = installed ? res.subgameLocalGameId(d.dir) : '';
|
||||
const gameId: string = localGameId !== '' ? localGameId : d.gameId;
|
||||
const decision: VersionDecision = cfg.resolveGame(remote, gameId);
|
||||
const localVer: number = installed ? res.subgameLocalVersion(d.dir) : -1;
|
||||
if (installed && decision.gameVersion <= localVer) {
|
||||
return true; // 已是最新
|
||||
}
|
||||
if (decision.gameDownload === '') {
|
||||
if (installed) {
|
||||
return true; // 无下载地址但已装:用旧的
|
||||
}
|
||||
Logger.tag('BridgeGameContainer').w(`subgame ${d.dir} no game_zip (gameId=${gameId})`);
|
||||
this.toast('未找到该游戏的下载地址');
|
||||
return false;
|
||||
}
|
||||
// 下载(缺目录=首装 / 远端更高=更新)
|
||||
this.subgamePercent = 0;
|
||||
this.subgameDownloading = true;
|
||||
try {
|
||||
await res.updateSubgame(d.dir, decision.gameDownload, (pct: number) => {
|
||||
if (pct >= 0) {
|
||||
this.subgamePercent = pct;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
Logger.tag('BridgeGameContainer').w(`subgame ${d.dir} download failed: ${(e as Error).message}`);
|
||||
if (installed) {
|
||||
return true; // 更新失败但旧版可用
|
||||
}
|
||||
this.toast('游戏下载失败,请重试');
|
||||
return false;
|
||||
} finally {
|
||||
this.subgameDownloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 远程配置(10 分钟缓存):对齐 Android 的 gameconfigtime 节流,避免每次进子游戏都重拉。 */
|
||||
private async remoteConfig(cfg: ConfigManager): Promise<RemoteConfig> {
|
||||
const now: number = Date.now();
|
||||
const cached = this.cachedRemote;
|
||||
if (cached !== undefined && now - this.cachedRemoteAt < BridgeGameContainer.REMOTE_TTL) {
|
||||
return cached;
|
||||
}
|
||||
const remote: RemoteConfig = await cfg.fetchRemote();
|
||||
this.cachedRemote = remote;
|
||||
this.cachedRemoteAt = now;
|
||||
return remote;
|
||||
}
|
||||
|
||||
private toast(msg: string): void {
|
||||
try {
|
||||
this.getUIContext().getPromptAction().showToast({ message: msg, duration: 2000 });
|
||||
} catch (e) {
|
||||
Logger.tag('BridgeGameContainer').w(`toast failed: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 子游戏 → 大厅(销毁子游戏 Web,大厅恢复并带回数据)。 */
|
||||
private backToLobby(p?: EventPayload): void {
|
||||
if (this.subgameUrl === '') {
|
||||
@@ -246,6 +341,23 @@ export struct BridgeGameContainer {
|
||||
.onRenderExited((event: OnRenderExitedEvent) => this.slotS?.onRenderExited(event.renderExitReason))
|
||||
}
|
||||
|
||||
// 子游戏按需下载进度(启动页同款:金色进度条 + 文案,覆盖全屏白底)
|
||||
if (this.subgameDownloading) {
|
||||
Column({ space: 14 }) {
|
||||
Progress({ value: this.subgamePercent, total: 100, type: ProgressType.Linear })
|
||||
.width('56%')
|
||||
.color('#D2A312')
|
||||
.backgroundColor('#E6E6E6')
|
||||
Row({ space: 8 }) {
|
||||
LoadingProgress().width(20).height(20).color('#D2A312')
|
||||
Text(`正在下载游戏… ${this.subgamePercent}%`).fontSize(15).fontColor('#1B2A4A')
|
||||
}
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.backgroundColor(Color.White)
|
||||
}
|
||||
|
||||
// 分享面板(顶层叠加;用户选平台/取消 → 一次性回投 ShareProvider)
|
||||
if (this.sharePanelVisible) {
|
||||
SharePanel({ onPick: (platform: string) => this.emitShareResult(platform) })
|
||||
|
||||
Reference in New Issue
Block a user