Initial MVP: local visual novel library with Ren'Py → PGVN converter
- FastAPI backend: admin bootstrap with forced credential change, admin-created users with one-time generated passwords, roles as permission sets - Games and versions with content-addressed blob storage; forks copy only the manifest, so mods share assets with the original - Worker: archive/RPA extraction, unrpyc for .rpyc, own .rpy parser, Python subset translated via ast, media converted to WebP/Opus/WebM - React panel (library search, game page, upload, users, roles) and a framework-free web player with saves, history, skip/auto, transitions - docker compose: postgres, backend, worker, nginx on a single HTTP port Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
364b312064
72 files changed
+9707
No files matched your search
@@ -0,0 +1,52 @@
|
||||
import "./player.css";
|
||||
import { Engine, type SaveApi } from "./engine";
|
||||
import type { GameData, Manifest, SaveData, ScriptModule } from "./types";
|
||||
|
||||
async function getJSON<T>(url: string): Promise<T> {
|
||||
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 `/blobs/${f.hash}`;
|
||||
}
|
||||
|
||||
function saveApi(versionId: number): SaveApi {
|
||||
const base = `/api/versions/${versionId}/saves`;
|
||||
return {
|
||||
list: () => getJSON(base),
|
||||
async get(slot) {
|
||||
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;
|
||||
},
|
||||
async put(slot, title, data) {
|
||||
const r = await fetch(`${base}/${slot}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title, data }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`${r.status}`);
|
||||
},
|
||||
async remove(slot) {
|
||||
await fetch(`${base}/${slot}`, { method: "DELETE" });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function launch(root: HTMLElement, versionId: number, onExit: () => void,
|
||||
onProgress: (text: string) => void): Promise<Engine> {
|
||||
onProgress("Загрузка манифеста");
|
||||
const manifest = await getJSON<Manifest>(`/api/versions/${versionId}/manifest`);
|
||||
const game = await getJSON<GameData>(blob(manifest, "game.json"));
|
||||
onProgress(`Загрузка скриптов (${game.scripts.length})`);
|
||||
const modules = await Promise.all(game.scripts.map((s) => getJSON<ScriptModule>(blob(manifest, s))));
|
||||
const engine = new Engine(root, game, manifest, modules, saveApi(versionId),
|
||||
`pgvn:persistent:${versionId}`, onExit);
|
||||
engine.start();
|
||||
return engine;
|
||||
}
|
||||
Reference in new issue
Block a user