import "./player.css"; import { Engine, type SaveApi } from "./engine"; import { loadTranslator, preferredLanguage } from "./i18n"; import { RUNTIMES } from "./runtimes"; import { cloudActive, makeSaveApi } from "./saves"; import type { GameData, Manifest, SaveData, ScriptModule } from "./types"; async function getJSON(url: string): Promise { const r = await fetch(url, { credentials: "same-origin" }); if (!r.ok) throw new Error(`${r.status} ${url}`); return r.json(); } function blob(manifest: Manifest, path: string) { const f = manifest.files[path]; if (!f) throw new Error(`в версии нет файла ${path}`); return `/api/versions/${manifest.versionId}/files/${f.hash}`; } function saveApi(versionId: number, cloud: boolean): SaveApi { return makeSaveApi(versionId, cloud); } export interface PlayerHandle { destroy(): void } export async function launch(root: HTMLElement, versionId: number, onExit: () => void, onProgress: (text: string) => void): Promise { onProgress("Загрузка манифеста"); const manifest = await getJSON(`/api/versions/${versionId}/manifest`); const game = await getJSON(blob(manifest, "game.json")); onProgress(`Загрузка скриптов (${game.scripts.length})`); // ?lang= and ?at= come from the translation editor ("play from here") const url = new URLSearchParams(location.search); const asked = url.get("lang"); const lang = asked && (asked === game.sourceLang || game.locales?.[asked]) ? asked : preferredLanguage(game); const [modules, tr] = await Promise.all([ Promise.all(game.scripts.map((s) => getJSON(blob(manifest, s)))), loadTranslator(game, manifest, lang), ]); // files the game reads while it starts (`renpy.file(...)`) have to be here before its first line runs if (game.dataFiles?.length) { const texts = await Promise.all(game.dataFiles.map((path) => fetch(blob(manifest, `data/${path}`), { credentials: "same-origin" }).then((r) => (r.ok ? r.text() : "")))); game.dataText = Object.fromEntries(game.dataFiles.map((path, i) => [path.toLowerCase(), texts[i]])); } const cloud = await cloudActive(); 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: (id) => saveApi(id, cloud) }); } // volume belongs to the game, not to one of its versions (forks and mods share it) const gameId = await getJSON<{ game_id: number }>(`/api/versions/${versionId}`).then((v) => v.game_id, () => null); const engine = new Engine(root, game, manifest, modules, saveApi(versionId, cloud), `pgvn:persistent:${versionId}`, onExit, tr, (code) => loadTranslator(game, manifest, code), gameId === null ? `pgvn:audio:version:${versionId}` : `pgvn:audio:game:${gameId}`); const at = url.get("at"); if (at) engine.startAt(at); else engine.start(); return engine; }