no message
This commit is contained in:
@@ -21,19 +21,41 @@ export interface LevelDatabaseFile {
|
||||
levels: Record<string, LevelConfig>;
|
||||
}
|
||||
|
||||
export interface LevelsDbIndex {
|
||||
version: number;
|
||||
mode: 'sharded';
|
||||
shardSize?: number;
|
||||
levelIdBase?: number;
|
||||
generatedAt?: string;
|
||||
source?: string;
|
||||
total: number;
|
||||
min: number;
|
||||
max: number;
|
||||
stats?: LevelDatabaseFile['stats'];
|
||||
shards: Array<{ min: number; max: number; file: string; count?: number }>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/** 主站 scratch-gui 注入:/unity/levels-database.json */
|
||||
__tfrhLevelsDatabaseUrl?: string;
|
||||
/** loader 预注入的 JSON(可选) */
|
||||
/** loader 预注入的 JSON(可选,legacy 全量) */
|
||||
__tfrhLevelsDatabaseJson?: LevelDatabaseFile;
|
||||
/** 分片索引 URL / 预注入 */
|
||||
__tfrhLevelsDbIndexUrl?: string;
|
||||
__tfrhLevelsDbIndex?: LevelsDbIndex;
|
||||
/** loader 按关拉取分片 */
|
||||
__tfrhEnsureLevelDbShard?: (levelId: number) => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
let fileCache: LevelDatabaseFile | null = null;
|
||||
let indexCache: LevelsDbIndex | null = null;
|
||||
let levelsMap: Record<number, LevelConfig> = {};
|
||||
let sortedIds: number[] = [];
|
||||
let loadPromise: Promise<void> | null = null;
|
||||
const loadedShardKeys = new Set<string>();
|
||||
const shardLoadPromises = new Map<string, Promise<void>>();
|
||||
|
||||
function rebuildIndex() {
|
||||
sortedIds = Object.keys(levelsMap)
|
||||
@@ -42,18 +64,62 @@ function rebuildIndex() {
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function ingestFile(data: LevelDatabaseFile) {
|
||||
fileCache = data;
|
||||
levelsMap = {};
|
||||
function ingestFile(data: LevelDatabaseFile, merge = false) {
|
||||
if (!merge) {
|
||||
fileCache = data;
|
||||
levelsMap = {};
|
||||
} else if (!fileCache) {
|
||||
fileCache = {
|
||||
version: data.version || 1,
|
||||
levels: {},
|
||||
};
|
||||
}
|
||||
for (const [k, cfg] of Object.entries(data.levels ?? {})) {
|
||||
const id = parseInt(k, 10);
|
||||
if (!Number.isNaN(id)) {
|
||||
levelsMap[id] = { ...cfg, levelID: id };
|
||||
if (fileCache?.levels) {
|
||||
fileCache.levels[String(id)] = levelsMap[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
rebuildIndex();
|
||||
}
|
||||
|
||||
function ingestIndex(data: LevelsDbIndex) {
|
||||
indexCache = data;
|
||||
if (!fileCache) {
|
||||
fileCache = {
|
||||
version: data.version,
|
||||
generatedAt: data.generatedAt,
|
||||
source: data.source,
|
||||
levelIdBase: data.levelIdBase,
|
||||
stats: data.stats,
|
||||
levels: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function findShardForLevel(levelId: number): LevelsDbIndex['shards'][0] | null {
|
||||
if (!indexCache?.shards?.length) return null;
|
||||
for (const shard of indexCache.shards) {
|
||||
if (levelId >= shard.min && levelId <= shard.max) return shard;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function shardKey(shard: { file: string }) {
|
||||
return shard.file;
|
||||
}
|
||||
|
||||
function validateIndex(index: LevelsDbIndex): void {
|
||||
if (index.total < 100 || index.min < LEVEL_ID_BASE) {
|
||||
throw new Error(
|
||||
`关卡库索引过旧 (${index.total} 关),请重新 package-for-project`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRemoteUrl(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const url = window.__tfrhLevelsDatabaseUrl;
|
||||
@@ -61,6 +127,10 @@ function resolveRemoteUrl(): string | null {
|
||||
}
|
||||
|
||||
function validateIngested(): void {
|
||||
if (indexCache) {
|
||||
validateIndex(indexCache);
|
||||
return;
|
||||
}
|
||||
const total = sortedIds.length;
|
||||
const minId = sortedIds[0] ?? 0;
|
||||
if (total < 100 || minId < LEVEL_ID_BASE) {
|
||||
@@ -90,6 +160,139 @@ function fetchCandidates(): string[] {
|
||||
return [...new Set(out)];
|
||||
}
|
||||
|
||||
function indexFetchCandidates(): string[] {
|
||||
const out: string[] = [];
|
||||
if (typeof window !== 'undefined') {
|
||||
const configured = window.__tfrhLevelsDbIndexUrl?.trim();
|
||||
if (configured && /^https?:\/\//i.test(configured)) {
|
||||
out.push(configured);
|
||||
}
|
||||
}
|
||||
if (canUseHttpFetch()) {
|
||||
out.push(new URL('levels-db-index.json', window.location.href).href);
|
||||
out.push(new URL('/unity/levels-db-index.json', window.location.origin).href);
|
||||
}
|
||||
return [...new Set(out)];
|
||||
}
|
||||
|
||||
async function fetchJsonWithBrotli(url: string): Promise<unknown> {
|
||||
const brUrl = url.replace(/\.json(\?.*)?$/i, '.json.br$1');
|
||||
try {
|
||||
const brRes = await fetch(brUrl);
|
||||
if (brRes.ok && typeof DecompressionStream !== 'undefined') {
|
||||
const text = await new Response(await brRes.arrayBuffer())
|
||||
.pipeThrough(new DecompressionStream('brotli'))
|
||||
.text();
|
||||
return JSON.parse(text);
|
||||
}
|
||||
} catch {
|
||||
/* fallback */
|
||||
}
|
||||
const res = await fetch(url, { credentials: 'same-origin' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function loadIndexFromNetwork(): Promise<void> {
|
||||
const candidates = indexFetchCandidates();
|
||||
if (!candidates.length) {
|
||||
throw new Error('[LevelDatabase] 无法 fetch 关卡库索引');
|
||||
}
|
||||
const errors: unknown[] = [];
|
||||
for (const url of candidates) {
|
||||
try {
|
||||
const json = await fetchJsonWithBrotli(url) as LevelsDbIndex;
|
||||
if (json?.mode !== 'sharded' || !json.shards?.length) {
|
||||
throw new Error('非分片索引');
|
||||
}
|
||||
ingestIndex(json);
|
||||
validateIngested();
|
||||
console.log(
|
||||
`[LevelDatabase] 已加载索引 ${json.total} 关 (${json.min}–${json.max})`,
|
||||
url,
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`[LevelDatabase] 无法加载关卡库索引: ${candidates.join(', ')}; `
|
||||
+ errors.map((e) => String(e)).join('; '),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveShardUrl(shardFile: string): string {
|
||||
const rel = shardFile.replace(/^\/+/, '');
|
||||
if (canUseHttpFetch()) {
|
||||
return new URL(`/unity/StreamingAssets/aa/${rel}`, window.location.origin).href;
|
||||
}
|
||||
return rel;
|
||||
}
|
||||
|
||||
async function loadShardFromNetwork(shard: LevelsDbIndex['shards'][0]): Promise<void> {
|
||||
const rel = shard.file.replace(/^\/+/, '');
|
||||
const url = resolveShardUrl(rel);
|
||||
const json = await fetchJsonWithBrotli(url) as LevelDatabaseFile;
|
||||
ingestFile(json, true);
|
||||
console.log(
|
||||
`[LevelDatabase] 已加载分片 ${shard.min}–${shard.max} (+${Object.keys(json.levels ?? {}).length} 关)`,
|
||||
);
|
||||
}
|
||||
|
||||
async function loadShardNow(shard: LevelsDbIndex['shards'][0]): Promise<void> {
|
||||
const key = shardKey(shard);
|
||||
if (loadedShardKeys.has(key)) return;
|
||||
const pending = shardLoadPromises.get(key);
|
||||
if (pending) {
|
||||
await pending;
|
||||
return;
|
||||
}
|
||||
const promise = (async () => {
|
||||
if (typeof window !== 'undefined' && window.__tfrhEnsureLevelDbShard) {
|
||||
await window.__tfrhEnsureLevelDbShard(shard.min);
|
||||
const json = window.__tfrhLevelsDatabaseJson;
|
||||
if (json?.levels) {
|
||||
const partial: LevelDatabaseFile = { version: json.version || 2, levels: {} };
|
||||
for (const [k, cfg] of Object.entries(json.levels)) {
|
||||
const id = parseInt(k, 10);
|
||||
if (!Number.isNaN(id) && id >= shard.min && id <= shard.max) {
|
||||
partial.levels[k] = cfg;
|
||||
}
|
||||
}
|
||||
if (Object.keys(partial.levels).length) {
|
||||
ingestFile(partial, true);
|
||||
loadedShardKeys.add(key);
|
||||
console.log(
|
||||
`[LevelDatabase] 已合并分片 ${shard.min}–${shard.max} (+${Object.keys(partial.levels).length} 关)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
await loadShardFromNetwork(shard);
|
||||
loadedShardKeys.add(key);
|
||||
})();
|
||||
shardLoadPromises.set(key, promise);
|
||||
try {
|
||||
await promise;
|
||||
} finally {
|
||||
shardLoadPromises.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/** 进关前确保该关所在分片已加载 */
|
||||
export async function ensureLevelShardLoaded(levelId: number): Promise<void> {
|
||||
if (!indexCache) return;
|
||||
if (levelId in levelsMap) return;
|
||||
const shard = findShardForLevel(levelId);
|
||||
if (!shard) {
|
||||
console.warn(`[LevelDatabase] 关卡 ${levelId} 不在索引范围`);
|
||||
return;
|
||||
}
|
||||
await loadShardNow(shard);
|
||||
}
|
||||
|
||||
function loadFromRemote(url: string): Promise<void> {
|
||||
const abs = /^https?:\/\//i.test(url)
|
||||
? url
|
||||
@@ -161,12 +364,19 @@ async function waitForInjection(maxMs = 2500): Promise<LevelDatabaseFile | null>
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 异步加载(AppBootstrap 启动时调用) */
|
||||
/** 异步加载(AppBootstrap 启动时调用;分片模式仅拉 index) */
|
||||
export function loadLevelDatabase(): Promise<void> {
|
||||
if (fileCache) return Promise.resolve();
|
||||
if (fileCache || indexCache) return Promise.resolve();
|
||||
if (loadPromise) return loadPromise;
|
||||
loadPromise = (async () => {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.__tfrhLevelsDbIndex) {
|
||||
ingestIndex(window.__tfrhLevelsDbIndex);
|
||||
validateIngested();
|
||||
console.log(`[LevelDatabase] 已注入索引 ${indexCache!.total} 关`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.__tfrhLevelsDatabaseJson) {
|
||||
ingestFile(window.__tfrhLevelsDatabaseJson);
|
||||
validateIngested();
|
||||
@@ -196,6 +406,13 @@ export function loadLevelDatabase(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await loadIndexFromNetwork();
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn('[LevelDatabase] 分片索引不可用,回退全量库', e);
|
||||
}
|
||||
|
||||
await loadFromNetwork();
|
||||
} catch (e) {
|
||||
loadPromise = null;
|
||||
@@ -206,7 +423,11 @@ export function loadLevelDatabase(): Promise<void> {
|
||||
}
|
||||
|
||||
export function isLevelDatabaseReady(): boolean {
|
||||
return fileCache !== null;
|
||||
return fileCache !== null || indexCache !== null;
|
||||
}
|
||||
|
||||
export function isLevelDatabaseSharded(): boolean {
|
||||
return indexCache !== null;
|
||||
}
|
||||
|
||||
// --- 查 ---
|
||||
@@ -216,7 +437,11 @@ export function getLevelConfig(levelID: number): LevelConfig | null {
|
||||
|
||||
/** 是否在 Cocos 导出的关卡库中 */
|
||||
export function hasLevel(levelID: number): boolean {
|
||||
return levelID in levelsMap;
|
||||
if (levelID in levelsMap) return true;
|
||||
if (indexCache && levelID >= indexCache.min && levelID <= indexCache.max) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getLevelIds(): number[] {
|
||||
@@ -227,19 +452,32 @@ export function getLevelCount(): number {
|
||||
return sortedIds.length;
|
||||
}
|
||||
|
||||
export const MIN_LEVEL_ID = (): number => sortedIds[0] ?? LEVEL_ID_BASE;
|
||||
export const MAX_LEVEL_ID = (): number => sortedIds[sortedIds.length - 1] ?? LEVEL_ID_BASE;
|
||||
export const MIN_LEVEL_ID = (): number => indexCache?.min ?? sortedIds[0] ?? LEVEL_ID_BASE;
|
||||
export const MAX_LEVEL_ID = (): number => indexCache?.max ?? sortedIds[sortedIds.length - 1] ?? LEVEL_ID_BASE;
|
||||
|
||||
export function nextLevelId(cur: number): number {
|
||||
const i = sortedIds.indexOf(cur);
|
||||
if (i < 0) return sortedIds[0] ?? cur;
|
||||
return sortedIds[(i + 1) % sortedIds.length];
|
||||
if (sortedIds.length) {
|
||||
const i = sortedIds.indexOf(cur);
|
||||
if (i < 0) return sortedIds[0] ?? cur;
|
||||
return sortedIds[(i + 1) % sortedIds.length];
|
||||
}
|
||||
const max = MAX_LEVEL_ID();
|
||||
const min = MIN_LEVEL_ID();
|
||||
if (cur < min) return min;
|
||||
if (cur >= max) return min;
|
||||
return cur + 1;
|
||||
}
|
||||
|
||||
export function prevLevelId(cur: number): number {
|
||||
const i = sortedIds.indexOf(cur);
|
||||
if (i < 0) return sortedIds[0] ?? cur;
|
||||
return sortedIds[(i - 1 + sortedIds.length) % sortedIds.length];
|
||||
if (sortedIds.length) {
|
||||
const i = sortedIds.indexOf(cur);
|
||||
if (i < 0) return sortedIds[0] ?? cur;
|
||||
return sortedIds[(i - 1 + sortedIds.length) % sortedIds.length];
|
||||
}
|
||||
const max = MAX_LEVEL_ID();
|
||||
const min = MIN_LEVEL_ID();
|
||||
if (cur <= min) return max;
|
||||
return cur - 1;
|
||||
}
|
||||
|
||||
// --- 增改(运行时 / 编辑器脚本) ---
|
||||
|
||||
Reference in New Issue
Block a user