refactor: move game-specific import logic into profiles; unblock Tomie import
- Add app/games: GameProfile base, registry, ReaderHooks and renpy/naninovel/ tomie profiles. pipeline, preflight, detect and mods dispatch via the registry - Move tomie_* converter modules to app/games/tomie and split out reader hooks, import, mod validation and known defects - Tolerate the 31 authoring defects of the pinned Tomie build as report warnings (exact list, per-script counts); any new defect still blocks import - Frontend: runtime registry (runtimes.ts) with lazy Tomie launcher - Add profile/registry tests and docs/GAME_PROFILES.md; update IMPORT.md Full media export of the original Tomie build is not yet verified. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bbbcc6a5d8
commit
12ec82c7e6
30 files changed
+700
-196
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import "./player.css";
|
||||
import { Engine, type SaveApi } from "./engine";
|
||||
import { loadTranslator, preferredLanguage } from "./i18n";
|
||||
import { RUNTIMES } from "./runtimes";
|
||||
import type { GameData, Manifest, SaveData, ScriptModule } from "./types";
|
||||
|
||||
async function getJSON<T>(url: string): Promise<T> {
|
||||
@@ -55,38 +56,10 @@ export async function launch(root: HTMLElement, versionId: number, onExit: () =>
|
||||
Promise.all(game.scripts.map((s) => getJSON<ScriptModule>(blob(manifest, s)))),
|
||||
loadTranslator(game, manifest, lang),
|
||||
]);
|
||||
if (game.runtime === "tomie/1") {
|
||||
if (!game.tomie) throw new Error("Не найдены исходные настройки Tomie");
|
||||
const { TomiePlayer } = await import("./tomie-player");
|
||||
const scripts: import("./nani-runtime").NaniPackage["scripts"] = Object.create(null);
|
||||
const resources: NonNullable<GameData["naniResources"]> = Object.create(null);
|
||||
for (const module of modules) {
|
||||
if (!module.nani) {
|
||||
for (const node of module.labels.flatMap((label) => label.body)) {
|
||||
const record = node.resource;
|
||||
if (!record || typeof node.what !== "string" || typeof record.collection !== "string" || typeof record.key !== "string") continue;
|
||||
const collection = resources[record.collection] ??= Object.create(null);
|
||||
(collection[record.key] ??= []).push({ ...record, tid: node.tid, text: node.what });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const nodes = module.labels.flatMap((label) => label.body);
|
||||
const commands = nodes.map((node) => {
|
||||
const command = structuredClone(node.command);
|
||||
if (node.textField && typeof node.what === "string") command.args[node.textField] = { template: node.what };
|
||||
command.tid = node.tid ?? command.tid; return command;
|
||||
});
|
||||
const labels = module.nani.label_ids ? Object.fromEntries(Object.entries(module.nani.label_ids).map(([label, id]) => {
|
||||
const index = id ? nodes.findIndex((node) => node.id === id) : nodes.length;
|
||||
if (index < 0) throw new Error("Не найдена команда метки Naninovel: " + label);
|
||||
return [label, index];
|
||||
})) : module.nani.labels;
|
||||
scripts[module.nani.name] = { commands, labels, label_depths: module.nani.label_depths };
|
||||
}
|
||||
if (url.get("at")) throw new Error("Предпросмотр Tomie с отдельной строки ещё не поддерживается. Откройте игру с начала.");
|
||||
const player = new TomiePlayer(root, { ...game.tomie, scripts, resources }, manifest,
|
||||
saveApi<import("./tomie-player").TomieSave>(versionId), tr, onExit);
|
||||
try { player.start(); return player; } catch (error) { player.destroy(); throw error; }
|
||||
if (game.runtime) {
|
||||
const load = RUNTIMES[game.runtime];
|
||||
if (!load) throw new Error(`Неизвестная среда исполнения игры: ${game.runtime}`);
|
||||
return (await load())({ root, game, manifest, modules, url, tr, onExit, versionId, saveApi });
|
||||
}
|
||||
const engine = new Engine(root, game, manifest, modules, saveApi(versionId),
|
||||
`pgvn:persistent:${versionId}`, onExit, tr,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Games that need their own player register a launcher here, keyed by game.runtime.
|
||||
import type { SaveApi } from "./engine";
|
||||
import type { Translator } from "./i18n";
|
||||
import type { GameData, Manifest, ScriptModule } from "./types";
|
||||
import type { PlayerHandle } from "./index";
|
||||
|
||||
export interface RuntimeContext {
|
||||
root: HTMLElement;
|
||||
game: GameData;
|
||||
manifest: Manifest;
|
||||
modules: ScriptModule[];
|
||||
url: URLSearchParams;
|
||||
tr: Translator;
|
||||
onExit: () => void;
|
||||
versionId: number;
|
||||
saveApi: <T>(versionId: number) => SaveApi<T>;
|
||||
}
|
||||
|
||||
export type RuntimeLauncher = (context: RuntimeContext) => Promise<PlayerHandle>;
|
||||
|
||||
export const RUNTIMES: Record<string, () => Promise<RuntimeLauncher>> = {
|
||||
"tomie/1": async () => (await import("./tomie-launch")).launchTomie,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Launcher for the dedicated Tomie runtime (game.runtime === "tomie/1").
|
||||
import type { GameData } from "./types";
|
||||
import type { RuntimeContext } from "./runtimes";
|
||||
|
||||
export async function launchTomie({ root, game, manifest, modules, url, tr, onExit, saveApi, versionId }: RuntimeContext) {
|
||||
if (!game.tomie) throw new Error("Не найдены исходные настройки Tomie");
|
||||
const { TomiePlayer } = await import("./tomie-player");
|
||||
const scripts: import("./nani-runtime").NaniPackage["scripts"] = Object.create(null);
|
||||
const resources: NonNullable<GameData["naniResources"]> = Object.create(null);
|
||||
for (const module of modules) {
|
||||
if (!module.nani) {
|
||||
for (const node of module.labels.flatMap((label) => label.body)) {
|
||||
const record = node.resource;
|
||||
if (!record || typeof node.what !== "string" || typeof record.collection !== "string" || typeof record.key !== "string") continue;
|
||||
const collection = resources[record.collection] ??= Object.create(null);
|
||||
(collection[record.key] ??= []).push({ ...record, tid: node.tid, text: node.what });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const nodes = module.labels.flatMap((label) => label.body);
|
||||
const commands = nodes.map((node) => {
|
||||
const command = structuredClone(node.command);
|
||||
if (node.textField && typeof node.what === "string") command.args[node.textField] = { template: node.what };
|
||||
command.tid = node.tid ?? command.tid; return command;
|
||||
});
|
||||
const labels = module.nani.label_ids ? Object.fromEntries(Object.entries(module.nani.label_ids).map(([label, id]) => {
|
||||
const index = id ? nodes.findIndex((node) => node.id === id) : nodes.length;
|
||||
if (index < 0) throw new Error("Не найдена команда метки Naninovel: " + label);
|
||||
return [label, index];
|
||||
})) : module.nani.labels;
|
||||
scripts[module.nani.name] = { commands, labels, label_depths: module.nani.label_depths };
|
||||
}
|
||||
if (url.get("at")) throw new Error("Предпросмотр Tomie с отдельной строки ещё не поддерживается. Откройте игру с начала.");
|
||||
const player = new TomiePlayer(root, { ...game.tomie, scripts, resources }, manifest,
|
||||
saveApi<import("./tomie-player").TomieSave>(versionId), tr, onExit);
|
||||
try { player.start(); return player; } catch (error) { player.destroy(); throw error; }
|
||||
}
|
||||
Reference in new issue
Block a user