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,72 @@
|
||||
"""Build a small Ren'Py-like game archive for tests and manual checks.
|
||||
|
||||
python -m tests.make_sample out.zip
|
||||
"""
|
||||
|
||||
import io
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
import wave
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from app.converter.rpa import write_rpa
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures" / "sample"
|
||||
|
||||
|
||||
def _png(size, color, text="", alpha=False) -> bytes:
|
||||
img = Image.new("RGBA" if alpha else "RGB", size, (0, 0, 0, 0) if alpha else color)
|
||||
d = ImageDraw.Draw(img)
|
||||
if alpha:
|
||||
w, h = size
|
||||
d.ellipse((w * 0.2, h * 0.05, w * 0.8, h * 0.45), fill=color)
|
||||
d.rectangle((w * 0.25, h * 0.45, w * 0.75, h), fill=color)
|
||||
else:
|
||||
for y in range(0, size[1], 40):
|
||||
d.line((0, y, size[0], y), fill=tuple(min(255, c + 25) for c in color), width=2)
|
||||
if text:
|
||||
d.text((20, 20), text, fill=(255, 255, 255, 255))
|
||||
out = io.BytesIO()
|
||||
img.save(out, "PNG")
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def _wav(seconds=2.0, freq=440) -> bytes:
|
||||
out = io.BytesIO()
|
||||
with wave.open(out, "wb") as w:
|
||||
w.setnchannels(1)
|
||||
w.setsampwidth(2)
|
||||
w.setframerate(22050)
|
||||
frames = b"".join(struct.pack("<h", int(8000 * math.sin(2 * math.pi * freq * i / 22050)))
|
||||
for i in range(int(22050 * seconds)))
|
||||
w.writeframes(frames)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def build(path: Path) -> Path:
|
||||
script = (FIXTURES / "script.rpy").read_bytes()
|
||||
options = 'define config.name = "Тестовая новелла"\ninit python:\n gui.init(1280, 720)\n'
|
||||
rpa_path = path.with_suffix(".rpa.tmp")
|
||||
write_rpa(rpa_path, {
|
||||
"script.rpy": script,
|
||||
"images/bg room.png": _png((1280, 720), (60, 70, 110), "bg room"),
|
||||
"images/eileen happy.png": _png((400, 650), (220, 160, 170), alpha=True),
|
||||
})
|
||||
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr("SampleGame-1.0-pc/game/archive.rpa", rpa_path.read_bytes())
|
||||
zf.writestr("SampleGame-1.0-pc/game/options.rpy", options)
|
||||
zf.writestr("SampleGame-1.0-pc/game/audio/theme.wav", _wav())
|
||||
zf.writestr("SampleGame-1.0-pc/game/gui/main_menu.png",
|
||||
_png((1280, 720), (30, 30, 50), "Main menu"))
|
||||
zf.writestr("SampleGame-1.0-pc/renpy/common/00start.rpy", "label _start:\n return\n")
|
||||
zf.writestr("SampleGame-1.0-pc/SampleGame.exe", b"MZ")
|
||||
rpa_path.unlink()
|
||||
return path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(build(Path(sys.argv[1] if len(sys.argv) > 1 else "sample.zip")))
|
||||
Reference in new issue
Block a user