feat: add native Tomie adapter and strict import compatibility checks

This commit is contained in:
mrleo1nid committed 2026-10-25 11:02:52 +03:00
1 parent c8d83b3fe7
commit 57a4edcbfd
45 files changed
+2985 -71

No files matched your search

+38 -3
View File
@@ -15,7 +15,7 @@ function blob(manifest: Manifest, path: string) {
return `/api/versions/${manifest.versionId}/files/${f.hash}`;
}
function saveApi(versionId: number): SaveApi {
function saveApi<T = SaveData>(versionId: number): SaveApi<T> {
const base = `/api/versions/${versionId}/saves`;
return {
list: () => getJSON(base),
@@ -23,7 +23,7 @@ function saveApi(versionId: number): SaveApi {
const r = await fetch(`${base}/${slot}`);
if (r.status === 404) return null;
if (!r.ok) throw new Error(`${r.status}`);
return (await r.json()).data as SaveData;
return (await r.json()).data as T;
},
async put(slot, title, data) {
const r = await fetch(`${base}/${slot}`, {
@@ -39,8 +39,10 @@ function saveApi(versionId: number): SaveApi {
};
}
export interface PlayerHandle { destroy(): void }
export async function launch(root: HTMLElement, versionId: number, onExit: () => void,
onProgress: (text: string) => void): Promise<Engine> {
onProgress: (text: string) => void): Promise<PlayerHandle> {
onProgress("Загрузка манифеста");
const manifest = await getJSON<Manifest>(`/api/versions/${versionId}/manifest`);
const game = await getJSON<GameData>(blob(manifest, "game.json"));
@@ -53,6 +55,39 @@ 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; }
}
const engine = new Engine(root, game, manifest, modules, saveApi(versionId),
`pgvn:persistent:${versionId}`, onExit, tr,
(code) => loadTranslator(game, manifest, code));